@vmz/ui-icons 0.2.5 → 0.2.7

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 CHANGED
@@ -12,8 +12,16 @@ semantic name → registry mark → <Icon name="tool.base64" />
12
12
  - Closed thin: semantic `name`, built-in registry, decorative vs labelled a11y, size density.
13
13
  - Still open: multi-surface path sets, locale-aware marks, icon-only Button integration depth.
14
14
 
15
+ Registry kinds: `stroke` (outlined UI chrome and tools), `text-badge` (proprietary converter names such as `tool.base64` → `B64`). `feature.ssr` is intentionally absent — use title copy instead of an icon.
16
+
15
17
  Verify:
16
18
 
17
19
  ```bash
18
20
  pnpm verify -- ui-icons
19
21
  ```
22
+
23
+ Contact sheet (local review):
24
+
25
+ ```bash
26
+ node packages/ui/vmz-ui-icons/scripts/contact-sheet.mjs
27
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vmz/ui-icons",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "type": "module",
5
5
  "description": "VMZ UI Icons — semantic icon names + glyph registry. Reuses @vmz/ui tokens; not a second Button/Tooltip shell or loose SVG dump.",
6
6
  "license": "MIT",
@@ -15,7 +15,7 @@
15
15
  "README.md"
16
16
  ],
17
17
  "peerDependencies": {
18
- "@vmz/ui": "0.2.5"
18
+ "@vmz/ui": "0.2.7"
19
19
  },
20
20
  "keywords": [
21
21
  "vmz",
package/src/Icon.vmz CHANGED
@@ -1,7 +1,20 @@
1
1
  <template>
2
2
  <span data-vmz-ui="icon" :data-icon="name" :data-size="size" :role="roleAttr" :aria-hidden="hiddenAttr" :aria-label="labelAttr" class="vmz-ui-icon">
3
- <svg viewBox="0 0 24 24" focusable="false" aria-hidden="true" class="vmz-ui-icon__svg">
4
- <path fill="currentColor" :d="iconPath()" />
3
+ <span v-if="isTextBadge()" class="vmz-ui-icon__badge">
4
+ {{ badgeLabel() }}
5
+ </span>
6
+ <svg v-else viewBox="0 0 24 24" focusable="false" aria-hidden="true" class="vmz-ui-icon__svg">
7
+ <g v-if="isStroke()" fill="none" stroke="currentColor" :stroke-width="strokeWidth()" stroke-linecap="round" stroke-linejoin="round">
8
+ <path v-for="d in strokePaths()" :key="d" :d="d" />
9
+ <circle v-for="c in strokeCircles()" :key="circleKey(c)" :cx="c.cx" :cy="c.cy" :r="c.r" />
10
+ <circle v-for="dot in strokeDots()" :key="dotKey(dot)" :cx="dot.cx" :cy="dot.cy" :r="dot.r" fill="currentColor" stroke="none" />
11
+ </g>
12
+ <g v-else-if="isMixed()">
13
+ <path v-for="node in mixedPaths()" :key="node.key" :d="node.d" :fill="node.fill" :stroke="node.stroke" :stroke-width="node.strokeWidth" :fill-rule="node.fillRule" />
14
+ <circle v-for="node in mixedCircles()" :key="node.key" :cx="node.cx" :cy="node.cy" :r="node.r" :fill="node.fill" :stroke="node.stroke" :stroke-width="node.strokeWidth" />
15
+ <rect v-for="node in mixedRects()" :key="node.key" :x="node.x" :y="node.y" :width="node.width" :height="node.height" :rx="node.rx" :fill="node.fill" :stroke="node.stroke" :stroke-width="node.strokeWidth" />
16
+ </g>
17
+ <path v-else-if="isFill()" fill="currentColor" :fill-rule="fillRule()" :d="fillD()" />
5
18
  </svg>
6
19
  </span>
7
20
  </template>
@@ -12,7 +25,7 @@
12
25
  justify-content: center;
13
26
  flex-shrink: 0;
14
27
  box-sizing: border-box;
15
- color: var(--vmz-action-primary-background);
28
+ color: inherit;
16
29
  line-height: 1;
17
30
  vertical-align: middle;
18
31
  user-select: none;
@@ -24,6 +37,24 @@
24
37
  height: 1em;
25
38
  }
26
39
 
40
+ .vmz-ui-icon__badge {
41
+ display: inline-flex;
42
+ align-items: center;
43
+ justify-content: center;
44
+ box-sizing: border-box;
45
+ min-width: 1.65em;
46
+ padding: 0.12em 0.22em;
47
+ border: 1.5px solid currentColor;
48
+ border-radius: 0.2em;
49
+ font-family: var(--vmz-font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
50
+ font-size: 0.52em;
51
+ font-weight: 600;
52
+ letter-spacing: -0.03em;
53
+ line-height: 1.1;
54
+ text-align: center;
55
+ text-transform: uppercase;
56
+ }
57
+
27
58
  .vmz-ui-icon[data-size="sm"] {
28
59
  width: 1.5rem;
29
60
  height: 1.5rem;
@@ -43,13 +74,14 @@
43
74
  }
44
75
  </style>
45
76
  <script client>
46
- import { FALLBACK_PATH, resolveIconPath } from "./registry.ts";
77
+ import { DEFAULT_STROKE_WIDTH, resolveIconDef } from "./registry.ts";
47
78
 
48
79
  /**
49
80
  * VMZ UI Icons — semantic Icon backed by the shared registry export.
50
81
  * Optional package: apps that only need UI chrome can use `@vmz/ui/Icon` instead.
51
82
  */
52
83
  export default class Icon {
84
+ static __vmzHostBox = "contents";
53
85
  /** Semantic icon id, e.g. tool.base64 / action.search */
54
86
  public name: string = "";
55
87
  /** sm | md | lg */
@@ -65,8 +97,127 @@ export default class Icon {
65
97
  this.syncA11y();
66
98
  }
67
99
 
68
- iconPath() {
69
- return resolveIconPath(this.name) || FALLBACK_PATH;
100
+ iconDef() {
101
+ return resolveIconDef(this.name);
102
+ }
103
+
104
+ isStroke() {
105
+ const def = this.iconDef();
106
+ return def?.kind === "stroke";
107
+ }
108
+
109
+ isFill() {
110
+ const def = this.iconDef();
111
+ return def?.kind === "fill";
112
+ }
113
+
114
+ isMixed() {
115
+ const def = this.iconDef();
116
+ return def?.kind === "mixed";
117
+ }
118
+
119
+ isTextBadge() {
120
+ const def = this.iconDef();
121
+ return def?.kind === "text-badge";
122
+ }
123
+
124
+ badgeLabel() {
125
+ const def = this.iconDef();
126
+ if (def?.kind !== "text-badge") return "";
127
+ return def.label;
128
+ }
129
+
130
+ strokeWidth() {
131
+ const def = this.iconDef();
132
+ if (def?.kind !== "stroke") return DEFAULT_STROKE_WIDTH;
133
+ return def.strokeWidth ?? DEFAULT_STROKE_WIDTH;
134
+ }
135
+
136
+ strokePaths() {
137
+ const def = this.iconDef();
138
+ if (def?.kind !== "stroke") return [];
139
+ return def.paths ?? [];
140
+ }
141
+
142
+ strokeCircles() {
143
+ const def = this.iconDef();
144
+ if (def?.kind !== "stroke") return [];
145
+ return def.circles ?? [];
146
+ }
147
+
148
+ strokeDots() {
149
+ const def = this.iconDef();
150
+ if (def?.kind !== "stroke") return [];
151
+ return def.dots ?? [];
152
+ }
153
+
154
+ circleKey(c) {
155
+ return `c-${c.cx}-${c.cy}-${c.r}`;
156
+ }
157
+
158
+ dotKey(dot) {
159
+ return `d-${dot.cx}-${dot.cy}-${dot.r}`;
160
+ }
161
+
162
+ fillD() {
163
+ const def = this.iconDef();
164
+ if (def?.kind !== "fill") return "";
165
+ return def.d;
166
+ }
167
+
168
+ fillRule() {
169
+ const def = this.iconDef();
170
+ if (def?.kind !== "fill" || !def.fillRule) return "";
171
+ return def.fillRule;
172
+ }
173
+
174
+ mixedPaths() {
175
+ const def = this.iconDef();
176
+ if (def?.kind !== "mixed") return [];
177
+ return def.nodes
178
+ .filter((node) => node.tag === "path")
179
+ .map((node, index) => ({
180
+ key: `p-${index}`,
181
+ d: node.attrs.d ?? "",
182
+ fill: node.attrs.fill ?? "none",
183
+ stroke: node.attrs.stroke ?? "currentColor",
184
+ strokeWidth: node.attrs["stroke-width"] ?? DEFAULT_STROKE_WIDTH,
185
+ fillRule: node.attrs["fill-rule"] ?? "",
186
+ }));
187
+ }
188
+
189
+ mixedCircles() {
190
+ const def = this.iconDef();
191
+ if (def?.kind !== "mixed") return [];
192
+ return def.nodes
193
+ .filter((node) => node.tag === "circle")
194
+ .map((node, index) => ({
195
+ key: `c-${index}`,
196
+ cx: node.attrs.cx ?? "12",
197
+ cy: node.attrs.cy ?? "12",
198
+ r: node.attrs.r ?? "9",
199
+ fill: node.attrs.fill ?? "none",
200
+ stroke: node.attrs.stroke ?? "currentColor",
201
+ strokeWidth: node.attrs["stroke-width"] ?? DEFAULT_STROKE_WIDTH,
202
+ }));
203
+ }
204
+
205
+ mixedRects() {
206
+ const def = this.iconDef();
207
+ if (def?.kind !== "mixed") return [];
208
+ return def.nodes
209
+ .filter((node) => node.tag === "rect")
210
+ .map((node, index) => ({
211
+ key: `r-${index}`,
212
+ x: node.attrs.x ?? "0",
213
+ y: node.attrs.y ?? "0",
214
+ width: node.attrs.width ?? "24",
215
+ height: node.attrs.height ?? "24",
216
+ rx: node.attrs.rx ?? "",
217
+ fill: node.attrs.fill ?? "none",
218
+ stroke: node.attrs.stroke ?? "currentColor",
219
+ strokeWidth: node.attrs["stroke-width"] ?? DEFAULT_STROKE_WIDTH,
220
+ }));
70
221
  }
71
222
 
72
223
  syncA11y() {
@@ -0,0 +1,35 @@
1
+ /** Structured icon definitions for `@vmz/ui-icons` registry. */
2
+
3
+ export type IconStrokeDef = {
4
+ kind: 'stroke';
5
+ paths: readonly string[];
6
+ circles?: readonly Readonly<{ cx: number; cy: number; r: number }>[];
7
+ dots?: readonly Readonly<{ cx: number; cy: number; r: number }>[];
8
+ strokeWidth?: number;
9
+ };
10
+
11
+ export type IconFillDef = {
12
+ kind: 'fill';
13
+ d: string;
14
+ fillRule?: 'evenodd' | 'nonzero';
15
+ };
16
+
17
+ export type IconMixedNode = {
18
+ tag: 'path' | 'circle' | 'rect' | 'line';
19
+ attrs: Readonly<Record<string, string>>;
20
+ };
21
+
22
+ export type IconMixedDef = {
23
+ kind: 'mixed';
24
+ nodes: readonly IconMixedNode[];
25
+ };
26
+
27
+ /** Monospace text mark for proprietary converter names (2–4 chars). */
28
+ export type IconTextBadgeDef = {
29
+ kind: 'text-badge';
30
+ label: string;
31
+ };
32
+
33
+ export type IconDef = IconStrokeDef | IconFillDef | IconMixedDef | IconTextBadgeDef;
34
+
35
+ export const DEFAULT_STROKE_WIDTH = 1.75;
@@ -0,0 +1,169 @@
1
+ import type { IconDef } from './icon-types.ts';
2
+
3
+ /** Outlined icon set — 24 grid, 1.75 stroke, 3–21 safe area. */
4
+ export const STROKE_ICONS: Record<string, IconDef> = {
5
+ 'action.search': {
6
+ kind: 'stroke',
7
+ circles: [{ cx: 10.5, cy: 10.5, r: 7.5 }],
8
+ paths: ['M15.8 15.8L20 20'],
9
+ },
10
+ 'action.close': {
11
+ kind: 'stroke',
12
+ paths: ['M7 7L17 17', 'M17 7L7 17'],
13
+ },
14
+ 'action.check': {
15
+ kind: 'stroke',
16
+ paths: ['M5 12L10 17L19 7'],
17
+ },
18
+ 'action.chevron-down': {
19
+ kind: 'stroke',
20
+ paths: ['M7 10L12 15L17 10'],
21
+ },
22
+ 'action.chevron-right': {
23
+ kind: 'stroke',
24
+ paths: ['M10 7L15 12L10 17'],
25
+ },
26
+ 'action.menu': {
27
+ kind: 'stroke',
28
+ paths: ['M4 7H20', 'M4 12H20', 'M4 17H20'],
29
+ },
30
+ 'action.plus': {
31
+ kind: 'stroke',
32
+ paths: ['M12 5V19', 'M5 12H19'],
33
+ },
34
+ 'action.copy': {
35
+ kind: 'stroke',
36
+ paths: ['M9 9H17V17H9Z', 'M6 6H14V14'],
37
+ },
38
+ 'nav.home': {
39
+ kind: 'stroke',
40
+ paths: ['M4 10.5L12 4L20 10.5', 'M6 10V19H18V10', 'M10 19V13H14V19'],
41
+ },
42
+ 'nav.language': {
43
+ kind: 'stroke',
44
+ circles: [{ cx: 12, cy: 12, r: 9 }],
45
+ paths: ['M12 3V21', 'M3 12H21', 'M5.5 7.5C8 5.5 16 5.5 18.5 7.5', 'M5.5 16.5C8 18.5 16 18.5 18.5 16.5'],
46
+ },
47
+ 'status.info': {
48
+ kind: 'stroke',
49
+ circles: [{ cx: 12, cy: 12, r: 9 }],
50
+ paths: ['M12 11V16'],
51
+ dots: [{ cx: 12, cy: 8, r: 1 }],
52
+ },
53
+ 'status.warning': {
54
+ kind: 'stroke',
55
+ paths: ['M12 4L20 19H4L12 4Z', 'M12 10V14'],
56
+ dots: [{ cx: 12, cy: 17, r: 1 }],
57
+ },
58
+ 'status.success': {
59
+ kind: 'stroke',
60
+ circles: [{ cx: 12, cy: 12, r: 9 }],
61
+ paths: ['M8 12L11 15L16 9'],
62
+ },
63
+ 'status.danger': {
64
+ kind: 'stroke',
65
+ circles: [{ cx: 12, cy: 12, r: 9 }],
66
+ paths: ['M12 8V13'],
67
+ dots: [{ cx: 12, cy: 16.5, r: 1 }],
68
+ },
69
+ 'feature.incremental': {
70
+ kind: 'stroke',
71
+ circles: [
72
+ { cx: 5, cy: 12, r: 2 },
73
+ { cx: 19, cy: 12, r: 2 },
74
+ { cx: 12, cy: 6, r: 2 },
75
+ ],
76
+ paths: ['M7 12H10', 'M14 12H17', 'M12 8V10', 'M8.2 11.2L11 8.5', 'M12.8 8.5L15.6 11.2'],
77
+ },
78
+ 'feature.runtime': {
79
+ kind: 'stroke',
80
+ paths: ['M8.5 7.5H15.5V16.5H8.5Z', 'M11 10L14.5 12L11 14Z'],
81
+ },
82
+ 'feature.deploy': {
83
+ kind: 'stroke',
84
+ paths: ['M4 16H20', 'M12 6V14', 'M8.5 10.5L12 6L15.5 10.5'],
85
+ },
86
+ 'tool.url': {
87
+ kind: 'stroke',
88
+ paths: ['M10.5 8.5a3 3 0 0 1 4.2 0l.8.8a3 3 0 0 1 0 4.2l-.8.8', 'M13.5 15.5a3 3 0 0 1-4.2 0l-.8-.8a3 3 0 0 1 0-4.2l.8-.8'],
89
+ },
90
+ 'tool.temperature': {
91
+ kind: 'stroke',
92
+ circles: [{ cx: 12, cy: 18, r: 3 }],
93
+ paths: ['M12 4V15', 'M10 9H14'],
94
+ },
95
+ 'tool.length': {
96
+ kind: 'stroke',
97
+ paths: ['M4 12H20', 'M6 9V15', 'M10 10V14', 'M14 10V14', 'M18 9V15', 'M4 12L7 9', 'M4 12L7 15', 'M20 12L17 9', 'M20 12L17 15'],
98
+ },
99
+ 'tool.weight': {
100
+ kind: 'stroke',
101
+ paths: ['M6 8H18', 'M12 8V16', 'M8 16H16', 'M9 6H15'],
102
+ },
103
+ 'tool.time-units': {
104
+ kind: 'stroke',
105
+ circles: [{ cx: 12, cy: 12, r: 9 }],
106
+ paths: ['M12 7V12L15.5 14'],
107
+ },
108
+ 'tool.color': {
109
+ kind: 'stroke',
110
+ paths: ['M12 4C8.5 4 6 6.5 6 10s2.5 6 6 6', 'M12 4c3.5 0 6 2.5 6 6s-2.5 6-6 6'],
111
+ dots: [
112
+ { cx: 8, cy: 10, r: 1.2 },
113
+ { cx: 12, cy: 8, r: 1.2 },
114
+ { cx: 16, cy: 10, r: 1.2 },
115
+ ],
116
+ },
117
+ 'tool.percentage': {
118
+ kind: 'stroke',
119
+ circles: [
120
+ { cx: 9.5, cy: 9.5, r: 2 },
121
+ { cx: 14.5, cy: 14.5, r: 2 },
122
+ ],
123
+ paths: ['M16 8L8 16'],
124
+ },
125
+ 'tool.area': {
126
+ kind: 'stroke',
127
+ paths: ['M5 5H19V19H5Z', 'M8 8H16V16H8Z'],
128
+ },
129
+ 'tool.volume': {
130
+ kind: 'stroke',
131
+ paths: ['M5 9H19V19H5Z', 'M8 6H16', 'M8 6V9', 'M16 6V9'],
132
+ },
133
+ 'tool.data-size': {
134
+ kind: 'stroke',
135
+ paths: ['M5 7H19', 'M5 11H15', 'M5 15H19'],
136
+ },
137
+ 'tool.ratio': {
138
+ kind: 'stroke',
139
+ paths: ['M5 7H19', 'M5 17H19', 'M8 10V14', 'M16 10V14'],
140
+ },
141
+ 'tool.average': {
142
+ kind: 'stroke',
143
+ paths: ['M4 18H20', 'M6 15L10 9L14 13L18 7'],
144
+ },
145
+ 'tool.tip': {
146
+ kind: 'stroke',
147
+ paths: ['M12 4C9.5 4 8 6 8 8.5c0 1.8 1 3.2 2.5 4V15h3v-2.5c1.5-.8 2.5-2.2 2.5-4C16 6 14.5 4 12 4Z', 'M9.5 18H14.5', 'M10 20H14'],
148
+ },
149
+ 'tool.aspect-ratio': {
150
+ kind: 'stroke',
151
+ paths: ['M5 7H19V17H5Z', 'M8 9H16V15H8Z'],
152
+ },
153
+ 'tool.bandwidth': {
154
+ kind: 'stroke',
155
+ paths: ['M4 12H6', 'M8 8V16', 'M11 10V14', 'M14 6V18', 'M17 9V15', 'M20 12H22'],
156
+ },
157
+ 'tool.percent-change': {
158
+ kind: 'stroke',
159
+ paths: ['M5 17L12 9L15 13L19 6', 'M4 19H20'],
160
+ },
161
+ 'tool.ohm': {
162
+ kind: 'stroke',
163
+ paths: ['M8 11c0-2.2 1.8-4 4-4s4 1.8 4 4', 'M8 11V16', 'M16 11V16', 'M8 16H16'],
164
+ },
165
+ 'tool.speed': {
166
+ kind: 'stroke',
167
+ paths: ['M5.5 14A7 7 0 0 1 18.5 14', 'M12 12L15.5 8.5'],
168
+ },
169
+ };
@@ -0,0 +1,17 @@
1
+ import type { IconDef } from './icon-types.ts';
2
+
3
+ /** Proprietary tool names — text badges instead of unreadable SVG silhouettes. */
4
+ export const TEXT_BADGE_ICONS: Record<string, IconDef> = {
5
+ 'tool.base64': { kind: 'text-badge', label: 'B64' },
6
+ 'tool.base36': { kind: 'text-badge', label: 'B36' },
7
+ 'tool.hex': { kind: 'text-badge', label: 'HEX' },
8
+ 'tool.radix': { kind: 'text-badge', label: 'RAD' },
9
+ 'tool.html-entities': { kind: 'text-badge', label: 'ENT' },
10
+ 'tool.ascii': { kind: 'text-badge', label: 'ASC' },
11
+ 'tool.binary-text': { kind: 'text-badge', label: '01' },
12
+ 'tool.rgb-hsl': { kind: 'text-badge', label: 'HSL' },
13
+ 'tool.px-rem': { kind: 'text-badge', label: 'px' },
14
+ 'tool.gpa': { kind: 'text-badge', label: 'GPA' },
15
+ 'tool.nearest-tenth': { kind: 'text-badge', label: '.1' },
16
+ 'tool.grade-curve': { kind: 'text-badge', label: 'CV' },
17
+ };
package/src/registry.ts CHANGED
@@ -1,64 +1,16 @@
1
- /** Shared SVG path registry for `@vmz/ui-icons` and optional app-level Icon shims. */
2
- export const REGISTRY = {
3
- 'action.search': 'M10.5 3a7.5 7.5 0 0 1 5.9 12.1l4 4-1.4 1.4-4-4A7.5 7.5 0 1 1 10.5 3zm0 2a5.5 5.5 0 1 0 0 11 5.5 5.5 0 0 0 0-11z',
4
- 'action.close': 'M6.4 7.8 7.8 6.4 12 10.6 16.2 6.4 17.6 7.8 13.4 12 17.6 16.2 16.2 17.6 12 13.4 7.8 17.6 6.4 16.2 10.6 12z',
5
- 'action.copy': 'M8 4h9a2 2 0 0 1 2 2v11h-2V6H8V4zm-2 4h9a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2zm0 2v10h9V10H6z',
6
- 'nav.home': 'M4 10.5 12 4l8 6.5V20a1 1 0 0 1-1 1h-5v-6H10v6H5a1 1 0 0 1-1-1v-9.5z',
7
- 'nav.language':
8
- 'M12 2a10 10 0 1 1 0 20 10 10 0 0 1 0-20zm-1 2.1a8 8 0 0 0-3.2 6.9H5.1a8 8 0 0 1 3.3-5.8L11 4.1zm2 0 .6.1a8 8 0 0 1 3.3 5.8h-3.7A8 8 0 0 0 13 4.1zM5.1 13h3.7a8 8 0 0 0 3.2 6.9l-2.6-.2A8 8 0 0 1 5.1 13zm8.8 0h3.7a8 8 0 0 1-3.3 5.8l-.4.1a8 8 0 0 0-3.2-5.9h3.2z',
9
- 'status.info':
10
- 'M12 2a10 10 0 1 1 0 20 10 10 0 0 1 0-20zm0 8.2a1 1 0 0 0-1 1V17a1 1 0 1 0 2 0v-5.8a1 1 0 0 0-1-1zm0-3.7a1.2 1.2 0 1 0 0 2.4 1.2 1.2 0 0 0 0-2.4z',
11
- 'status.warning':
12
- 'M12 3.2 21 19H3L12 3.2zm0 5.3a1 1 0 0 0-1 1v4.2a1 1 0 1 0 2 0V9.5a1 1 0 0 0-1-1zm0 8.1a1.2 1.2 0 1 0 0 2.4 1.2 1.2 0 0 0 0-2.4z',
13
- 'status.success': 'M12 2a10 10 0 1 1 0 20 10 10 0 0 1 0-20zm-2.5 10.3-2-2 1.4-1.4 2 2 5-5 1.4 1.4z',
14
- 'feature.incremental': 'M13 2 4 14h7l-1 8 10-14h-7l0-6z',
15
- 'feature.runtime': 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zm0 2a7 7 0 1 1 0 14 7 7 0 0 1 0-14zm-.5 3.5v4.2l3.6 2.1.9-1.5-2.8-1.6V8.5h-1.7z',
16
- 'feature.ssr': 'M4 6h16v4H4V6zm0 6h10v4H4v-4zm12 0h4v4h-4v-4z',
17
- 'feature.deploy': 'M4 5h7v7H4V5zm9 0h7v7h-7V5zM4 14h7v5H4v-5zm9 3h7v2h-7v-2z',
18
- 'tool.base64': 'M5 5h6v3H8v3h3v3H5V5zm8 0h6v14h-6V5zm2 2v10h2V7h-2z',
19
- 'tool.base36': 'M6 6h4l3 6-3 6H6l3-6-3-6zm8 0h4v12h-4V6z',
20
- 'tool.hex': 'M7 5h3l2 4-2 4H7l2-4-2-4zm7 0h3l2 4-2 4h-3l2-4-2-4z',
21
- 'tool.radix': 'M5 7h14v2H5V7zm0 4h10v2H5v-2zm0 4h14v2H5v-2z',
22
- 'tool.url':
23
- 'M10.6 8.4a3.5 3.5 0 0 1 4.9 0l1.1 1.1a3.5 3.5 0 0 1 0 4.9l-.8.8-1.4-1.4.8-.8a1.5 1.5 0 0 0 0-2.1l-1.1-1.1a1.5 1.5 0 0 0-2.1 0l-.8.8L9.8 9.2l.8-.8zm2.8 2.8 1.4 1.4-4.2 4.2a3.5 3.5 0 0 1-4.9 0l-1.1-1.1a3.5 3.5 0 0 1 0-4.9l.8-.8 1.4 1.4-.8.8a1.5 1.5 0 0 0 0 2.1l1.1 1.1a1.5 1.5 0 0 0 2.1 0l4.2-4.2z',
24
- 'tool.html-entities': 'M8 7 5 12l3 5h2l-2.5-5L10 7H8zm8 0h-2l2.5 5L14 17h2l3-5-3-5zM11 16h2v2h-2v-2z',
25
- 'tool.ascii': 'M6 16V8h3a3 3 0 0 1 0 6H8v2H6zm2-4h1a1 1 0 1 0 0-2H8v2zm5 4V8h2v8h-2zm4 0V8h2l-2 4 2 4h-2z',
26
- 'tool.binary-text':
27
- 'M5 5h4v4H5V5zm10 0h4v4h-4V5zM5 15h4v4H5v-4zm6-8h2v2h-2V7zm0 4h2v2h-2v-2zm0 4h2v2h-2v-2zm4-8h2v2h-2V7zm0 4h2v2h-2v-2zm0 4h2v2h-2v-2z',
28
- 'tool.temperature':
29
- 'M12 3a4 4 0 0 0-4 4v6.1a6 6 0 1 0 8 0V7a4 4 0 0 0-4-4zm0 2a2 2 0 0 1 2 2v6.3a4 4 0 1 1-4 0V7a2 2 0 0 1 2-2zm0 4.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z',
30
- 'tool.length': 'M4 11h16v2H4v-2zm2-4 3 3-1.4 1.4L4.6 8.4 6 7zm12 0 1.4 1.4-3 3L15 10l3-3z',
31
- 'tool.weight': 'M12 3a3 3 0 0 1 2.8 4H19l-2 11H7L5 7h4.2A3 3 0 0 1 12 3zm0 2a1 1 0 1 0 0 2 1 1 0 0 0 0-2z',
32
- 'tool.speed': 'M13 4.1A8 8 0 1 0 12 20v-2a6 6 0 1 1 1-11.9V4.1zM12 8l4.5 2.6L12 13.2 7.5 10.6 12 8zm0 3.8 2.7 1.6L12 15 9.3 13.4 12 11.8z',
33
- 'tool.area': 'M5 5h14v14H5V5zm2 2v10h10V7H7z',
34
- 'tool.volume': 'M6 8h12v10a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V8zm2 2v8h8v-8H8zm2-6h4v2H10V4z',
35
- 'tool.time-units': 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zm0 2a7 7 0 1 1 0 14 7 7 0 0 1 0-14zm-.8 3.5V12l3.2 1.9.8-1.4-2.4-1.4V8.5h-1.6z',
36
- 'tool.data-size': 'M5 6h14v3H5V6zm0 5h10v3H5v-3zm0 5h14v2H5v-2z',
37
- 'tool.color':
38
- 'M12 3c-3.9 0-7 2.7-7 6.2 0 2.2 1.4 3.8 3.5 3.8 1 0 1.8-.5 2.4-1.2.6.7 1.4 1.2 2.4 1.2 2.1 0 3.5-1.6 3.5-3.8C17 5.7 13.9 3 12 3zm-4.5 5.5a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3zm9 0a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3z',
39
- 'tool.rgb-hsl': 'M6 18 10 6h1l4 12h-2l-.8-2.4H8.8L8 18H6zm2.6-4.4h2.8L9.7 9.8 8.6 13.6zm7.1 4.4V6h2v12h-2z',
40
- 'tool.px-rem': 'M6 16V8h3l2 4-2 4H6zm8 0V8h2v8h-2zm-6-4h1.2l.8-1.6.8 1.6H11z',
41
- 'tool.percentage': 'M7 7h4v4H7V7zm6 6h4v4h-4v-4zM8.8 15.2 15.2 8.8l1.4 1.4-6.4 6.4-1.4-1.4z',
42
- 'tool.ratio': 'M5 7h14v2H5V7zm0 8h14v2H5v-2zm4-6h2v8h-2V9zm6 0h2v8h-2V9z',
43
- 'tool.average': 'M5 18h14v2H5v-2zm2.2-4 2.4-7 2.2 5.5L14.4 8l2.6 6H5.2z',
44
- 'tool.tip': 'M12 3c-3.3 0-6 2-6 4.5 0 1.4.8 2.6 2.1 3.3L7 14h10l-1.1-3.2c1.3-.7 2.1-1.9 2.1-3.3C18 5 15.3 3 12 3zm-2 15h4v2h-4v-2z',
45
- 'tool.discount': 'M7 7h4v4H7V7zm6 6h4v4h-4v-4zM8.8 15.2 15.2 8.8l1.4 1.4-6.4 6.4-1.4-1.4zM5 18h3v2H5v-2zm11 0h3v2h-3v-2z',
46
- 'tool.aspect-ratio': 'M5 7h14v10H5V7zm2 2v6h10V9H7z',
47
- 'tool.nearest-tenth': 'M6 7h12v2H6V7zm0 4h8v2H6v-2zm0 4h12v2H6v-2z',
48
- 'tool.gpa': 'M6 6h4v12H6V6zm8 0h4v12h-4V6zm-8 8h12v2H6v-2z',
49
- 'tool.bandwidth': 'M4 12h3l2-4 3 8 2-4h6v2h-4.5l-2 4-3-8-2 4H4v-2z',
50
- 'tool.percent-change': 'M6 16 12 8l3 4 5-8 2 3v5H6zm-1 2h14v2H5v-2z',
51
- 'tool.grade-curve': 'M5 17c2-6 4-8 7-8s5 2 7 8H5z',
52
- 'tool.ohm': 'M7 8h2v3h6V8h2v8h-2v-3H9v3H7V8z',
53
- 'action.check': 'M9.2 16.6 4.8 12.2l1.4-1.4 3 3 8-8 1.4 1.4z',
54
- 'action.chevron-down': 'M7.4 9.1 12 13.7l4.6-4.6 1.4 1.4L12 16.5 6 10.5z',
55
- 'action.chevron-right': 'M9.1 6 15.1 12l-6 6-1.4-1.4L12.3 12 7.7 7.4z',
56
- 'action.menu': 'M4 7h16v2H4zm0 4h16v2H4zm0 4h16v2H4z',
57
- 'action.plus': 'M11 5h2v6h6v2h-6v6h-2v-6H5v-2h6z',
58
- 'status.danger': 'M12 2a10 10 0 1 1 0 20 10 10 0 0 1 0-20zm-1 5h2v6h-2zm0 8h2v2h-2z',
59
- };
1
+ /** Shared icon registry for `@vmz/ui-icons` and optional app-level Icon shims. */
2
+ import type { IconDef, IconFillDef } from './icon-types.ts';
3
+ import { DEFAULT_STROKE_WIDTH } from './icon-types.ts';
4
+ import { STROKE_ICONS } from './icons-stroke.ts';
5
+ import { TEXT_BADGE_ICONS } from './icons-text-badge.ts';
6
+
7
+ export type { IconDef, IconFillDef, IconMixedDef, IconMixedNode, IconStrokeDef, IconTextBadgeDef } from './icon-types.ts';
8
+ export { DEFAULT_STROKE_WIDTH } from './icon-types.ts';
60
9
 
61
- export const FALLBACK_PATH = 'M12 2a10 10 0 1 1 0 20 10 10 0 0 1 0-20zm1 5h-2v2h2zm0 4h-2v7h2z';
10
+ export const REGISTRY: Record<string, IconDef> = {
11
+ ...STROKE_ICONS,
12
+ ...TEXT_BADGE_ICONS,
13
+ };
62
14
 
63
15
  export const LEGACY_ICON_ALIASES: Record<string, string> = {
64
16
  check: 'action.check',
@@ -72,6 +24,10 @@ export const LEGACY_ICON_ALIASES: Record<string, string> = {
72
24
  danger: 'status.danger',
73
25
  menu: 'action.menu',
74
26
  plus: 'action.plus',
27
+ home: 'nav.home',
28
+ language: 'nav.language',
29
+ copy: 'action.copy',
30
+ discount: 'tool.percentage',
75
31
  };
76
32
 
77
33
  export function resolveIconName(name: string): string {
@@ -81,9 +37,23 @@ export function resolveIconName(name: string): string {
81
37
  return key;
82
38
  }
83
39
 
84
- export function resolveIconPath(name: string): string {
40
+ export function resolveIconDef(name: string): IconDef | null {
85
41
  const key = resolveIconName(name);
86
- if (!key) return FALLBACK_PATH;
87
- if (REGISTRY[key]) return REGISTRY[key];
88
- return FALLBACK_PATH;
42
+ if (!key) return null;
43
+ const def = REGISTRY[key];
44
+ if (!def) {
45
+ throw new Error(`@vmz/ui-icons: unknown icon name "${name}" (resolved: "${key}")`);
46
+ }
47
+ return def;
48
+ }
49
+
50
+ /** @deprecated Prefer `resolveIconDef`. Returns primary path data for fill icons or first stroke path. */
51
+ export function resolveIconPath(name: string): string {
52
+ const def = resolveIconDef(name);
53
+ if (!def) return '';
54
+ if (def.kind === 'fill') return def.d;
55
+ if (def.kind === 'text-badge') return '';
56
+ if (def.kind === 'stroke') return def.paths[0] ?? '';
57
+ const pathNode = def.nodes.find((node) => node.tag === 'path');
58
+ return pathNode?.attrs.d ?? '';
89
59
  }