From 0d0fd02b95d938ab396ae5c8e5d9ffc82fa87787 Mon Sep 17 00:00:00 2001 From: userquin Date: Sat, 27 May 2023 15:22:30 +0200 Subject: [PATCH 1/5] feat(preset-icons): Add CSS SVG Sprite support --- examples/vite-vue3/src/App.vue | 95 +++++++++ examples/vite-vue3/vite.config.ts | 13 ++ packages/preset-icons/src/core.ts | 201 ++++++++++++------ packages/preset-icons/src/types.ts | 35 +++ packages/vite/src/index.ts | 2 + packages/vite/src/svg-sprite.ts | 138 ++++++++++++ playground/src/auto-imports.d.ts | 330 +++++++++++++++++++++++++++++ 7 files changed, 754 insertions(+), 60 deletions(-) create mode 100644 packages/vite/src/svg-sprite.ts diff --git a/examples/vite-vue3/src/App.vue b/examples/vite-vue3/src/App.vue index 45d3e32c62..e5ca4a8e97 100644 --- a/examples/vite-vue3/src/App.vue +++ b/examples/vite-vue3/src/App.vue @@ -1,3 +1,6 @@ + + + + diff --git a/examples/vite-vue3/vite.config.ts b/examples/vite-vue3/vite.config.ts index fbfdad2939..aa61c1bd74 100644 --- a/examples/vite-vue3/vite.config.ts +++ b/examples/vite-vue3/vite.config.ts @@ -21,6 +21,7 @@ export default defineConfig({ presetUno(), presetAttributify(), presetIcons({ + warn: true, extraProperties: { 'display': 'inline-block', 'vertical-align': 'middle', @@ -28,6 +29,18 @@ export default defineConfig({ collections: { custom: FileSystemIconLoader(iconDirectory), }, + sprites: { + collections: ['custom'], + loader: (name) => { + if (name === 'custom') { + return { + 'close': '', + 'chevron-down': '', + 'chevron-up': '', + } + } + }, + }, }), ], }), diff --git a/packages/preset-icons/src/core.ts b/packages/preset-icons/src/core.ts index 753008953c..5c39c4571f 100644 --- a/packages/preset-icons/src/core.ts +++ b/packages/preset-icons/src/core.ts @@ -1,16 +1,23 @@ -import type { Preset } from '@unocss/core' +import type { DynamicMatcher, Preset, Rule } from '@unocss/core' import { warnOnce } from '@unocss/core' import type { + CustomIconLoader, IconifyLoaderOptions, UniversalIconLoader, } from '@iconify/utils/lib/loader/types' import { encodeSvgForCss } from '@iconify/utils/lib/svg/encode-svg-for-css' -import type { IconsOptions } from './types' + +import type { CSSSVGSprites, IconsOptions } from './types' const COLLECTION_NAME_PARTS_MAX = 3 export { IconsOptions } +interface CSSSVGSpritesOptions extends CSSSVGSprites { + svgCollections: Record> + customCollections: Record +} + export function createPresetIcons(lookupIconLoader: (options: IconsOptions) => Promise) { return function presetIcons(options: IconsOptions = {}): Preset { const { @@ -24,6 +31,7 @@ export function createPresetIcons(lookupIconLoader: (options: IconsOptions) => P autoInstall = false, layer = 'icons', unit, + sprites, } = options const loaderOptions: IconifyLoaderOptions = { @@ -49,76 +57,149 @@ export function createPresetIcons(lookupIconLoader: (options: IconsOptions) => P }, } + const rules: Rule[] = [[ + /^([a-z0-9:_-]+)(?:\?(mask|bg|auto))?$/, + createDynamicMatcher(warn, mode, loaderOptions, iconLoaderResolver), + { layer, prefix }, + ]] + + if (sprites) { + const collections = Array.isArray(sprites.collections) ? sprites.collections : [sprites.collections] + const svgCollections: Record> = {} + const originalLoader = sprites.loader + const customCollections = collections.reduce((acc, c) => { + acc[c] = async (name) => { + let collection: Record | undefined = svgCollections[c] + if (!collection) { + collection = await originalLoader(c) + svgCollections[c] = collection ?? {} + } + + return collection?.[name] + } + + return acc + }, >{}) + const spriteOptions: CSSSVGSpritesOptions = { + ...sprites, + // override loader to cache collections + async loader(name) { + let collection: Record | undefined = svgCollections[name] + if (!collection) { + collection = await originalLoader(name) + svgCollections[name] = collection ?? {} + } + + return collection + }, + svgCollections, + customCollections, + } + rules.push([ + /^([a-z0-9:_-]+)(?:\?(mask|bg|auto))?$/, + createDynamicMatcher(warn, sprites.mode ?? mode, loaderOptions, iconLoaderResolver, spriteOptions), + { layer, prefix: sprites.prefix ?? 'sprite-' }, + ]) + } + let iconLoader: UniversalIconLoader + async function iconLoaderResolver() { + iconLoader = iconLoader || await lookupIconLoader(options) + return iconLoader + } + return { name: '@unocss/preset-icons', enforce: 'pre', options, layers: { icons: -30 }, - rules: [[ - /^([a-z0-9:_-]+)(?:\?(mask|bg|auto))?$/, - async ([full, body, _mode = mode]) => { - let collection = '' - let name = '' - let svg: string | undefined + rules, + } + } +} - iconLoader = iconLoader || await lookupIconLoader(options) +function createDynamicMatcher( + warn: boolean, + mode: string, + loaderOptions: IconifyLoaderOptions, + iconLoaderResolver: () => Promise, + sprites?: CSSSVGSpritesOptions, +) { + return (async ([full, body, _mode = mode]) => { + let collection = '' + let name = '' + let svg: string | undefined - const usedProps = {} - if (body.includes(':')) { - [collection, name] = body.split(':') - svg = await iconLoader(collection, name, { ...loaderOptions, usedProps }) - } - else { - const parts = body.split(/-/g) - for (let i = COLLECTION_NAME_PARTS_MAX; i >= 1; i--) { - collection = parts.slice(0, i).join('-') - name = parts.slice(i).join('-') - svg = await iconLoader(collection, name, { ...loaderOptions, usedProps }) - if (svg) - break - } - } + const iconLoader = await iconLoaderResolver() - if (!svg) { - if (warn) - warnOnce(`failed to load icon "${full}"`) - return - } + const usedProps = {} + if (body.includes(':')) { + [collection, name] = body.split(':') + svg = sprites + ? await iconLoader(collection, name, { + ...loaderOptions, + usedProps, + customCollections: sprites.customCollections, + }) + : await iconLoader(collection, name, { ...loaderOptions, usedProps }) + } + else { + const parts = body.split(/-/g) + for (let i = COLLECTION_NAME_PARTS_MAX; i >= 1; i--) { + collection = parts.slice(0, i).join('-') + name = parts.slice(i).join('-') + svg = sprites + ? await iconLoader(collection, name, { + ...loaderOptions, + usedProps, + customCollections: sprites.customCollections, + }) + : await iconLoader(collection, name, { ...loaderOptions, usedProps }) + if (svg) + break + } + } - const url = `url("data:image/svg+xml;utf8,${encodeSvgForCss(svg)}")` - - if (_mode === 'auto') - _mode = svg.includes('currentColor') ? 'mask' : 'bg' - - if (_mode === 'mask') { - // Thanks to https://codepen.io/noahblon/post/coloring-svgs-in-css-background-images - return { - '--un-icon': url, - '-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', - // for Safari https://github.com/elk-zone/elk/pull/264 - 'color': 'inherit', - ...usedProps, - } - } - else { - return { - 'background': `${url} no-repeat`, - 'background-size': '100% 100%', - 'background-color': 'transparent', - ...usedProps, - } - } - }, - { layer, prefix }, - ]], + if (!svg) { + if (warn) + warnOnce(`failed to load icon "${full}"`) + return } - } + + let url: string + // TODO: resolve base path + if (sprites) + url = `url("unocss-${collection}-sprite.svg#shapes-${name}-view")` + else + url = `url("data:image/svg+xml;utf8,${encodeSvgForCss(svg)}")` + + if (_mode === 'auto') + _mode = svg.includes('currentColor') ? 'mask' : 'bg' + + if (_mode === 'mask') { + // Thanks to https://codepen.io/noahblon/post/coloring-svgs-in-css-background-images + return { + '--un-icon': url, + '-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', + // for Safari https://github.com/elk-zone/elk/pull/264 + 'color': 'inherit', + ...usedProps, + } + } + else { + return { + 'background': `${url} no-repeat`, + 'background-size': '100% 100%', + 'background-color': 'transparent', + ...usedProps, + } + } + }) } export function combineLoaders(loaders: UniversalIconLoader[]) { diff --git a/packages/preset-icons/src/types.ts b/packages/preset-icons/src/types.ts index d3249a579b..c40f4bb0f6 100644 --- a/packages/preset-icons/src/types.ts +++ b/packages/preset-icons/src/types.ts @@ -2,6 +2,36 @@ import type { CustomIconLoader, IconCustomizations, InlineCollection } from '@ic import type { Awaitable } from '@unocss/core' import type { IconifyJSON } from '@iconify/types' +export interface CSSSVGSprites { + /** + * Mode of generated CSS icons. + * + * - `mask` - use background color and the `mask` property for monochrome icons + * - `background-img` - use background image for the icons, colors are static + * - `auto` - smartly decide mode between `mask` and `background-img` per icon based on its style + * - if omitted it will be used + * + * @default 'auto' + * @see https://antfu.me/posts/icons-in-pure-css + */ + mode?: 'mask' | 'background-img' | 'auto' + /** + * Class prefix for matching icon rules. + * + * @default `sprite-` + */ + prefix?: string | string[] + /** + * Collections to load. + */ + collections: string | string[] + /** + * Loader function. + * @param name SVG Sprite name. + */ + loader: (name: string) => Awaitable | undefined> +} + export interface IconsOptions { /** * Scale related to the current font size (1em). @@ -76,4 +106,9 @@ export interface IconsOptions { * - https://cdn.skypack.dev/ */ cdn?: string + + /** + * SVG Sprites: only available in Node. + */ + sprites?: CSSSVGSprites } diff --git a/packages/vite/src/index.ts b/packages/vite/src/index.ts index 53ba5604af..0bb79ba856 100644 --- a/packages/vite/src/index.ts +++ b/packages/vite/src/index.ts @@ -11,6 +11,7 @@ import { ConfigHMRPlugin } from './config-hmr' import type { VitePluginConfig } from './types' import { createTransformerPlugins } from './transformers' import { createDevtoolsPlugin } from './devtool' +import { SVGSpritePlugin } from './svg-sprite' export * from './types' export * from './modes/chunk-build' @@ -32,6 +33,7 @@ export default function UnocssPlugin( const plugins = [ ConfigHMRPlugin(ctx), + SVGSpritePlugin(ctx), ...createTransformerPlugins(ctx), ...createDevtoolsPlugin(ctx), ] diff --git a/packages/vite/src/svg-sprite.ts b/packages/vite/src/svg-sprite.ts new file mode 100644 index 0000000000..74cc8871c9 --- /dev/null +++ b/packages/vite/src/svg-sprite.ts @@ -0,0 +1,138 @@ +import type { Awaitable, Preset, UnocssPluginContext } from '@unocss/core' +import type { Plugin } from 'vite' +import { warnOnce } from '@unocss/core' + +const ENTRY_ALIAS = /^unocss-(.*)-sprite\.svg$/ + +type PresetIcons = Preset & { + options: { + warn?: boolean + sprites?: { + collections: string | string[] + loader: (name: string) => Awaitable | undefined> + } + } +} + +interface SpriteEntry { + content: string + rawViewBox: string + x: number + y: number + width: number + height: number + entry: string +} + +interface Sprites { + content: string + previous?: Pick +} + +const sprites = new Map>() + +export function SVGSpritePlugin(ctx: UnocssPluginContext): Plugin { + return { + name: 'unocss:css-svg-sprite', + enforce: 'pre', + configureServer(server) { + server.middlewares.use(async (req, res, next) => { + const uri = req.url + if (!uri) + return next() + + let pathname = new URL(uri, 'http://localhost').pathname + if (pathname.startsWith(server.config.base)) + pathname = pathname.slice(server.config.base.length) + + const match = pathname.match(ENTRY_ALIAS) + if (!match) + return next() + + const sprite = await prepareSVGSprite(ctx, match[1]) + + res.setHeader('Content-Type', 'image/svg+xml') + if (sprite) { + res.statusCode = 200 + res.write(sprite, 'utf-8') + } + else { + res.statusCode = 404 + } + res.end() + }) + }, + async generateBundle() { + + }, + } +} + +// TODO: move this to utils package in @iconify/iconify +function parseSVGData(data: string) { + const match = data.match(/]+viewBox="([^"]+)"[^>]*>([\s\S]+)<\/svg>/) + if (!match) + return + + const [, viewBox, path] = match + const [x, y, width, height] = viewBox.split(' ').map(Number) + return { rawViewBox: match[1], content: path, x, y, width, height } +} + +// TODO: move this to utils package in @iconify/iconify +function createCSSSVGSprite(icons: Record) { + return `${ + Object.entries(icons).reduce((acc, [name, icon]) => { + const data = parseSVGData(icon) + if (data) { + const newY = acc.previous ? (acc.previous.y + 1) : data.y + acc.content += ` + ${data.content} + + ` + + acc.previous = { + y: data.height + (acc.previous ? acc.previous.y : 0), + } + } + + return acc + }, { content: '' }).content} +` +} + +function createSVGSpritePromise(icons: Record) { + return new Promise((resolve, reject) => { + try { + resolve(createCSSSVGSprite(icons)) + } + catch (e) { + reject(e) + } + }) +} + +async function loadSVGSpriteIcons(preset: PresetIcons, sprite: string) { + const sprites = preset.options!.sprites! + if ((typeof sprites.collections === 'string' && sprites.collections !== sprite) || !sprites.collections.includes(sprite)) { + if (preset.options.warn) + warnOnce(`missing collection in sprites.collections "${sprite}", svg sprite will not generated in build`) + + return '' + } + + const icons = await sprites.loader(sprite) + return icons ? await createSVGSpritePromise(icons) : '' +} + +async function prepareSVGSprite(ctx: UnocssPluginContext, sprite: string) { + await ctx.ready + const preset: PresetIcons = ctx.uno.config?.presets?.find(p => 'options' in p && p.options && 'sprites' in p.options && typeof p.options.sprites !== 'undefined') as any + if (!preset || typeof preset.options?.sprites === 'undefined') + return + + const spriteEntry = sprites.get(sprite) ?? sprites.set(sprite, loadSVGSpriteIcons(preset, sprite)).get(sprite)! + + const data = await spriteEntry + return data.length ? data : undefined +} diff --git a/playground/src/auto-imports.d.ts b/playground/src/auto-imports.d.ts index d5b24ee1a0..e1a00c92f7 100644 --- a/playground/src/auto-imports.d.ts +++ b/playground/src/auto-imports.d.ts @@ -126,6 +126,7 @@ declare global { const toReactive: typeof import('@vueuse/core')['toReactive'] const toRef: typeof import('vue')['toRef'] const toRefs: typeof import('vue')['toRefs'] + const toValue: typeof import('vue')['toValue'] const toggleDark: typeof import('./composables/dark')['toggleDark'] const togglePanel: typeof import('./composables/panel')['togglePanel'] const transformedHTML: typeof import('./composables/uno')['transformedHTML'] @@ -459,6 +460,335 @@ declare module 'vue' { readonly toReactive: UnwrapRef readonly toRef: UnwrapRef readonly toRefs: UnwrapRef + readonly toValue: UnwrapRef + readonly toggleDark: UnwrapRef + readonly togglePanel: UnwrapRef + readonly transformedHTML: UnwrapRef + readonly triggerRef: UnwrapRef + readonly tryOnBeforeMount: UnwrapRef + readonly tryOnBeforeUnmount: UnwrapRef + readonly tryOnMounted: UnwrapRef + readonly tryOnScopeDispose: UnwrapRef + readonly tryOnUnmounted: UnwrapRef + readonly uno: UnwrapRef + readonly unref: UnwrapRef + readonly unrefElement: UnwrapRef + readonly until: UnwrapRef + readonly useAbs: UnwrapRef + readonly useActiveElement: UnwrapRef + readonly useAnimate: UnwrapRef + readonly useArrayDifference: UnwrapRef + readonly useArrayEvery: UnwrapRef + readonly useArrayFilter: UnwrapRef + readonly useArrayFind: UnwrapRef + readonly useArrayFindIndex: UnwrapRef + readonly useArrayFindLast: UnwrapRef + readonly useArrayIncludes: UnwrapRef + readonly useArrayJoin: UnwrapRef + readonly useArrayMap: UnwrapRef + readonly useArrayReduce: UnwrapRef + readonly useArraySome: UnwrapRef + readonly useArrayUnique: UnwrapRef + readonly useAsyncQueue: UnwrapRef + readonly useAsyncState: UnwrapRef + readonly useAttrs: UnwrapRef + readonly useAverage: UnwrapRef + readonly useBase64: UnwrapRef + readonly useBattery: UnwrapRef + readonly useBluetooth: UnwrapRef + readonly useBreakpoints: UnwrapRef + readonly useBroadcastChannel: UnwrapRef + readonly useBrowserLocation: UnwrapRef + readonly useCached: UnwrapRef + readonly useCeil: UnwrapRef + readonly useClamp: UnwrapRef + readonly useClipboard: UnwrapRef + readonly useCloned: UnwrapRef + readonly useColorMode: UnwrapRef + readonly useConfirmDialog: UnwrapRef + readonly useCounter: UnwrapRef + readonly useCssModule: UnwrapRef + readonly useCssVar: UnwrapRef + readonly useCssVars: UnwrapRef + readonly useCurrentElement: UnwrapRef + readonly useCycleList: UnwrapRef + readonly useDark: UnwrapRef + readonly useDateFormat: UnwrapRef + readonly useDebounce: UnwrapRef + readonly useDebounceFn: UnwrapRef + readonly useDebouncedRefHistory: UnwrapRef + readonly useDeviceMotion: UnwrapRef + readonly useDeviceOrientation: UnwrapRef + readonly useDevicePixelRatio: UnwrapRef + readonly useDevicesList: UnwrapRef + readonly useDisplayMedia: UnwrapRef + readonly useDocumentVisibility: UnwrapRef + readonly useDraggable: UnwrapRef + readonly useDropZone: UnwrapRef + readonly useElementBounding: UnwrapRef + readonly useElementByPoint: UnwrapRef + readonly useElementHover: UnwrapRef + readonly useElementSize: UnwrapRef + readonly useElementVisibility: UnwrapRef + readonly useEventBus: UnwrapRef + readonly useEventListener: UnwrapRef + readonly useEventSource: UnwrapRef + readonly useEyeDropper: UnwrapRef + readonly useFavicon: UnwrapRef + readonly useFetch: UnwrapRef + readonly useFileDialog: UnwrapRef + readonly useFileSystemAccess: UnwrapRef + readonly useFloor: UnwrapRef + readonly useFocus: UnwrapRef + readonly useFocusWithin: UnwrapRef + readonly useFps: UnwrapRef + readonly useFullscreen: UnwrapRef + readonly useGamepad: UnwrapRef + readonly useGeolocation: UnwrapRef + readonly useIdle: UnwrapRef + readonly useImage: UnwrapRef + readonly useInfiniteScroll: UnwrapRef + readonly useIntersectionObserver: UnwrapRef + readonly useInterval: UnwrapRef + readonly useIntervalFn: UnwrapRef + readonly useKeyModifier: UnwrapRef + readonly useLastChanged: UnwrapRef + readonly useLocalStorage: UnwrapRef + readonly useMagicKeys: UnwrapRef + readonly useManualRefHistory: UnwrapRef + readonly useMath: UnwrapRef + readonly useMax: UnwrapRef + readonly useMediaControls: UnwrapRef + readonly useMediaQuery: UnwrapRef + readonly useMemoize: UnwrapRef + readonly useMemory: UnwrapRef + readonly useMin: UnwrapRef + readonly useMounted: UnwrapRef + readonly useMouse: UnwrapRef + readonly useMouseInElement: UnwrapRef + readonly useMousePressed: UnwrapRef + readonly useMutationObserver: UnwrapRef + readonly useNavigatorLanguage: UnwrapRef + readonly useNetwork: UnwrapRef + readonly useNow: UnwrapRef + readonly useObjectUrl: UnwrapRef + readonly useOffsetPagination: UnwrapRef + readonly useOnline: UnwrapRef + readonly usePageLeave: UnwrapRef + readonly useParallax: UnwrapRef + readonly useParentElement: UnwrapRef + readonly usePerformanceObserver: UnwrapRef + readonly usePermission: UnwrapRef + readonly usePointer: UnwrapRef + readonly usePointerLock: UnwrapRef + readonly usePointerSwipe: UnwrapRef + readonly usePrecision: UnwrapRef + readonly usePreferredColorScheme: UnwrapRef + readonly usePreferredContrast: UnwrapRef + readonly usePreferredDark: UnwrapRef + readonly usePreferredLanguages: UnwrapRef + readonly usePreferredReducedMotion: UnwrapRef + readonly usePrevious: UnwrapRef + readonly useProjection: UnwrapRef + readonly useRafFn: UnwrapRef + readonly useRefHistory: UnwrapRef + readonly useResize: UnwrapRef + readonly useResizeObserver: UnwrapRef + readonly useRound: UnwrapRef + readonly useScreenOrientation: UnwrapRef + readonly useScreenSafeArea: UnwrapRef + readonly useScriptTag: UnwrapRef + readonly useScroll: UnwrapRef + readonly useScrollLock: UnwrapRef + readonly useSessionStorage: UnwrapRef + readonly useShare: UnwrapRef + readonly useSlots: UnwrapRef + readonly useSorted: UnwrapRef + readonly useSpeechRecognition: UnwrapRef + readonly useSpeechSynthesis: UnwrapRef + readonly useStepper: UnwrapRef + readonly useStorage: UnwrapRef + readonly useStorageAsync: UnwrapRef + readonly useStyleTag: UnwrapRef + readonly useSum: UnwrapRef + readonly useSupported: UnwrapRef + readonly useSwipe: UnwrapRef + readonly useTemplateRefsList: UnwrapRef + readonly useTextDirection: UnwrapRef + readonly useTextSelection: UnwrapRef + readonly useTextareaAutosize: UnwrapRef + readonly useThrottle: UnwrapRef + readonly useThrottleFn: UnwrapRef + readonly useThrottledRefHistory: UnwrapRef + readonly useTimeAgo: UnwrapRef + readonly useTimeout: UnwrapRef + readonly useTimeoutFn: UnwrapRef + readonly useTimeoutPoll: UnwrapRef + readonly useTimestamp: UnwrapRef + readonly useTitle: UnwrapRef + readonly useToNumber: UnwrapRef + readonly useToString: UnwrapRef + readonly useToggle: UnwrapRef + readonly useTransition: UnwrapRef + readonly useTrunc: UnwrapRef + readonly useUrlSearchParams: UnwrapRef + readonly useUserMedia: UnwrapRef + readonly useVModel: UnwrapRef + readonly useVModels: UnwrapRef + readonly useVibrate: UnwrapRef + readonly useVirtualList: UnwrapRef + readonly useWakeLock: UnwrapRef + readonly useWebNotification: UnwrapRef + readonly useWebSocket: UnwrapRef + readonly useWebWorker: UnwrapRef + readonly useWebWorkerFn: UnwrapRef + readonly useWindowFocus: UnwrapRef + readonly useWindowScroll: UnwrapRef + readonly useWindowSize: UnwrapRef + readonly version: UnwrapRef + readonly watch: UnwrapRef + readonly watchArray: UnwrapRef + readonly watchAtMost: UnwrapRef + readonly watchDebounced: UnwrapRef + readonly watchDeep: UnwrapRef + readonly watchEffect: UnwrapRef + readonly watchIgnorable: UnwrapRef + readonly watchImmediate: UnwrapRef + readonly watchOnce: UnwrapRef + readonly watchPausable: UnwrapRef + readonly watchPostEffect: UnwrapRef + readonly watchSyncEffect: UnwrapRef + readonly watchThrottled: UnwrapRef + readonly watchTriggerable: UnwrapRef + readonly watchWithFilter: UnwrapRef + readonly whenever: UnwrapRef + } +} +declare module '@vue/runtime-core' { + interface ComponentCustomProperties { + readonly EffectScope: UnwrapRef + readonly STORAGE_KEY: UnwrapRef + readonly asyncComputed: UnwrapRef + readonly autoResetRef: UnwrapRef + readonly computed: UnwrapRef + readonly computedAsync: UnwrapRef + readonly computedEager: UnwrapRef + readonly computedInject: UnwrapRef + readonly computedWithControl: UnwrapRef + readonly controlledComputed: UnwrapRef + readonly controlledRef: UnwrapRef + readonly createApp: UnwrapRef + readonly createEventHook: UnwrapRef + readonly createGenericProjection: UnwrapRef + readonly createGlobalState: UnwrapRef + readonly createInjectionState: UnwrapRef + readonly createProjection: UnwrapRef + readonly createReactiveFn: UnwrapRef + readonly createReusableTemplate: UnwrapRef + readonly createSharedComposable: UnwrapRef + readonly createTemplatePromise: UnwrapRef + readonly createUnrefFn: UnwrapRef + readonly cssFormatted: UnwrapRef + readonly customCSS: UnwrapRef + readonly customCSSLayerName: UnwrapRef + readonly customConfigError: UnwrapRef + readonly customConfigRaw: UnwrapRef + readonly customRef: UnwrapRef + readonly debouncedRef: UnwrapRef + readonly debouncedWatch: UnwrapRef + readonly defaultCSS: UnwrapRef + readonly defaultConfig: UnwrapRef + readonly defaultConfigRaw: UnwrapRef + readonly defaultHTML: UnwrapRef + readonly defaultOptions: UnwrapRef + readonly defineAsyncComponent: UnwrapRef + readonly defineComponent: UnwrapRef + readonly eagerComputed: UnwrapRef + readonly effectScope: UnwrapRef + readonly extendRef: UnwrapRef + readonly formatCSS: UnwrapRef + readonly formatConfig: UnwrapRef + readonly formatHTML: UnwrapRef + readonly generate: UnwrapRef + readonly getCurrentInstance: UnwrapRef + readonly getCurrentScope: UnwrapRef + readonly getHint: UnwrapRef + readonly getInitialPanelSizes: UnwrapRef + readonly h: UnwrapRef + readonly ignorableWatch: UnwrapRef + readonly init: UnwrapRef + readonly inject: UnwrapRef + readonly inputHTML: UnwrapRef + readonly isCSSPrettify: UnwrapRef + readonly isCollapsed: UnwrapRef + readonly isDark: UnwrapRef + readonly isDefined: UnwrapRef + readonly isProxy: UnwrapRef + readonly isReactive: UnwrapRef + readonly isReadonly: UnwrapRef + readonly isRef: UnwrapRef + readonly load: UnwrapRef + readonly logicAnd: UnwrapRef + readonly logicNot: UnwrapRef + readonly logicOr: UnwrapRef + readonly makeDestructurable: UnwrapRef + readonly markRaw: UnwrapRef + readonly nextTick: UnwrapRef + readonly normalizePanels: UnwrapRef + readonly onActivated: UnwrapRef + readonly onBeforeMount: UnwrapRef + readonly onBeforeUnmount: UnwrapRef + readonly onBeforeUpdate: UnwrapRef + readonly onClickOutside: UnwrapRef + readonly onDeactivated: UnwrapRef + readonly onErrorCaptured: UnwrapRef + readonly onKeyStroke: UnwrapRef + readonly onLongPress: UnwrapRef + readonly onMounted: UnwrapRef + readonly onRenderTracked: UnwrapRef + readonly onRenderTriggered: UnwrapRef + readonly onScopeDispose: UnwrapRef + readonly onServerPrefetch: UnwrapRef + readonly onStartTyping: UnwrapRef + readonly onUnmounted: UnwrapRef + readonly onUpdated: UnwrapRef + readonly options: UnwrapRef + readonly output: UnwrapRef + readonly panelEl: UnwrapRef + readonly panelSizes: UnwrapRef + readonly pausableWatch: UnwrapRef + readonly provide: UnwrapRef + readonly reactify: UnwrapRef + readonly reactifyObject: UnwrapRef + readonly reactive: UnwrapRef + readonly reactiveComputed: UnwrapRef + readonly reactiveOmit: UnwrapRef + readonly reactivePick: UnwrapRef + readonly readonly: UnwrapRef + readonly ref: UnwrapRef + readonly refAutoReset: UnwrapRef + readonly refDebounced: UnwrapRef + readonly refDefault: UnwrapRef + readonly refThrottled: UnwrapRef + readonly refWithControl: UnwrapRef + readonly resolveComponent: UnwrapRef + readonly resolveRef: UnwrapRef + readonly resolveUnref: UnwrapRef + readonly shallowReactive: UnwrapRef + readonly shallowReadonly: UnwrapRef + readonly shallowRef: UnwrapRef + readonly showPreflights: UnwrapRef + readonly syncRef: UnwrapRef + readonly syncRefs: UnwrapRef + readonly templateRef: UnwrapRef + readonly throttledRef: UnwrapRef + readonly throttledWatch: UnwrapRef + readonly titleHeightPercent: UnwrapRef + readonly toRaw: UnwrapRef + readonly toReactive: UnwrapRef + readonly toRef: UnwrapRef + readonly toRefs: UnwrapRef + readonly toValue: UnwrapRef readonly toggleDark: UnwrapRef readonly togglePanel: UnwrapRef readonly transformedHTML: UnwrapRef From 626a9c14074c5abf03aa1b3f72c137f3d30eb73b Mon Sep 17 00:00:00 2001 From: userquin Date: Sat, 27 May 2023 18:16:06 +0200 Subject: [PATCH 2/5] chore: add sprite.svg + minor changes --- examples/vite-vue3/public/sprite.svg | 17 +++++++++++++++++ packages/preset-icons/src/core.ts | 20 ++++++++++---------- packages/preset-icons/src/types.ts | 2 +- packages/vite/src/svg-sprite.ts | 4 ++-- 4 files changed, 30 insertions(+), 13 deletions(-) create mode 100644 examples/vite-vue3/public/sprite.svg diff --git a/examples/vite-vue3/public/sprite.svg b/examples/vite-vue3/public/sprite.svg new file mode 100644 index 0000000000..7ccb912509 --- /dev/null +++ b/examples/vite-vue3/public/sprite.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/preset-icons/src/core.ts b/packages/preset-icons/src/core.ts index 5c39c4571f..8849e2379c 100644 --- a/packages/preset-icons/src/core.ts +++ b/packages/preset-icons/src/core.ts @@ -119,6 +119,16 @@ export function createPresetIcons(lookupIconLoader: (options: IconsOptions) => P } } +export function combineLoaders(loaders: UniversalIconLoader[]) { + return (async (...args) => { + for (const loader of loaders) { + const result = await loader(...args) + if (result) + return result + } + }) +} + function createDynamicMatcher( warn: boolean, mode: string, @@ -201,13 +211,3 @@ function createDynamicMatcher( } }) } - -export function combineLoaders(loaders: UniversalIconLoader[]) { - return (async (...args) => { - for (const loader of loaders) { - const result = await loader(...args) - if (result) - return result - } - }) -} diff --git a/packages/preset-icons/src/types.ts b/packages/preset-icons/src/types.ts index c40f4bb0f6..a36ad50576 100644 --- a/packages/preset-icons/src/types.ts +++ b/packages/preset-icons/src/types.ts @@ -9,7 +9,7 @@ export interface CSSSVGSprites { * - `mask` - use background color and the `mask` property for monochrome icons * - `background-img` - use background image for the icons, colors are static * - `auto` - smartly decide mode between `mask` and `background-img` per icon based on its style - * - if omitted it will be used + * - if omitted, icons mode will be used * * @default 'auto' * @see https://antfu.me/posts/icons-in-pure-css diff --git a/packages/vite/src/svg-sprite.ts b/packages/vite/src/svg-sprite.ts index 74cc8871c9..b55691db87 100644 --- a/packages/vite/src/svg-sprite.ts +++ b/packages/vite/src/svg-sprite.ts @@ -68,7 +68,7 @@ export function SVGSpritePlugin(ctx: UnocssPluginContext): Plugin { } } -// TODO: move this to utils package in @iconify/iconify +// TODO: move this to @iconify/utils repo function parseSVGData(data: string) { const match = data.match(/]+viewBox="([^"]+)"[^>]*>([\s\S]+)<\/svg>/) if (!match) @@ -79,7 +79,7 @@ function parseSVGData(data: string) { return { rawViewBox: match[1], content: path, x, y, width, height } } -// TODO: move this to utils package in @iconify/iconify +// TODO: move this to @iconify/utils repo function createCSSSVGSprite(icons: Record) { return `${ Object.entries(icons).reduce((acc, [name, icon]) => { From 2857487e52f61d562b2e889da257da4ec1afee6b Mon Sep 17 00:00:00 2001 From: userquin Date: Sat, 27 May 2023 18:32:16 +0200 Subject: [PATCH 3/5] chore: add icon animation for testing purposes --- examples/vite-vue3/src/App.vue | 3 +++ examples/vite-vue3/vite.config.ts | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/examples/vite-vue3/src/App.vue b/examples/vite-vue3/src/App.vue index e5ca4a8e97..099d63052c 100644 --- a/examples/vite-vue3/src/App.vue +++ b/examples/vite-vue3/src/App.vue @@ -23,16 +23,19 @@
+
Custom SVG Sprite background image
+
Custom SVG Sprite (attributify)
+
diff --git a/examples/vite-vue3/vite.config.ts b/examples/vite-vue3/vite.config.ts index aa61c1bd74..0eb8f31f6a 100644 --- a/examples/vite-vue3/vite.config.ts +++ b/examples/vite-vue3/vite.config.ts @@ -34,6 +34,20 @@ export default defineConfig({ loader: (name) => { if (name === 'custom') { return { + 'animated': ``, 'close': '', 'chevron-down': '', 'chevron-up': '', From 22d141423675365a52be67e85e95db9661376342 Mon Sep 17 00:00:00 2001 From: userquin Date: Sun, 6 Aug 2023 19:44:28 +0200 Subject: [PATCH 4/5] chore: update logic + include build --- examples/vite-vue3/src/App.vue | 9 + examples/vite-vue3/vite.config.ts | 101 +++++++++-- packages/preset-icons/src/core.ts | 175 +++++++++++++------ packages/preset-icons/src/create-sprite.ts | 177 +++++++++++++++++++ packages/preset-icons/src/index.ts | 3 + packages/preset-icons/src/types.ts | 31 +++- packages/vite/src/svg-sprite.ts | 191 +++++++++++---------- 7 files changed, 528 insertions(+), 159 deletions(-) create mode 100644 packages/preset-icons/src/create-sprite.ts diff --git a/examples/vite-vue3/src/App.vue b/examples/vite-vue3/src/App.vue index 099d63052c..dc7ce38df8 100644 --- a/examples/vite-vue3/src/App.vue +++ b/examples/vite-vue3/src/App.vue @@ -31,6 +31,15 @@
+
Custom MDI SVG Sprite
+