Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(preset-icons): Add CSS SVG Sprite support #2675

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion examples/remix/app/root.tsx
@@ -1,4 +1,3 @@
/* eslint-disable n/prefer-global/process */
import reset from '@unocss/reset/tailwind.css'
import type { LinksFunction, MetaFunction } from 'remix'
import {
Expand Down Expand Up @@ -40,7 +39,7 @@
<Outlet />
<ScrollRestoration />
<Scripts />
{process.env.NODE_ENV === 'development' && <LiveReload />}

Check failure on line 42 in examples/remix/app/root.tsx

View workflow job for this annotation

GitHub Actions / lint

Unexpected use of the global variable 'process'. Use 'require("process")' instead
</body>
</html>
)
Expand Down
34 changes: 34 additions & 0 deletions examples/vite-vue3/public/sprite.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
110 changes: 110 additions & 0 deletions examples/vite-vue3/src/App.vue
Expand Up @@ -9,5 +9,115 @@
<!-- @unocss-skip-start -->
<div hidden class="bg-[url(../src/uno.svg)] bg-[url(/uno.svg)]" />
<!-- @unocss-skip-end -->

<div>SVG Sprite test</div>
<div class="mdi-close" />
<div class="mdi-close-bg" />
<div class="mdi-chevron-down" />
<div class="mdi-chevron-down-bg" />
<div class="mdi-chevron-up" />
<div class="mdi-chevron-up-bg" />

<div>Custom SVG Sprite</div>
<div class="sprite-custom-close" />
<div class="sprite-custom-chevron-down" />
<div class="sprite-custom-chevron-up" />
<div class="sprite-custom-animated" />

<div>Custom SVG Sprite background image</div>
<div class="sprite-custom-close?bg" />
<div class="sprite-custom-chevron-down?bg" />
<div class="sprite-custom-chevron-up?bg" />
<div class="sprite-custom-animated?bg" />

<div>Custom MDI SVG Sprite</div>
<div class="sprite-custom-mdi-account" />
<div class="sprite-custom-mdi-alert-octagram" />
<div class="sprite-custom-mdi-access-point-network" />

<div>Custom SVG Sprite From file system</div>
<div class="sprite-custom-fs-icon" />
<div class="sprite-custom-fs-multi-line-attr" />

<div>Custom SVG Sprite (attributify)</div>
<div sprite-custom-close />
<div sprite-custom-chevron-down />
<div sprite-custom-chevron-up />
<div sprite-custom-animated />
</div>
</template>

<style>
.mdi-close {
/*--un-icon: url("/sprite.svg#shapes-close-view");*/
--un-icon: url("/sprite.svg#close");
-webkit-mask: var(--un-icon) no-repeat;
mask: var(--un-icon) no-repeat;
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
background-color: currentColor;
color: inherit;
width: 1em;
height: 1em;
display: inline-block;
vertical-align: middle;
}
.mdi-close-bg {
/*background: url("/sprite.svg#shapes-close-view") no-repeat;*/
background: url("/sprite.svg#close") no-repeat;
background-size: 100% 100%;
background-color: transparent;
display: inline-block;
vertical-align: middle;
width: 1em;
height: 1em;
}
.mdi-chevron-down {
/*--un-icon: url("/sprite.svg#shapes-chevron-down-view");*/
--un-icon: url("/sprite.svg#chevron-down");
-webkit-mask: var(--un-icon) no-repeat;
mask: var(--un-icon) no-repeat;
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
background-color: currentColor;
color: inherit;
width: 1em;
height: 1em;
display: inline-block;
vertical-align: middle;
}
.mdi-chevron-down-bg {
/*background: url("/sprite.svg#shapes-chevron-down-view") no-repeat;*/
background: url("/sprite.svg#chevron-down") no-repeat;
background-size: 100% 100%;
background-color: transparent;
display: inline-block;
vertical-align: middle;
width: 1em;
height: 1em;
}
.mdi-chevron-up {
/*--un-icon: url("/sprite.svg#shapes-chevron-up-view");*/
--un-icon: url("/sprite.svg#chevron-up");
-webkit-mask: var(--un-icon) no-repeat;
mask: var(--un-icon) no-repeat;
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
background-color: currentColor;
color: inherit;
width: 1em;
height: 1em;
display: inline-block;
vertical-align: middle;
}
.mdi-chevron-up-bg {
/*background: url("/sprite.svg#shapes-chevron-up-view") no-repeat;*/
background: url("/sprite.svg#chevron-up") no-repeat;
background-size: 100% 100%;
background-color: transparent;
display: inline-block;
vertical-align: middle;
width: 1em;
height: 1em;
}
</style>
108 changes: 107 additions & 1 deletion examples/vite-vue3/vite.config.ts
@@ -1,16 +1,22 @@
import { resolve } from 'node:path'
import { basename, dirname, extname, resolve } from 'node:path'
import { opendir, readFile } from 'node:fs/promises'
import { defineConfig } from 'vite'
import Vue from '@vitejs/plugin-vue'
import UnoCSS from 'unocss/vite'
import presetAttributify from '@unocss/preset-attributify'
import presetIcons from '@unocss/preset-icons'
import presetUno from '@unocss/preset-uno'
import type { AsyncSpriteIconsFactory, SpriteIcon } from '@unocss/preset-icons'
import { FileSystemIconLoader } from '@iconify/utils/lib/loader/node-loaders'
import type { AutoInstall } from '@iconify/utils/lib/loader/fs'
import { loadCollectionFromFS } from '@iconify/utils/lib/loader/fs'
import { searchForIcon } from '@iconify/utils/lib/loader/modern'

