@salmexio/ui 1.1.0 → 1.2.1

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.
Files changed (40) hide show
  1. package/dist/dialogs/ContextMenu/ContextMenu.svelte +11 -1
  2. package/dist/dialogs/Modal/Modal.svelte +34 -1
  3. package/dist/feedback/Alert/Alert.svelte +54 -11
  4. package/dist/feedback/ProgressBar/ProgressBar.svelte +11 -8
  5. package/dist/feedback/ProgressBar/ProgressBar.svelte.d.ts +2 -2
  6. package/dist/feedback/ProgressBar/ProgressBar.svelte.d.ts.map +1 -1
  7. package/dist/feedback/Skeleton/Skeleton.svelte +7 -3
  8. package/dist/feedback/Spinner/Spinner.svelte +2 -0
  9. package/dist/feedback/Toast/Toaster.svelte +35 -3
  10. package/dist/forms/Checkbox/Checkbox.svelte +30 -7
  11. package/dist/forms/Select/Select.svelte +19 -3
  12. package/dist/forms/Slider/Slider.svelte +41 -13
  13. package/dist/forms/Slider/Slider.svelte.d.ts +1 -1
  14. package/dist/forms/Slider/Slider.svelte.d.ts.map +1 -1
  15. package/dist/forms/TextInput/TextInput.svelte +19 -1
  16. package/dist/forms/Textarea/Textarea.svelte +18 -3
  17. package/dist/forms/Toggle/Toggle.svelte +70 -11
  18. package/dist/layout/Card/Card.svelte +125 -4
  19. package/dist/layout/Card/Card.svelte.d.ts +3 -0
  20. package/dist/layout/Card/Card.svelte.d.ts.map +1 -1
  21. package/dist/layout/ThermalBackground/ThermalBackground.svelte +2 -40
  22. package/dist/layout/ThermalBackground/ThermalBackground.svelte.d.ts +0 -2
  23. package/dist/layout/ThermalBackground/ThermalBackground.svelte.d.ts.map +1 -1
  24. package/dist/navigation/CommandPalette/CommandPalette.svelte +37 -3
  25. package/dist/navigation/NavMenu/NavMenu.svelte +800 -0
  26. package/dist/navigation/NavMenu/NavMenu.svelte.d.ts +81 -0
  27. package/dist/navigation/NavMenu/NavMenu.svelte.d.ts.map +1 -0
  28. package/dist/navigation/NavMenu/index.d.ts +3 -0
  29. package/dist/navigation/NavMenu/index.d.ts.map +1 -0
  30. package/dist/navigation/NavMenu/index.js +1 -0
  31. package/dist/navigation/Tabs/Tabs.svelte +37 -8
  32. package/dist/navigation/index.d.ts +2 -0
  33. package/dist/navigation/index.d.ts.map +1 -1
  34. package/dist/navigation/index.js +1 -0
  35. package/dist/primitives/Badge/Badge.svelte +55 -10
  36. package/dist/primitives/Button/Button.svelte +220 -71
  37. package/dist/primitives/Tooltip/Tooltip.svelte +33 -1
  38. package/dist/primitives/Tooltip/Tooltip.svelte.d.ts.map +1 -1
  39. package/dist/styles/tokens.css +1 -0
  40. package/package.json +1 -1
