@ra3orblade/swarm 0.4.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/web/fm.css ADDED
@@ -0,0 +1,456 @@
1
+ /**
2
+ * fancy-menus runtime stylesheet.
3
+ *
4
+ * Defines the public design-system surface as CSS custom properties on
5
+ * `:root`. Consumers override any of these to retheme — everything in the
6
+ * runtime references them through `var(--fm-*)`.
7
+ *
8
+ * Import once at app entry:
9
+ * import '@react-fancy-menus/core/runtime/runtime.css';
10
+ */
11
+
12
+ :root {
13
+ /* ─── Surface ───────────────────────────────────────────────────────── */
14
+ --fm-surface-bg: hsl(0 0% 100%);
15
+ --fm-surface-fg: hsl(222.2 84% 4.9%);
16
+ --fm-surface-border: hsl(214.3 31.8% 91.4%);
17
+ --fm-surface-radius: 8px;
18
+ --fm-surface-shadow: 0 10px 28px -8px rgb(0 0 0 / 0.18), 0 2px 6px -2px rgb(0 0 0 / 0.06);
19
+
20
+ /* ─── Stack ─────────────────────────────────────────────────────────── */
21
+ --fm-z-dimmer: 1000;
22
+ --fm-z-menu: 1001;
23
+
24
+ /* ─── Motion ────────────────────────────────────────────────────────── */
25
+ /* Decelerate — fast out, slow in. Best for menu open / overlay reveal. */
26
+ --fm-ease-decelerate: cubic-bezier(0.2, 0, 0, 1);
27
+ /* Spring — slight overshoot. Good for affirmative state changes. */
28
+ --fm-ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
29
+ /* Smooth — symmetric, no overshoot. Default for hover / micro-state. */
30
+ --fm-ease-smooth: cubic-bezier(0.4, 0, 0.2, 1);
31
+ --fm-duration-open: 120ms;
32
+ --fm-duration-close: 90ms;
33
+ --fm-duration-hover: 60ms;
34
+ --fm-duration-toggle: 120ms;
35
+
36
+ /* ─── Density ───────────────────────────────────────────────────────── */
37
+ /* Outer inset between body and menu border. Inset is what keeps row
38
+ hover backgrounds from touching the menu's edge. */
39
+ --fm-body-padding-x: 4px;
40
+ --fm-body-padding-y: 4px;
41
+
42
+ /* Row geometry */
43
+ --fm-row-padding-x: 8px;
44
+ --fm-row-padding-y: 4px;
45
+ --fm-row-gap: 8px;
46
+ --fm-row-radius: 4px;
47
+ --fm-row-min-h: 28px;
48
+ --fm-row-big-min-h: 44px;
49
+
50
+ /* Section header */
51
+ --fm-section-padding-x: 8px;
52
+ --fm-section-padding-top: 8px;
53
+ --fm-section-padding-bot: 4px;
54
+
55
+ /* Header / footer chrome */
56
+ --fm-chrome-padding-x: 12px;
57
+ --fm-chrome-padding-y: 8px;
58
+
59
+ /* ─── Typography ────────────────────────────────────────────────────── */
60
+ --fm-font-size-row: 13px;
61
+ --fm-font-size-caption: 11px;
62
+ --fm-font-size-section: 10px;
63
+ --fm-font-weight-section: 600;
64
+ --fm-letter-section: 0.06em;
65
+ --fm-line-height-row: 1.3;
66
+
67
+ /* ─── States ────────────────────────────────────────────────────────── */
68
+ --fm-row-hover-bg: hsl(210 40% 96.1% / 0.6);
69
+ --fm-row-active-bg: hsl(210 40% 96.1%);
70
+ --fm-row-active-fg: hsl(222.2 47.4% 11.2%);
71
+ --fm-row-disabled-opacity: 0.5;
72
+ --fm-divider-color: var(--fm-surface-border);
73
+
74
+ /* Selection (multi-select check mark, switch on-state, etc.) */
75
+ --fm-accent: hsl(222.2 47.4% 11.2%);
76
+ --fm-accent-fg: hsl(210 40% 98%);
77
+ --fm-destructive: hsl(0 84.2% 60.2%);
78
+ --fm-muted-fg: hsl(215.4 16.3% 46.9%);
79
+ }
80
+
81
+ /* Dark mode — override only what differs. */
82
+ .dark,
83
+ [data-theme="dark"] {
84
+ --fm-surface-bg: hsl(222.2 84% 4.9%);
85
+ --fm-surface-fg: hsl(210 40% 98%);
86
+ --fm-surface-border: hsl(217.2 32.6% 17.5%);
87
+ --fm-row-hover-bg: hsl(217.2 32.6% 17.5% / 0.6);
88
+ --fm-row-active-bg: hsl(217.2 32.6% 17.5%);
89
+ --fm-row-active-fg: hsl(210 40% 98%);
90
+ --fm-accent: hsl(210 40% 98%);
91
+ --fm-accent-fg: hsl(222.2 47.4% 11.2%);
92
+ --fm-muted-fg: hsl(215 20.2% 65.1%);
93
+ }
94
+
95
+ /* ─── Components ──────────────────────────────────────────────────────── */
96
+
97
+ .fm-menu {
98
+ position: fixed;
99
+ z-index: var(--fm-z-menu);
100
+ display: flex;
101
+ flex-direction: column;
102
+ overflow: hidden;
103
+ background: var(--fm-surface-bg);
104
+ color: var(--fm-surface-fg);
105
+ border: 1px solid var(--fm-surface-border);
106
+ border-radius: var(--fm-surface-radius);
107
+ box-shadow: var(--fm-surface-shadow);
108
+ max-height: calc(100vh - 32px);
109
+ /* `transform-origin` is set inline from the resolved placement so the
110
+ scale-in animation appears to grow from the trigger edge. */
111
+ animation: fm-open var(--fm-duration-open) var(--fm-ease-decelerate);
112
+ will-change: transform, opacity;
113
+ }
114
+ .fm-menu--closing {
115
+ /* Forwards keeps the final keyframe (opacity 0) painted until React
116
+ removes the node, so the menu doesn't pop back to opacity:1 in the
117
+ gap between animationend and unmount. */
118
+ animation: fm-close var(--fm-duration-close, 90ms) var(--fm-ease-smooth) forwards;
119
+ }
120
+
121
+ @keyframes fm-open {
122
+ from {
123
+ opacity: 0;
124
+ transform: scale(0.96);
125
+ }
126
+ to {
127
+ opacity: 1;
128
+ transform: scale(1);
129
+ }
130
+ }
131
+ @keyframes fm-close {
132
+ from {
133
+ opacity: 1;
134
+ transform: scale(1);
135
+ }
136
+ to {
137
+ opacity: 0;
138
+ transform: scale(0.97);
139
+ }
140
+ }
141
+
142
+ .fm-dimmer {
143
+ position: fixed;
144
+ inset: 0;
145
+ z-index: var(--fm-z-dimmer);
146
+ animation: fm-dimmer-in var(--fm-duration-open) var(--fm-ease-smooth);
147
+ }
148
+ .fm-dimmer--closing {
149
+ animation: fm-dimmer-out var(--fm-duration-close, 90ms) var(--fm-ease-smooth) forwards;
150
+ }
151
+ .fm-dimmer--passthrough {
152
+ pointer-events: none;
153
+ }
154
+ .fm-dimmer--visible {
155
+ background: rgb(0 0 0 / 0.3);
156
+ }
157
+
158
+ @keyframes fm-dimmer-in {
159
+ from {
160
+ opacity: 0;
161
+ }
162
+ to {
163
+ opacity: 1;
164
+ }
165
+ }
166
+ @keyframes fm-dimmer-out {
167
+ from {
168
+ opacity: 1;
169
+ }
170
+ to {
171
+ opacity: 0;
172
+ }
173
+ }
174
+
175
+ .fm-body {
176
+ flex: 1 1 auto;
177
+ min-height: 0;
178
+ overflow: hidden;
179
+ padding: var(--fm-body-padding-y) var(--fm-body-padding-x);
180
+ }
181
+
182
+ .fm-chrome {
183
+ display: flex;
184
+ align-items: center;
185
+ gap: 8px;
186
+ padding: var(--fm-chrome-padding-y) var(--fm-chrome-padding-x);
187
+ border-bottom: 1px solid var(--fm-surface-border);
188
+ }
189
+ .fm-chrome--footer {
190
+ border-top: 1px solid var(--fm-surface-border);
191
+ border-bottom: 0;
192
+ }
193
+
194
+ .fm-list {
195
+ position: relative;
196
+ max-height: 60vh;
197
+ overflow-y: auto;
198
+ outline: none;
199
+ }
200
+
201
+ /* Horizontal toolbar variant — icon bars, format strips, segmented controls. */
202
+ .fm-list--horizontal {
203
+ display: flex;
204
+ flex-direction: row;
205
+ align-items: center;
206
+ gap: 2px;
207
+ max-height: none;
208
+ min-height: 0;
209
+ overflow-y: visible;
210
+ overflow-x: auto;
211
+ padding: 0;
212
+ }
213
+ .fm-list--horizontal > * {
214
+ display: contents;
215
+ }
216
+ .fm-list--horizontal .fm-row {
217
+ width: auto;
218
+ min-width: 28px;
219
+ min-height: 28px;
220
+ padding: 4px 6px;
221
+ gap: 0;
222
+ justify-content: center;
223
+ }
224
+ /* In horizontal mode the keyboard "active" state is subtle so it doesn't
225
+ shout when the toolbar is opened with a mouse. */
226
+ .fm-list--horizontal .fm-row[data-active="true"] {
227
+ background: var(--fm-row-hover-bg);
228
+ color: inherit;
229
+ }
230
+ .fm-list--horizontal .fm-row__name {
231
+ display: none;
232
+ }
233
+ .fm-list--horizontal .fm-row[data-show-name="true"] .fm-row__name {
234
+ display: inline;
235
+ margin-left: 4px;
236
+ }
237
+ .fm-list--horizontal .fm-row__icon {
238
+ color: var(--fm-surface-fg);
239
+ }
240
+
241
+ .fm-row {
242
+ display: flex;
243
+ width: 100%;
244
+ align-items: center;
245
+ gap: var(--fm-row-gap);
246
+ padding: var(--fm-row-padding-y) var(--fm-row-padding-x);
247
+ min-height: var(--fm-row-min-h);
248
+ font-size: var(--fm-font-size-row);
249
+ line-height: var(--fm-line-height-row);
250
+ border-radius: var(--fm-row-radius);
251
+ cursor: pointer;
252
+ user-select: none;
253
+ color: var(--fm-surface-fg);
254
+ transition: background-color var(--fm-duration-hover) var(--fm-ease-smooth);
255
+ }
256
+ .fm-row:hover {
257
+ background: var(--fm-row-hover-bg);
258
+ }
259
+ .fm-row[data-active="true"] {
260
+ background: var(--fm-row-active-bg);
261
+ color: var(--fm-row-active-fg);
262
+ }
263
+ /* Toggle-button "pressed" state for toolbar usage (active marks). */
264
+ .fm-row[data-pressed="true"] {
265
+ background: var(--fm-accent);
266
+ color: var(--fm-accent-fg);
267
+ }
268
+ .fm-row[data-pressed="true"] .fm-row__icon {
269
+ color: inherit;
270
+ }
271
+ .fm-row[aria-disabled="true"],
272
+ .fm-row[data-disabled="true"] {
273
+ opacity: var(--fm-row-disabled-opacity);
274
+ pointer-events: none;
275
+ }
276
+ .fm-row--big {
277
+ min-height: var(--fm-row-big-min-h);
278
+ padding-block: 6px;
279
+ }
280
+ .fm-row--readonly {
281
+ cursor: default;
282
+ }
283
+
284
+ .fm-row__name {
285
+ flex: 1 1 auto;
286
+ min-width: 0;
287
+ overflow: hidden;
288
+ text-overflow: ellipsis;
289
+ white-space: nowrap;
290
+ }
291
+ .fm-row__caption {
292
+ flex: 0 0 auto;
293
+ font-size: var(--fm-font-size-caption);
294
+ color: var(--fm-muted-fg);
295
+ }
296
+ .fm-row__icon {
297
+ flex: 0 0 auto;
298
+ color: var(--fm-muted-fg);
299
+ display: inline-flex;
300
+ }
301
+ .fm-row__suffix {
302
+ flex: 0 0 auto;
303
+ display: inline-flex;
304
+ align-items: center;
305
+ gap: 4px;
306
+ }
307
+ /* Defensive: if a slot ever ends up rendered but empty, collapse it so the
308
+ parent flex `gap` doesn't reserve space around nothing. */
309
+ .fm-row__icon:empty,
310
+ .fm-row__suffix:empty,
311
+ .fm-row__caption:empty {
312
+ display: none;
313
+ }
314
+
315
+ .fm-section {
316
+ padding: var(--fm-section-padding-top) var(--fm-section-padding-x) var(--fm-section-padding-bot);
317
+ font-size: var(--fm-font-size-section);
318
+ font-weight: var(--fm-font-weight-section);
319
+ letter-spacing: var(--fm-letter-section);
320
+ text-transform: uppercase;
321
+ color: var(--fm-muted-fg);
322
+ }
323
+
324
+ .fm-divider {
325
+ height: 1px;
326
+ background: var(--fm-divider-color);
327
+ margin: 4px 6px;
328
+ }
329
+
330
+ .fm-switch {
331
+ display: inline-flex;
332
+ align-items: center;
333
+ width: 28px;
334
+ height: 16px;
335
+ border-radius: 9999px;
336
+ background: hsl(214.3 31.8% 91.4%);
337
+ transition: background-color var(--fm-duration-toggle) var(--fm-ease-smooth);
338
+ flex: 0 0 auto;
339
+ }
340
+ .fm-switch[data-on="true"] {
341
+ background: var(--fm-accent);
342
+ }
343
+ .fm-switch__thumb {
344
+ width: 12px;
345
+ height: 12px;
346
+ border-radius: 9999px;
347
+ background: var(--fm-surface-bg);
348
+ transform: translateX(2px);
349
+ /* Spring easing on the thumb gives the toggle a small overshoot —
350
+ reads as more responsive than a symmetric ease. */
351
+ transition: transform var(--fm-duration-toggle) var(--fm-ease-spring);
352
+ box-shadow: 0 1px 2px rgb(0 0 0 / 0.15);
353
+ }
354
+ .fm-switch[data-on="true"] .fm-switch__thumb {
355
+ transform: translateX(14px);
356
+ }
357
+
358
+ /* DragOverlay clone — the dragged row renders here while floating with the
359
+ cursor. The styling deliberately exaggerates the lift (accent border + tall
360
+ shadow + slight scale) so the user always sees which row is moving against
361
+ the (semi-transparent) source placeholder underneath. */
362
+ .fm-row-overlay {
363
+ background: var(--fm-surface-bg);
364
+ border: 1px solid var(--fm-accent);
365
+ border-radius: var(--fm-row-radius);
366
+ box-shadow: 0 16px 32px -8px rgb(0 0 0 / 0.28), 0 4px 12px -2px rgb(0 0 0 / 0.18), 0 0 0 1px var(--fm-accent);
367
+ padding: 0 var(--fm-body-padding-x);
368
+ cursor: grabbing;
369
+ transform: scale(1.02);
370
+ transition: transform var(--fm-duration-hover) var(--fm-ease-smooth);
371
+ }
372
+ /* Source-row placeholder while its clone floats with the cursor — a dashed
373
+ outline marks the slot the cursor would return to, which is more legible
374
+ than opacity-alone fading. The data-attr lives on the wrapper that
375
+ useSortable() mounts; the visible row sits one level inside. */
376
+ [data-fm-drag-source="true"] .fm-row {
377
+ background: var(--fm-row-hover-bg);
378
+ outline: 1.5px dashed var(--fm-accent);
379
+ outline-offset: -2px;
380
+ border-radius: var(--fm-row-radius);
381
+ }
382
+
383
+ .fm-swatch {
384
+ width: 16px;
385
+ height: 16px;
386
+ border-radius: 3px;
387
+ border: 1px solid var(--fm-surface-border);
388
+ display: inline-flex;
389
+ align-items: center;
390
+ justify-content: center;
391
+ flex: 0 0 auto;
392
+ }
393
+ .fm-swatch__letter {
394
+ font-size: 10px;
395
+ font-weight: 700;
396
+ line-height: 1;
397
+ }
398
+
399
+ /* Checkbox row — right-aligned checkbox indicator. */
400
+ .fm-checkbox {
401
+ display: inline-flex;
402
+ align-items: center;
403
+ justify-content: center;
404
+ width: 16px;
405
+ height: 16px;
406
+ border-radius: 4px;
407
+ border: 1.5px solid var(--fm-surface-border);
408
+ margin-left: auto;
409
+ flex: 0 0 auto;
410
+ color: var(--fm-accent-fg);
411
+ background: transparent;
412
+ transition: background var(--fm-dur-toggle, 120ms) var(--fm-ease-smooth);
413
+ }
414
+ .fm-checkbox[data-checked="true"] {
415
+ background: var(--fm-accent);
416
+ border-color: var(--fm-accent);
417
+ }
418
+ .fm-checkbox[aria-disabled="true"] {
419
+ opacity: var(--fm-row-disabled-opacity, 0.5);
420
+ }
421
+
422
+ /* Chip row — pill style with optional trailing buttons. */
423
+ .fm-row--chip {
424
+ min-height: auto;
425
+ padding: 2px 4px 2px 6px;
426
+ border-radius: 999px;
427
+ background: var(--fm-row-hover-bg);
428
+ border: 1px solid var(--fm-surface-border);
429
+ display: inline-flex;
430
+ gap: 4px;
431
+ font-size: var(--fm-font-size-row);
432
+ }
433
+ .fm-row__chip-trailing,
434
+ .fm-row__chip-delete {
435
+ display: inline-flex;
436
+ align-items: center;
437
+ justify-content: center;
438
+ width: 16px;
439
+ height: 16px;
440
+ border-radius: 999px;
441
+ color: var(--fm-muted-fg);
442
+ background: transparent;
443
+ border: 0;
444
+ cursor: pointer;
445
+ }
446
+ .fm-row__chip-trailing:hover,
447
+ .fm-row__chip-delete:hover {
448
+ background: var(--fm-row-active-bg);
449
+ color: var(--fm-row-active-fg);
450
+ }
451
+
452
+ /* FilterRule row — relation icon + condition label + value preview. */
453
+ .fm-row--rule .fm-row__name {
454
+ flex: 1;
455
+ min-width: 0;
456
+ }
package/web/icons.js ADDED
@@ -0,0 +1,4 @@
1
+ // generated by tools/build.ts — pixelarticons (MIT), 24x24 grid
2
+ const ICON_PATHS = {"squares-four":"<path d=\"M4 2h16v2H4zM2 4h2v16H2zm2 7h16v2H4zm16-7h2v16h-2z\"/>\n <path d=\"M11 4h2v18h-2z\"/>\n <path d=\"M4 20h16v2H4z\"/>","timer":"<path d=\"M6 2h12v2H6zM2 6h2v12H2zm18 0h2v12h-2zm-2-2h2v2h-2zM4 4h2v2H4zm2 18h12v-2H6zm12-2h2v-2h-2zM4 20h2v-2H4zm7-14h2v7h-2zm2 7h2v2h-2zm2 2h2v2h-2z\"/>","coins":"<path d=\"M6 2h6v2H6zM4 4h2v2H4zm8 0h2v2h-2zm-8 8h2v2H4zm8 0h2v2h-2zm-6 2h6v2H6zM2 6h2v6H2zm12 0h2v6h-2z\"/>\n <path d=\"M14 8h4v2h-4zm-4 10h2v2h-2zm8-8h2v2h-2zm-6 10h2v2h-2zm6-2h2v2h-2z\"/>\n <path d=\"M12 20h6v2h-6zm-4-6h2v4H8zm12-2h2v6h-2zM7 6h4v2H7z\"/>\n <path d=\"M9 6h2v6H9zm6 8h2v4h-2zm-1-2h3v2h-3z\"/>","pulse":"<path d=\"M22 22H4v-2h18v2ZM4 20H2V2h2v18Zm4-6H6v-2h2v2Zm8 0h-2v-2h2v2Zm-6-2H8v-2h2v2Zm4 0h-2v-2h2v2Zm4 0h-2v-2h2v2Zm-6-2h-2V8h2v2Zm8 0h-2V8h2v2Zm2-2h-2V6h2v2Z\"/>","folder-simple":"<path d=\"M4 4h6v2H4zm0 14h16v2H4zM20 8h2v10h-2zM2 6h2v12H2zm8 0h10v2H10z\"/>","folders":"<path d=\"M9 3H7v14h2zM5 7H3v14h2zm12-6H9v2h8zm4 4h-2v12h2zm-2 12H9v2h10zm-4 4H5v2h10zm2-18h2v2h-2zm-4 0h2v6h-2z\"/>\n <path d=\"M13 7h6v2h-6zM5 5h2v2H5zm10 14h2v2h-2z\"/>","push-pin":"<path d=\"M7 2h10v2H7zM5 4h2v2H5zm14 0h-2v2h2zM7 17h2v2H7zm2 2h2v2H9zm6-2h2v2h-2zm-2 2h2v2h-2zm-2 2h2v2h-2zm-6-7h2v3H5zm12 0h2v3h-2zM3 6h2v8H3zm18 0h-2v8h2zM10 6h4v2h-4zM8 8h2v4H8zm2 4h4v2h-4zm4-4h2v4h-2z\"/>","push-pin-slash":"<path d=\"M7 2h10v2H7zM5 4h2v2H5zm14 0h-2v2h2zM7 17h2v2H7zm2 2h2v2H9zm6-2h2v2h-2zm-2 2h2v2h-2zm-2 2h2v2h-2zm-6-7h2v3H5zm12 0h2v3h-2zM3 6h2v8H3zm18 0h-2v8h2zM10 6h4v2h-4zM8 8h2v4H8zm2 4h4v2h-4zm4-4h2v4h-2z\"/>","x":"<path d=\"M7 19H5V17H7V19ZM19 19H17V17H19V19ZM9 15V17H7V15H9ZM17 17H15V15H17V17ZM11 15H9V13H11V15ZM15 15H13V13H15V15ZM13 13H11V11H13V13ZM11 11H9V9H11V11ZM15 11H13V9H15V11ZM9 9H7V7H9V9ZM17 9H15V7H17V9ZM7 7H5V5H7V7ZM19 7H17V5H19V7Z\"/>","plus":"<path d=\"M13 11h7v2h-7v7h-2v-7H4v-2h7V4h2v7Z\"/>","dots-three":"<path d=\"M3 9h2v2H3zm8 0h2v2h-2zm8 0h2v2h-2zM1 11h2v2H1zm8 0h2v2H9zm8 0h2v2h-2zM3 13h2v2H3zm8 0h2v2h-2zm8 0h2v2h-2zM5 11h2v2H5zm8 0h2v2h-2zm8 0h2v2h-2z\"/>","dots-three-vertical":"<path d=\"M15 3v2h-2V3zm0 8v2h-2v-2zm0 8v2h-2v-2zM13 1v2h-2V1zm0 8v2h-2V9zm0 8v2h-2v-2zM11 3v2H9V3zm0 8v2H9v-2zm0 8v2H9v-2zm2-14v2h-2V5zm0 8v2h-2v-2zm0 8v2h-2v-2z\"/>","keyboard":"<path d=\"M21 21H3v-2h18v2ZM3 19H1V5h2v14Zm20 0h-2V5h2v14Zm-5-2H6v-2h12v2Zm-9-4H7v-2h2v2Zm4 0h-2v-2h2v2Zm4 0h-2v-2h2v2ZM7 9H5V7h2v2Zm4 0H9V7h2v2Zm4 0h-2V7h2v2Zm4 0h-2V7h2v2Zm2-4H3V3h18v2Z\"/>","play":"<path d=\"M15 11h-2V9h2zm0 4h-2v-2h2zm-2 2h-2v-2h2zm0-8h-2V7h2zm-2-2H9V5h2zM9 21H7V3h2zm6-8h2v-2h-2zm-6 4h2v2H9z\"/>","tree-structure":"<path d=\"M6 4h2v2H6zm2-2h8v2H8zm10 4h2v4h-2zm2 4h2v6h-2zm-2 6h2v2h-2zM4 16h2v2H4zm-2-6h2v6H2zm4 8h12v2H6zM4 6h2v4H4z\"/>\n <path d=\"M11 18h2v4h-2zm5-14h2v2h-2z\"/>","robot":"<path d=\"M5 7h14v2H5zm0 12h14v2H5zM3 9h2v10H3zm16 0h2v10h-2zM1 13h2v2H1zm20 0h2v2h-2zM11 5h2v2h-2zM7 3h4v2H7zm1 9h2v4H8zm6 0h2v4h-2z\"/>","coin":"<path d=\"M6 2h6v2H6zM4 4h2v2H4zm8 0h2v2h-2zm-8 8h2v2H4zm8 0h2v2h-2zm-6 2h6v2H6zM2 6h2v6H2zm12 0h2v6h-2z\"/>\n <path d=\"M14 8h4v2h-4zm-4 10h2v2h-2zm8-8h2v2h-2zm-6 10h2v2h-2zm6-2h2v2h-2z\"/>\n <path d=\"M12 20h6v2h-6zm-4-6h2v4H8zm12-2h2v6h-2zM7 6h4v2H7z\"/>\n <path d=\"M9 6h2v6H9zm6 8h2v4h-2zm-1-2h3v2h-3z\"/>","cpu":"<path d=\"M5 3h14v2H5zm0 16h14v2H5zM3 5h2v14H3zm16 0h2v14h-2zM9 7h6v2H9zm0 8h6v2H9zM7 9h2v6H7zm8 0h2v6h-2zm-4-8h2v2h-2zm0 20h2v2h-2zM1 11h2v2H1zm20 0h2v2h-2zm0-4h2v2h-2zm0 8h2v2h-2zM1 15h2v2H1zm0-8h2v2H1zm6-6h2v2H7zm8 0h2v2h-2zm0 20h2v2h-2zm-8 0h2v2H7z\"/>","arrows-clockwise":"<path d=\"M13 22H11V15H13V22ZM5 21H3V19H5V21ZM21 19V21H19V19H21ZM7 19H5V17H7V19ZM19 19H17V17H19V19ZM9 17H7V15H9V17ZM17 17H15V15H17V17ZM9 13H2V11H9V13ZM22 13H15V11H22V13ZM9 9H7V7H9V9ZM13 9H11V2H13V9ZM17 9H15V7H17V9ZM7 7H5V5H7V7ZM19 7H17V5H19V7ZM5 5H3V3H5V5ZM21 5H19V3H21V5Z\"/>","lightning":"<path d=\"M4 20h16v2H4zM4 2h16v2H4zM2 4h2v16H2zm18 0h2v16h-2zM9 15h6v2H9zM7 9h2v6H7zm8 0h2v6h-2zm-4-2h2v5h-2z\"/>","clock":"<path d=\"M6 2h12v2H6zM2 6h2v12H2zm18 0h2v12h-2zm-2-2h2v2h-2zM4 4h2v2H4zm2 18h12v-2H6zm12-2h2v-2h-2zM4 20h2v-2H4zm7-14h2v7h-2zm2 7h2v2h-2zm2 2h2v2h-2z\"/>","wrench":"<path d=\"M18 22H13V24H11V22H6V20H18V22ZM4 22H2V20H4V22ZM22 22H20V20H22V22ZM6 20H4V18H6V20ZM20 20H18V18H20V20ZM4 18H2V13H0V11H2V6H4V18ZM8 18H6V16H8V18ZM22 11H24V13H22V18H20V13H16V16H8V8H16V11H20V6H22V11ZM10 10V14H14V10H10ZM8 8H6V6H8V8ZM6 6H4V4H6V6ZM20 6H18V4H20V6ZM4 4H2V2H4V4ZM13 2H18V4H6V2H11V0H13V2ZM22 4H20V2H22V4Z\"/>","file-text":"<path d=\"M6 4H4v16h2zm10-2H6v2h10zm4 4h-2v14h2zm-2 14H6v2h12zM16 4h2v2h-2zm-4 0h2v6h-2z\"/>\n <path d=\"M12 8h6v2h-6zm-4 8h8v2H8zm0-4h8v2H8zm0-4h2v2H8z\"/>","git-branch":"<path d=\"M2 14h8v2H2zm0 6h8v2H2zm0-4h2v4H2zm6 0h2v4H8zm6-14h8v2h-8zm0 6h8v2h-8zm0-4h2v4h-2zm6 0h2v4h-2zm-8 13h7v2h-7zm5-5h2v5h-2zM5 2h2v10H5z\"/>","git-commit":"<path d=\"M7 7h10v2H7zm0 2h2v6H7zm0 6h10v2H7zm8-6h2v6h-2zM0 11h5v2H0zm19 0h5v2h-5z\"/>","arrow-left":"<path d=\"M20 11v2H4v-2zM8 13v2H6v-2zm2 2v2H8v-2zm2 2v2h-2v-2zm-4-6V9H6v2z\"/>\n <path d=\"M10 15V7H8v8zm2 2V5h-2v12z\"/>","warning":"<path d=\"M4 2h16v2H4zm0 18h16v2H4zM20 4h2v16h-2zM2 4h2v16H2zm9 2h2v8h-2zm0 10h2v2h-2z\"/>","check":"<path d=\"M10 18H8v-2h2v2Zm-2-2H6v-2h2v2Zm4-2v2h-2v-2h2Zm-6 0H4v-2h2v2Zm8 0h-2v-2h2v2Zm2-2h-2v-2h2v2Zm2-2h-2V8h2v2Zm2-2h-2V6h2v2Z\"/>","copy":"<path d=\"M8 6h12v2H8zM4 2h12v2H4zm2 6h2v12H6zM2 4h2v12H2zm6 16h12v2H8zM20 8h2v12h-2zm-4-4h2v2h-2zM4 16h2v2H4z\"/>","arrow-square-out":"<path d=\"M11 5H5v2h6V5ZM5 7H3v12h2V7Zm12 12H5v2h12v-2Zm2-6h-2v6h2v-6Zm-8 0H9v2h2v-2Zm2-2h-2v2h2v-2Zm2-2h-2v2h2V9Zm2-2h-2v2h2V7Zm2-2h-2v2h2V5Zm2-2h-2v8h2V3Z\"/>\n <path d=\"M21 3h-8v2h8V3Z\"/>","moon":"<path d=\"M18 22H8v-2h10v2ZM8 20H6v-2h2v2Zm12 0h-2v-2h2v2ZM6 18H4v-2h2v2Zm16 0h-2v-4h-2v-2h2v-2h2v8ZM4 16H2V6h2v10Zm14 0h-6v-2h6v2Zm-6-2h-2v-2h2v2Zm-2-2H8V6h2v6ZM6 6H4V4h2v2Zm8-2h-2v2h-2V4H6V2h8v2Z\"/>","sun":"<path d=\"M13 22h-2v-3h2v3Zm-6-3H5v-2h2v2Zm12 0h-2v-2h2v2Zm-4-2H9v-2h6v2Zm-6-2H7V9h2v6Zm8 0h-2V9h2v6ZM5 13H2v-2h3v2Zm17 0h-3v-2h3v2Zm-7-4H9V7h6v2ZM7 7H5V5h2v2Zm12 0h-2V5h2v2Zm-6-2h-2V2h2v3Z\"/>","monitor":"<path d=\"M4 2h16v2H4zm0 14h16v2H4zM2 4h2v12H2zm18 0h2v12h-2zm-9 14h2v2h-2zm-3 2h8v2H8z\"/>","gear":"<path d=\"M18 22H13V24H11V22H6V20H18V22ZM4 22H2V20H4V22ZM22 22H20V20H22V22ZM6 20H4V18H6V20ZM20 20H18V18H20V20ZM4 18H2V13H0V11H2V6H4V18ZM8 18H6V16H8V18ZM22 11H24V13H22V18H20V13H16V16H8V8H16V11H20V6H22V11ZM10 10V14H14V10H10ZM8 8H6V6H8V8ZM6 6H4V4H6V6ZM20 6H18V4H20V6ZM4 4H2V2H4V4ZM13 2H18V4H6V2H11V0H13V2ZM22 4H20V2H22V4Z\"/>","book-open":"<path d=\"M2 3h9v2H2zM0 19h11v2H0zM13 3h9v2h-9zm0 16h11v2H13zM11 5h2v18h-2zM0 5h2v14H0zm22 0h2v14h-2zm-7 2h5v2h-5zm0 4h5v2h-5zm0 4h2v2h-2z\"/>","trash":"<path d=\"M18 22H6V20H18V22ZM9 6H15V4H17V6H22V8H20V20H18V8H6V20H4V8H2V6H7V4H9V6ZM15 4H9V2H15V4Z\"/>","magnifying-glass":"<path d=\"M22 22h-2v-2h2v2Zm-2-2h-2v-2h2v2Zm-6-2H6v-2h8v2Zm4 0h-2v-2h2v2ZM6 16H4v-2h2v2Zm10 0h-2v-2h2v2ZM4 14H2V6h2v8Zm14 0h-2V6h2v8ZM6 6H4V4h2v2Zm10 0h-2V4h2v2Zm-2-2H6V2h8v2Z\"/>","terminal-window":"<path d=\"M4 2h16v2H4zm0 18h16v2H4zM2 4h2v16H2zm18 0h2v16h-2zM6 16h2v2H6zm2-2h2v2H8zm-2-2h2v2H6z\"/>","stack":"<path d=\"M9 3H7v14h2zM5 7H3v14h2zm12-6H9v2h8zm4 4h-2v12h2zm-2 12H9v2h10zm-4 4H5v2h10zm2-18h2v2h-2zm-4 0h2v6h-2z\"/>\n <path d=\"M13 7h6v2h-6zM5 5h2v2H5zm10 14h2v2h-2z\"/>","chart-bar":"<path d=\"M4 20h18v2H4zM2 2h2v18H2zm16 11v3h-2v-3zM8 13v3H6v-3zm8-2v2H8v-2zm0 5v2H8v-2zm4-12v3h-2V4zM8 4v3H6V4zm10-2v2H8V2zm0 5v2H8V7z\"/>","clock-counter-clockwise":"<path d=\"M6 2h12v2H6zM2 6h2v12H2zm18 0h2v12h-2zm-2-2h2v2h-2zM4 4h2v2H4zm2 18h12v-2H6zm12-2h2v-2h-2zM4 20h2v-2H4zm7-14h2v7h-2zm2 7h2v2h-2zm2 2h2v2h-2z\"/>","brain":"<path d=\"M4 6h16v2H4zm0 14h16v2H4zM2 8h2v12H2zm18 0h2v12h-2z\"/>\n <path d=\"M11 4h2v4h-2zm-3 6h2v2H8zm6 0h4v2h-4zm-1-8h4v2h-4zM0 12h2v2H0zm22 0h2v2h-2zm-12 4h4v2h-4zm-2-2h2v2H8zm6 0h2v2h-2z\"/>","eye":"<path d=\"M16 20H8v-2h8v2Zm-8-2H4v-2h4v2Zm12 0h-4v-2h4v2ZM4 16H2v-2h2v2Zm10-6h-2v2h2v-2h2v4h-2v2h-4v-2H8v-4h2V8h4v2Zm8 6h-2v-2h2v2ZM2 14H0v-4h2v4Zm22 0h-2v-4h2v4ZM4 10H2V8h2v2Zm18 0h-2V8h2v2ZM8 8H4V6h4v2Zm12 0h-4V6h4v2Zm-4-2H8V4h8v2Z\"/>","pause":"<path d=\"M10 20H4V4h6v16Zm8-16v16h-6V4h6Zm-4 2v12h2V6h-2ZM6 18h2V6H6v12Z\"/>","stop":"<path d=\"M20 20H4V4H20V20ZM6 18H18V6H6V18ZM14 14H10V10H14V14Z\"/>","broadcast":"<path d=\"M4 2h16v2H4zm0 18h16v2H4zM20 4h2v16h-2zM2 4h2v16H2zm5 11h2v2H7zm0-4h4v2H7zm0-4h6v2H7zm4 6h2v4h-2zm4-2h2v6h-2zm-2-2h2v2h-2z\"/>","plugs-connected":"<path d=\"M16 18h-3v4h-2v-4H8v-2h8v2Zm-8-2H6v-2h2v2Zm10 0h-2v-2h2v2Zm-8-9h4V2h2v5h5v2h-1v5h-2V9H6v5H4V9H3V7h5V2h2v5Z\"/>","plugs":"<path d=\"M16 18h-3v4h-2v-4H8v-2h8v2Zm-8-2H6v-2h2v2Zm10 0h-2v-2h2v2Zm-8-9h4V2h2v5h5v2h-1v5h-2V9H6v5H4V9H3V7h5V2h2v5Z\"/>","currency-dollar":"<path d=\"M17 18h-4v4h-2v-4H7v-2h10v2Zm2-2h-2v-3h2v3Zm-2-3H7v-2h10v2ZM7 11H5V8h2v3Zm6-5h4v2H7V6h4V2h2v4Z\"/>","rows":"<path d=\"M10 5h12v2H10zm0 4h8v2h-8zm0 4h12v2H10zm0 4h8v2h-8zm-4-6H4V9h2v2ZM4 9H2V7h2v2Zm4 0H6V7h2v2ZM6 7H4V5h2v2Zm-2 6h2v2H4zm0 4h2v2H4zm-2 0v-2h2v2zm4 0v-2h2v2z\"/>","list-bullets":"<path d=\"M10 5h12v2H10zm0 4h8v2h-8zm0 4h12v2H10zm0 4h8v2h-8zm-4-6H4V9h2v2ZM4 9H2V7h2v2Zm4 0H6V7h2v2ZM6 7H4V5h2v2Zm-2 6h2v2H4zm0 4h2v2H4zm-2 0v-2h2v2zm4 0v-2h2v2z\"/>","sliders":"<path d=\"M8 14H7v6H5v-6H2v-2h6v2Zm5 6h-2V10h2v10Zm9-2h-3v2h-2v-2h-1v-2h6v2Zm-3-4h-2V4h2v10ZM7 10H5V4h2v6Zm6-4h2v2H9V6h2V4h2v2Z\"/>","comment-text":"<path d=\"M4 2h16v2H4zm0 14h14v2H4zm2-6h6v2H6zm0-4h12v2H6zM2 4h2v12H2zm18 0h2v18h-2zm-2 14h2v2h-2z\"/>","arrow-bar-left":"<path d=\"M4 4v16H2V4zm18 7v2H6v-2zm-12 2v2H8v-2zm2 2v2h-2v-2zm2 2v2h-2v-2zm-4-8v2H8V9zm2-2v2h-2V7zm2-2v2h-2V5z\"/>","arrow-bar-right":"<path d=\"M20 4v16h2V4zM2 11v2h16v-2zm12 2v2h2v-2zm-2 2v2h2v-2zm-2 2v2h2v-2zm4-8v2h2V9zm-2-2v2h2V7zm-2-2v2h2V5z\"/>","git-pull-request":"<path d=\"M2 10h8V8H2zm0-6h8V2H2zm0 4h2V4H2zm6 0h2V4H8zm6 14h8v-2h-8zm0-6h8v-2h-8zm0 4h2v-4h-2zm6 0h2v-4h-2zM12 7h5V5h-5zM5 22h2V12H5z\"/>","star":"<path d=\"M5 20H8V22H3V16H5V20ZM21 22H16V20H19V16H21V22ZM10 20H8V18H10V20ZM16 20H14V18H16V20ZM14 18H10V16H14V18ZM7 16H5V13H7V16ZM19 16H17V13H19V16ZM5 13H3V11H5V13ZM21 13H19V11H21V13ZM9 9H3V11H1V7H9V9ZM23 11H21V9H15V7H23V11ZM11 7H9V3H11V7ZM15 7H13V3H15V7ZM13 3H11V1H13V3Z\"/>"};
3
+ window.icon = (name, size = 16, cls = "") => { const p = ICON_PATHS[name]; if (!p) return ""; return `<svg class="ph ${cls}" width="${size}" height="${size}" viewBox="0 0 24 24" fill="currentColor" shape-rendering="crispEdges" aria-hidden="true">${p}</svg>`; };
4
+ window.ICON_NAMES = ["squares-four","timer","coins","pulse","folder-simple","folders","push-pin","push-pin-slash","x","plus","dots-three","dots-three-vertical","keyboard","play","tree-structure","robot","coin","cpu","arrows-clockwise","lightning","clock","wrench","file-text","git-branch","git-commit","arrow-left","warning","check","copy","arrow-square-out","moon","sun","monitor","gear","book-open","trash","magnifying-glass","terminal-window","stack","chart-bar","clock-counter-clockwise","brain","eye","pause","stop","broadcast","plugs-connected","plugs","currency-dollar","rows","list-bullets","sliders","comment-text","arrow-bar-left","arrow-bar-right","git-pull-request","star"];