unocss-iconify-helper 1.0.0
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 +134 -0
- package/dist/client.min.js +162 -0
- package/dist/index-DGl8JXfa.d.cts +52 -0
- package/dist/index-DGl8JXfa.d.ts +52 -0
- package/dist/index.cjs +321 -0
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +313 -0
- package/dist/unocss/index.cjs +125 -0
- package/dist/unocss/index.d.cts +31 -0
- package/dist/unocss/index.d.ts +31 -0
- package/dist/unocss/index.js +119 -0
- package/dist/vite/index.cjs +205 -0
- package/dist/vite/index.d.cts +2 -0
- package/dist/vite/index.d.ts +2 -0
- package/dist/vite/index.js +201 -0
- package/package.json +73 -0
package/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# unocss-iconify-helper
|
|
2
|
+
|
|
3
|
+
Dev-only Iconify icon picker for UnoCSS. Framework-agnostic — works with Vue, React, Svelte, Vanilla, and any other Vite-based project.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Pure JS popup** — no framework dependency, works in any Vite + UnoCSS project
|
|
8
|
+
- **Hotkey trigger** — default `Ctrl+I`, fully configurable
|
|
9
|
+
- Fixed modal layout: 80% width, 10% from top, max 80vh height
|
|
10
|
+
- Auto-scans project source files to detect used Iconify collections
|
|
11
|
+
- Collection filter: used / selected / all
|
|
12
|
+
- Collection badge on each icon (top-right corner)
|
|
13
|
+
- Click an icon to copy its UnoCSS class (e.g. `i-mdi-home`)
|
|
14
|
+
- **Auto-install**: first time an icon collection is used, the package is installed automatically
|
|
15
|
+
- HMR support: collection list updates without restarting dev server
|
|
16
|
+
- Chinese / English UI switcher (persisted in `localStorage`)
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pnpm add -D unocss-iconify-helper
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
### 1. Vite Plugin
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
// vite.config.ts
|
|
30
|
+
import { defineConfig } from 'vite'
|
|
31
|
+
import UnoCSS from 'unocss/vite'
|
|
32
|
+
import { iconifyHelperPlugin } from 'unocss-iconify-helper/vite'
|
|
33
|
+
|
|
34
|
+
export default defineConfig({
|
|
35
|
+
plugins: [
|
|
36
|
+
UnoCSS(),
|
|
37
|
+
iconifyHelperPlugin(), // dev-only, auto-disabled in production
|
|
38
|
+
],
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Available options:
|
|
43
|
+
|
|
44
|
+
| Option | Type | Default | Description |
|
|
45
|
+
|--------|------|---------|-------------|
|
|
46
|
+
| `hotkey` | `string` | `'ctrl+i'` | Trigger hotkey, e.g. `'meta+shift+i'` |
|
|
47
|
+
| `extraCollections` | `Record<string, string>` | `{}` | Extra collections to always include in the picker |
|
|
48
|
+
| `scanDirs` | `string[]` | `['src']` | Directories to scan for used icon classes |
|
|
49
|
+
| `scanExts` | `string[]` | `['.vue','.ts',...]` | File extensions to scan |
|
|
50
|
+
|
|
51
|
+
### 2. UnoCSS Preset
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
// uno.config.ts
|
|
55
|
+
import { defineConfig, presetUno } from 'unocss'
|
|
56
|
+
import { presetIconifyHelper } from 'unocss-iconify-helper/unocss'
|
|
57
|
+
|
|
58
|
+
export default defineConfig({
|
|
59
|
+
presets: [
|
|
60
|
+
presetUno(),
|
|
61
|
+
presetIconifyHelper(),
|
|
62
|
+
],
|
|
63
|
+
})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`presetIconifyHelper` accepts all [`@unocss/preset-icons`](https://unocss.dev/presets/icons) options plus auto-collection loading:
|
|
67
|
+
|
|
68
|
+
| Option | Type | Default | Description |
|
|
69
|
+
|--------|------|---------|-------------|
|
|
70
|
+
| `autoInstall` | `boolean \| fn` | `true` | Auto-install missing `@iconify-json/*` packages |
|
|
71
|
+
| `scale` | `number` | `1.2` | Icon scale factor |
|
|
72
|
+
| `extraProperties` | `Record<string, string>` | `{display:'inline-block',...}` | Extra CSS properties added to every icon |
|
|
73
|
+
| `collections` | `Record<string, ...>` | auto-detected | Override / extend auto-detected collections |
|
|
74
|
+
| _...all other `presetIcons` options_ | | | Passed through directly |
|
|
75
|
+
|
|
76
|
+
You do **not** need to install `@iconify-json/*` packages manually beforehand. On first use, the preset auto-installs them and adds them to your `package.json`.
|
|
77
|
+
|
|
78
|
+
## How It Works
|
|
79
|
+
|
|
80
|
+
### Icon picker (Vite plugin)
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
Vite dev server
|
|
84
|
+
├── configureServer → GET /__iph_client
|
|
85
|
+
│ window.__IHP_CONFIG__ = { hotkey, collections } ← runtime config
|
|
86
|
+
│ + dist/client.min.js ← pure-JS picker UI (IIFE)
|
|
87
|
+
│
|
|
88
|
+
├── transformIndexHtml → <script src="/__iph_client" defer>
|
|
89
|
+
│
|
|
90
|
+
└── handleHotUpdate → server.ws.send('iph:collections', {...})
|
|
91
|
+
↑ client reconnects via WebSocket and updates filter list
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The client script (`src/client/index.ts`) is bundled by esbuild into a self-contained IIFE with no external dependencies. It creates the picker overlay, calls the [Iconify API](https://api.iconify.design) for search, and writes the selected class to the clipboard.
|
|
95
|
+
|
|
96
|
+
### Auto-install preset (`presetIconifyHelper`)
|
|
97
|
+
|
|
98
|
+
Standard `presetIcons` relies on `mlly` to resolve `@iconify-json/*` packages, which **fails silently on Windows + pnpm workspaces** because `mlly` receives a raw Windows path instead of a `file://` URL.
|
|
99
|
+
|
|
100
|
+
`presetIconifyHelper` solves this with two layers:
|
|
101
|
+
|
|
102
|
+
**Layer 1 — pre-scan** (fast path, zero network):
|
|
103
|
+
At startup, it scans `node_modules/@iconify-json/` with Node's `createRequire`, builds a `collections` map directly from the installed packages, and passes it to `presetIcons`. No `mlly` involved.
|
|
104
|
+
|
|
105
|
+
**Layer 2 — dynamic Proxy** (auto-install path):
|
|
106
|
+
The `collections` map is wrapped in a JavaScript `Proxy`. When an unknown icon prefix is encountered (e.g. `phosphor` from `i-phosphor-star`):
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
Proxy.get('phosphor')
|
|
110
|
+
↓
|
|
111
|
+
createRequire('@iconify-json/phosphor') → not found
|
|
112
|
+
↓
|
|
113
|
+
detect package manager (traverse dirs for pnpm-lock.yaml / yarn.lock / bun.lockb)
|
|
114
|
+
↓
|
|
115
|
+
execSync('pnpm add -D @iconify-json/phosphor', { cwd, shell })
|
|
116
|
+
↓
|
|
117
|
+
createRequire('@iconify-json/phosphor') → OK → return IconifyJSON
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Concurrent requests for the same prefix all await the same `Promise`, so the package is installed exactly once. After installation, pnpm writes the package to `package.json`, so subsequent dev server restarts load it from Layer 1.
|
|
121
|
+
|
|
122
|
+
As a fallback, `collectionsNodeResolvePath` is set to a proper `file://` URL so that `mlly`-based resolution also works where available.
|
|
123
|
+
|
|
124
|
+
## Limitations
|
|
125
|
+
|
|
126
|
+
- **Dev-only**: the Vite plugin (`iconifyHelperPlugin`) is automatically disabled in production builds. The UnoCSS preset (`presetIconifyHelper`) is build-time safe.
|
|
127
|
+
- **Node.js environment required**: auto-install uses `child_process.execSync`. Browser / SSR builds without Node access should pass `autoInstall: false`.
|
|
128
|
+
- **First-use latency**: auto-installing a new icon collection blocks the event loop for a few seconds (the install command is synchronous). Subsequent builds are instant.
|
|
129
|
+
- **pnpm workspace root detection**: `detectPm` traverses parent directories to find lock files. It works for workspaces where the root contains `pnpm-lock.yaml`.
|
|
130
|
+
- The icon picker UI calls `https://api.iconify.design` for search — an internet connection is required during development.
|
|
131
|
+
|
|
132
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";(()=>{var H="https://api.iconify.design",I="iph-lang",O={zh:{title:"Iconify \u56FE\u6807\u9009\u62E9\u5668",colsLabel:"\u56FE\u6807\u5E93\u8303\u56F4",all:"\u5168\u90E8",add:"+ \u6DFB\u52A0",removeTitle:"\u70B9\u51FB\u79FB\u9664",resetToProj:"\u21BA \u9879\u76EE\u5DF2\u7528",panelTitle:"\u9009\u62E9\u56FE\u6807\u5E93",projUsedHint:"\u25CF = \u9879\u76EE\u5DF2\u7528",resetBtn:"\u91CD\u7F6E\u4E3A\u9879\u76EE\u5DF2\u7528",confirmBtn:"\u786E\u5B9A",placeholder:"\u8F93\u5165\u82F1\u6587\u5173\u952E\u8BCD\uFF0C\u5982 home arrow user...",searching:"\u641C\u7D22\u4E2D...",scope:"\u8303\u56F4",closeTitle:"\u5173\u95ED (ESC)",escClose:"ESC \u5173\u95ED",results:e=>`${e} \u4E2A\u7ED3\u679C`,noResultScoped:'\u6240\u9009\u56FE\u6807\u5E93\u4E2D\u6CA1\u6709\u7ED3\u679C\uFF0C\u53EF\u5207\u6362\u4E3A"\u5168\u90E8"\u6A21\u5F0F\u641C\u7D22',noResult:"\u6CA1\u6709\u627E\u5230\u76F8\u5173\u56FE\u6807",searchError:"\u641C\u7D22\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5",copyTitle:"\u70B9\u51FB\u590D\u5236",langBtn:"EN"},en:{title:"Iconify Icon Picker",colsLabel:"Collections",all:"All",add:"+ Add",removeTitle:"Remove",resetToProj:"\u21BA Reset",panelTitle:"Select Collections",projUsedHint:"\u25CF = Used in project",resetBtn:"Reset to project",confirmBtn:"Confirm",placeholder:"Search icons, e.g. home arrow user...",searching:"Searching...",scope:"Scope",closeTitle:"Close (ESC)",escClose:"ESC to close",results:e=>`${e} results`,noResultScoped:'No results in selected collections, try switching to "All"',noResult:"No icons found",searchError:"Search failed, check network connection",copyTitle:"Click to copy",langBtn:"\u4E2D"}},w=localStorage.getItem(I)??"zh",f=()=>O[w];if(window.__IHP_LOADED__)throw new Error("[iph] already loaded");window.__IHP_LOADED__=!0;var a=window.__IHP_CONFIG__,U=`
|
|
2
|
+
#iph-root *,#iph-root *::before,#iph-root *::after{box-sizing:border-box}
|
|
3
|
+
#iph-overlay{
|
|
4
|
+
position:fixed;inset:0;z-index:2147483646;
|
|
5
|
+
background:rgba(0,0,0,.55);backdrop-filter:blur(4px);
|
|
6
|
+
display:none;
|
|
7
|
+
}
|
|
8
|
+
#iph-overlay.iph-open{display:block}
|
|
9
|
+
#iph-modal{
|
|
10
|
+
position:fixed;top:10%;left:50%;transform:translateX(-50%);
|
|
11
|
+
width:80%;max-width:900px;max-height:80vh;
|
|
12
|
+
background:#fff;border-radius:16px;
|
|
13
|
+
box-shadow:0 24px 60px rgba(0,0,0,.3);
|
|
14
|
+
display:flex;flex-direction:column;overflow:hidden;
|
|
15
|
+
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif !important;
|
|
16
|
+
font-size:14px !important;color:#374151;line-height:1.5 !important;
|
|
17
|
+
z-index:2147483647;
|
|
18
|
+
text-align:left;letter-spacing:normal;word-spacing:normal;
|
|
19
|
+
}
|
|
20
|
+
.iph-header{display:flex;align-items:center;justify-content:space-between;padding:14px 16px 10px;border-bottom:1px solid #f3f4f6;flex-shrink:0}
|
|
21
|
+
.iph-header-left{display:flex;align-items:center;gap:8px}
|
|
22
|
+
.iph-title{font-size:15px;font-weight:700;color:#111}
|
|
23
|
+
.iph-dev-badge{font-size:9px;font-weight:700;background:#dbeafe;color:#1d4ed8;border-radius:4px;padding:1px 5px;letter-spacing:.5px}
|
|
24
|
+
.iph-close-btn{background:#f3f4f6;border:none;width:28px;height:28px;border-radius:50%;cursor:pointer;font-size:14px;color:#6b7280;display:flex;align-items:center;justify-content:center;line-height:1}
|
|
25
|
+
.iph-close-btn:hover{background:#e5e7eb;color:#111}
|
|
26
|
+
.iph-lang-btn{background:none;border:1px solid #e5e7eb;border-radius:4px;padding:2px 7px;font-size:10px;font-weight:700;color:#6b7280;cursor:pointer;letter-spacing:.3px;font-family:inherit;line-height:1.6}
|
|
27
|
+
.iph-lang-btn:hover{background:#f3f4f6;color:#374151;border-color:#d1d5db}
|
|
28
|
+
/* Collections bar */
|
|
29
|
+
.iph-cols-bar{padding:8px 12px 6px;border-bottom:1px solid #f3f4f6;flex-shrink:0;position:relative}
|
|
30
|
+
.iph-cols-label{font-size:10px;font-weight:600;color:#9ca3af;margin-bottom:6px;text-transform:uppercase;letter-spacing:.5px}
|
|
31
|
+
.iph-chips{display:flex;flex-wrap:wrap;gap:5px;align-items:center}
|
|
32
|
+
.iph-chip{display:inline-flex;align-items:center;gap:4px;padding:3px 9px;border-radius:100px;font-size:12px;font-weight:500;cursor:pointer;border:1.5px solid;white-space:nowrap;line-height:1.6;background:none;font-family:inherit}
|
|
33
|
+
.iph-chip-all{border-color:#e5e7eb;color:#6b7280}
|
|
34
|
+
.iph-chip-all.active{border-color:#1989fa;background:#eff6ff;color:#1989fa}
|
|
35
|
+
.iph-chip-col{border-color:#1989fa;background:#eff6ff;color:#1d4ed8}
|
|
36
|
+
.iph-chip-col:hover{background:#dbeafe}
|
|
37
|
+
.iph-chip-dot{display:inline-block;width:6px;height:6px;border-radius:50%;background:#07c160;flex-shrink:0}
|
|
38
|
+
.iph-chip-rm{font-size:15px;color:#93c5fd;line-height:1}
|
|
39
|
+
.iph-chip-add{border-color:#d1d5db;color:#6b7280;border-style:dashed}
|
|
40
|
+
.iph-chip-add:hover,.iph-chip-add.open{border-color:#1989fa;color:#1989fa;background:#eff6ff}
|
|
41
|
+
.iph-chip-rst{border-color:#d1d5db;color:#9ca3af;border-style:dashed;font-size:11px}
|
|
42
|
+
.iph-chip-rst:hover{color:#374151;border-color:#9ca3af}
|
|
43
|
+
/* Collection panel */
|
|
44
|
+
.iph-col-panel{background:#fff;border:1px solid #e5e7eb;border-radius:12px;box-shadow:0 8px 24px rgba(0,0,0,.15);padding:12px;max-height:280px;display:flex;flex-direction:column;overflow:hidden}
|
|
45
|
+
.iph-col-panel-title{font-size:12px;font-weight:600;color:#374151;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0}
|
|
46
|
+
.iph-col-panel-hint{font-size:11px;font-weight:400;color:#9ca3af}
|
|
47
|
+
.iph-col-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:2px;overflow-y:auto;flex:1}
|
|
48
|
+
.iph-col-item{display:flex;align-items:center;gap:6px;padding:6px 8px;border-radius:7px;border:none;background:transparent;cursor:pointer;text-align:left;font-size:12px;font-family:inherit;width:100%}
|
|
49
|
+
.iph-col-item:hover{background:#f3f4f6}
|
|
50
|
+
.iph-col-item.active{background:#eff6ff}
|
|
51
|
+
.iph-col-name{font-weight:600;color:#374151;flex-shrink:0;min-width:80px}
|
|
52
|
+
.iph-col-desc{color:#9ca3af;font-size:11px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
53
|
+
.iph-col-chk{font-size:12px;color:#1989fa;font-weight:700;flex-shrink:0;width:14px;text-align:right}
|
|
54
|
+
.iph-col-proj-dot{width:6px;height:6px;border-radius:50%;background:#07c160;flex-shrink:0}
|
|
55
|
+
.iph-col-proj-ph{width:6px;display:inline-block;flex-shrink:0}
|
|
56
|
+
.iph-panel-footer{display:flex;gap:8px;justify-content:flex-end;margin-top:10px;padding-top:8px;border-top:1px solid #f3f4f6;flex-shrink:0}
|
|
57
|
+
.iph-panel-btn{padding:5px 14px;border-radius:6px;font-size:12px;cursor:pointer;border:1px solid #e5e7eb;background:#f9fafb;color:#374151;font-family:inherit}
|
|
58
|
+
.iph-panel-btn:hover{background:#e5e7eb}
|
|
59
|
+
.iph-panel-btn.primary{background:#1989fa;color:#fff;border-color:#1989fa}
|
|
60
|
+
.iph-panel-btn.primary:hover{background:#1671d9}
|
|
61
|
+
/* Search */
|
|
62
|
+
.iph-search{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid #f3f4f6;flex-shrink:0;background:#f9fafb}
|
|
63
|
+
.iph-search-ico{width:16px;height:16px;flex-shrink:0;color:#9ca3af}
|
|
64
|
+
.iph-search-input{flex:1;border:none;outline:none;font-size:14px;color:#111;background:transparent;min-width:0;font-family:inherit}
|
|
65
|
+
.iph-search-input::placeholder{color:#d1d5db}
|
|
66
|
+
.iph-search-clear{background:#e5e7eb;border:none;width:18px;height:18px;border-radius:50%;font-size:10px;color:#6b7280;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
|
67
|
+
/* Status */
|
|
68
|
+
.iph-status{padding:32px 16px;text-align:center;font-size:13px;color:#9ca3af;line-height:1.8;flex-shrink:0}
|
|
69
|
+
.iph-status.error{color:#ef4444}
|
|
70
|
+
.iph-status code{background:#f3f4f6;padding:1px 6px;border-radius:4px;font-size:12px;color:#1989fa}
|
|
71
|
+
.iph-spin{display:inline-block;animation:iph-spin .8s linear infinite}
|
|
72
|
+
@keyframes iph-spin{to{transform:rotate(360deg)}}
|
|
73
|
+
/* Grid */
|
|
74
|
+
.iph-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(80px,1fr));gap:2px;padding:8px;overflow-y:auto;flex:1;-webkit-overflow-scrolling:touch}
|
|
75
|
+
.iph-cell{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;padding:10px 4px 7px;border-radius:10px;border:2px solid transparent;background:#f9fafb;cursor:pointer;overflow:hidden;min-height:72px;transition:background .15s,border-color .15s;outline:none;font-family:inherit}
|
|
76
|
+
.iph-cell:hover{background:#eff6ff;border-color:#bfdbfe}
|
|
77
|
+
.iph-cell:active{transform:scale(.95)}
|
|
78
|
+
.iph-cell.copied{background:#f0fdf4;border-color:#86efac}
|
|
79
|
+
.iph-icon-img{display:block;width:36px;height:36px;object-fit:contain}
|
|
80
|
+
.iph-icon-name{font-size:9px;color:#9ca3af;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%;text-align:center;padding:0 2px}
|
|
81
|
+
.iph-col-tag{position:absolute;top:3px;right:4px;font-size:8px;font-weight:700;color:#93c5fd;background:#eff6ff;border-radius:3px;padding:1px 3px;line-height:1.4;max-width:42px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
82
|
+
.iph-copy-mask{position:absolute;inset:0;background:rgba(34,197,94,.92);border-radius:8px;color:#fff;font-size:11px;font-weight:700;display:flex;align-items:center;justify-content:center}
|
|
83
|
+
/* Footer */
|
|
84
|
+
.iph-footer{padding:7px 16px 10px;font-size:11px;color:#d1d5db;text-align:center;flex-shrink:0;border-top:1px solid #f3f4f6}
|
|
85
|
+
`,F=`
|
|
86
|
+
<div id="iph-overlay">
|
|
87
|
+
<div id="iph-modal">
|
|
88
|
+
<div class="iph-header">
|
|
89
|
+
<div class="iph-header-left">
|
|
90
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
|
91
|
+
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"
|
|
92
|
+
stroke="#1989fa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
93
|
+
</svg>
|
|
94
|
+
<span id="iph-title" class="iph-title"></span>
|
|
95
|
+
<span class="iph-dev-badge">DEV</span>
|
|
96
|
+
</div>
|
|
97
|
+
<div style="display:flex;align-items:center;gap:6px">
|
|
98
|
+
<button id="iph-lang-btn" class="iph-lang-btn"></button>
|
|
99
|
+
<button class="iph-close-btn" id="iph-close">✕</button>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div class="iph-cols-bar" id="iph-cols-bar">
|
|
104
|
+
<div id="iph-cols-label" class="iph-cols-label"></div>
|
|
105
|
+
<div class="iph-chips" id="iph-chips"></div>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<div class="iph-search">
|
|
109
|
+
<svg class="iph-search-ico" viewBox="0 0 24 24" fill="none">
|
|
110
|
+
<circle cx="11" cy="11" r="8" stroke="currentColor" stroke-width="2"/>
|
|
111
|
+
<path d="m21 21-4.35-4.35" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
|
112
|
+
</svg>
|
|
113
|
+
<input id="iph-input" class="iph-search-input"
|
|
114
|
+
autocomplete="off" spellcheck="false">
|
|
115
|
+
<button id="iph-clear" class="iph-search-clear" style="display:none">✕</button>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div id="iph-status" class="iph-status"></div>
|
|
119
|
+
<div id="iph-grid" class="iph-grid" style="display:none"></div>
|
|
120
|
+
<div id="iph-footer" class="iph-footer" style="display:none"></div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
`,t={open:!1,query:"",loading:!1,error:"",icons:[],activeCollections:a.usedCollections.length>0?[...a.usedCollections]:["mdi","tabler"],showPanel:!1},b,P,B,u,k,c,y,m,_,C;function G(e){let[o,i]=e.split(":");return`${H}/${o}/${i}.svg?width=48&height=48&color=%23374151`}function z(e){return`i-${e.replace(":","-")}`}function K(e){return e.split(":")[0]??""}function W(e){return e.split(":")[1]??e}async function X(e){try{await navigator.clipboard.writeText(e)}catch{let o=document.createElement("textarea");o.value=e,o.style.cssText="position:fixed;opacity:0;pointer-events:none",document.body.appendChild(o),o.focus(),o.select(),document.execCommand("copy"),document.body.removeChild(o)}}function J(e,o){let i=o.toLowerCase().split("+"),n=i[i.length-1];return e.key.toLowerCase()===n&&!!e.ctrlKey===i.includes("ctrl")&&!!e.metaKey===i.includes("meta")&&!!e.altKey===i.includes("alt")&&!!e.shiftKey===i.includes("shift")}function j(){let e=f(),o=document.getElementById("iph-title");o&&(o.textContent=e.title);let i=document.getElementById("iph-cols-label");i&&(i.textContent=e.colsLabel);let n=document.getElementById("iph-close");n&&(n.title=e.closeTitle),C&&(C.textContent=e.langBtn),u&&(u.placeholder=e.placeholder),p(),m.style.display!=="none"&&N()}function L(){let e=document.createElement("style");e.id="iph-styles",e.textContent=U,document.head.appendChild(e);let o=document.createElement("div");o.id="iph-root",o.innerHTML=F,document.body.appendChild(o),b=document.getElementById("iph-overlay"),P=document.getElementById("iph-modal"),B=document.getElementById("iph-chips"),u=document.getElementById("iph-input"),k=document.getElementById("iph-clear"),c=document.getElementById("iph-status"),y=document.getElementById("iph-grid"),m=document.getElementById("iph-footer"),_=document.getElementById("iph-cols-bar"),C=document.getElementById("iph-lang-btn"),V(),j()}var E=null,T=null;function V(){document.addEventListener("keydown",e=>{if(J(e,a.hotkey)){let o=document.activeElement?.tagName;if(o==="INPUT"||o==="TEXTAREA"||o==="SELECT")return;e.preventDefault(),M();return}e.key==="Escape"&&t.open&&(t.showPanel?v():x())}),document.getElementById("iph-close").addEventListener("click",x),C.addEventListener("click",()=>{w=w==="zh"?"en":"zh",localStorage.setItem(I,w),j()}),b.addEventListener("click",e=>{e.target===b&&x()}),u.addEventListener("input",()=>{let e=u.value.trim();t.query=e,k.style.display=e?"flex":"none",E&&clearTimeout(E),E=setTimeout(()=>g(e),350)}),k.addEventListener("click",()=>{u.value="",t.query="",k.style.display="none",R(),A()}),P.addEventListener("click",e=>{let o=e.target.closest("[data-action]");if(!o)return;let i=o.dataset.action,n=o.dataset.col??"",r=o.dataset.icon??"";switch(i){case"all-mode":t.activeCollections=[],t.showPanel=!1,p(),t.query&&g(t.query);break;case"remove-col":t.activeCollections=t.activeCollections.filter(l=>l!==n),p(),t.query&&g(t.query);break;case"toggle-panel":t.showPanel=!t.showPanel,p();break;case"reset-proj":t.activeCollections=a.usedCollections.length>0?[...a.usedCollections]:["mdi","tabler"],p(),t.query&&g(t.query);break;case"copy":r&&Y(o,r);break}}),document.addEventListener("click",e=>{let o=e.target,i=e.composedPath(),n=o.closest("#iph-col-panel [data-action]");if(n){let s=n.dataset.action,d=n.dataset.col??"";s==="toggle-col"?(t.activeCollections.includes(d)?t.activeCollections=t.activeCollections.filter(h=>h!==d):t.activeCollections.push(d),p(),t.query&&g(t.query)):s==="close-panel"?v():s==="reset-panel"&&(t.activeCollections=a.usedCollections.length>0?[...a.usedCollections]:["mdi","tabler"],v(),t.query&&g(t.query));return}if(!t.showPanel)return;let r=i.some(s=>s.id==="iph-cols-bar"),l=i.some(s=>s.id==="iph-col-panel");r||l||v()})}function M(){t.open?x():S()}function S(){t.open=!0,b.classList.add("iph-open"),setTimeout(()=>u?.focus(),60)}function x(){t.open=!1,b.classList.remove("iph-open"),t.showPanel=!1,document.getElementById("iph-col-panel")?.remove(),p()}function v(){t.showPanel=!1,p()}function R(){t.icons=[],t.error="",t.loading=!1,y.style.display="none",m.style.display="none",c.style.display="none"}function A(){c.style.display="none"}async function g(e){if(!e){R(),A();return}t.loading=!0,t.error="",y.style.display="none",m.style.display="none",c.className="iph-status",c.style.display="block",c.innerHTML=`<span class="iph-spin">↻</span> ${f().searching}`;try{let o=`${H}/search?query=${encodeURIComponent(e)}&limit=80`;t.activeCollections.length>0&&(o+=`&prefixes=${t.activeCollections.join(",")}`);let i=await fetch(o);if(!i.ok)throw new Error(`HTTP ${i.status}`);let n=await i.json();t.icons=n.icons??[],t.loading=!1,t.icons.length===0?(c.className="iph-status error",c.textContent=t.activeCollections.length?f().noResultScoped:f().noResult):(c.style.display="none",N())}catch{t.loading=!1,c.className="iph-status error",c.textContent=f().searchError}}function N(){c.style.display="none",y.style.display="grid",m.style.display="block";let e=f();y.innerHTML=t.icons.map(i=>{let n=K(i),r=W(i),l=z(i);return`
|
|
124
|
+
<button class="iph-cell" data-action="copy" data-icon="${i}" title="${l} ${e.copyTitle}">
|
|
125
|
+
<span class="iph-col-tag">${n}</span>
|
|
126
|
+
<img src="${G(i)}" alt="${i}" width="36" height="36"
|
|
127
|
+
class="iph-icon-img" loading="lazy">
|
|
128
|
+
<span class="iph-icon-name">${r}</span>
|
|
129
|
+
</button>`}).join("");let o=t.activeCollections.length?` \xB7 ${e.scope}: ${t.activeCollections.join(", ")}`:"";m.textContent=`${e.results(t.icons.length)}${o} \xB7 ${e.escClose}`}function p(){let e=Array.from(new Set([...Object.keys(a.popular),...a.usedCollections])).sort(),o=t.activeCollections.length===0,i=f(),n=t.activeCollections.map(s=>{let d=a.usedCollections.includes(s);return`
|
|
130
|
+
<button class="iph-chip iph-chip-col"
|
|
131
|
+
data-action="remove-col" data-col="${s}" title="${i.removeTitle}">
|
|
132
|
+
${d?'<span class="iph-chip-dot"></span>':""}
|
|
133
|
+
${s}<span class="iph-chip-rm">×</span>
|
|
134
|
+
</button>`}).join(""),r=a.usedCollections.length>0&&[...t.activeCollections].sort().join(",")!==[...a.usedCollections].sort().join(",");B.innerHTML=`
|
|
135
|
+
<button class="iph-chip iph-chip-all${o?" active":""}" data-action="all-mode">${i.all}</button>
|
|
136
|
+
${n}
|
|
137
|
+
<button class="iph-chip iph-chip-add${t.showPanel?" open":""}"
|
|
138
|
+
data-action="toggle-panel">${i.add}</button>
|
|
139
|
+
${r?`<button class="iph-chip iph-chip-rst" data-action="reset-proj">${i.resetToProj}</button>`:""}
|
|
140
|
+
`;let l=document.getElementById("iph-col-panel");if(t.showPanel){l||(l=document.createElement("div"),l.id="iph-col-panel",l.className="iph-col-panel",document.body.appendChild(l));let s=_.getBoundingClientRect();l.style.cssText=`
|
|
141
|
+
position:fixed;
|
|
142
|
+
top:${s.bottom+4}px;
|
|
143
|
+
left:${s.left}px;
|
|
144
|
+
width:${s.width}px;
|
|
145
|
+
z-index:2147483647;
|
|
146
|
+
`;let d=e.map(h=>{let $=t.activeCollections.includes(h),q=a.usedCollections.includes(h),D=a.popular[h]??"";return`
|
|
147
|
+
<button class="iph-col-item${$?" active":""}"
|
|
148
|
+
data-action="toggle-col" data-col="${h}">
|
|
149
|
+
${q?'<span class="iph-col-proj-dot"></span>':'<span class="iph-col-proj-ph"></span>'}
|
|
150
|
+
<span class="iph-col-name">${h}</span>
|
|
151
|
+
<span class="iph-col-desc">${D}</span>
|
|
152
|
+
<span class="iph-col-chk">${$?"✓":""}</span>
|
|
153
|
+
</button>`}).join("");l.innerHTML=`
|
|
154
|
+
<div class="iph-col-panel-title">
|
|
155
|
+
${i.panelTitle}
|
|
156
|
+
<span class="iph-col-panel-hint">${i.projUsedHint}</span>
|
|
157
|
+
</div>
|
|
158
|
+
<div class="iph-col-grid">${d}</div>
|
|
159
|
+
<div class="iph-panel-footer">
|
|
160
|
+
<button class="iph-panel-btn" data-action="reset-panel">${i.resetBtn}</button>
|
|
161
|
+
<button class="iph-panel-btn primary" data-action="close-panel">${i.confirmBtn}</button>
|
|
162
|
+
</div>`}else l&&l.remove()}function Y(e,o){let i=z(o);X(i).then(()=>{e.classList.add("copied");let n=document.createElement("span");n.className="iph-copy-mask",n.textContent=`\u2713 ${i}`,e.appendChild(n),T&&clearTimeout(T),T=setTimeout(()=>{e.classList.remove("copied"),n.parentNode?.removeChild(n)},1800)})}window.__IHP__={open:S,close:x,toggle:M,_refreshChips:p};function Q(){try{let e=window.location,o=e.protocol==="https:"?"wss":"ws";new WebSocket(`${o}://${e.host}`).addEventListener("message",n=>{try{let r=JSON.parse(n.data);r.type==="custom"&&r.event==="iph:collections"&&(a.usedCollections=r.data.collections,p())}catch{}})}catch{}}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",L):L();Q();})();
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Plugin } from 'vite';
|
|
2
|
+
|
|
3
|
+
interface IconifyHelperOptions {
|
|
4
|
+
/**
|
|
5
|
+
* 打开/关闭 picker 的键盘快捷键
|
|
6
|
+
* 格式:修饰键用 + 连接,最后一个是主键,不区分大小写
|
|
7
|
+
* 例:'ctrl+i' 'meta+shift+i' 'alt+q'
|
|
8
|
+
* @default 'ctrl+i'
|
|
9
|
+
*/
|
|
10
|
+
hotkey?: string;
|
|
11
|
+
/**
|
|
12
|
+
* 额外的图标库显示名称(集合名 → 描述)
|
|
13
|
+
* 会与内置常用列表合并显示在选择面板中
|
|
14
|
+
*/
|
|
15
|
+
extraCollections?: Record<string, string>;
|
|
16
|
+
/**
|
|
17
|
+
* 要扫描的源码目录(相对于项目根目录)
|
|
18
|
+
* @default ['src']
|
|
19
|
+
*/
|
|
20
|
+
scanDirs?: string[];
|
|
21
|
+
/**
|
|
22
|
+
* 要扫描的文件后缀
|
|
23
|
+
* @default ['.vue', '.ts', '.tsx', '.js', '.jsx', '.svelte', '.astro']
|
|
24
|
+
*/
|
|
25
|
+
scanExts?: string[];
|
|
26
|
+
}
|
|
27
|
+
/** 传给浏览器端 picker 的运行时配置(由 Vite 插件注入) */
|
|
28
|
+
interface PickerRuntimeConfig {
|
|
29
|
+
usedCollections: string[];
|
|
30
|
+
hotkey: string;
|
|
31
|
+
popular: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
declare const POPULAR_COLLECTIONS: Record<string, string>;
|
|
35
|
+
/**
|
|
36
|
+
* Vite 插件:仅在开发模式下注入 Iconify 图标选择器。
|
|
37
|
+
*
|
|
38
|
+
* 工作原理:
|
|
39
|
+
* 1. 扫描源码,收集已用到的 Iconify 图标库列表
|
|
40
|
+
* 2. 通过 configureServer 注册中间件,在 GET /__iph_client 时返回:
|
|
41
|
+
* - window.__IHP_CONFIG__ = {...} (运行时配置)
|
|
42
|
+
* - dist/client.min.js IIFE (picker 实现)
|
|
43
|
+
* 3. transformIndexHtml 注入 <script src="/__iph_client">
|
|
44
|
+
* 4. handleHotUpdate:源码新增图标库时推送 custom WS event 给浏览器
|
|
45
|
+
*
|
|
46
|
+
* 注意:虚拟模块方案(virtual:xxx)中 inline script 的 import 不会被
|
|
47
|
+
* Vite HTML pipeline 二次处理,浏览器拿到原始 virtual: 协议会静默失败。
|
|
48
|
+
* 中间件方案完全规避这个问题。
|
|
49
|
+
*/
|
|
50
|
+
declare function iconifyHelperPlugin(options?: IconifyHelperOptions): Plugin;
|
|
51
|
+
|
|
52
|
+
export { type IconifyHelperOptions as I, type PickerRuntimeConfig as P, POPULAR_COLLECTIONS as a, iconifyHelperPlugin as i };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Plugin } from 'vite';
|
|
2
|
+
|
|
3
|
+
interface IconifyHelperOptions {
|
|
4
|
+
/**
|
|
5
|
+
* 打开/关闭 picker 的键盘快捷键
|
|
6
|
+
* 格式:修饰键用 + 连接,最后一个是主键,不区分大小写
|
|
7
|
+
* 例:'ctrl+i' 'meta+shift+i' 'alt+q'
|
|
8
|
+
* @default 'ctrl+i'
|
|
9
|
+
*/
|
|
10
|
+
hotkey?: string;
|
|
11
|
+
/**
|
|
12
|
+
* 额外的图标库显示名称(集合名 → 描述)
|
|
13
|
+
* 会与内置常用列表合并显示在选择面板中
|
|
14
|
+
*/
|
|
15
|
+
extraCollections?: Record<string, string>;
|
|
16
|
+
/**
|
|
17
|
+
* 要扫描的源码目录(相对于项目根目录)
|
|
18
|
+
* @default ['src']
|
|
19
|
+
*/
|
|
20
|
+
scanDirs?: string[];
|
|
21
|
+
/**
|
|
22
|
+
* 要扫描的文件后缀
|
|
23
|
+
* @default ['.vue', '.ts', '.tsx', '.js', '.jsx', '.svelte', '.astro']
|
|
24
|
+
*/
|
|
25
|
+
scanExts?: string[];
|
|
26
|
+
}
|
|
27
|
+
/** 传给浏览器端 picker 的运行时配置(由 Vite 插件注入) */
|
|
28
|
+
interface PickerRuntimeConfig {
|
|
29
|
+
usedCollections: string[];
|
|
30
|
+
hotkey: string;
|
|
31
|
+
popular: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
declare const POPULAR_COLLECTIONS: Record<string, string>;
|
|
35
|
+
/**
|
|
36
|
+
* Vite 插件:仅在开发模式下注入 Iconify 图标选择器。
|
|
37
|
+
*
|
|
38
|
+
* 工作原理:
|
|
39
|
+
* 1. 扫描源码,收集已用到的 Iconify 图标库列表
|
|
40
|
+
* 2. 通过 configureServer 注册中间件,在 GET /__iph_client 时返回:
|
|
41
|
+
* - window.__IHP_CONFIG__ = {...} (运行时配置)
|
|
42
|
+
* - dist/client.min.js IIFE (picker 实现)
|
|
43
|
+
* 3. transformIndexHtml 注入 <script src="/__iph_client">
|
|
44
|
+
* 4. handleHotUpdate:源码新增图标库时推送 custom WS event 给浏览器
|
|
45
|
+
*
|
|
46
|
+
* 注意:虚拟模块方案(virtual:xxx)中 inline script 的 import 不会被
|
|
47
|
+
* Vite HTML pipeline 二次处理,浏览器拿到原始 virtual: 协议会静默失败。
|
|
48
|
+
* 中间件方案完全规避这个问题。
|
|
49
|
+
*/
|
|
50
|
+
declare function iconifyHelperPlugin(options?: IconifyHelperOptions): Plugin;
|
|
51
|
+
|
|
52
|
+
export { type IconifyHelperOptions as I, type PickerRuntimeConfig as P, POPULAR_COLLECTIONS as a, iconifyHelperPlugin as i };
|