@pikacss/plugin-icons 0.0.46 → 0.0.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # @pikacss/plugin-icons
2
+
3
+ Iconify icons plugin for PikaCSS. Renders icons from Iconify collections as CSS mask-image or background-image.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add -D @pikacss/plugin-icons
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { defineEngineConfig } from '@pikacss/core'
15
+ import { icons } from '@pikacss/plugin-icons'
16
+
17
+ export default defineEngineConfig({
18
+ plugins: [icons()],
19
+ icons: {
20
+ autoInstall: true,
21
+ },
22
+ })
23
+ ```
24
+
25
+ Then use in templates:
26
+
27
+ ```vue
28
+ <div :class="pika('i-mdi:home')" />
29
+ ```
30
+
31
+ ## Documentation
32
+
33
+ See the [full documentation](https://pikacss.com/guide/plugins/icons).
34
+
35
+ ## License
36
+
37
+ MIT
package/dist/index.d.mts CHANGED
@@ -1,28 +1,163 @@
1
- import { EnginePlugin, Simplify, StyleItem } from "@pikacss/core";
2
- import { IconsOptions } from "@unocss/preset-icons";
1
+ import { CustomCollections, IconCustomizations, IconifyLoaderOptions } from "@iconify/utils";
2
+ import { EnginePlugin, StyleItem } from "@pikacss/core";
3
3
 
4
4
  //#region src/index.d.ts
5
5
  interface IconMeta {
6
6
  collection: string;
7
7
  name: string;
8
8
  svg: string;
9
+ source: IconSource;
9
10
  mode?: IconsConfig['mode'];
10
11
  }
11
- type IconsConfig = Simplify<Omit<IconsOptions, 'warn' | 'layer' | 'processor' | 'customFetcher'> & {
12
+ type IconSource = 'custom' | 'local' | 'cdn';
13
+ /**
14
+ * Configuration options for the PikaCSS icons plugin.
15
+ *
16
+ * @remarks Controls how icon utilities are resolved, loaded, and rendered as CSS.
17
+ * Icons are loaded from custom collections first, then from locally installed
18
+ * Iconify packages, and finally from a CDN if configured.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * import { defineEngineConfig } from '@pikacss/core'
23
+ * import { icons } from '@pikacss/plugin-icons'
24
+ *
25
+ * export default defineEngineConfig({
26
+ * plugins: [icons()],
27
+ * icons: {
28
+ * prefix: 'i-',
29
+ * mode: 'auto',
30
+ * scale: 1,
31
+ * cdn: 'https://esm.sh/@iconify-json/{collection}/icons.json',
32
+ * },
33
+ * })
34
+ * ```
35
+ */
36
+ interface IconsConfig {
12
37
  /**
13
- * Processor for the CSS object before stringify
38
+ * One or more prefixes used to match icon utility names. When a utility
39
+ * matches `<prefix><collection>:<name>`, it resolves to an icon style.
40
+ *
41
+ * @default `'i-'`
42
+ */
43
+ prefix?: string | string[];
44
+ /**
45
+ * Rendering strategy for icon SVGs. `'mask'` uses a CSS mask with
46
+ * `currentColor` as the fill, allowing color inheritance. `'bg'` renders
47
+ * the SVG as a background image with its original colors. `'auto'`
48
+ * chooses `'mask'` when the SVG contains `currentColor`, otherwise `'bg'`.
49
+ *
50
+ * @default `'auto'`
51
+ */
52
+ mode?: 'auto' | 'mask' | 'bg';
53
+ /**
54
+ * Multiplier applied to the icon's intrinsic width and height.
55
+ * Combined with `unit` to produce the final CSS dimensions.
56
+ *
57
+ * @default `1`
58
+ */
59
+ scale?: number;
60
+ /**
61
+ * Custom icon collections keyed by collection name. Each entry maps
62
+ * icon names to SVG strings or async loaders, checked before local
63
+ * packages and the CDN.
64
+ *
65
+ * @default `undefined`
66
+ */
67
+ collections?: CustomCollections;
68
+ /**
69
+ * Iconify customization hooks applied when loading icons. Allows
70
+ * transforming SVG attributes, trimming whitespace, and running
71
+ * per-icon logic via `iconCustomizer`.
72
+ *
73
+ * @default `{}`
74
+ */
75
+ customizations?: IconCustomizations;
76
+ /**
77
+ * When enabled, automatically installs missing `@iconify-json/*`
78
+ * packages on demand during local icon resolution.
79
+ *
80
+ * @default `false`
81
+ */
82
+ autoInstall?: IconifyLoaderOptions['autoInstall'];
83
+ /**
84
+ * Working directory used by the Iconify node loader when resolving
85
+ * locally installed icon packages.
86
+ *
87
+ * @default `undefined`
88
+ */
89
+ cwd?: IconifyLoaderOptions['cwd'];
90
+ /**
91
+ * CDN URL template for fetching remote icon sets. Use `{collection}`
92
+ * as a placeholder for the collection name, or provide a base URL
93
+ * and the collection name will be appended as `<url>/<collection>.json`.
94
+ *
95
+ * @default `undefined`
96
+ */
97
+ cdn?: string;
98
+ /**
99
+ * CSS unit appended to the icon's width and height (e.g. `'em'`, `'rem'`).
100
+ * When set, produces explicit dimension values like `1em` based on `scale`.
101
+ * When omitted, dimensions are left to the SVG's intrinsic size.
102
+ *
103
+ * @default `undefined`
104
+ */
105
+ unit?: string;
106
+ /**
107
+ * Additional CSS properties merged into every generated icon style item.
108
+ * Useful for adding `display`, `vertical-align`, or other layout properties.
109
+ *
110
+ * @default `{}`
111
+ */
112
+ extraProperties?: Record<string, string>;
113
+ /**
114
+ * Post-processing callback invoked on each generated icon style item before
115
+ * it is returned. Receives the mutable style item and resolved icon metadata,
116
+ * allowing custom property injection or conditional transformations.
117
+ *
118
+ * @default `undefined`
14
119
  */
15
120
  processor?: (styleItem: StyleItem, meta: Required<IconMeta>) => void;
16
121
  /**
17
- * Specify the icons for auto-completion.
122
+ * Explicit list of icon identifiers (e.g. `'mdi:home'`) to include in
123
+ * editor autocomplete suggestions. Each entry is combined with every
124
+ * configured prefix.
125
+ *
126
+ * @default `undefined`
18
127
  */
19
128
  autocomplete?: string[];
20
- }>;
129
+ }
21
130
  declare module '@pikacss/core' {
22
131
  interface EngineConfig {
132
+ /**
133
+ * Configuration for the icons plugin. Requires the `icons()` plugin
134
+ * to be registered in `plugins` for this configuration to take effect.
135
+ *
136
+ * @default `undefined`
137
+ */
23
138
  icons?: IconsConfig;
24
139
  }
25
140
  }
