@w5-ui/svelte 0.2.0 → 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 +4 -4
  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
@@ -14,15 +14,17 @@
14
14
  }
15
15
 
16
16
  const TEXT_OPS = ['contains', 'not contains', 'equals', 'not equals', 'starts with'];
17
- const NUM_OPS = ['equals', 'not equals', 'greater than', 'less than', 'at least', 'at most'];
18
- const SEL_OPS = ['is', 'is not'];
17
+ const NUM_OPS = ['equals', 'not equals', 'greater than', 'less than', 'at least', 'at most'];
18
+ const SEL_OPS = ['is', 'is not'];
19
19
 
20
20
  function opsFor(type?: string) {
21
21
  if (type === 'number') return NUM_OPS;
22
22
  if (type === 'select') return SEL_OPS;
23
23
  return TEXT_OPS;
24
24
  }
25
- function defaultOp(type?: string) { return opsFor(type)[0]; }
25
+ function defaultOp(type?: string) {
26
+ return opsFor(type)[0];
27
+ }
26
28
  </script>
27
29
 
28
30
  <script lang="ts">
@@ -43,7 +45,6 @@
43
45
  }: Props = $props();
44
46
 
45
47
  const uid = `dash-ui-fb-${++counter}`;
46
-
47
48
  const CONJUNCTIONS = ['and', 'or'] as const;
48
49
 
