@w5-ui/svelte 0.2.1 → 0.3.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.
Files changed (138) hide show
  1. package/README.md +116 -4
  2. package/package.json +2 -2
  3. package/src/lib/components/Accordion.svelte +1 -1
  4. package/src/lib/components/AccordionItem.svelte +6 -7
  5. package/src/lib/components/ActionMenu.svelte +17 -9
  6. package/src/lib/components/ActivityFeed.svelte +20 -14
  7. package/src/lib/components/Alert.svelte +22 -4
  8. package/src/lib/components/AppLayout.svelte +34 -0
  9. package/src/lib/components/Avatar.svelte +28 -11
  10. package/src/lib/components/AvatarGroup.svelte +21 -4
  11. package/src/lib/components/Badge.svelte +22 -6
  12. package/src/lib/components/Banner.svelte +36 -7
  13. package/src/lib/components/Body.svelte +29 -0
  14. package/src/lib/components/BodyLg.svelte +26 -0
  15. package/src/lib/components/Breadcrumb.svelte +19 -12
  16. package/src/lib/components/Button.svelte +30 -1
  17. package/src/lib/components/CIDRInput.svelte +17 -9
  18. package/src/lib/components/Callout.svelte +29 -8
  19. package/src/lib/components/Caption.svelte +26 -0
  20. package/src/lib/components/Card.svelte +11 -3
  21. package/src/lib/components/CardMore.svelte +20 -0
  22. package/src/lib/components/CardTitle.svelte +20 -0
  23. package/src/lib/components/Carousel.svelte +27 -20
  24. package/src/lib/components/Checkbox.svelte +24 -3
  25. package/src/lib/components/Code.svelte +24 -0
  26. package/src/lib/components/CodeBlock.svelte +8 -7
  27. package/src/lib/components/ColorPicker.svelte +13 -13
  28. package/src/lib/components/ColumnToggle.svelte +10 -9
  29. package/src/lib/components/Combobox.svelte +31 -17
  30. package/src/lib/components/CommandPalette.svelte +42 -26
  31. package/src/lib/components/ConfirmDialog.svelte +21 -15
  32. package/src/lib/components/ContextMenu.svelte +12 -12
  33. package/src/lib/components/ContextualHelp.svelte +11 -6
  34. package/src/lib/components/CopyButton.svelte +2 -1
  35. package/src/lib/components/DatePicker.svelte +27 -23
  36. package/src/lib/components/DateRangePicker.svelte +46 -39
  37. package/src/lib/components/Delta.svelte +25 -0
  38. package/src/lib/components/Display1.svelte +29 -0
  39. package/src/lib/components/Display2.svelte +29 -0
  40. package/src/lib/components/Drawer.svelte +6 -5
  41. package/src/lib/components/DurationInput.svelte +16 -8
  42. package/src/lib/components/EmptyState.svelte +16 -7
  43. package/src/lib/components/ExpandableRow.svelte +8 -15
  44. package/src/lib/components/Eyebrow.svelte +25 -0
  45. package/src/lib/components/Field.svelte +7 -7
  46. package/src/lib/components/FieldStack.svelte +18 -0
  47. package/src/lib/components/FileUpload.svelte +8 -10
  48. package/src/lib/components/FilterBuilder.svelte +52 -28
  49. package/src/lib/components/FormActions.svelte +18 -0
  50. package/src/lib/components/FormRow.svelte +17 -0
  51. package/src/lib/components/Grid.svelte +17 -0
  52. package/src/lib/components/GroupedList.svelte +35 -21
  53. package/src/lib/components/H1.svelte +29 -0
  54. package/src/lib/components/H2.svelte +29 -0
  55. package/src/lib/components/H3.svelte +29 -0
  56. package/src/lib/components/H4.svelte +29 -0
  57. package/src/lib/components/HealthBar.svelte +6 -3
  58. package/src/lib/components/HoverCard.svelte +16 -5
  59. package/src/lib/components/IPInput.svelte +14 -8
  60. package/src/lib/components/IconButton.svelte +1 -2
  61. package/src/lib/components/InlineEdit.svelte +25 -14
  62. package/src/lib/components/Input.svelte +13 -4
  63. package/src/lib/components/InputGroup.svelte +14 -3
  64. package/src/lib/components/JsonViewer.svelte +7 -2
  65. package/src/lib/components/JsonViewerNode.svelte +38 -32
  66. package/src/lib/components/KVTable.svelte +10 -4
  67. package/src/lib/components/KanbanBoard.svelte +29 -27
  68. package/src/lib/components/Kbd.svelte +8 -5
  69. package/src/lib/components/Label.svelte +28 -0
  70. package/src/lib/components/LogViewer.svelte +24 -20
  71. package/src/lib/components/MACInput.svelte +14 -8
  72. package/src/lib/components/Mac.svelte +23 -0
  73. package/src/lib/components/Menubar.svelte +30 -13
  74. package/src/lib/components/Modal.svelte +15 -6
  75. package/src/lib/components/Mono.svelte +28 -0
  76. package/src/lib/components/MultiSelect.svelte +43 -23
  77. package/src/lib/components/Muted.svelte +24 -0
  78. package/src/lib/components/NameCell.svelte +35 -0
  79. package/src/lib/components/NotifDot.svelte +23 -0
  80. package/src/lib/components/NotificationPanel.svelte +44 -31
  81. package/src/lib/components/NumberInput.svelte +13 -12
  82. package/src/lib/components/OTPInput.svelte +14 -8
  83. package/src/lib/components/PageHeader.svelte +25 -0
  84. package/src/lib/components/Pagination.svelte +16 -12
  85. package/src/lib/components/PasswordInput.svelte +8 -7
  86. package/src/lib/components/Pill.svelte +34 -6
  87. package/src/lib/components/Popover.svelte +91 -16
  88. package/src/lib/components/ProgressBar.svelte +10 -7
  89. package/src/lib/components/RadioGroup.svelte +22 -7
  90. package/src/lib/components/RangeSlider.svelte +17 -12
  91. package/src/lib/components/RankedList.svelte +12 -18
  92. package/src/lib/components/ResizablePanel.svelte +25 -14
  93. package/src/lib/components/RowToggle.svelte +6 -4
  94. package/src/lib/components/SearchBox.svelte +6 -2
  95. package/src/lib/components/SegmentedControl.svelte +16 -2
  96. package/src/lib/components/Select.svelte +20 -8
  97. package/src/lib/components/SelectionToolbar.svelte +13 -9
  98. package/src/lib/components/Sidebar.svelte +32 -11
  99. package/src/lib/components/Signal.svelte +3 -2
  100. package/src/lib/components/Skeleton.svelte +21 -9
  101. package/src/lib/components/SkipLink.svelte +5 -2
  102. package/src/lib/components/Slider.svelte +16 -9
  103. package/src/lib/components/SortHeader.svelte +13 -4
  104. package/src/lib/components/SortableList.svelte +17 -18
  105. package/src/lib/components/Sparkline.svelte +20 -14
  106. package/src/lib/components/Spinner.svelte +16 -1
  107. package/src/lib/components/SplitButton.svelte +27 -8
  108. package/src/lib/components/Spoiler.svelte +8 -6
  109. package/src/lib/components/SrOnly.svelte +23 -0
  110. package/src/lib/components/StackedProgress.svelte +17 -9
  111. package/src/lib/components/StarRating.svelte +35 -10
  112. package/src/lib/components/Stat.svelte +19 -5
  113. package/src/lib/components/StatNumber.svelte +22 -0
  114. package/src/lib/components/StatusIndicator.svelte +9 -2
  115. package/src/lib/components/StatusRing.svelte +36 -0
  116. package/src/lib/components/Stepper.svelte +30 -8
  117. package/src/lib/components/Submeta.svelte +19 -0
  118. package/src/lib/components/SwitchPortGrid.svelte +20 -7
  119. package/src/lib/components/TablePager.svelte +22 -0
  120. package/src/lib/components/TableShell.svelte +19 -0
  121. package/src/lib/components/Tabs.svelte +16 -5
  122. package/src/lib/components/Tag.svelte +5 -3
  123. package/src/lib/components/TagInput.svelte +9 -10
  124. package/src/lib/components/Textarea.svelte +7 -1
  125. package/src/lib/components/TimePicker.svelte +14 -8
  126. package/src/lib/components/TimeRange.svelte +20 -22
  127. package/src/lib/components/Timeline.svelte +20 -9
  128. package/src/lib/components/Toast.svelte +24 -5
  129. package/src/lib/components/Toggle.svelte +10 -4
  130. package/src/lib/components/ToggleGroup.svelte +15 -5
  131. package/src/lib/components/Tooltip.svelte +20 -5
  132. package/src/lib/components/Topbar.svelte +108 -27
  133. package/src/lib/components/TopbarActions.svelte +52 -0
  134. package/src/lib/components/TransferList.svelte +30 -22
  135. package/src/lib/components/TreeItem.svelte +11 -13
  136. package/src/lib/components/Unit.svelte +22 -0
  137. package/src/lib/components/VirtualList.svelte +12 -9
  138. package/src/lib/index.ts +35 -0