141
+ /**
142
+ * Creates the PikaCSS icons engine plugin.
143
+ *
144
+ * @returns An engine plugin that registers icon shortcut rules and autocomplete entries.
145
+ *
146
+ * @remarks Resolves icon SVGs from custom collections, locally installed
147
+ * `@iconify-json/*` packages, or a remote CDN. Each matched utility is
148
+ * expanded into a CSS style item using either mask or background rendering.
149
+ * Configure behavior through the `icons` key in your engine config.
150
+ *
151
+ * @example
152
+ * ```ts
153
+ * import { icons } from '@pikacss/plugin-icons'
154
+ *
155
+ * export default defineEngineConfig({
156
+ * plugins: [icons()],
157
+ * icons: { prefix: 'i-', mode: 'auto' },
158
+ * })
159
+ * ```
160
+ */
26
161
  declare function icons(): EnginePlugin;
27
162
  //#endregion
28
163
  export { IconsConfig, icons };
package/dist/index.mjs CHANGED
@@ -1,18 +1,11 @@
1
1
  import process from "node:process";
2
- import { encodeSvgForCss, loadIcon } from "@iconify/utils";
2
+ import { encodeSvgForCss, loadIcon, quicklyValidateIconSet, searchForIcon, stringToIcon } from "@iconify/utils";
3
+ import { loadNodeIcon } from "@iconify/utils/lib/loader/node-loader";
3
4
  import { defineEnginePlugin, log } from "@pikacss/core";