49
50
  function emit(rules: FilterRule[], conj: 'and' | 'or' = conjunction) {
@@ -61,56 +62,77 @@
61
62
 
62
63
  function setField(id: string, key: string) {
63
64
  const f = fields.find((x) => x.key === key);
64
- emit(value.map((r) => r.id === id ? { ...r, field: key, op: defaultOp(f?.type), value: '' } : r));
65
+ emit(value.map((r) => (r.id === id ? { ...r, field: key, op: defaultOp(f?.type), value: '' } : r)));
65
66
  }
66
67
 
67
68
  function setOp(id: string, op: string) {
68
- emit(value.map((r) => r.id === id ? { ...r, op } : r));
69
+ emit(value.map((r) => (r.id === id ? { ...r, op } : r)));
69
70
  }
70
71
 
71
72
  function setValue(id: string, v: string) {
72
- emit(value.map((r) => r.id === id ? { ...r, value: v } : r));
73
+ emit(value.map((r) => (r.id === id ? { ...r, value: v } : r)));
73
74
  }
75
+
76
+ // Field chrome: select chevron uses a data-URL bg so the chrome stays in
77
+ // class strings rather than scoped CSS.
78
+ const SELECT_CHEVRON =
79
+ 'background-image:url("data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'10\' height=\'6\' viewBox=\'0 0 10 6\'%3E%3Cpath d=\'M1 1l4 4 4-4\' stroke=\'%236E7079\' stroke-width=\'1.5\' fill=\'none\' stroke-linecap=\'round\'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 8px center;';
80
+
81
+ const SELECT_CLS =
82
+ 'h-[30px] cursor-pointer appearance-none rounded border border-border-2 bg-row-active px-2 pr-6 text-13 leading-none text-text-1 outline-none transition-colors duration-100 hover:border-border-3 focus:border-brand-05';
83
+ const INPUT_CLS =
84
+ 'h-[30px] min-w-[140px] flex-1 rounded border border-border-2 bg-row-active px-2 text-13 leading-none text-text-1 outline-none transition-colors duration-100 placeholder:text-text-4 hover:border-border-3 focus:border-brand-05';
74
85
  </script>
75
86
 
76
- <div class="fb" role="group" aria-label={ariaLabel}>
77
- <div class="fb-header">
78
- <span class="fb-label">Filters</span>
87
+ <div class="flex flex-col gap-1.5" role="group" aria-label={ariaLabel}>
88
+ <div class="flex min-h-8 flex-wrap items-center gap-2">
89
+ <span class="mr-1 text-12 font-semibold uppercase tracking-[0.05em] text-text-3">Filters</span>
79
90
  {#if value.length > 0}
80
- <span class="fb-conj-label">Match</span>
81
- <div class="fb-conj" role="group" aria-label="Conjunction">
91
+ <span class="text-13 text-text-4">Match</span>
92
+ <div class="flex overflow-hidden rounded border border-border-2" role="group" aria-label="Conjunction">
82
93
  {#each CONJUNCTIONS as c}
83
94
  <button
84
95
  type="button"
85
- class="fb-conj-btn{conjunction === c ? ' is-active' : ''}"
86
96
  aria-pressed={conjunction === c}
97
+ class="cursor-pointer border-0 bg-transparent px-2.5 py-0.5 text-11 font-bold uppercase tracking-[0.06em] text-text-4 hover:bg-row-active hover:text-text-1 aria-pressed:bg-brand-05/[0.18] aria-pressed:text-status-info focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-brand-05"
87
98
  onclick={() => emit(value, c)}
88
99
  >{c.toUpperCase()}</button>
89
100
  {/each}
90
101
  </div>
91
- <span class="fb-conj-label">conditions</span>
102
+ <span class="text-13 text-text-4">conditions</span>
92
103
  {/if}
93
- <button type="button" class="fb-add" onclick={addRule} aria-label="Add filter rule">+ Add rule</button>
104
+ <button
105
+ type="button"
106
+ aria-label="Add filter rule"
107
+ class="ml-auto cursor-pointer rounded border border-dashed border-border-3 bg-transparent px-2.5 py-1 text-12 leading-none text-brand-05 hover:border-brand-05 hover:bg-brand-05/[0.08] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
108
+ onclick={addRule}
109
+ >+ Add rule</button>
94
110
  {#if value.length > 0}
95
- <button type="button" class="fb-clear" onclick={() => emit([])} aria-label="Clear all filters">Clear</button>
111
+ <button
112
+ type="button"
113
+ aria-label="Clear all filters"
114
+ class="cursor-pointer border-0 bg-transparent px-1.5 py-1 text-12 leading-none text-text-4 hover:text-text-1 focus-visible:rounded-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
115
+ onclick={() => emit([])}
116
+ >Clear</button>
96
117
  {/if}
97
118
  </div>
98
119
  {#if value.length > 0}
99
- <ul class="fb-rules" aria-label="Filter rules">
120
+ <ul class="m-0 flex list-none flex-col gap-1 p-0" aria-label="Filter rules">
100
121
  {#each value as rule, i}
101
122
  {@const f = fields.find((x) => x.key === rule.field)}
102
123
  {@const ops = opsFor(f?.type)}
103
124
  {@const fieldId = `${uid}-f${i}`}
104
125
  {@const opId = `${uid}-op${i}`}
105
126
  {@const valId = `${uid}-v${i}`}
106
- <li class="fb-rule">
127
+ <li class="flex items-center gap-1.5">
107
128
  <label class="sr-only" for={fieldId}>Field {i + 1}</label>
108
129
  <select
109
130
  id={fieldId}
110
- class="fb-select"
131
+ class={SELECT_CLS}
132
+ style={SELECT_CHEVRON}
111
133
  value={rule.field}
112
134
  onchange={(e) => setField(rule.id, e.currentTarget.value)}
113
- aria-label="Rule {i + 1} field"
135
+ aria-label={`Rule ${i + 1} field`}
114
136
  >
115
137
  {#each fields as x}
116
138
  <option value={x.key}>{x.label}</option>
@@ -119,10 +141,11 @@
119
141
  <label class="sr-only" for={opId}>Operator {i + 1}</label>
120
142
  <select
121
143
  id={opId}
122
- class="fb-select fb-select--op"
144
+ class={SELECT_CLS}
145
+ style={SELECT_CHEVRON}
123
146
  value={rule.op}
124
147
  onchange={(e) => setOp(rule.id, e.currentTarget.value)}
125
- aria-label="Rule {i + 1} operator"
148
+ aria-label={`Rule ${i + 1} operator`}
126
149
  >
127
150
  {#each ops as op}
128
151
  <option value={op}>{op}</option>
@@ -132,10 +155,11 @@
132
155
  {#if f?.type === 'select'}
133
156
  <select
134
157
  id={valId}
135
- class="fb-select fb-select--val"
158
+ class={SELECT_CLS}
159
+ style={SELECT_CHEVRON}
136
160
  value={rule.value}
137
161
  onchange={(e) => setValue(rule.id, e.currentTarget.value)}
138
- aria-label="Rule {i + 1} value"
162
+ aria-label={`Rule ${i + 1} value`}
139
163
  >
140
164
  <option value="">Any</option>
141
165
  {#each f.options ?? [] as o}
@@ -145,19 +169,19 @@
145
169
  {:else}
146
170
  <input
147
171
  id={valId}
148
- class="fb-input"
172
+ class={INPUT_CLS}
149
173
  type={f?.type === 'number' ? 'number' : 'text'}
150
174
  value={rule.value}
151
175
  placeholder="Value…"
152
176
  oninput={(e) => setValue(rule.id, e.currentTarget.value)}
153
- aria-label="Rule {i + 1} value"
177
+ aria-label={`Rule ${i + 1} value`}
154
178
  />
155
179
  {/if}
156
180
  <button
157
181
  type="button"
158
- class="fb-remove icon-btn"
182
+ aria-label={`Remove rule ${i + 1}`}
183
+ class="flex h-[26px] w-[26px] shrink-0 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent text-text-4 hover:bg-status-danger/10 hover:text-status-danger focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
159
184
  onclick={() => removeRule(rule.id)}
160
- aria-label="Remove rule {i + 1}"
161
185
  >
162
186
  <svg viewBox="0 0 12 12" width="10" height="10" aria-hidden="true" fill="currentColor">
163
187
  <path d="M1 1l10 10M11 1L1 11" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
@@ -0,0 +1,18 @@
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
+ Right-aligned action row closing a form <Card> or modal body
12
+ (display:flex; justify-content:flex-end; gap:8px; margin-top:16px).
13
+ Typed equivalent of `<div class="form-actions">` from
14
+ @w5-ui/tokens/dashboard.css.
15
+ -->
16
+ <div class="form-actions {className}">
17
+ {@render children?.()}
18
+ </div>
@@ -0,0 +1,17 @@
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
+ Two-column form grid (1fr 1fr, 14px gap, 6px top offset) that every Settings
12
+ card uses. Typed equivalent of `<div class="form-row">` from
13
+ @w5-ui/tokens/dashboard.css. Use inside wider <Card> tiles for dense editors.
14
+ -->
15
+ <div class="form-row {className}">
16
+ {@render children?.()}
17
+ </div>
@@ -0,0 +1,17 @@
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
+ The canonical 12-column page grid (repeat(12, 1fr), 12px gap, 16px 24px
12
+ padding, reserves room for the bottom panel). Typed equivalent of
13
+ `<div class="grid">` from @w5-ui/tokens/dashboard.css.
14
+ -->
15
+ <div class="grid {className}">
16
+ {@render children?.()}
17
+ </div>
@@ -34,57 +34,71 @@
34
34
  };
35
35
 
36
36
  const uid = Math.random().toString(36).slice(2, 9);
37
- // Initial open state mirrors each group's `defaultOpen` once; afterwards
38
- // the user toggles each entry independently.
39
37
  // svelte-ignore state_referenced_locally
40
38
  let open: boolean[] = $state(groups.map((g) => g.defaultOpen !== false));
41
39
 
42
- function toggle(i: number) { open[i] = !open[i]; open = [...open]; }
40
+ function toggle(i: number) {
41
+ open[i] = !open[i];
42
+ open = [...open];
43
+ }
44
+
45
+ const HEADER_CLS =
46
+ 'flex w-full items-center gap-2 rounded-md border border-border-1 bg-divider px-3 py-2 text-13 text-text-1';
47
+ const HEADER_BTN =
48
+ HEADER_CLS + ' cursor-pointer hover:bg-row-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05';
43
49
  </script>
44
50
 
45
- <div class="gl" aria-label={ariaLabel}>
51
+ <div class="flex flex-col gap-1.5" aria-label={ariaLabel}>
46
52
  {#each groups as group, gi}
47
53
  {@const isOpen = open[gi]}
48
54
  {@const panelId = `${uid}-gl-${gi}`}
49
- <div class="gl-group">
55
+ <div class="flex flex-col gap-0.5">
50
56
  {#if collapsible}
51
57
  <button
52
58
  type="button"
53
- class="gl-header"
54
59
  aria-expanded={isOpen}
55
60
  aria-controls={panelId}
61
+ class={HEADER_BTN}
56
62
  onclick={() => toggle(gi)}
57
63
  >
58
- {#if group.color}<span class="gl-color-dot" style="background:{group.color}" aria-hidden="true"></span>{/if}
59
- <span class="gl-title">{group.label}</span>
60
- <span class="gl-count">{group.items.length}</span>
64
+ {#if group.color}
65
+ <span class="inline-block h-2 w-2 shrink-0 rounded-full" style="background:{group.color}" aria-hidden="true"></span>
66
+ {/if}
67
+ <span class="flex-1 text-left font-medium">{group.label}</span>
68
+ <span class="rounded bg-row-active px-1.5 py-0.5 text-11 text-text-3 tabular-nums">{group.items.length}</span>
61
69
  <svg
62
- class="gl-chevron{isOpen ? ' gl-chevron--open' : ''}"
63
70
  width="12" height="12" viewBox="0 0 12 12"
64
71
  aria-hidden="true" focusable="false"
72
+ class="text-text-4 transition-transform duration-100 motion-reduce:transition-none {isOpen ? 'rotate-180' : ''}"
65
73
  >
66
74
  <path d="M2 4l4 4 4-4" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
67
75
  </svg>
68
76
  </button>
69
77
  {:else}
70
- <div class="gl-header gl-header--static">
71
- {#if group.color}<span class="gl-color-dot" style="background:{group.color}" aria-hidden="true"></span>{/if}
72
- <span class="gl-title">{group.label}</span>
73
- <span class="gl-count">{group.items.length}</span>
78
+ <div class={HEADER_CLS}>
79
+ {#if group.color}
80
+ <span class="inline-block h-2 w-2 shrink-0 rounded-full" style="background:{group.color}" aria-hidden="true"></span>
81
+ {/if}
82
+ <span class="flex-1 text-left font-medium">{group.label}</span>
83
+ <span class="rounded bg-row-active px-1.5 py-0.5 text-11 text-text-3 tabular-nums">{group.items.length}</span>
74
84
  </div>
75
85
  {/if}
76
86
  {#if !collapsible || isOpen}
77
- <ul id={panelId} class="gl-items" role="list">
87
+ <ul id={panelId} class="m-0 flex list-none flex-col gap-0.5 p-0 pl-4" role="list">
78
88
  {#each group.items as item}
79
- <li class="gl-item">
89
+ <li class="flex items-center gap-2 rounded px-2 py-1.5 text-13 hover:bg-divider">
80
90
  {#if item.status}
81
- <span class="gl-item-dot" style="background:{STATUS_COLORS[item.status]}" aria-label={item.status}></span>
91
+ <span
92
+ class="inline-block h-2 w-2 shrink-0 rounded-full"
93
+ style="background:{STATUS_COLORS[item.status]}"
94
+ aria-label={item.status}
95
+ ></span>
82
96
  {/if}
83
- <div class="gl-item-body">
84
- <div class="gl-item-label">{item.label}</div>
85
- {#if item.sublabel}<div class="gl-item-sublabel">{item.sublabel}</div>{/if}
97
+ <div class="min-w-0 flex-1">
98
+ <div class="truncate text-text-2">{item.label}</div>
99
+ {#if item.sublabel}<div class="truncate text-11 text-text-4">{item.sublabel}</div>{/if}
86
100
  </div>
87
- {#if item.meta}<span class="gl-item-meta">{item.meta}</span>{/if}
101
+ {#if item.meta}<span class="shrink-0 text-11 text-text-4 tabular-nums">{item.meta}</span>{/if}
88
102
  </li>
89
103
  {/each}
90
104
  </ul>
@@ -0,0 +1,29 @@
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
+ Page title - the 38 px / bold / --lh-snug rhythm with
12
+ --tracking-display (-0.02em) used for the top-of-page heading on
13
+ marketing surfaces (login, onboarding splash, top-level landing).
14
+ Renders <h1 class="dash-ui-h1"> so the .dash-ui-h1 rule in
15
+ @w5-ui/tokens/tokens.css pins the canonical size, weight, line-
16
+ height, letter-spacing, and --text-1 colour. Stops userland reaching
17
+ for a one-off <h1 style="font-size: 36px"> (or worse, leaving an
18
+ unstyled browser default <h1> at 32 px / 700 / 1.2 with no negative
19
+ tracking, which is the documented "headings break awkwardly across
20
+ two lines" drift fixed by --tracking-display). Typed equivalent of
21
+ <h1 class="dash-ui-h1"> (the userland-facing semantic class
22
+ documented in Foundations / Type). Use <H1> for marketing-grade page
23
+ titles. In-app pages should reach for <PageHeader> (which already
24
+ styles the page title) and use <H2> for section heads, <H3> for sub-
25
+ sections, <H4> for card titles.
26
+ -->
27
+ <h1 class="dash-ui-h1 {className}">
28
+ {@render children?.()}
29
+ </h1>
@@ -0,0 +1,29 @@
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
+ Page section heading - the 30 px / semibold / --lh-snug rhythm used
12
+ for top-of-section labels on a dashboard page (above a grid of cards,
13
+ above a table, above a settings group). Renders
14
+ <h2 class="dash-ui-h2"> so the .dash-ui-h2 rule in
15
+ @w5-ui/tokens/tokens.css pins the canonical size, weight, line-height,
16
+ and --text-1 colour. Stops userland reaching for a one-off
17
+ <h2 style="font-size: 30px; font-weight: 600"> (or worse, leaving an
18
+ unstyled browser default <h2> at ~24 px with 700 weight and 1.2 line-
19
+ height, which is the documented "section headings feel cramped on one
20
+ line" drift). Typed equivalent of <h2 class="dash-ui-h2"> (the
21
+ userland-facing semantic class documented in Foundations / Type). Use
22
+ <H2> for the page section head ("Devices", "Wi-Fi", "Settings")
23
+ sitting above a content block. For card titles prefer <H4> (or just
24
+ <Card title="..."> which injects .dash-ui-h4 for you); for full page
25
+ titles in marketing pages prefer <H1>.
26
+ -->
27
+ <h2 class="dash-ui-h2 {className}">
28
+ {@render children?.()}
29
+ </h2>
@@ -0,0 +1,29 @@
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
+ Sub-section heading - the 24 px / semibold / --lh-snug rhythm used
12
+ for sub-section labels on a dashboard page (a heading inside a card
13
+ region, a settings sub-group, a divider above a secondary table).
14
+ Renders <h3 class="dash-ui-h3"> so the .dash-ui-h3 rule in
15
+ @w5-ui/tokens/tokens.css pins the canonical size, weight, line-height,
16
+ and --text-1 colour. Stops userland reaching for a one-off
17
+ <h3 style="font-size: 24px; font-weight: 600"> (or worse, leaving an
18
+ unstyled browser default <h3> at ~18.7 px with 700 weight and 1.2
19
+ line-height, which is the documented "sub-section headings drift away
20
+ from the reference" case). Typed equivalent of
21
+ <h3 class="dash-ui-h3"> (the userland-facing semantic class
22
+ documented in Foundations / Type). Use <H3> for a sub-section head
23
+ sitting below an <H2> page section head. For card titles prefer <H4>
24
+ (or just <Card title="..."> which injects .dash-ui-h4 for you); for
25
+ page section heads prefer <H2>.
26
+ -->
27
+ <h3 class="dash-ui-h3 {className}">
28
+ {@render children?.()}
29
+ </h3>
@@ -0,0 +1,29 @@
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
+ Card / tile title - the 18 px / semibold / --lh-snug rhythm used for
12
+ the heading that sits inside a card, a stat tile, or a settings
13
+ sub-panel. Renders <h4 class="dash-ui-h4"> so the .dash-ui-h4 rule in
14
+ @w5-ui/tokens/tokens.css pins the canonical size, weight, line-height,
15
+ and --text-1 colour. Stops userland reaching for a one-off
16
+ <h3 style="font-size: 20px"> or text-xl (the documented "card title
17
+ size doesn't match the reference" case), and stops them leaving an
18
+ unstyled browser default <h4> at ~13.3 px / 700 / 1.2 which reads as
19
+ caption text instead of a title. Typed equivalent of
20
+ <h4 class="dash-ui-h4"> (the userland-facing semantic class documented
21
+ in Foundations / Type). Use <H4> when you build a card-like tile by
22
+ hand and need the canonical 18 / 600 title. If you reach for
23
+ <Card title="..."> you get the same class injected for you - prefer
24
+ that. For page section heads prefer <H2>; for sub-section heads
25
+ prefer <H3>.
26
+ -->
27
+ <h4 class="dash-ui-h4 {className}">
28
+ {@render children?.()}
29
+ </h4>
@@ -10,13 +10,13 @@
10
10
  value,
11
11
  fillStyle = '',
12
12
  label = undefined,
13
- valueText = undefined
13
+ valueText = undefined,
14
14
  }: Props = $props();
15
15
  let clamped = $derived(Math.max(0, Math.min(100, value)));
16
16
  </script>
17
17
 
18
18
  <div
19
- class="hb"
19
+ class="h-1 overflow-hidden rounded-sm bg-row-active"
20
20
  role="progressbar"
21
21
  aria-valuenow={Math.round(clamped)}
22
22
  aria-valuemin={0}
@@ -24,5 +24,8 @@
24
24
  aria-label={label}
25
25
  aria-valuetext={valueText}
26
26
  >
27
- <div class="hb-fill" style="width:{clamped}%; {fillStyle}"></div>
27
+ <div
28
+ class="h-full bg-gradient-to-r from-brand-05 to-brand-06"
29
+ style="width:{clamped}%; {fillStyle}"
30
+ ></div>
28
31
  </div>
@@ -4,6 +4,7 @@
4
4
 
5
5
  <script lang="ts">
6
6
  import { onDestroy } from 'svelte';
7
+
7
8
  interface Props {
8
9
  placement?: 'top' | 'bottom' | 'left' | 'right';
9
10
  delay?: number;
@@ -17,9 +18,8 @@
17
18
  delay = 300,
18
19
  class: className = '',
19
20
  trigger,
20
- content
21
+ content,
21
22
  }: Props = $props();
22
-
23
23
 
24
24
  const id = `dash-ui-hovercard-${++counter}`;
25
25
  let open = $state(false);
@@ -36,21 +36,32 @@
36
36
  }
37
37
 
38
38
  onDestroy(() => { if (timer) clearTimeout(timer); });
39
+
40
+ const PLACEMENT: Record<NonNullable<Props['placement']>, string> = {
41
+ top: 'bottom-[calc(100%+6px)] left-0',
42
+ bottom: 'top-[calc(100%+6px)] left-0',
43
+ left: 'right-[calc(100%+6px)] top-0',
44
+ right: 'left-[calc(100%+6px)] top-0',
45
+ };
39
46
  </script>
40
47
 
41
48
  <div
42
49
  role="group"
43
- class="hovercard-wrapper hovercard-{placement} {className}"
50
+ class="relative block {className}"
44
51
  onmouseenter={show}
45
52
  onmouseleave={hide}
46
53
  onfocusin={show}
47
54
  onfocusout={hide}
48
55
  >
49
- <div class="hovercard-trigger" aria-describedby={open ? id : undefined}>
56
+ <div class="block" aria-describedby={open ? id : undefined}>
50
57
  {@render trigger?.()}
51
58
  </div>
52
59
  {#if open}
53
- <div {id} role="tooltip" class="hovercard">
60
+ <div
61
+ {id}
62
+ role="tooltip"
63
+ class="absolute z-[9998] min-w-[200px] max-w-[280px] rounded-[10px] border border-border-3 bg-bg-2 p-3 text-12 shadow-[0_8px_32px_rgba(0,0,0,0.5)] {PLACEMENT[placement]}"
64
+ >
54
65
  {@render content?.()}
55
66
  </div>
56
67
  {/if}
@@ -3,8 +3,6 @@
3
3
  </script>
4
4
 
5
5
  <script lang="ts">
6
-
7
-
8
6
  interface Props {
9
7
  label?: string | undefined;
10
8
  value?: string;
@@ -75,13 +73,21 @@
75
73
  setTimeout(() => focusAt(3), 0);
76
74
  }
77
75
  }
76
+
77
+ const OCTET_CLS =
78
+ 'w-9 border-0 bg-transparent p-0 text-center font-mono text-13 text-text-1 outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:m-0 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:m-0 [&::-webkit-outer-spin-button]:appearance-none';
78
79
  </script>
79
80
 
80
- <div class="ip-input-wrapper {className}">
81
+ <div class={className}>
81
82
  {#if label}
82
- <label for="{uid}-0" class="ip-input__label">{label}</label>
83
+ <label for="{uid}-0" class="text-12 text-text-4">{label}</label>
83
84
  {/if}
84
- <div class="ip-input{disabled ? ' ip-input--disabled' : ''}" role="group" aria-label={label ?? 'IP address'}>
85
+ <div
86
+ role="group"
87
+ aria-label={label ?? 'IP address'}
88
+ data-disabled={disabled ? 'true' : undefined}
89
+ class="inline-flex h-[34px] items-center gap-0.5 rounded-md border border-border-2 bg-bg-page px-2 transition-colors duration-100 focus-within:border-brand-05 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-40"
90
+ >
85
91
  {#each octets as oct, i}
86
92
  <input
87
93
  bind:this={inputs[i]}
@@ -90,21 +96,21 @@
90
96
  inputmode="numeric"
91
97
  pattern="[0-9]*"
92
98
  role="spinbutton"
93
- aria-label="Octet {i + 1} of 4"
99
+ aria-label={`Octet ${i + 1} of 4`}
94
100
  aria-valuenow={oct === '' ? 0 : Number(oct)}
95
101
  aria-valuemin={0}
96
102
  aria-valuemax={255}
97
103
  value={oct}
98
104
  {disabled}
99
105
  maxlength={3}
100
- class="ip-input__octet"
106
+ class={OCTET_CLS}
101
107
  oninput={(e) => commitOctet(i, e.currentTarget.value)}
102
108
  onkeydown={(e) => handleKey(i, e)}
103
109
  onpaste={handlePaste}
104
110
  onfocus={(e) => e.currentTarget.select()}
105
111
  />
106
112
  {#if i < 3}
107
- <span class="ip-input__dot" aria-hidden="true">.</span>
113
+ <span aria-hidden="true" class="select-none text-13 leading-none text-text-4">.</span>
108
114
  {/if}
109
115
  {/each}
110
116
  </div>
@@ -11,7 +11,6 @@
11
11
  [key: string]: unknown;
12
12
  }
13
13
 
14
- // svelte-ignore state_referenced_locally
15
14
  // svelte-ignore state_referenced_locally
16
15
  let {
17
16
  title = undefined,
@@ -26,7 +25,7 @@
26
25
 
27
26
  <button
28
27
  {type}
29
- class="icon-btn {className}"
28
+ class="inline-flex h-8 w-8 cursor-pointer items-center justify-center rounded-md text-text-3 hover:bg-row-hover hover:text-text-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05 {className}"
30
29
  {title}
31
30
  {style}
32
31
  {...rest}