@@ -5,6 +5,21 @@
5
5
  }
6
6
 
7
7
  let { size = 'md', label = 'Loading' }: Props = $props();
8
+
9
+ // Pre-compose so Tailwind's scanner picks up every variant. Tailwind's
10
+ // `animate-spin` is `animation: spin 1s linear infinite` (rotate 360deg);
11
+ // dashboard.css used 0.65s — we mirror that with arbitrary-value
12
+ // `[animation-duration:0.65s]` to stay pixel-equivalent.
13
+ const SIZE: Record<NonNullable<Props['size']>, string> = {
14
+ sm: 'h-2.5 w-2.5 border-[1.5px]',
15
+ md: 'h-3.5 w-3.5 border-2',
16
+ lg: 'h-5 w-5 border-2',
17
+ };
8
18
  </script>
9
19
 
10
- <span role="status" aria-label={label} class="spinner {size}"></span>
20
+ <span
21
+ role="status"
22
+ aria-label={label}
23
+ class="inline-block shrink-0 animate-spin rounded-full border-border-3 border-t-current [animation-duration:0.65s] motion-reduce:[animation-duration:10s]
24
+ {SIZE[size]}"
25
+ ></span>
@@ -1,6 +1,10 @@
1
1
  <script module lang="ts">
2
2
  let counter = 0;