4
- import { combineLoaders, createCDNFetchLoader, createNodeLoader, parseIconWithLoader } from "@unocss/preset-icons";
5
5
  import { $fetch } from "ofetch";
6
-
7
6
  //#region src/index.ts
8
7
  /**
9
8
  * Environment flags helper function to detect the current runtime environment.
10
- * This replaces the removed `getEnvFlags` export from `@unocss/preset-icons` v66+.
11
- *
12
- * @returns An object containing:
13
- * - `isNode`: Whether the code is running in a Node.js environment
14
- * - `isVSCode`: Whether the code is running within VS Code (extension host)
15
- * - `isESLint`: Whether the code is running within ESLint
16
9
  */
17
10
  function getEnvFlags() {
18
11
  const isNode = typeof process !== "undefined" && typeof process.versions?.node !== "undefined";
@@ -22,75 +15,183 @@ function getEnvFlags() {
22
15
  isESLint: isNode && !!process.env.ESLINT
23
16
  };
24
17
  }
18
+ const RE_ESCAPE_REGEXP = /[|\\{}()[\]^$+*?.-]/g;
19
+ const RE_CAMEL_CASE_ICON_BOUNDARY = /([a-z])([A-Z])/g;
20
+ const RE_DIGIT_ICON_BOUNDARY = /([a-z])(\d+)/g;
21
+ const RE_TRAILING_SLASH = /\/$/;
22
+ /**
23
+ * Creates the PikaCSS icons engine plugin.
24
+ *
25
+ * @returns An engine plugin that registers icon shortcut rules and autocomplete entries.
26
+ *
27
+ * @remarks Resolves icon SVGs from custom collections, locally installed
28
+ * `@iconify-json/*` packages, or a remote CDN. Each matched utility is
29
+ * expanded into a CSS style item using either mask or background rendering.
30
+ * Configure behavior through the `icons` key in your engine config.
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * import { icons } from '@pikacss/plugin-icons'
35
+ *
36
+ * export default defineEngineConfig({
37
+ * plugins: [icons()],
38
+ * icons: { prefix: 'i-', mode: 'auto' },
39
+ * })
40
+ * ```
41
+ */
25
42
  function icons() {
26
- return createIconsPlugin(createIconsLoader);
43
+ return createIconsPlugin();
44
+ }
45
+ const globalColonRE = /:/g;
46
+ const currentColorRE = /currentColor/;
47
+ function normalizePrefixes(prefix) {
48
+ const prefixes = [prefix].flat().filter(Boolean);
49
+ return [...new Set(prefixes)];
50
+ }
51
+ function escapeRegExp(value) {
52
+ return value.replace(RE_ESCAPE_REGEXP, "\\$&");
53
+ }
54
+ function createShortcutRegExp(prefixes) {
55
+ return new RegExp(`^(?:${prefixes.map(escapeRegExp).join("|")})([\\w:-]+)(?:\\?(mask|bg|auto))?$`);
56
+ }
57
+ function getPossibleIconNames(iconName) {
58
+ return [
59
+ iconName,
60
+ iconName.replace(RE_CAMEL_CASE_ICON_BOUNDARY, "$1-$2").toLowerCase(),
61
+ iconName.replace(RE_DIGIT_ICON_BOUNDARY, "$1-$2")
62
+ ];
27
63
  }