const iconDirectory = resolve(__dirname, 'icons')

// https://vitejs.dev/config/
export default defineConfig({
base: '/app/',
plugins: [
Vue(),
UnoCSS({
Expand All @@ -21,15 +27,115 @@ export default defineConfig({
presetUno(),
presetAttributify(),
presetIcons({
warn: true,
extraProperties: {
'display': 'inline-block',
'vertical-align': 'middle',
},
collections: {
custom: FileSystemIconLoader(iconDirectory),
},
sprites: {
sprites: {
'custom-mdi': createLoadCollectionFromFSAsyncIterator('mdi', {
include: ['account', 'alert-octagram', 'access-point-network'],
}),
'custom-fs': createFileSystemIconLoaderAsyncIterator('icons', 'custom-fs'),
'custom': <SpriteIcon[]>[{
name: 'animated',
svg: `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><style>
path.animated {
fill-opacity: 0;
animation: animated-test-animation 2s linear forwards;
}
@keyframes animated-test-animation {
from {
fill-opacity: 0;
}
to {
fill-opacity: 1;
}
}
</style><path class="animated" fill="currentColor" d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12L19 6.41Z"/></svg>`,
}, {
name: 'close',
svg: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12L19 6.41Z"/></svg>',
}, {
name: 'chevron-down',
svg: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M7.41 8.58L12 13.17l4.59-4.59L18 10l-6 6l-6-6l1.41-1.42Z"/></svg>',
}, {
name: 'chevron-up',
svg: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6l-6 6l1.41 1.41Z"/></svg>',
}],
},
},
}),
],
}),
],
})

/* TODO BEGIN-CLEANUP: types from @iconify/utils: remove them once published */
function createLoadCollectionFromFSAsyncIterator(
collection: string,
options: {
autoInstall?: AutoInstall
include?: string[] | ((icon: string) => boolean)
} = { autoInstall: false },
) {
const include = options.include ?? (() => true)
const useInclude: (icon: string) => boolean
= typeof include === 'function'
? include
: (icon: string) => include.includes(icon)

return <AsyncSpriteIconsFactory> async function* () {
const iconSet = await loadCollectionFromFS(collection)
if (iconSet) {
const icons = Object.keys(iconSet.icons).filter(useInclude)
for (const id of icons) {
const iconData = await searchForIcon(
iconSet,
collection,
[id],
options,
)
if (iconData) {
yield {
name: id,
svg: iconData,
collection,
}
}
}
}
}
}
function createFileSystemIconLoaderAsyncIterator(
dir: string,
collection = dirname(dir),
include: string[] | ((icon: string) => boolean) = () => true,
) {
const useInclude: (icon: string) => boolean
= typeof include === 'function'
? include
: (icon: string) => include.includes(icon)

return <AsyncSpriteIconsFactory> async function* () {
const stream = await opendir(dir)
for await (const file of stream) {
if (!file.isFile() || extname(file.name) !== '.svg')
continue

const name = basename(file.name).slice(0, -4)
if (useInclude(name)) {
yield {
name,
svg: await readFile(resolve(dir, file.name), 'utf-8'),
collection,
}
}
}
}
}
/* TODO END-CLEANUP: types from @iconify/utils: remove them once published */