3
- export interface SplitButtonItem { id: string; label: string; disabled?: boolean; }
3
+ export interface SplitButtonItem {
4
+ id: string;
5
+ label: string;
6
+ disabled?: boolean;
7
+ }
4
8
  </script>
5
9
 
6
10
  <script lang="ts">
@@ -70,24 +74,34 @@
70
74
 
71
75
  onMount(() => document.addEventListener('mousedown', handleOutside));
72
76
  onDestroy(() => document.removeEventListener('mousedown', handleOutside));
77
+
78
+ // Pre-composed variant chrome so Tailwind's static scanner sees the literals.
79
+ const VARIANT: Record<NonNullable<Props['variant']>, string> = {
80
+ primary: 'bg-brand-05 text-white hover:bg-brand-06 border-brand-05',
81
+ ghost: 'bg-transparent text-text-2 border-border-2 hover:bg-row-hover hover:text-text-1',
82
+ danger: 'bg-transparent text-status-danger border-status-danger/30',
83
+ };
84
+ // Shared button base — same metrics as the migrated Button.
85
+ const BTN_BASE =
86
+ 'inline-flex h-[30px] cursor-pointer items-center gap-1.5 whitespace-nowrap border px-3 text-13 font-medium transition-all duration-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05 disabled:cursor-not-allowed disabled:opacity-55';
73
87
  </script>
74
88
 
75
- <div class="split-btn" bind:this={wrapperEl}>
89
+ <div class="relative inline-flex items-stretch" bind:this={wrapperEl}>
76
90
  <button
77
91
  type="button"
78
- class="btn btn-{variant} split-btn-primary"
79
92
  {disabled}
93
+ class="{BTN_BASE} {VARIANT[variant]} rounded-l-md border-r-0 rounded-r-none"
80
94
  onclick={() => onprimary?.()}
81
95
  >{label}</button>
82
96
  <button
83
97
  bind:this={caretEl}
84
98
  type="button"
85
- class="btn btn-{variant} split-btn-caret"
86
99
  {disabled}
87
- aria-label="{label} options"
100
+ aria-label={`${label} options`}
88
101
  aria-haspopup="menu"
89
102
  aria-expanded={open}
90
103
  aria-controls={open ? menuId : undefined}
104
+ class="{BTN_BASE} {VARIANT[variant]} w-[26px] shrink-0 justify-center rounded-r-md rounded-l-none px-0 border-l border-l-white/15"
91
105
  onclick={toggleMenu}
92
106
  onkeydown={onKeyDown}
93
107
  >
@@ -96,14 +110,19 @@
96
110
  </svg>
97
111
  </button>
98
112
  {#if open}
99
- <ul id={menuId} role="menu" aria-label="{label} options" class="action-menu">
113
+ <ul
114
+ id={menuId}
115
+ role="menu"
116
+ aria-label={`${label} options`}
117
+ class="absolute right-0 top-[calc(100%+4px)] z-[9100] m-0 min-w-[148px] list-none rounded-lg border border-border-3 bg-bg-2 p-1 shadow-[0_8px_24px_rgba(0,0,0,0.5)]"
118
+ >
100
119
  {#each items as item, idx (item.id)}
101
120
  <li
102
121
  role="menuitem"
103
- tabindex="-1"
122
+ tabindex={-1}
104
123
  aria-disabled={item.disabled}
105
124
  data-active={idx === activeIdx ? 'true' : undefined}
106
- class="action-menu-item"
125
+ class="flex cursor-pointer items-center whitespace-nowrap rounded-[5px] px-2.5 py-1.5 text-13 text-text-2 outline-none hover:bg-row-active hover:text-text-1 data-[active=true]:bg-row-active data-[active=true]:text-text-1 aria-disabled:cursor-not-allowed aria-disabled:opacity-40"
107
126
  onmouseenter={() => { activeIdx = idx; }}
108
127
  onmousedown={(e) => { e.preventDefault(); (() => { if (!item.disabled) activate(item.id); })(); }}
109
128
  >{item.label}</li>
@@ -14,9 +14,8 @@
14
14
  showLabel = 'Show more',
15
15
  hideLabel = 'Show less',
16
16
  class: klass = '',
17
- children
17
+ children,
18
18
  }: Props = $props();
19
-
20
19
 
21
20
  let expanded = $state(false);
22
21
  let clipped = $state(false);
@@ -27,8 +26,8 @@
27
26
  });
28
27
  </script>
29
28
 
30
- <div class="spoiler {klass}">
31
- <div class="spoiler-outer" style="position:relative">
29
+ <div class="flex flex-col {klass}">
30
+ <div class="relative">
32
31
  <div