28
- function createCDNLoader(cdnBase) {
29
- return createCDNFetchLoader($fetch, cdnBase);
64
+ function createAutocomplete(prefixes, autocomplete = []) {
65
+ const prefixRE = new RegExp(`^(?:${prefixes.map(escapeRegExp).join("|")})`);
66
+ return [...prefixes, ...prefixes.flatMap((prefix) => autocomplete.map((icon) => `${prefix}${icon.replace(prefixRE, "")}`))];
30
67
  }
31
- async function createIconsLoader(config) {
32
- const { cdn } = config;
33
- const loaders = [];
34
- const { isNode, isVSCode, isESLint } = getEnvFlags();
35
- if (isNode && !isVSCode && !isESLint) {
36
- const nodeLoader = await createNodeLoader();
37
- if (nodeLoader != null) loaders.push(nodeLoader);
68
+ function createAutocompletePatterns(prefixes) {
69
+ return prefixes.flatMap((prefix) => [
70
+ `\`${prefix}\${string}:\${string}\``,
71
+ `\`${prefix}\${string}:\${string}?mask\``,
72
+ `\`${prefix}\${string}:\${string}?bg\``,
73
+ `\`${prefix}\${string}:\${string}?auto\``
74
+ ]);
75
+ }
76
+ function resolveCdnCollectionUrl(cdn, collection) {
77
+ if (cdn.includes("{collection}")) return cdn.replaceAll("{collection}", collection);
78
+ return `${cdn.replace(RE_TRAILING_SLASH, "")}/${collection}.json`;
79
+ }
80
+ function createLoaderOptions(config, usedProps) {
81
+ const { scale = 1, collections, autoInstall = false, cwd, unit, extraProperties = {}, customizations = {} } = config;
82
+ const iconCustomizer = customizations.iconCustomizer;
83
+ return {
84
+ addXmlNs: true,
85
+ scale,
86
+ customCollections: collections,
87
+ autoInstall,
88
+ cwd,
89
+ usedProps,
90
+ customizations: {
91
+ ...customizations,
92
+ additionalProps: {
93
+ ...customizations.additionalProps,
94
+ ...extraProperties
95
+ },
96
+ trimCustomSvg: customizations.trimCustomSvg ?? true,
97
+ async iconCustomizer(collection, icon, props) {
98
+ await iconCustomizer?.(collection, icon, props);
99
+ if (!unit) return;
100
+ if (!props.width) props.width = `${scale}${unit}`;
101
+ if (!props.height) props.height = `${scale}${unit}`;
102
+ }
103
+ }
104
+ };
105
+ }
106
+ async function loadCollectionFromCdn(cdn, collection, cache) {
107
+ if (!cache.has(collection)) cache.set(collection, (async () => {
108
+ try {
109
+ return quicklyValidateIconSet(await $fetch(resolveCdnCollectionUrl(cdn, collection))) ?? void 0;
110
+ } catch {
111
+ return;
112
+ }
113
+ })());
114
+ return cache.get(collection);
115
+ }
116
+ async function resolveIcon(body, config, flags, cdnCollectionCache) {
117
+ const parsed = stringToIcon(body, true);
118
+ if (parsed == null || !parsed.prefix) return null;
119
+ const customProps = {};
120
+ const customSvg = await loadIcon(parsed.prefix, parsed.name, createLoaderOptions(config, customProps));
121
+ if (customSvg != null) return {
122
+ collection: parsed.prefix,
123
+ name: parsed.name,
124
+ svg: customSvg,
125
+ usedProps: customProps,
126
+ source: "custom"
127
+ };
128
+ if (flags.isNode && !flags.isVSCode && !flags.isESLint) {
129
+ const localProps = {};
130
+ const localSvg = await loadNodeIcon(parsed.prefix, parsed.name, {
131
+ ...createLoaderOptions(config, localProps),
132
+ customCollections: void 0
133
+ });
134
+ if (localSvg != null) return {
135
+ collection: parsed.prefix,
136
+ name: parsed.name,
137
+ svg: localSvg,
138
+ usedProps: localProps,
139
+ source: "local"
140
+ };
141
+ }
142
+ if (config.cdn) {
143
+ const iconSet = await loadCollectionFromCdn(config.cdn, parsed.prefix, cdnCollectionCache);
144
+ if (iconSet != null) {
145
+ const remoteProps = {};
146
+ const remoteSvg = await searchForIcon(iconSet, parsed.prefix, getPossibleIconNames(parsed.name), createLoaderOptions(config, remoteProps));
147
+ if (remoteSvg != null) return {
148
+ collection: parsed.prefix,
149
+ name: parsed.name,
150
+ svg: remoteSvg,
151
+ usedProps: remoteProps,
152
+ source: "cdn"
153
+ };
154
+ }
38
155
  }
39
- if (cdn) loaders.push(createCDNLoader(cdn));
40
- loaders.push(loadIcon);
41
- return combineLoaders(loaders);
156
+ return {
157
+ collection: parsed.prefix,
158
+ name: parsed.name,
159
+ svg: null,
160
+ usedProps: {},
161
+ source: null
162
+ };
42
163
  }
43
- const globalColonRE = /:/g;
44
- function createIconsPlugin(lookupIconLoader) {
164
+ function createIconsPlugin() {
45
165
  let engine;
46
- let iconsConfig;
166
+ let iconsConfig = {};
167
+ const flags = getEnvFlags();
168
+ const cdnCollectionCache = /* @__PURE__ */ new Map();
47
169
  return defineEnginePlugin({
48
170
  name: "icons",
49
171
  configureRawConfig: async (config) => {
50
- iconsConfig = config.icons || {};
172
+ iconsConfig = config.icons ?? {};
51
173
  },
52
174
  configureEngine: async (_engine) => {
53
175
  engine = _engine;
54
- const { scale = 1, mode = "auto", prefix = "i-", iconifyCollectionsNames, collections: customCollections, customizations = {}, autoInstall = false, collectionsNodeResolvePath, unit, extraProperties = {}, processor, autocomplete: _autocomplete } = iconsConfig;
55
- const loaderOptions = {
56
- addXmlNs: true,
57
- scale,
58
- customCollections,
59
- autoInstall,
60
- cwd: collectionsNodeResolvePath,
61
- warn: void 0,
62
- customizations: {
63
- ...customizations,
64
- additionalProps: { ...extraProperties },
65
- trimCustomSvg: true,
66
- async iconCustomizer(collection, icon, props) {
67
- await customizations.iconCustomizer?.(collection, icon, props);
68
- if (unit) {
69
- if (!props.width) props.width = `${scale}${unit}`;
70
- if (!props.height) props.height = `${scale}${unit}`;
71
- }
72
- }
73
- }
74
- };
75
- const prefixRE = new RegExp(`^(${[prefix].flat().join("|")})`);
76
- const autocompletePrefix = [prefix].flat();
77
- const autocomplete = [...autocompletePrefix, ...autocompletePrefix.flatMap((p) => _autocomplete?.map((a) => `${p}${a.replace(prefixRE, "")}`) || [])];
78
- let iconLoader;
176
+ const { mode = "auto", prefix = "i-", processor, autocomplete: _autocomplete } = iconsConfig;
177
+ const prefixes = normalizePrefixes(prefix);
178
+ const autocomplete = createAutocomplete(prefixes, _autocomplete);
179
+ const autocompletePatterns = createAutocompletePatterns(prefixes);
180
+ engine.appendAutocomplete({ patterns: { shortcuts: autocompletePatterns } });
79
181
  engine.shortcuts.add({
80
- shortcut: new RegExp(`^(?:${[prefix].flat().join("|")})([\\w:-]+)(?:\\?(mask|bg|auto))?$`),
182
+ shortcut: createShortcutRegExp(prefixes),
81
183
  value: async (match) => {
82
184
  let [full, body, _mode = mode] = match;
83
- iconLoader = iconLoader || await lookupIconLoader(iconsConfig);
84
- const usedProps = {};
85
- const parsed = await parseIconWithLoader(body, iconLoader, {
86
- ...loaderOptions,
87
- usedProps
88
- }, iconifyCollectionsNames);
89
- if (parsed == null) {
185
+ const resolved = await resolveIcon(body, iconsConfig, flags, cdnCollectionCache);
186
+ if (resolved == null) {
187
+ log.warn(`invalid icon name "${full}"`);
188
+ return {};
189
+ }
190
+ if (resolved.svg == null) {
90
191
  log.warn(`failed to load icon "${full}"`);
91
192
  return {};
92
193
  }
93
- const url = `url("data:image/svg+xml;utf8,${encodeSvgForCss(parsed.svg)}")`;
194
+ const url = `url("data:image/svg+xml;utf8,${encodeSvgForCss(resolved.svg)}")`;
94
195
  const varName = `--${engine.config.prefix}svg-icon-${body.replace(globalColonRE, "-")}`;
95
196
  if (engine.variables.store.has(varName) === false) engine.variables.add({ [varName]: {
96
197
  value: url,
@@ -100,7 +201,7 @@ function createIconsPlugin(lookupIconLoader) {
100
201
  },
101
202
  pruneUnused: true
102
203
  } });
103
- if (_mode === "auto") _mode = parsed.svg.includes("currentColor") ? "mask" : "bg";
204
+ if (_mode === "auto") _mode = currentColorRE.test(resolved.svg) ? "mask" : "bg";
104
205
  let styleItem;
105
206
  if (_mode === "mask") styleItem = {
106
207
  "--svg-icon": `var(${varName})`,
@@ -110,17 +211,20 @@ function createIconsPlugin(lookupIconLoader) {
110
211
  "mask-size": "100% 100%",
111
212
  "background-color": "currentColor",
112
213
  "color": "inherit",
113
- ...usedProps
214
+ ...resolved.usedProps
114
215
  };
115
216
  else styleItem = {
116
217
  "--svg-icon": `var(${varName})`,
117
218
  "background": "var(--svg-icon) no-repeat",
118
219
  "background-size": "100% 100%",
119
220
  "background-color": "transparent",
120
- ...usedProps
221
+ ...resolved.usedProps
121
222
  };
122
223
  processor?.(styleItem, {
123
- ...parsed,
224
+ collection: resolved.collection,
225
+ name: resolved.name,
226
+ svg: resolved.svg,
227
+ source: resolved.source,
124
228
  mode: _mode
125
229
  });
126
230
  return styleItem;
@@ -130,6 +234,5 @@ function createIconsPlugin(lookupIconLoader) {
130
234
  }
131
235
  });
132
236
  }
133
-
134
237
  //#endregion
135
- export { icons };
238
+ export { icons };
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@pikacss/plugin-icons",
3
3
  "type": "module",
4
- "version": "0.0.46",
4
+ "version": "0.0.48",
5
5
  "author": "DevilTea <ch19980814@gmail.com>",
6
6
  "license": "MIT",
7
+ "homepage": "https://pikacss.com",
7
8
  "repository": {
8
9
  "type": "git",
9
- "url": "https://github.com/pikacss/pikacss.git",
10
+ "url": "git+https://github.com/pikacss/pikacss.git",
10
11
  "directory": "packages/plugin-icons"
11
12
  },
12
13
  "bugs": {
@@ -20,6 +21,7 @@
20
21
  "pikacss-plugin",
21
22
  "icons"
22
23
  ],
24
+ "sideEffects": false,
23
25
  "exports": {
24
26
  ".": {
25
27
  "import": {
@@ -36,24 +38,26 @@
36
38
  "files": [
37
39
  "dist"
38
40
  ],
41
+ "engines": {
42
+ "node": ">=22"
43
+ },
39
44
  "peerDependencies": {
40
- "@pikacss/core": "0.0.46"
45
+ "@pikacss/core": "0.0.48"
41
46
  },
42
47
  "dependencies": {
43
48
  "@iconify/utils": "^3.1.0",
44
- "@unocss/preset-icons": "^66.6.1",
45
49
  "ofetch": "^1.5.1"
46
50
  },
47
51
  "devDependencies": {
48
- "@pikacss/core": "0.0.46"
52
+ "@pikacss/core": "0.0.48"
49
53
  },
50
54
  "scripts": {
51
- "build": "tsdown && pnpm exec publint",
55
+ "build": "tsdown",
52
56
  "build:watch": "tsdown --watch",
53
57
  "typecheck": "pnpm typecheck:package && pnpm typecheck:test",
54
58
  "typecheck:package": "tsc --project ./tsconfig.package.json --noEmit",
55
59
  "typecheck:test": "tsc --project ./tsconfig.tests.json --noEmit",
56
- "test": "vitest run",
57
- "test:watch": "vitest"
60
+ "test": "vitest run --config ./vitest.config.ts",
61
+ "test:watch": "vitest --config ./vitest.config.ts"
58
62
  }
59
63
  }