@@ -0,0 +1,800 @@
1
+ <!--
2
+ @component NavMenu
3
+
4
+ INFRARED — Persistent navigation menu with collapsible groups, sub-items,
5
+ icons, badges, and keyboard navigation. Built for sidebars, settings panels,
6
+ and any vertical nav structure.
7
+
8
+ @example
9
+ <NavMenu
10
+ items={[
11
+ {
12
+ type: 'group',
13
+ label: 'Getting Started',
14
+ items: [
15
+ { type: 'item', id: 'intro', label: 'Introduction', href: '/' },
16
+ { type: 'item', id: 'install', label: 'Installation', href: '/install', badge: 'New' },
17
+ ]
18
+ },
19
+ { type: 'separator' },
20
+ {
21
+ type: 'item',
22
+ id: 'settings',
23
+ label: 'Settings',
24
+ href: '/settings',
25
+ icon: settingsIcon,
26
+ }
27
+ ]}
28
+ activeId="intro"
29
+ />
30
+ -->
31
+ <script lang="ts" module>
32
+ import type { Snippet } from 'svelte';
33
+
34
+ export interface NavMenuItem {
35
+ type: 'item';
36
+ /** Unique identifier for active state matching */
37
+ id: string;
38
+ /** Display label */
39
+ label: string;
40
+ /** Link target (renders <a>); omit for button behavior */
41
+ href?: string;
42
+ /** Icon snippet */
43
+ icon?: Snippet;
44
+ /** Badge text (e.g. "New", count) */
45
+ badge?: string | number;
46
+ /** Disabled state */
47
+ disabled?: boolean;
48
+ /** Nested sub-items */
49
+ children?: NavMenuItem[];
50
+ }
51
+
52
+ export interface NavMenuGroup {
53
+ type: 'group';
54
+ /** Group heading label */
55
+ label: string;
56
+ /** Whether the group starts collapsed */
57
+ defaultCollapsed?: boolean;
58
+ /** Items in this group */
59
+ items: NavMenuEntry[];
60
+ }
61
+
62
+ export interface NavMenuSeparator {
63
+ type: 'separator';
64
+ }
65
+
66
+ export type NavMenuEntry = NavMenuItem | NavMenuGroup | NavMenuSeparator;
67
+ </script>
68
+
69
+ <script lang="ts">
70
+ import { cn } from '../../utils/cn.js';
71
+ import { Keys } from '../../utils/keyboard.js';
72
+
73
+ interface Props {
74
+ /** Menu entries: items, groups, and separators */
75
+ items: NavMenuEntry[];
76
+ /** ID of the currently active item */
77
+ activeId?: string;
78
+ /** Fired when an item is activated (click or Enter) */
79
+ onselect?: (id: string, href?: string) => void;
80
+ /** Whether groups can be collapsed (default true) */
81
+ collapsible?: boolean;
82
+ /** Additional CSS class */
83
+ class?: string;
84
+ /** Accessible label for the nav element */
85
+ label?: string;
86
+ /** Test ID */
87
+ testId?: string;
88
+ }
89
+
90
+ let {
91
+ items,
92
+ activeId = '',
93
+ onselect,
94
+ collapsible = true,
95
+ class: className = '',
96
+ label = 'Navigation',
97
+ testId
98
+ }: Props = $props();
99
+
100
+ // Track collapsed state per group label
101
+ let collapsedGroups = $state<Record<string, boolean>>({});
102
+
103
+ // Initialize collapsed state from defaultCollapsed
104
+ $effect(() => {
105
+ for (const entry of items) {
106
+ if (entry.type === 'group' && entry.defaultCollapsed && !(entry.label in collapsedGroups)) {
107
+ collapsedGroups[entry.label] = true;
108
+ }
109
+ }
110
+ });
111
+
112
+ function toggleGroup(label: string) {
113
+ collapsedGroups[label] = !collapsedGroups[label];
114
+ }
115
+
116
+ function isGroupCollapsed(label: string): boolean {
117
+ return !!collapsedGroups[label];
118
+ }
119
+
120
+ function handleItemClick(item: NavMenuItem) {
121
+ if (item.disabled) return;
122
+ onselect?.(item.id, item.href);
123
+ }
124
+
125
+ function handleItemKeydown(e: KeyboardEvent, item: NavMenuItem) {
126
+ if (e.key === Keys.Enter || e.key === Keys.Space) {
127
+ e.preventDefault();
128
+ handleItemClick(item);
129
+ }
130
+ }
131
+
132
+ function handleGroupKeydown(e: KeyboardEvent, label: string) {
133
+ if (e.key === Keys.Enter || e.key === Keys.Space) {
134
+ e.preventDefault();
135
+ toggleGroup(label);
136
+ }
137
+ }
138
+
139
+ // Track expanded sub-items per item id
140
+ let expandedSubs = $state<Record<string, boolean>>({});
141
+
142
+ function toggleSub(id: string) {
143
+ expandedSubs[id] = !expandedSubs[id];
144
+ }
145
+
146
+ function isSubExpanded(id: string): boolean {
147
+ // Auto-expand if a child is active
148
+ return !!expandedSubs[id];
149
+ }
150
+
151
+ function hasActiveChild(item: NavMenuItem, currentActiveId: string): boolean {
152
+ if (!item.children) return false;
153
+ return item.children.some(
154
+ (child) => child.id === currentActiveId || hasActiveChild(child, currentActiveId)
155
+ );
156
+ }
157
+
158
+ // Auto-expand parents of active item
159
+ $effect(() => {
160
+ if (!activeId) return;
161
+ for (const entry of items) {
162
+ if (entry.type === 'group') {
163
+ for (const gEntry of entry.items) {
164
+ if (gEntry.type === 'item' && gEntry.children && hasActiveChild(gEntry, activeId)) {
165
+ expandedSubs[gEntry.id] = true;
166
+ }
167
+ }
168
+ }
169
+ if (entry.type === 'item' && entry.children && hasActiveChild(entry, activeId)) {
170
+ expandedSubs[entry.id] = true;
171
+ }
172
+ }
173
+ });
174
+ </script>
175
+
176
+ <nav
177
+ class={cn('sx-navmenu', className)}
178
+ aria-label={label}
179
+ data-testid={testId}
180
+ >
181
+ {#each items as entry}
182
+ {#if entry.type === 'separator'}
183
+ <div class="sx-navmenu-separator" role="separator"></div>
184
+
185
+ {:else if entry.type === 'group'}
186
+ <div class="sx-navmenu-group" role="group" aria-label={entry.label}>
187
+ {#if collapsible}
188
+ <button
189
+ type="button"
190
+ class={cn(
191
+ 'sx-navmenu-group-trigger',
192
+ isGroupCollapsed(entry.label) && 'sx-navmenu-group-collapsed'
193
+ )}
194
+ aria-expanded={!isGroupCollapsed(entry.label)}
195
+ onclick={() => toggleGroup(entry.label)}
196
+ onkeydown={(e) => handleGroupKeydown(e, entry.label)}
197
+ >
198
+ <span class="sx-navmenu-chevron" aria-hidden="true">
199
+ <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
200
+ <path d="M4 2.5L7.5 6L4 9.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
201
+ </svg>
202
+ </span>
203
+ <span class="sx-navmenu-group-label">{entry.label}</span>
204
+ </button>
205
+ {:else}
206
+ <span class="sx-navmenu-group-trigger sx-navmenu-group-static" aria-hidden="true">
207
+ <span class="sx-navmenu-group-label">{entry.label}</span>
208
+ </span>
209
+ {/if}
210
+
211
+ {#if !collapsible || !isGroupCollapsed(entry.label)}
212
+ <ul class="sx-navmenu-list" role="list">
213
+ {#each entry.items as gEntry}
214
+ {#if gEntry.type === 'separator'}
215
+ <li role="separator" class="sx-navmenu-separator"></li>
216
+ {:else if gEntry.type === 'item'}
217
+ {@const isActive = gEntry.id === activeId}
218
+ {@const hasSubs = !!gEntry.children?.length}
219
+ {@const isExpanded = hasSubs && (isSubExpanded(gEntry.id) || hasActiveChild(gEntry, activeId))}
220
+ <li>
221
+ {#if gEntry.href && !gEntry.disabled}
222
+ <a
223
+ href={gEntry.href}
224
+ class={cn(
225
+ 'sx-navmenu-item',
226
+ isActive && 'sx-navmenu-item-active',
227
+ gEntry.disabled && 'sx-navmenu-item-disabled'
228
+ )}
229
+ aria-current={isActive ? 'page' : undefined}
230
+ onclick={() => handleItemClick(gEntry)}
231
+ >
232
+ {#if gEntry.icon}
233
+ <span class="sx-navmenu-icon" aria-hidden="true">{@render gEntry.icon()}</span>
234
+ {/if}
235
+ <span class="sx-navmenu-label">{gEntry.label}</span>
236
+ {#if gEntry.badge !== undefined && gEntry.badge !== null && gEntry.badge !== ''}
237
+ <span class="sx-navmenu-badge">{gEntry.badge}</span>
238
+ {/if}
239
+ {#if hasSubs}
240
+ <button
241
+ type="button"
242
+ class={cn('sx-navmenu-sub-toggle', isExpanded && 'sx-navmenu-sub-toggle-open')}
243
+ aria-label="Toggle {gEntry.label} sub-items"
244
+ aria-expanded={isExpanded}
245
+ onclick={(e) => { e.stopPropagation(); toggleSub(gEntry.id); }}
246
+ >
247
+ <svg width="10" height="10" viewBox="0 0 12 12" fill="none">
248
+ <path d="M4 2.5L7.5 6L4 9.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
249
+ </svg>
250
+ </button>
251
+ {/if}
252
+ </a>
253
+ {:else}
254
+ <button
255
+ type="button"
256
+ class={cn(
257
+ 'sx-navmenu-item',
258
+ isActive && 'sx-navmenu-item-active',
259
+ gEntry.disabled && 'sx-navmenu-item-disabled'
260
+ )}
261
+ aria-current={isActive ? 'page' : undefined}
262
+ disabled={gEntry.disabled}
263
+ onclick={() => {
264
+ if (hasSubs) toggleSub(gEntry.id);
265
+ handleItemClick(gEntry);
266
+ }}
267
+ onkeydown={(e) => handleItemKeydown(e, gEntry)}
268
+ >
269
+ {#if gEntry.icon}
270
+ <span class="sx-navmenu-icon" aria-hidden="true">{@render gEntry.icon()}</span>
271
+ {/if}
272
+ <span class="sx-navmenu-label">{gEntry.label}</span>
273
+ {#if gEntry.badge !== undefined && gEntry.badge !== null && gEntry.badge !== ''}
274
+ <span class="sx-navmenu-badge">{gEntry.badge}</span>
275
+ {/if}
276
+ {#if hasSubs}
277
+ <span
278
+ class={cn('sx-navmenu-sub-toggle', isExpanded && 'sx-navmenu-sub-toggle-open')}
279
+ aria-hidden="true"
280
+ >
281
+ <svg width="10" height="10" viewBox="0 0 12 12" fill="none">
282
+ <path d="M4 2.5L7.5 6L4 9.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
283
+ </svg>
284
+ </span>
285
+ {/if}
286
+ </button>
287
+ {/if}
288
+
289
+ <!-- Sub-items -->
290
+ {#if hasSubs && isExpanded}
291
+ <ul class="sx-navmenu-sublist" role="list">
292
+ {#each gEntry.children as subItem}
293
+ {@const isSubActive = subItem.id === activeId}
294
+ <li>
295
+ {#if subItem.href && !subItem.disabled}
296
+ <a
297
+ href={subItem.href}
298
+ class={cn(
299
+ 'sx-navmenu-subitem',
300
+ isSubActive && 'sx-navmenu-subitem-active',
301
+ subItem.disabled && 'sx-navmenu-item-disabled'
302
+ )}
303
+ aria-current={isSubActive ? 'page' : undefined}
304
+ onclick={() => handleItemClick(subItem)}
305
+ >
306
+ {#if subItem.icon}
307
+ <span class="sx-navmenu-icon" aria-hidden="true">{@render subItem.icon()}</span>
308
+ {/if}
309
+ <span class="sx-navmenu-label">{subItem.label}</span>
310
+ {#if subItem.badge !== undefined && subItem.badge !== null && subItem.badge !== ''}
311
+ <span class="sx-navmenu-badge">{subItem.badge}</span>
312
+ {/if}
313
+ </a>
314
+ {:else}
315
+ <button
316
+ type="button"
317
+ class={cn(
318
+ 'sx-navmenu-subitem',
319
+ isSubActive && 'sx-navmenu-subitem-active',
320
+ subItem.disabled && 'sx-navmenu-item-disabled'
321
+ )}
322
+ disabled={subItem.disabled}
323
+ aria-current={isSubActive ? 'page' : undefined}
324
+ onclick={() => handleItemClick(subItem)}
325
+ onkeydown={(e) => handleItemKeydown(e, subItem)}
326
+ >
327
+ {#if subItem.icon}
328
+ <span class="sx-navmenu-icon" aria-hidden="true">{@render subItem.icon()}</span>
329
+ {/if}
330
+ <span class="sx-navmenu-label">{subItem.label}</span>
331
+ {#if subItem.badge !== undefined && subItem.badge !== null && subItem.badge !== ''}
332
+ <span class="sx-navmenu-badge">{subItem.badge}</span>
333
+ {/if}
334
+ </button>
335
+ {/if}
336
+ </li>
337
+ {/each}
338
+ </ul>
339
+ {/if}
340
+ </li>
341
+ {/if}
342
+ {/each}
343
+ </ul>
344
+ {/if}
345
+ </div>
346
+
347
+ {:else if entry.type === 'item'}
348
+ {@const isActive = entry.id === activeId}
349
+ {@const hasSubs = !!entry.children?.length}
350
+ {@const isExpanded = hasSubs && (isSubExpanded(entry.id) || hasActiveChild(entry, activeId))}
351
+ <div class="sx-navmenu-solo">
352
+ {#if entry.href && !entry.disabled}
353
+ <a
354
+ href={entry.href}
355
+ class={cn(
356
+ 'sx-navmenu-item',
357
+ isActive && 'sx-navmenu-item-active',
358
+ entry.disabled && 'sx-navmenu-item-disabled'
359
+ )}
360
+ aria-current={isActive ? 'page' : undefined}
361
+ onclick={() => handleItemClick(entry)}
362
+ >
363
+ {#if entry.icon}
364
+ <span class="sx-navmenu-icon" aria-hidden="true">{@render entry.icon()}</span>
365
+ {/if}
366
+ <span class="sx-navmenu-label">{entry.label}</span>
367
+ {#if entry.badge !== undefined && entry.badge !== null && entry.badge !== ''}
368
+ <span class="sx-navmenu-badge">{entry.badge}</span>
369
+ {/if}
370
+ </a>
371
+ {:else}
372
+ <button
373
+ type="button"
374
+ class={cn(
375
+ 'sx-navmenu-item',
376
+ isActive && 'sx-navmenu-item-active',
377
+ entry.disabled && 'sx-navmenu-item-disabled'
378
+ )}
379
+ disabled={entry.disabled}
380
+ aria-current={isActive ? 'page' : undefined}
381
+ onclick={() => {
382
+ if (hasSubs) toggleSub(entry.id);
383
+ handleItemClick(entry);
384
+ }}
385
+ onkeydown={(e) => handleItemKeydown(e, entry)}
386
+ >
387
+ {#if entry.icon}
388
+ <span class="sx-navmenu-icon" aria-hidden="true">{@render entry.icon()}</span>
389
+ {/if}
390
+ <span class="sx-navmenu-label">{entry.label}</span>
391
+ {#if entry.badge !== undefined && entry.badge !== null && entry.badge !== ''}
392
+ <span class="sx-navmenu-badge">{entry.badge}</span>
393
+ {/if}
394
+ {#if hasSubs}
395
+ <span
396
+ class={cn('sx-navmenu-sub-toggle', isExpanded && 'sx-navmenu-sub-toggle-open')}
397
+ aria-hidden="true"
398
+ >
399
+ <svg width="10" height="10" viewBox="0 0 12 12" fill="none">
400
+ <path d="M4 2.5L7.5 6L4 9.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
401
+ </svg>
402
+ </span>
403
+ {/if}
404
+ </button>
405
+ {/if}
406
+
407
+ {#if hasSubs && isExpanded}
408
+ <ul class="sx-navmenu-sublist" role="list">
409
+ {#each entry.children as subItem}
410
+ {@const isSubActive = subItem.id === activeId}
411
+ <li>
412
+ {#if subItem.href && !subItem.disabled}
413
+ <a
414
+ href={subItem.href}
415
+ class={cn(
416
+ 'sx-navmenu-subitem',
417
+ isSubActive && 'sx-navmenu-subitem-active',
418
+ subItem.disabled && 'sx-navmenu-item-disabled'
419
+ )}
420
+ aria-current={isSubActive ? 'page' : undefined}
421
+ onclick={() => handleItemClick(subItem)}
422
+ >
423
+ {#if subItem.icon}
424
+ <span class="sx-navmenu-icon" aria-hidden="true">{@render subItem.icon()}</span>
425
+ {/if}
426
+ <span class="sx-navmenu-label">{subItem.label}</span>
427
+ {#if subItem.badge !== undefined && subItem.badge !== null && subItem.badge !== ''}
428
+ <span class="sx-navmenu-badge">{subItem.badge}</span>
429
+ {/if}
430
+ </a>
431
+ {:else}
432
+ <button
433
+ type="button"
434
+ class={cn(
435
+ 'sx-navmenu-subitem',
436
+ isSubActive && 'sx-navmenu-subitem-active',
437
+ subItem.disabled && 'sx-navmenu-item-disabled'
438
+ )}
439
+ disabled={subItem.disabled}
440
+ aria-current={isSubActive ? 'page' : undefined}
441
+ onclick={() => handleItemClick(subItem)}
442
+ onkeydown={(e) => handleItemKeydown(e, subItem)}
443
+ >
444
+ {#if subItem.icon}
445
+ <span class="sx-navmenu-icon" aria-hidden="true">{@render subItem.icon()}</span>
446
+ {/if}
447
+ <span class="sx-navmenu-label">{subItem.label}</span>
448
+ {#if subItem.badge !== undefined && subItem.badge !== null && subItem.badge !== ''}
449
+ <span class="sx-navmenu-badge">{subItem.badge}</span>
450
+ {/if}
451
+ </button>
452
+ {/if}
453
+ </li>
454
+ {/each}
455
+ </ul>
456
+ {/if}
457
+ </div>
458
+ {/if}
459
+ {/each}
460
+ </nav>
461
+
462
+ <style>
463
+ /* ========================================
464
+ ROOT
465
+ ======================================== */
466
+ .sx-navmenu {
467
+ display: flex;
468
+ flex-direction: column;
469
+ gap: var(--sx-space-1);
470
+ padding: var(--sx-space-2) 0;
471
+ font-family: var(--sx-font-body);
472
+ }
473
+
474
+ /* ========================================
475
+ SEPARATOR
476
+ ======================================== */
477
+ .sx-navmenu-separator {
478
+ height: 0;
479
+ margin: var(--sx-space-2) var(--sx-space-3);
480
+ border-top: 1px solid var(--sx-color-border);
481
+ box-shadow: 0 1px 0 0 rgba(255, 255, 255, 0.02);
482
+ }
483
+
484
+ /* ========================================
485
+ GROUP — Collapsible section
486
+ ======================================== */
487
+ .sx-navmenu-group {
488
+ display: flex;
489
+ flex-direction: column;
490
+ }
491
+
492
+ .sx-navmenu-group-trigger {
493
+ display: flex;
494
+ align-items: center;
495
+ gap: var(--sx-space-1-5);
496
+ padding: var(--sx-space-2) var(--sx-space-3);
497
+ margin: 0;
498
+ border: none;
499
+ background: transparent;
500
+ cursor: pointer;
501
+ user-select: none;
502
+ font-family: var(--sx-font-body);
503
+ font-size: var(--sx-text-xs);
504
+ font-weight: 600;
505
+ text-transform: uppercase;
506
+ letter-spacing: 0.06em;
507
+ color: var(--sx-color-text-disabled);
508
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
509
+ transition:
510
+ color var(--sx-transition-fast);
511
+ border-radius: var(--sx-radius-sm);
512
+ }
513
+
514
+ .sx-navmenu-group-trigger:hover {
515
+ color: var(--sx-color-text-secondary);
516
+ }
517
+
518
+ .sx-navmenu-group-trigger:focus-visible {
519
+ outline: 2px solid var(--sx-color-primary);
520
+ outline-offset: 2px;
521
+ }
522
+
523
+ .sx-navmenu-group-label {
524
+ flex: 1;
525
+ text-align: left;
526
+ }
527
+
528
+ /* Static (non-collapsible) group header — no cursor, no hover */
529
+ .sx-navmenu-group-static {
530
+ cursor: default;
531
+ pointer-events: none;
532
+ }
533
+
534
+ /* ========================================
535
+ CHEVRON — Rotates on expand/collapse
536
+ ======================================== */
537
+ .sx-navmenu-chevron {
538
+ display: flex;
539
+ align-items: center;
540
+ justify-content: center;
541
+ width: 14px;
542
+ height: 14px;
543
+ flex-shrink: 0;
544
+ transition: transform var(--sx-transition-fast);
545
+ transform: rotate(90deg);
546
+ color: var(--sx-color-text-disabled);
547
+ }
548
+
549
+ .sx-navmenu-group-collapsed .sx-navmenu-chevron {
550
+ transform: rotate(0deg);
551
+ }
552
+
553
+ /* ========================================
554
+ LIST — Items within a group
555
+ ======================================== */
556
+ .sx-navmenu-list {
557
+ list-style: none;
558
+ margin: 0;
559
+ padding: 0 0 0 calc(var(--sx-space-3) + 2px);
560
+ display: flex;
561
+ flex-direction: column;
562
+ gap: 1px;
563
+ border-left: none;
564
+ margin-left: var(--sx-space-3);
565
+ box-shadow:
566
+ inset 2px 0 0 0 rgba(0, 0, 0, 0.25),
567
+ inset 3px 0 0 0 rgba(255, 255, 255, 0.03);
568
+ }
569
+
570
+ /* ========================================
571
+ ITEM — Nav link or button
572
+ ======================================== */
573
+ .sx-navmenu-item {
574
+ display: flex;
575
+ align-items: center;
576
+ gap: var(--sx-space-2);
577
+ padding: var(--sx-space-1-5) var(--sx-space-3);
578
+ border-radius: var(--sx-radius-sm);
579
+ font-size: var(--sx-text-sm);
580
+ font-weight: 500;
581
+ font-family: var(--sx-font-body);
582
+ color: var(--sx-color-text-secondary);
583
+ text-decoration: none;
584
+ cursor: pointer;
585
+ user-select: none;
586
+ border: none;
587
+ background: transparent;
588
+ width: 100%;
589
+ text-align: left;
590
+ position: relative;
591
+ transition:
592
+ background var(--sx-transition-fast),
593
+ color var(--sx-transition-fast),
594
+ box-shadow var(--sx-transition-fast),
595
+ transform var(--sx-transition-fast);
596
+ }
597
+
598
+ .sx-navmenu-item:hover:not(.sx-navmenu-item-disabled) {
599
+ background: var(--sx-color-surface-2);
600
+ color: var(--sx-color-text);
601
+ box-shadow:
602
+ 0 1px 0 0 rgba(0, 0, 0, 0.1),
603
+ 0 2px 4px -1px rgba(0, 0, 0, 0.15);
604
+ transform: translateX(1px);
605
+ }
606
+
607
+ /* Active — forge-gradient left accent, raised tab */
608
+ .sx-navmenu-item-active {
609
+ background: var(--sx-color-surface-2);
610
+ color: var(--sx-color-primary);
611
+ box-shadow:
612
+ inset 0 -1px 0 0 rgba(255, 255, 255, 0.03),
613
+ 0 1px 0 0 rgba(0, 0, 0, 0.12),
614
+ 0 2px 4px -1px rgba(0, 0, 0, 0.15);
615
+ border-left: 3px solid transparent;
616
+ border-image: linear-gradient(180deg, var(--sx-color-primary), var(--sx-color-secondary)) 1;
617
+ padding-left: calc(var(--sx-space-3) - 3px);
618
+ }
619
+
620
+ .sx-navmenu-item-active:hover {
621
+ transform: none;
622
+ }
623
+
624
+ /* Disabled */
625
+ .sx-navmenu-item-disabled {
626
+ color: var(--sx-color-text-disabled);
627
+ cursor: not-allowed;
628
+ opacity: 0.5;
629
+ }
630
+
631
+ .sx-navmenu-item-disabled:hover {
632
+ background: transparent;
633
+ box-shadow: none;
634
+ transform: none;
635
+ }
636
+
637
+ /* Focus */
638
+ .sx-navmenu-item:focus-visible {
639
+ outline: 2px solid var(--sx-color-primary);
640
+ outline-offset: 2px;
641
+ }
642
+
643
+ /* ========================================
644
+ ICON
645
+ ======================================== */
646
+ .sx-navmenu-icon {
647
+ display: flex;
648
+ flex-shrink: 0;
649
+ align-items: center;
650
+ justify-content: center;
651
+ width: 16px;
652
+ height: 16px;
653
+ color: var(--sx-color-text-disabled);
654
+ transition: color var(--sx-transition-fast);
655
+ }
656
+
657
+ .sx-navmenu-item:hover:not(.sx-navmenu-item-disabled) .sx-navmenu-icon {
658
+ color: var(--sx-color-text-secondary);
659
+ }
660
+
661
+ .sx-navmenu-item-active .sx-navmenu-icon {
662
+ color: var(--sx-color-primary);
663
+ }
664
+
665
+ /* ========================================
666
+ LABEL
667
+ ======================================== */
668
+ .sx-navmenu-label {
669
+ flex: 1;
670
+ overflow: hidden;
671
+ text-overflow: ellipsis;
672
+ white-space: nowrap;
673
+ }
674
+
675
+ /* ========================================
676
+ BADGE — Stamped tag (matches Badge component)
677
+ ======================================== */
678
+ .sx-navmenu-badge {
679
+ display: inline-flex;
680
+ align-items: center;
681
+ padding: 1px 6px;
682
+ font-size: 0.65rem;
683
+ font-weight: 600;
684
+ border-radius: var(--sx-radius-full);
685
+ background: var(--sx-color-primary-hover);
686
+ color: var(--sx-color-primary);
687
+ white-space: nowrap;
688
+ box-shadow:
689
+ 0 1px 0 0 rgba(160, 50, 10, 0.2),
690
+ 0 2px 4px -1px rgba(0, 0, 0, 0.2),
691
+ 0 0 6px -2px rgba(255, 107, 53, 0.12);
692
+ }
693
+
694
+ /* ========================================
695
+ SUB-TOGGLE — Expand/collapse sub-items
696
+ ======================================== */
697
+ .sx-navmenu-sub-toggle {
698
+ display: flex;
699
+ align-items: center;
700
+ justify-content: center;
701
+ width: 18px;
702
+ height: 18px;
703
+ flex-shrink: 0;
704
+ border: none;
705
+ background: transparent;
706
+ cursor: pointer;
707
+ border-radius: var(--sx-radius-sm);
708
+ color: var(--sx-color-text-disabled);
709
+ transition: transform var(--sx-transition-fast), color var(--sx-transition-fast);
710
+ transform: rotate(0deg);
711
+ padding: 0;
712
+ }
713
+
714
+ .sx-navmenu-sub-toggle:hover {
715
+ color: var(--sx-color-text-secondary);
716
+ }
717
+
718
+ .sx-navmenu-sub-toggle-open {
719
+ transform: rotate(90deg);
720
+ }
721
+
722
+ /* ========================================
723
+ SUB-LIST — Nested items
724
+ ======================================== */
725
+ .sx-navmenu-sublist {
726
+ list-style: none;
727
+ margin: var(--sx-space-1) 0 0 0;
728
+ padding: 0 0 0 calc(var(--sx-space-3) + 2px);
729
+ display: flex;
730
+ flex-direction: column;
731
+ gap: 1px;
732
+ margin-left: var(--sx-space-4);
733
+ box-shadow:
734
+ inset 2px 0 0 0 rgba(0, 0, 0, 0.2),
735
+ inset 3px 0 0 0 rgba(255, 255, 255, 0.02);
736
+ }
737
+
738
+ /* ========================================
739
+ SUB-ITEM
740
+ ======================================== */
741
+ .sx-navmenu-subitem {
742
+ display: flex;
743
+ align-items: center;
744
+ gap: var(--sx-space-2);
745
+ padding: var(--sx-space-1) var(--sx-space-3);
746
+ border-radius: var(--sx-radius-sm);
747
+ font-size: var(--sx-text-xs);
748
+ font-weight: 500;
749
+ font-family: var(--sx-font-body);
750
+ color: var(--sx-color-text-disabled);
751
+ text-decoration: none;
752
+ cursor: pointer;
753
+ user-select: none;
754
+ border: none;
755
+ background: transparent;
756
+ width: 100%;
757
+ text-align: left;
758
+ transition:
759
+ background var(--sx-transition-fast),
760
+ color var(--sx-transition-fast);
761
+ }
762
+
763
+ .sx-navmenu-subitem:hover:not(.sx-navmenu-item-disabled) {
764
+ background: var(--sx-color-surface-2);
765
+ color: var(--sx-color-text);
766
+ }
767
+
768
+ .sx-navmenu-subitem-active {
769
+ color: var(--sx-color-primary);
770
+ }
771
+
772
+ .sx-navmenu-subitem-active:hover {
773
+ color: var(--sx-color-primary);
774
+ }
775
+
776
+ .sx-navmenu-subitem:focus-visible {
777
+ outline: 2px solid var(--sx-color-primary);
778
+ outline-offset: 2px;
779
+ }
780
+
781
+ /* ========================================
782
+ SOLO ITEM — Top-level items outside groups
783
+ ======================================== */
784
+ .sx-navmenu-solo {
785
+ padding: 0 var(--sx-space-2);
786
+ }
787
+
788
+ /* ========================================
789
+ REDUCED MOTION
790
+ ======================================== */
791
+ @media (prefers-reduced-motion: reduce) {
792
+ .sx-navmenu-item,
793
+ .sx-navmenu-subitem,
794
+ .sx-navmenu-chevron,
795
+ .sx-navmenu-sub-toggle,
796
+ .sx-navmenu-group-trigger {
797
+ transition: none;
798
+ }
799
+ }
800
+ </style>