33
32
  bind:this={bodyEl}
34
33
  style={!expanded && clipped ? `max-height:${maxHeight}px;overflow:hidden` : undefined}
@@ -36,14 +35,17 @@
36
35
  {@render children?.()}
37
36
  </div>
38
37
  {#if !expanded && clipped}
39
- <div class="spoiler-fade" aria-hidden="true"></div>
38
+ <div
39
+ class="pointer-events-none absolute bottom-0 left-0 right-0 h-12 bg-gradient-to-b from-transparent to-[#1a1d23]"
40
+ aria-hidden="true"
41
+ ></div>
40
42
  {/if}
41
43
  </div>
42
44
  {#if clipped}
43
45
  <button
44
46
  type="button"
45
- class="spoiler-toggle"
46
47
  aria-expanded={expanded}
48
+ class="mt-1 inline-flex items-center border-0 bg-transparent px-0 py-1 text-12 leading-none text-brand-05 cursor-pointer hover:text-status-info focus-visible:rounded-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
47
49
  onclick={() => (expanded = !expanded)}
48
50
  >
49
51
  {expanded ? hideLabel : showLabel}
@@ -0,0 +1,23 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ class?: string;
4
+ children?: import('svelte').Snippet;
5
+ }
6
+
7
+ let { class: className = '', children }: Props = $props();
8
+ </script>
9
+
10
+ <!--
11
+ Visually-hidden text that stays available to assistive tech. Renders a
12
+ <span class="sr-only"> so the .sr-only rule in @w5-ui/tokens/dashboard.css
13
+ clips it from the visual layout while keeping it announceable by screen
14
+ readers. Use it to name regions that the design intentionally leaves
15
+ unlabelled, the way the reference Tables recipe wires
16
+ <caption class="sr-only"> onto every <table> (WCAG 1.3.1). Typed equivalent
17
+ of <span class="sr-only"> (apply the class directly to a <caption> /
18
+ <label> when the whole element is the hidden text, or wrap an inline
19
+ phrase with <SrOnly> when it sits alongside visible markup).
20
+ -->
21
+ <span class="sr-only {className}">
22
+ {@render children?.()}
23
+ </span>
@@ -18,25 +18,33 @@
18
18
  segments,
19
19
  total = undefined,
20
20
  ariaLabel = undefined,
21
- showLegend = true
21
+ showLegend = true,
22
22
  }: Props = $props();
23
23
  let sum = $derived(total ?? segments.reduce((a, s) => a + s.value, 0));
24
- let text = $derived(ariaLabel ?? segments.map((s) => `${s.label} ${Math.round((s.value / sum) * 100)}%`).join(', '));
24
+ let text = $derived(
25
+ ariaLabel ?? segments.map((s) => `${s.label} ${Math.round((s.value / sum) * 100)}%`).join(', '),
26
+ );
25
27
  </script>
26
28
 
27
- <div class="sp" role="img" aria-label={text}>
28
- <div class="sp-bar" aria-hidden="true">
29
+ <div class="flex w-full flex-col gap-2" role="img" aria-label={text}>
30
+ <div
31
+ class="flex h-2 w-full overflow-hidden rounded-sm bg-row-active"
32
+ aria-hidden="true"
33
+ >
29
34
  {#each segments as { label, value, color }}
30
- <div class="sp-seg" style="width:{((value / sum) * 100).toFixed(2)}%;background:{color}"></div>
35
+ <div
36
+ class="h-full first:rounded-l-sm last:rounded-r-sm"
37
+ style="width:{((value / sum) * 100).toFixed(2)}%;background:{color}"
38
+ ></div>
31
39
  {/each}
32
40
  </div>
33
41
  {#if showLegend}
34
- <div class="sp-legend" aria-hidden="true">
42
+ <div class="flex flex-wrap items-center gap-3" aria-hidden="true">
35
43
  {#each segments as { label, value, color }}
36
- <span class="sp-item">
37
- <span class="sp-dot" style="background:{color}"></span>
44
+ <span class="inline-flex items-center gap-1.5 text-12 text-text-2">
45
+ <span class="inline-block h-2 w-2 shrink-0 rounded-full" style="background:{color}"></span>
38
46
  {label}
39
- <span class="sp-pct">{Math.round((value / sum) * 100)}%</span>
47
+ <span class="text-text-4 tabular-nums">{Math.round((value / sum) * 100)}%</span>
40
48
  </span>
41
49
  {/each}
42
50
  </div>
@@ -18,7 +18,7 @@
18
18
  max = 5,
19
19
  readOnly = false,
20
20
  size = 'md',
21
- onChange = undefined
21
+ onChange = undefined,
22
22
  }: Props = $props();
23
23
 
24
24
  const gid = `dash-ui-sr-${++counter}`;
@@ -28,31 +28,48 @@
28
28
  value = star;
29
29
  onChange?.(star);
30
30
  }
31
+
32
+ const STAR_SIZE: Record<NonNullable<Props['size']>, string> = {
33
+ sm: 'h-[15px] w-[15px]',
34
+ md: 'h-5 w-5',
35
+ };
31
36
  </script>
32
37
 
33
38
  {#if readOnly}
34
39
  <span
35
- class="star-rating{size === 'sm' ? ' star-rating--sm' : ''} star-rating--readonly"
40
+ class="m-0 inline-flex items-center gap-0.5 border-0 bg-transparent p-0"
36
41
  role="img"
37
- aria-label="{value} out of {max} stars"
42
+ aria-label={`${value} out of ${max} stars`}
38
43
  >
39
44
  {#each Array(max) as _, i}
40
- <span class="star-icon{i < value ? ' star-icon--on' : ''}" aria-hidden="true">
41
- <svg viewBox="0 0 24 24" fill={i < value ? 'currentColor' : 'none'} stroke="currentColor" stroke-width="1.5" aria-hidden="true" focusable="false">
45
+ {@const on = i < value}
46
+ <span
47
+ aria-hidden="true"
48
+ class="flex items-center {on ? 'text-status-warning' : 'text-white/[0.18]'}"
49
+ >
50
+ <svg
51
+ class={STAR_SIZE[size]}
52
+ viewBox="0 0 24 24"
53
+ fill={on ? 'currentColor' : 'none'}
54
+ stroke="currentColor"
55
+ stroke-width="1.5"
56
+ aria-hidden="true"
57
+ focusable="false"
58
+ >
42
59
  <polygon points="12,2 15.09,8.26 22,9.27 17,14.14 18.18,21.02 12,17.77 5.82,21.02 7,14.14 2,9.27 8.91,8.26" />
43
60
  </svg>
44
61
  </span>
45
62
  {/each}
46
63
  </span>
47
64
  {:else}
48
- <fieldset class="star-rating{size === 'sm' ? ' star-rating--sm' : ''}">
65
+ <fieldset class="m-0 inline-flex items-center gap-0.5 border-0 bg-transparent p-0">
49
66
  <legend class="sr-only">{label}</legend>
50
67
  {#each Array(max) as _, i}
51
68
  {@const star = i + 1}
52
69
  {@const on = star <= (hovered || value)}
53
70
  <label
54
71
  for="{gid}-{star}"
55
- class="star-label{on ? ' star-label--on' : ''}"
72
+ class="group/star relative flex cursor-pointer items-center rounded-sm p-px transition-colors duration-100 {on ? 'text-status-warning' : 'text-white/[0.18]'} has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-brand-05"
56
73
  onmouseenter={() => (hovered = star)}
57
74
  onmouseleave={() => (hovered = 0)}
58
75
  >
@@ -62,11 +79,19 @@
62
79
  name={gid}
63
80
  value={String(star)}
64
81
  checked={value === star}
65
- class="star-radio"
66
- aria-label="{star} out of {max}"
82
+ class="sr-only"
83
+ aria-label={`${star} out of ${max}`}
67
84
  onchange={() => pick(star)}
68
85
  />
69
- <svg viewBox="0 0 24 24" fill={on ? 'currentColor' : 'none'} stroke="currentColor" stroke-width="1.5" aria-hidden="true" focusable="false">
86
+ <svg
87
+ class={STAR_SIZE[size]}
88
+ viewBox="0 0 24 24"
89
+ fill={on ? 'currentColor' : 'none'}
90
+ stroke="currentColor"
91
+ stroke-width="1.5"
92
+ aria-hidden="true"
93
+ focusable="false"
94
+ >
70
95
  <polygon points="12,2 15.09,8.26 22,9.27 17,14.14 18.18,21.02 12,17.77 5.82,21.02 7,14.14 2,9.27 8.91,8.26" />
71
96
  </svg>
72
97
  </label>
@@ -29,18 +29,32 @@
29
29
  }: Props = $props();
30
30
 
31
31
  let deltaClass = $derived(
32
- deltaDir === 'up' ? 'delta-up' : deltaDir === 'down' ? 'delta-down' : '',
32
+ deltaDir === 'up'
33
+ ? 'text-status-success'
34
+ : deltaDir === 'down'
35
+ ? 'text-status-danger'
36
+ : '',
33
37
  );
34
38
  </script>
35
39
 
36
40
  <Card {span}>
41
+ <!--
42
+ `.card h3` rules in dashboard.css still target this <h3> for now;
43
+ leaving them there until every Card consumer migrates its heading
44
+ too. Stat's value + submeta are Tailwind utilities below.
45
+ -->
37
46
  <h3>{label}</h3>
38
- <div class="stat" style={color ? `color:${color};` : ''}>
39
- {#if valueSlot}{@render valueSlot()}{:else}{value}{/if}{#if unit}<span class="unit">{unit}</span>{/if}
47
+ <div
48
+ class="text-[28px] font-semibold leading-[1.05] tracking-[-0.01em] tabular-nums text-text-1"
49
+ style={color ? `color:${color};` : ''}
50
+ >
51
+ {#if valueSlot}{@render valueSlot()}{:else}{value}{/if}{#if unit}<span
52
+ class="ml-1 text-13 font-normal text-text-4"
53
+ >{unit}</span>{/if}
40
54
  </div>
41
55
  {#if sub || delta}
42
- <div class="submeta">
43
- {#if sub}{sub}{/if}{#if sub && delta} · {/if}{#if delta}<span class="sr-only">Trend: </span><span class={deltaClass}>{delta}</span>{/if}
56
+ <div class="text-[11px] tabular-nums text-text-4">
57
+ {#if sub}{sub}{/if}{#if sub && delta} \u00b7 {/if}{#if delta}<span class="sr-only">Trend: </span><span class={deltaClass}>{delta}</span>{/if}
44
58
  </div>
45
59
  {/if}
46
60
  </Card>
@@ -0,0 +1,22 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ class?: string;
4
+ children?: import('svelte').Snippet;
5
+ }
6
+
7
+ let { class: className = '', children }: Props = $props();
8
+ </script>
9
+
10
+ <!--
11
+ Hero-number row inside a <Card>: the big tabular-nums value (optionally
12
+ followed by a trailing <Unit>). Renders a <div class="stat"> so the .stat
13
+ rule in @w5-ui/tokens/dashboard.css sets the canonical 30 px size, semibold
14
+ weight, tabular figures, and primary text colour. Stops userland reaching
15
+ for inline font-size / font-weight overrides on bare numbers. Pair with
16
+ <Unit> for the suffix label and <Submeta> for the muted line underneath;
17
+ <Stat> already wires all three internally, so reach for <StatNumber> when
18
+ you compose .stat by hand (e.g. number above a chart inside a <Card>).
19
+ -->
20
+ <div class="stat {className}">
21
+ {@render children?.()}
22
+ </div>
@@ -8,7 +8,14 @@
8
8
  let { color, text, textColor = undefined }: Props = $props();
9
9
  </script>
10
10
 
11
- <span style="display:inline-flex;align-items:center;gap:6px;color:{textColor ?? color};font-size:12px;">
12
- <span aria-hidden="true" style="width:6px;height:6px;border-radius:50%;background:{color};"></span>
11
+ <span
12
+ class="inline-flex items-center gap-1.5 text-12"
13
+ style="color:{textColor ?? color};"
14
+ >
15
+ <span
16
+ class="h-1.5 w-1.5 rounded-full"
17
+ aria-hidden="true"
18
+ style="background:{color};"
19
+ ></span>
13
20
  {text}
14
21
  </span>
@@ -0,0 +1,36 @@
1
+ <script module lang="ts">
2
+ export type StatusRingStatus = 'ok' | 'warn' | 'danger' | 'neutral';
3
+ </script>
4
+
5
+ <script lang="ts">
6
+ interface Props {
7
+ class?: string;
8
+ style?: string;
9
+ /** Health colour of the ring. Defaults to `'ok'` (success green). */
10
+ status?: StatusRingStatus;
11
+ }
12
+
13
+ let { class: className = '', style = '', status = 'ok' }: Props = $props();
14
+
15
+ const MODIFIER: Record<StatusRingStatus, string> = {
16
+ ok: '',
17
+ warn: 'status-ring--warn',
18
+ danger: 'status-ring--danger',
19
+ neutral: 'status-ring--neutral',
20
+ };
21
+ let modifier = $derived(MODIFIER[status]);
22
+ </script>
23
+
24
+ <!--
25
+ Pulsing status indicator: a soft translucent ring around a solid coloured
26
+ inner dot. Renders <span aria-hidden class="status-ring"> so the .status-ring
27
+ rule in @w5-ui/tokens/dashboard.css paints the canonical 18 px ring + 8 px
28
+ inner dot without userland having to hand-roll the two layers. The `status`
29
+ prop swaps the ring + dot colour: 'ok' (default, success green), 'warn'
30
+ (amber), 'danger' (red), 'neutral' (grey). Used by <Topbar> next to the site
31
+ name to signal site health; also suitable for tunnel / session / device
32
+ tiles where a small live-state dot sits at the leading edge of a row.
33
+ Decorative — the typed wrapper sets aria-hidden; pair with a label so
34
+ screen readers announce the state.
35
+ -->
36
+ <span aria-hidden="true" class="status-ring {modifier} {className}" {style}></span>
@@ -13,19 +13,41 @@
13
13
  }
14
14
 
15
15
  let { steps, active, class: className = '' }: Props = $props();
16
-
16
+ let activeIdx = $derived(steps.findIndex((s) => s.id === active));
17
17
 
18
- let activeIdx = $derived(steps.findIndex(s => s.id === active));
18
+ // Per-state ramp:
19
+ // - num circle: ring + bg + text colour
20
+ // - label: colour + weight
21
+ // - connector line (::before via Tailwind arbitrary): bg colour
22
+ const NUM: Record<'active' | 'done' | 'upcoming', string> = {
23
+ active: 'border-brand-05 bg-brand-05 text-white',
24
+ done: 'border-brand-05 bg-brand-05/[0.18] text-status-info',
25
+ upcoming: 'border-bg-3 bg-bg-1 text-text-4',
26
+ };
27
+ const LABEL: Record<'active' | 'done' | 'upcoming', string> = {
28
+ active: 'text-text-1 font-medium',
29
+ done: 'text-status-info',
30
+ upcoming: 'text-text-4',
31
+ };
32
+ // Connector tail to the left, drawn from item to item.
33
+ const TAIL: Record<'active' | 'done' | 'upcoming', string> = {
34
+ active: 'before:bg-brand-05',
35
+ done: 'before:bg-brand-05',
36
+ upcoming: 'before:bg-bg-3',
37
+ };
19
38
  </script>
20
39
 
21
- <nav aria-label="Progress" class="stepper {className}">
22
- <ol class="stepper__list">
40
+ <nav aria-label="Progress" class="w-full {className}">
41
+ <ol class="m-0 flex list-none items-start p-0">
23
42
  {#each steps as step, idx}
24
43
  {@const isActive = step.id === active}
25
44
  {@const isDone = idx < activeIdx}
26
- {@const state = isActive ? 'active' : isDone ? 'done' : 'upcoming'}
27
- <li class="stepper__item stepper__item--{state}">
28
- <span class="stepper__num" aria-hidden="true">
45
+ {@const state = (isActive ? 'active' : isDone ? 'done' : 'upcoming') as 'active' | 'done' | 'upcoming'}
46
+ <li class="relative flex flex-1 flex-col items-center gap-1.5 [&:not(:first-child)]:before:absolute [&:not(:first-child)]:before:left-[-50%] [&:not(:first-child)]:before:right-1/2 [&:not(:first-child)]:before:top-[13px] [&:not(:first-child)]:before:h-px [&:not(:first-child)]:before:content-[''] {TAIL[state]}">
47
+ <span
48
+ class="relative z-10 flex h-[26px] w-[26px] shrink-0 items-center justify-center rounded-full border-[1.5px] text-11 font-semibold {NUM[state]}"
49
+ aria-hidden="true"
50
+ >
29
51
  {#if isDone}
30
52
  <svg viewBox="0 0 12 12" width="12" height="12" fill="none">
31
53
  <path d="M2 6l3 3 5-5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
@@ -35,7 +57,7 @@
35
57
  {/if}
36
58
  </span>
37
59
  {#if isDone}<span class="sr-only">completed: </span>{/if}
38
- <span class="stepper__label" aria-current={isActive ? 'step' : undefined}>
60
+ <span class="text-center text-11 leading-[1.3] {LABEL[state]}" aria-current={isActive ? 'step' : undefined}>
39
61
  {step.label}
40
62
  </span>
41
63
  </li>
@@ -0,0 +1,19 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ class?: string;
4
+ children?: import('svelte').Snippet;
5
+ }
6
+
7
+ let { class: className = '', children }: Props = $props();
8
+ </script>
9
+
10
+ <!--
11
+ Secondary one-line caption inside a <Card> tile: the muted row that sits
12
+ under the headline .stat number and carries supporting metrics, deltas, or
13
+ status. Typed equivalent of `<div class="submeta">` from
14
+ @w5-ui/tokens/dashboard.css. Pair with `.delta-up` / `.delta-down` spans
15
+ for trend text so screen readers announce the direction.
16
+ -->
17
+ <div class="submeta {className}">
18
+ {@render children?.()}
19
+ </div>
@@ -13,7 +13,6 @@
13
13
  </script>
14
14
 
15
15
  <script lang="ts">
16
-
17
16
  interface Props {
18
17
  ports?: SwitchPort[];
19
18
  columns?: number;
@@ -21,29 +20,43 @@
21
20
  onportclick?: (payload: number) => void;
22
21
  }
23
22
 
24
- let { ports = [], columns = 12, ariaLabel = 'Switch port panel',
23
+ let {
24
+ ports = [],
25
+ columns = 12,
26
+ ariaLabel = 'Switch port panel',
25
27
  onportclick,
26
28
  }: Props = $props();
29
+
30
+ const STATUS_BTN: Record<SwitchPortStatus, string> = {
31
+ up: 'border-status-success/40 bg-status-success/[0.08] text-status-success hover:bg-status-success/15',
32
+ poe: 'border-status-warning/40 bg-status-warning/[0.10] text-status-warning hover:bg-status-warning/20',
33
+ down: 'border-border-1 bg-divider text-text-4 hover:bg-row-hover',
34
+ };
27
35
  </script>
28
36
 
29
- <ul class="spg" role="list" aria-label={ariaLabel} style="grid-template-columns: repeat({columns}, 1fr);">
37
+ <ul
38
+ class="m-0 grid list-none gap-1 p-0"
39
+ role="list"
40
+ aria-label={ariaLabel}
41
+ style="grid-template-columns: repeat({columns}, 1fr);"
42
+ >
30
43
  {#each ports as port, i}
31
44
  {@const n = i + 1}
32
45
  {@const desc = `Port ${n}: ${STATUS_LABEL[port.status]}${port.speed ? ` · ${port.speed}` : ''}${port.label ? ` · ${port.label}` : ''}`}
33
46
  <li>
34
47
  <button
35
48
  type="button"
36
- class="spg__btn"
37
49
  data-status={port.status}
38
50
  aria-label={desc}
51
+ class="flex aspect-square w-full cursor-pointer flex-col items-center justify-center gap-0.5 rounded border p-1 text-11 font-medium transition-colors duration-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05 {STATUS_BTN[port.status]}"
39
52
  onclick={() => onportclick?.(i)}
40
53
  >
41
- <span class="spg__num">{n}</span>
54
+ <span class="font-bold leading-none">{n}</span>
42
55
  {#if port.speed}
43
- <span class="spg__speed--{port.status}">{port.speed}</span>
56
+ <span class="text-[9px] leading-none opacity-75">{port.speed}</span>
44
57
  {/if}
45
58
  {#if port.status === 'poe'}
46
- <span class="spg__poe" aria-hidden="true">PoE</span>
59
+ <span class="text-[9px] font-bold leading-none" aria-hidden="true">PoE</span>
47
60
  {/if}
48
61
  </button>
49
62
  </li>
@@ -0,0 +1,22 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ class?: string;
4
+ children?: import('svelte').Snippet;
5
+ }
6
+
7
+ let { class: className = '', children }: Props = $props();
8
+ </script>
9
+
10
+ <!--
11
+ Sibling row that sits directly under a <TableShell> (or
12
+ <div class="table-shell">) and carries the rows-per-page control on the
13
+ left and the <Pagination> nav on the right. Renders the canonical
14
+ <div class="table-pager"> markup so the flex / space-between / 12px top
15
+ margin from @w5-ui/tokens/dashboard.css applies and userland drops the
16
+ inline flex / space-between wrapper that every reference page used to ship.
17
+ Render as a sibling of <TableShell>, never inside it, so focus rings on the
18
+ pager buttons aren't clipped.
19
+ -->
20
+ <div class="table-pager {className}">
21
+ {@render children?.()}
22
+ </div>
@@ -0,0 +1,19 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ class?: string;
4
+ children?: import('svelte').Snippet;
5
+ }
6
+
7
+ let { class: className = '', children }: Props = $props();
8
+ </script>
9
+
10
+ <!--
11
+ Rounded, bordered wrapper for any full-width <table> on a page. Carries the
12
+ 8px radius, --neutral-09 background, single 1px border and overflow: hidden
13
+ clip. Typed equivalent of `<div class="table-shell">` from
14
+ @w5-ui/tokens/dashboard.css. Render <Pagination> as a sibling below, not
15
+ inside, so focus rings aren't clipped.
16
+ -->
17
+ <div class="table-shell {className}">
18
+ {@render children?.()}
19
+ </div>
@@ -3,7 +3,6 @@
3
3
  </script>
4
4
 
5
5
  <script lang="ts">
6
-
7
6
  interface Props {
8
7
  items?: TabItem[];
9
8
  active: string;
@@ -11,7 +10,10 @@
11
10
  onchange?: (payload: string) => void;
12
11
  }
13
12
 
14
- let { items = [], active = $bindable(), ariaLabel = undefined,
13
+ let {
14
+ items = [],
15
+ active = $bindable(),
16
+ ariaLabel = undefined,
15
17
  onchange,
16
18
  }: Props = $props();
17
19
  let buttons: (HTMLButtonElement | null)[] = $state([]);
@@ -32,12 +34,19 @@
32
34
  }
33
35
  </script>
34
36
 
35
- <div class="tabs" role="tablist" aria-label={ariaLabel}>
37
+ <div
38
+ class="flex gap-0 overflow-x-auto border-b border-border-1 px-6"
39
+ role="tablist"
40
+ aria-label={ariaLabel}
41
+ >
36
42
  {#each items as t, i (t.id)}
37
43
  <button
38
44
  bind:this={buttons[i]}
39
45
  type="button"
40
- class="tab {t.id === active ? 'active' : ''}"
46
+ class="-mb-px cursor-pointer select-none whitespace-nowrap border-0 border-b-2 bg-transparent px-3.5 py-[11px] text-13 leading-none focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-brand-05
47
+ {t.id === active
48
+ ? 'border-b-brand-05 text-text-1'
49
+ : 'border-b-transparent text-text-3 hover:text-text-1'}"
41
50
  role="tab"
42
51
  id="tab-{t.id}"
43
52
  aria-selected={t.id === active}
@@ -47,7 +56,9 @@
47
56
  onkeydown={(e) => onKey(e, i)}
48
57
  >
49
58
  {t.label}
50
- {#if t.badge !== undefined}<span class="badge">{t.badge}</span>{/if}
59
+ {#if t.badge !== undefined}
60
+ <span class="ml-1.5 tabular-nums text-text-4">{t.badge}</span>
61
+ {/if}
51
62
  </button>
52
63
  {/each}
53
64
  </div>