@w5-ui/svelte 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +116 -4
  2. package/package.json +2 -2
  3. package/src/lib/actions/portal.ts +55 -0
  4. package/src/lib/components/Accordion.svelte +1 -1
  5. package/src/lib/components/AccordionItem.svelte +6 -7
  6. package/src/lib/components/ActionMenu.svelte +17 -9
  7. package/src/lib/components/ActivityFeed.svelte +20 -14
  8. package/src/lib/components/Alert.svelte +22 -4
  9. package/src/lib/components/AppLayout.svelte +34 -0
  10. package/src/lib/components/Avatar.svelte +28 -11
  11. package/src/lib/components/AvatarGroup.svelte +21 -4
  12. package/src/lib/components/Badge.svelte +22 -6
  13. package/src/lib/components/Banner.svelte +36 -7
  14. package/src/lib/components/Body.svelte +29 -0
  15. package/src/lib/components/BodyLg.svelte +26 -0
  16. package/src/lib/components/Breadcrumb.svelte +19 -12
  17. package/src/lib/components/Button.svelte +30 -1
  18. package/src/lib/components/CIDRInput.svelte +17 -9
  19. package/src/lib/components/Callout.svelte +29 -8
  20. package/src/lib/components/Caption.svelte +26 -0
  21. package/src/lib/components/Card.svelte +11 -3
  22. package/src/lib/components/CardMore.svelte +20 -0
  23. package/src/lib/components/CardTitle.svelte +20 -0
  24. package/src/lib/components/Carousel.svelte +27 -20
  25. package/src/lib/components/Checkbox.svelte +24 -3
  26. package/src/lib/components/Code.svelte +24 -0
  27. package/src/lib/components/CodeBlock.svelte +8 -7
  28. package/src/lib/components/ColorPicker.svelte +13 -13
  29. package/src/lib/components/ColumnToggle.svelte +10 -9
  30. package/src/lib/components/Combobox.svelte +31 -17
  31. package/src/lib/components/CommandPalette.svelte +42 -26
  32. package/src/lib/components/ConfirmDialog.svelte +21 -15
  33. package/src/lib/components/ContextMenu.svelte +12 -12
  34. package/src/lib/components/ContextualHelp.svelte +11 -6
  35. package/src/lib/components/CopyButton.svelte +2 -1
  36. package/src/lib/components/DatePicker.svelte +27 -23
  37. package/src/lib/components/DateRangePicker.svelte +46 -39
  38. package/src/lib/components/Delta.svelte +25 -0
  39. package/src/lib/components/Display1.svelte +29 -0
  40. package/src/lib/components/Display2.svelte +29 -0
  41. package/src/lib/components/Drawer.svelte +6 -5
  42. package/src/lib/components/DurationInput.svelte +16 -8
  43. package/src/lib/components/EmptyState.svelte +16 -7
  44. package/src/lib/components/ExpandableRow.svelte +8 -15
  45. package/src/lib/components/Eyebrow.svelte +25 -0
  46. package/src/lib/components/Field.svelte +7 -7
  47. package/src/lib/components/FieldStack.svelte +18 -0
  48. package/src/lib/components/FileUpload.svelte +8 -10
  49. package/src/lib/components/FilterBuilder.svelte +52 -28
  50. package/src/lib/components/FormActions.svelte +18 -0
  51. package/src/lib/components/FormRow.svelte +17 -0
  52. package/src/lib/components/Grid.svelte +17 -0
  53. package/src/lib/components/GroupedList.svelte +35 -21
  54. package/src/lib/components/H1.svelte +29 -0
  55. package/src/lib/components/H2.svelte +29 -0
  56. package/src/lib/components/H3.svelte +29 -0
  57. package/src/lib/components/H4.svelte +29 -0
  58. package/src/lib/components/HealthBar.svelte +6 -3
  59. package/src/lib/components/HoverCard.svelte +16 -5
  60. package/src/lib/components/IPInput.svelte +14 -8
  61. package/src/lib/components/IconButton.svelte +1 -2
  62. package/src/lib/components/InlineEdit.svelte +25 -14
  63. package/src/lib/components/Input.svelte +13 -4
  64. package/src/lib/components/InputGroup.svelte +14 -3
  65. package/src/lib/components/JsonViewer.svelte +7 -2
  66. package/src/lib/components/JsonViewerNode.svelte +38 -32
  67. package/src/lib/components/KVTable.svelte +10 -4
  68. package/src/lib/components/KanbanBoard.svelte +29 -27
  69. package/src/lib/components/Kbd.svelte +8 -5
  70. package/src/lib/components/Label.svelte +28 -0
  71. package/src/lib/components/LogViewer.svelte +24 -20
  72. package/src/lib/components/MACInput.svelte +14 -8
  73. package/src/lib/components/Mac.svelte +23 -0
  74. package/src/lib/components/Menubar.svelte +30 -13
  75. package/src/lib/components/Modal.svelte +15 -6
  76. package/src/lib/components/Mono.svelte +28 -0
  77. package/src/lib/components/MultiSelect.svelte +43 -23
  78. package/src/lib/components/Muted.svelte +24 -0
  79. package/src/lib/components/NameCell.svelte +35 -0
  80. package/src/lib/components/NotifDot.svelte +23 -0
  81. package/src/lib/components/NotificationPanel.svelte +44 -31
  82. package/src/lib/components/NumberInput.svelte +13 -12
  83. package/src/lib/components/OTPInput.svelte +14 -8
  84. package/src/lib/components/PageHeader.svelte +25 -0
  85. package/src/lib/components/Pagination.svelte +16 -12
  86. package/src/lib/components/PasswordInput.svelte +8 -7
  87. package/src/lib/components/Pill.svelte +34 -6
  88. package/src/lib/components/Popover.svelte +101 -17
  89. package/src/lib/components/ProgressBar.svelte +10 -7
  90. package/src/lib/components/RadioGroup.svelte +22 -7
  91. package/src/lib/components/RangeSlider.svelte +17 -12
  92. package/src/lib/components/RankedList.svelte +12 -18
  93. package/src/lib/components/ResizablePanel.svelte +25 -14
  94. package/src/lib/components/RowToggle.svelte +6 -4
  95. package/src/lib/components/SearchBox.svelte +6 -2
  96. package/src/lib/components/SegmentedControl.svelte +16 -2
  97. package/src/lib/components/Select.svelte +20 -8
  98. package/src/lib/components/SelectionToolbar.svelte +13 -9
  99. package/src/lib/components/Sidebar.svelte +32 -11
  100. package/src/lib/components/Signal.svelte +3 -2
  101. package/src/lib/components/Skeleton.svelte +21 -9
  102. package/src/lib/components/SkipLink.svelte +5 -2
  103. package/src/lib/components/Slider.svelte +16 -9
  104. package/src/lib/components/SortHeader.svelte +13 -4
  105. package/src/lib/components/SortableList.svelte +17 -18
  106. package/src/lib/components/Sparkline.svelte +20 -14
  107. package/src/lib/components/Spinner.svelte +16 -1
  108. package/src/lib/components/SplitButton.svelte +27 -8
  109. package/src/lib/components/Spoiler.svelte +8 -6
  110. package/src/lib/components/SrOnly.svelte +23 -0
  111. package/src/lib/components/StackedProgress.svelte +17 -9
  112. package/src/lib/components/StarRating.svelte +35 -10
  113. package/src/lib/components/Stat.svelte +19 -5
  114. package/src/lib/components/StatNumber.svelte +22 -0
  115. package/src/lib/components/StatusIndicator.svelte +9 -2
  116. package/src/lib/components/StatusRing.svelte +36 -0
  117. package/src/lib/components/Stepper.svelte +30 -8
  118. package/src/lib/components/Submeta.svelte +19 -0
  119. package/src/lib/components/SwitchPortGrid.svelte +20 -7
  120. package/src/lib/components/TablePager.svelte +22 -0
  121. package/src/lib/components/TableShell.svelte +19 -0
  122. package/src/lib/components/Tabs.svelte +16 -5
  123. package/src/lib/components/Tag.svelte +5 -3
  124. package/src/lib/components/TagInput.svelte +9 -10
  125. package/src/lib/components/Textarea.svelte +7 -1
  126. package/src/lib/components/TimePicker.svelte +14 -8
  127. package/src/lib/components/TimeRange.svelte +20 -22
  128. package/src/lib/components/Timeline.svelte +20 -9
  129. package/src/lib/components/Toast.svelte +24 -5
  130. package/src/lib/components/Toggle.svelte +10 -4
  131. package/src/lib/components/ToggleGroup.svelte +15 -5
  132. package/src/lib/components/Tooltip.svelte +20 -5
  133. package/src/lib/components/Topbar.svelte +161 -28
  134. package/src/lib/components/TopbarActions.svelte +52 -0
  135. package/src/lib/components/TransferList.svelte +30 -22
  136. package/src/lib/components/TreeItem.svelte +11 -13
  137. package/src/lib/components/Unit.svelte +22 -0
  138. package/src/lib/components/VirtualList.svelte +12 -9
  139. package/src/lib/index.ts +37 -0
@@ -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
+ Inline monospace label for MAC addresses, IPs, SKUs, serial numbers and
12
+ other fixed-width identifiers that read inside a table cell or a property
13
+ row. Renders a <span class="mac"> so the .mac rule in
14
+ @w5-ui/tokens/dashboard.css applies the canonical JetBrains Mono family,
15
+ 12 px size, and muted colour. Stops userland reaching for inline
16
+ font-family / font-size overrides on bare identifiers. Typed equivalent of
17
+ <span class="mac"> (apply the class directly to a <td> when the entire
18
+ cell is the identifier, or wrap the cell contents with <Mac> when the cell
19
+ carries other markup too).
20
+ -->
21
+ <span class="mac {className}">
22
+ {@render children?.()}
23
+ </span>
@@ -1,7 +1,16 @@
1
1
  <script module lang="ts">
2
2
  let counter = 0;
3
- export interface MenubarItem { id: string; label: string; disabled?: boolean; separator?: boolean; }
4
- export interface MenubarMenu { id: string; label: string; items: MenubarItem[]; }
3
+ export interface MenubarItem {
4
+ id: string;
5
+ label: string;
6
+ disabled?: boolean;
7
+ separator?: boolean;
8
+ }
9
+ export interface MenubarMenu {
10
+ id: string;
11
+ label: string;
12
+ items: MenubarItem[];
13
+ }
5
14
  </script>
6
15
 
7
16
  <script lang="ts">
@@ -27,9 +36,9 @@
27
36
  let triggerEls: HTMLButtonElement[] = $state([]);
28
37
  let rootEl = $state<HTMLDivElement | undefined>(undefined);
29
38
 
30
- let currentEligible = $derived(openIdx !== null
31
- ? menus[openIdx].items.filter((i) => !i.separator && !i.disabled)
32
- : []);
39
+ let currentEligible = $derived(
40
+ openIdx !== null ? menus[openIdx].items.filter((i) => !i.separator && !i.disabled) : [],
41
+ );
33
42
 
34
43
  function openMenu(idx: number, focusLast = false) {
35
44
  const items = menus[idx].items.filter((i) => !i.separator && !i.disabled);
@@ -37,7 +46,9 @@
37
46
  openIdx = idx;
38
47
  }
39
48
 
40
- function closeMenu() { openIdx = null; }
49
+ function closeMenu() {
50
+ openIdx = null;
51
+ }
41
52
 
42
53
  function activate(menuId: string, itemId: string) {
43
54
  onaction?.({ menuId, itemId });
@@ -93,13 +104,13 @@
93
104
  bind:this={rootEl}
94
105
  role="menubar"
95
106
  aria-label={label}
96
- class={`menubar${klass ? ' ' + klass : ''}`}
107
+ class="inline-flex items-center gap-0.5 rounded-md border border-border-1 bg-divider p-0.5 {klass}"
97
108
  >
98
109
  {#each menus as menu, idx (menu.id)}
99
110
  {@const isOpen = openIdx === idx}
100
111
  {@const menuId = `${uid}-menu-${idx}`}
101
112
  {@const elig = menu.items.filter((i) => !i.separator && !i.disabled)}
102
- <div class="menubar-menu">
113
+ <div class="relative">
103
114
  <button
104
115
  bind:this={triggerEls[idx]}
105
116
  type="button"
@@ -107,25 +118,31 @@
107
118
  aria-haspopup="menu"
108
119
  aria-expanded={isOpen}
109
120
  aria-controls={isOpen ? menuId : undefined}
110
- class={`menubar-trigger${isOpen ? ' is-open' : ''}`}
121
+ data-open={isOpen ? 'true' : undefined}
122
+ class="inline-flex cursor-pointer items-center rounded border-0 bg-transparent px-2.5 py-1.5 text-13 leading-none text-text-3 transition-[background-color,color] duration-100 hover:bg-row-active hover:text-text-1 focus-visible:outline-2 focus-visible:outline-offset-[1px] focus-visible:outline-brand-05 data-[open=true]:bg-row-active data-[open=true]:text-text-1"
111
123
  onclick={() => { if (isOpen) closeMenu(); else openMenu(idx); }}
112
124
  onkeydown={(e) => handleTriggerKeyDown(e, idx)}
113
125
  >
114
126
  {menu.label}
115
127
  </button>
116
128
  {#if isOpen}
117
- <ul id={menuId} role="menu" aria-label={menu.label} class="menubar-dropdown">
129
+ <ul
130
+ id={menuId}
131
+ role="menu"
132
+ aria-label={menu.label}
133
+ class="absolute left-0 top-[calc(100%+2px)] z-[200] m-0 min-w-[180px] list-none rounded-md border border-border-3 bg-bg-2 p-1 shadow-[0_8px_24px_rgba(0,0,0,0.4)]"
134
+ >
118
135
  {#each menu.items as item (item.id)}
119
136
  {#if item.separator}
120
- <li role="separator" class="menubar-sep" aria-hidden="true"></li>
137
+ <li role="separator" aria-hidden="true" class="my-[3px] h-px list-none bg-row-active"></li>
121
138
  {:else}
122
139
  {@const eligIdx = elig.indexOf(item)}
123
140
  <li
124
141
  role="menuitem"
125
- tabindex="-1"
142
+ tabindex={-1}
126
143
  aria-disabled={item.disabled}
127
144
  data-active={eligIdx === activeItemIdx && !item.disabled ? 'true' : undefined}
128
- class="menubar-item"
145
+ class="flex cursor-pointer select-none items-center whitespace-nowrap rounded px-2.5 py-1.5 text-13 text-text-3 transition-colors duration-75 hover:bg-row-active hover:text-text-1 data-[active=true]:bg-row-active data-[active=true]:text-text-1 aria-disabled:cursor-default aria-disabled:text-white/25 aria-disabled:pointer-events-none"
129
146
  onmouseenter={() => { if (!item.disabled) activeItemIdx = eligIdx; }}
130
147
  onmousedown={(e) => { e.preventDefault(); (() => { if (!item.disabled) activate(menu.id, item.id); })(); }}
131
148
  >{item.label}</li>
@@ -72,8 +72,15 @@
72
72
 
73
73
  <svelte:window onkeydown={onKeydown} />
74
74
 
75
+ <!--
76
+ Modal chrome matches dashboard.css literal values for cross-motif
77
+ consistency: backdrop is solid 55% black with 8px blur, modal panel is
78
+ bg #141415 (neutral-09) with rgba(255,255,255,0.1) border, header +
79
+ footer separators at rgba(255,255,255,0.06), footer surface at #0f0f10.
80
+ -->
75
81
  <div
76
- class="backdrop {open ? 'show' : ''}"
82
+ class="fixed inset-0 z-[60] items-center justify-center bg-black/55 backdrop-blur-lg
83
+ {open ? 'flex' : 'hidden'}"
77
84
  onmousedown={(e) => (downOnBackdrop = e.target === e.currentTarget)}
78
85
  onclick={(e) => {
79
86
  if (downOnBackdrop && e.target === e.currentTarget) open = false;
@@ -83,21 +90,23 @@
83
90
  >
84
91
  <div
85
92
  bind:this={modalEl}
86
- class="modal"
93
+ class="w-[520px] max-w-[90vw] overflow-hidden rounded-xl border border-border-2 bg-bg-1 shadow-modal"
87
94
  role="dialog"
88
95
  aria-modal="true"
89
96
  aria-labelledby={titleId}
90
97
  tabindex="-1"
91
98
  >
92
- <div class="modal-h">
93
- <h2 id={titleId}>{title}</h2>
99
+ <div class="flex items-center justify-between border-b border-border-1 px-5 py-4">
100
+ <h2 id={titleId} class="m-0 text-16 font-semibold">{title}</h2>
94
101
  <IconButton title="Close" onclick={() => (open = false)}>
95
102
  <CloseIcon />
96
103
  </IconButton>
97
104
  </div>
98
- <div class="modal-b">{@render children?.()}</div>
105
+ <div class="px-5 py-4">{@render children?.()}</div>
99
106
  {#if footer}
100
- <div class="modal-f">{@render footer?.()}</div>
107
+ <div
108
+ class="flex justify-end gap-2 border-t border-border-1 bg-bg-0 px-5 py-3.5"
109
+ >{@render footer?.()}</div>
101
110
  {/if}
102
111
  </div>
103
112
  </div>
@@ -0,0 +1,28 @@
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
+ Inline monospace identifier for MAC addresses, IPs, hex IDs, ports,
12
+ firmware versions, config keys, and other machine-shaped values that
13
+ read inside body copy or a property row. Renders a
14
+ <span class="dash-ui-mono"> so the .dash-ui-mono rule in
15
+ @w5-ui/tokens/tokens.css applies the canonical JetBrains Mono family,
16
+ 13 px size, regular weight, 1.4 line-height, and --text-2 colour. The
17
+ class also enables font-variant-numeric: tabular-nums so a column of
18
+ MACs / IPs / versions aligns vertically. Stops userland reaching for
19
+ bare font-family: monospace (which falls back to a serif Courier with
20
+ proportional digits) or one-off font-size overrides on identifiers.
21
+ Typed equivalent of <span class="dash-ui-mono"> (the userland-facing
22
+ semantic class documented in Foundations / Type). Prefer <Mono> for
23
+ new userland code; reach for <Mac> only when you need the dashboard's
24
+ denser 12 px .mac variant (reference Devices / Clients table cells).
25
+ -->
26
+ <span class="dash-ui-mono {className}">
27
+ {@render children?.()}
28
+ </span>
@@ -1,6 +1,9 @@
1
1
  <script module lang="ts">
2
2
  let counter = 0;
3
- export interface MultiSelectOption { value: string; label: string; }
3
+ export interface MultiSelectOption {
4
+ value: string;
5
+ label: string;
6
+ }
4
7
  </script>
5
8
 
6
9
  <script lang="ts">
@@ -27,7 +30,6 @@
27
30
  class: className = '',
28
31
  onchange,
29
32
  }: Props = $props();
30
-
31
33
 
32
34
  const uid = `dash-ui-ms-${++counter}`;
33
35
  let inputId = $derived(id ?? uid);
@@ -40,13 +42,15 @@
40
42
 
41
43
  let selectedSet = $derived(new Set(value));
42
44
 
43
- let filtered =
44
- $derived(query.trim() === ''
45
+ let filtered = $derived(
46
+ query.trim() === ''
45
47
  ? options
46
- : options.filter((o) => o.label.toLowerCase().includes(query.toLowerCase())));
48
+ : options.filter((o) => o.label.toLowerCase().includes(query.toLowerCase())),
49
+ );
47
50
 
48
- let activeOptionId =
49
- $derived(activeIdx >= 0 && filtered[activeIdx] ? `${listboxId}-opt-${activeIdx}` : undefined);
51
+ let activeOptionId = $derived(
52
+ activeIdx >= 0 && filtered[activeIdx] ? `${listboxId}-opt-${activeIdx}` : undefined,
53
+ );
50
54
 
51
55
  function toggle(optValue: string) {
52
56
  const next = new Set(selectedSet);
@@ -88,22 +92,23 @@
88
92
  onDestroy(() => document.removeEventListener('mousedown', handleOutside));
89
93
  </script>
90
94
 
91
- <div class="multiselect-wrapper {className}" bind:this={wrapperEl}>
95
+ <div class="relative block {className}" bind:this={wrapperEl}>
92
96
  {#if label}<label for={inputId} class="sr-only">{label}</label>{/if}
93
97
  <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
94
98
  <div
95
- class="multiselect-field{open ? ' multiselect-field--open' : ''}"
99
+ data-open={open ? 'true' : undefined}
100
+ class="relative flex min-h-[34px] cursor-text flex-wrap items-center gap-1 rounded-md border border-border-2 bg-bg-page py-[3px] pl-2 pr-9 transition-colors duration-100 hover:border-border-3 data-[open=true]:border-brand-05 data-[open=true]:shadow-[0_0_0_2px_rgba(0,111,255,0.2)]"
96
101
  onclick={() => { if (!disabled) inputEl?.focus(); }}
97
102
  >
98
103
  {#each value as v (v)}
99
104
  {@const opt = options.find((o) => o.value === v)}
100
105
  {#if opt}
101
- <span class="tag">
102
- <span class="tag__label">{opt.label}</span>
106
+ <span class="inline-flex h-6 items-center gap-1 whitespace-nowrap rounded-full border border-brand-05/25 bg-brand-05/[0.14] pl-2.5 pr-1 text-12 font-medium text-status-info">
107
+ <span class="leading-none">{opt.label}</span>
103
108
  <button
104
109
  type="button"
105
- class="tag__remove"
106
- aria-label="Remove {opt.label} filter"
110
+ aria-label={`Remove ${opt.label} filter`}
111
+ class="inline-flex h-[18px] w-[18px] shrink-0 cursor-pointer items-center justify-center rounded-full border-0 bg-transparent p-0 text-status-info transition-colors duration-100 hover:bg-brand-05/25 hover:text-text-1 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-brand-05"
107
112
  onclick={(e) => { e.stopPropagation(); (() => toggle(v))(); }}
108
113
  >
109
114
  <svg viewBox="0 0 10 10" width="10" height="10" fill="none" aria-hidden="true" focusable="false">
@@ -130,38 +135,53 @@
130
135
  oninput={onInput}
131
136
  onfocus={() => { if (!disabled) open = true; }}
132
137
  onkeydown={onKeyDown}
133
- class="multiselect-input"
138
+ class="h-6 min-w-[80px] flex-1 border-0 bg-transparent px-1 text-13 leading-none text-text-1 outline-none placeholder:text-text-4"
134
139
  />
135
140
  <button
136
141
  type="button"
137
- tabindex="-1"
142
+ tabindex={-1}
138
143
  aria-hidden="true"
139
- class="combobox-chevron-btn"
144
+ class="absolute right-2 flex cursor-pointer items-center border-0 bg-transparent p-0.5 leading-none text-text-4"
140
145
  onclick={(e) => { e.stopPropagation(); (() => { if (open) { open = false; query = ''; } else { inputEl?.focus(); open = true; } })(); }}
141
146
  >
142
- <svg class="select-chevron{open ? ' combobox-chevron-open' : ''}" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
147
+ <svg
148
+ viewBox="0 0 16 16"
149
+ aria-hidden="true"
150
+ focusable="false"
151
+ class="h-3.5 w-3.5 transition-transform duration-100 motion-reduce:transition-none {open ? 'rotate-180' : ''}"
152
+ >
143
153
  <path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none" />
144
154
  </svg>
145
155
  </button>
146
156
  </div>
147
157
  {#if open}
148
- <ul id={listboxId} role="listbox" aria-label={label} aria-multiselectable="true" class="select-listbox multiselect-listbox">
158
+ <ul
159
+ id={listboxId}
160
+ role="listbox"
161
+ aria-label={label}
162
+ aria-multiselectable="true"
163
+ class="absolute left-0 top-[calc(100%+4px)] z-[9000] m-0 max-h-60 min-w-full list-none overflow-y-auto rounded-lg border border-border-3 bg-bg-2 p-1 shadow-[0_8px_24px_rgba(0,0,0,0.5)]"
164
+ >
149
165
  {#if filtered.length === 0}
150
- <li class="combobox-empty">No results</li>
166
+ <li class="list-none p-2.5 text-center text-13 text-text-4">No results</li>
151
167
  {:else}
152
168
  {#each filtered as opt, idx (opt.value)}
169
+ {@const selected = selectedSet.has(opt.value)}
153
170
  <!-- svelte-ignore a11y_click_events_have_key_events -->
154
171
  <li
155
172
  id="{listboxId}-opt-{idx}"
156
173
  role="option"
157
- aria-selected={selectedSet.has(opt.value)}
174
+ aria-selected={selected}
158
175
  data-active={idx === activeIdx ? 'true' : undefined}
159
- class="select-option multiselect-option"
176
+ class="flex cursor-pointer items-center gap-2 rounded-[5px] px-2.5 py-1.5 text-13 text-text-2 hover:bg-row-active hover:text-text-1 data-[active=true]:bg-row-active data-[active=true]:text-text-1"
160
177
  onmousedown={(e) => { e.preventDefault(); (() => toggle(opt.value))(); }}
161
178
  onmouseenter={() => { activeIdx = idx; }}
162
179
  >
163
- <span class="multiselect-check" aria-hidden="true">
164
- {#if selectedSet.has(opt.value)}
180
+ <span
181
+ aria-hidden="true"
182
+ class="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded-sm border border-border-3 {selected ? 'border-brand-05 bg-brand-05 text-white' : 'text-transparent'}"
183
+ >
184
+ {#if selected}
165
185
  <svg viewBox="0 0 12 12" width="12" height="12" fill="none" aria-hidden="true">
166
186
  <path d="M2 6l3 3 5-5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
167
187
  </svg>
@@ -0,0 +1,24 @@
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
+ Inline span that drops a phrase to the tertiary text colour
12
+ (--text-3). Renders <span class="t-muted"> so the .t-muted rule in
13
+ @w5-ui/tokens/dashboard.css applies the canonical muted colour used by
14
+ .submeta, .stat .unit and the .card h3 title chrome. Use for
15
+ "X active" counters next to an icon, caption-style metadata sitting
16
+ beside a primary label, or any sub-line that would otherwise tempt
17
+ userland to reach for a one-off color: #... override. Typed equivalent
18
+ of <span class="t-muted"> (the contrast pair is AA against every motif
19
+ background, verified for --text-3 over --depthBg-0/--depthBg-1/--depthBg-2
20
+ in both dark and light motifs).
21
+ -->
22
+ <span class="t-muted {className}">
23
+ {@render children?.()}
24
+ </span>
@@ -0,0 +1,35 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ class?: string;
4
+ /**
5
+ * Extra class for the inner .nc-thumb (e.g. "cam" for the camera
6
+ * gradient variant). Ignored when the thumb snippet is not supplied.
7
+ */
8
+ thumbClass?: string;
9
+ /**
10
+ * Square 28px avatar / initial that sits to the left of the name. Pass
11
+ * a snippet rendering a letter, emoji, or small SVG. Omit if the row
12
+ * has no leading affordance.
13
+ */
14
+ thumb?: import('svelte').Snippet;
15
+ children?: import('svelte').Snippet;
16
+ }
17
+
18
+ let { class: className = '', thumbClass = '', thumb, children }: Props = $props();
19
+ </script>
20
+
21
+ <!--
22
+ First-column row in a list / table: a 28px square thumbnail or initial
23
+ followed by the device / client / alert name. Renders the canonical
24
+ <div class="name-cell"><span class="nc-thumb">...</span>...</div> markup
25
+ so the .name-cell and .nc-thumb rules from @w5-ui/tokens/dashboard.css
26
+ apply (28px thumb, 10px gap, white name colour, JetBrains Mono initial).
27
+ Typed equivalent of the manual <div class="name-cell">... pattern used
28
+ across the Dashboard, Devices, Wi-Fi, and Alarms reference pages.
29
+ -->
30
+ <div class="name-cell {className}">
31
+ {#if thumb}
32
+ <span class="nc-thumb {thumbClass}">{@render thumb()}</span>
33
+ {/if}
34
+ {@render children?.()}
35
+ </div>
@@ -0,0 +1,23 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ class?: string;
4
+ }
5
+
6
+ let { class: className = '' }: Props = $props();
7
+ </script>
8
+
9
+ <!--
10
+ Tiny red dot that sits on top of a bell / mail / inbox icon to flag unread
11
+ activity. Renders <span aria-hidden class="topbar-notif-dot"> so the
12
+ .topbar-notif-dot rule in @w5-ui/tokens/dashboard.css paints the canonical
13
+ 6 px circle with the danger colour and a neutral-10 ring (the ring is what
14
+ keeps the dot legible over the deep topbar background). The dot is
15
+ presentational — wrap your bell <IconButton> and put the live count in its
16
+ aria-label (e.g. aria-label="Notifications, 3 new") so screen readers
17
+ announce the number while sighted users see the indicator. The hosting
18
+ button must be position: relative for the dot to anchor in the top-right
19
+ corner. Typed equivalent of <span aria-hidden class="topbar-notif-dot" />
20
+ for userland that slots a custom <Topbar actions> block and wants to keep
21
+ the unread indicator wired up without copying the class name.
22
+ -->
23
+ <span aria-hidden="true" class="topbar-notif-dot {className}"></span>
@@ -15,8 +15,6 @@
15
15
  <script lang="ts">
16
16
  import { onDestroy } from 'svelte';
17
17
 
18
-
19
-
20
18
  interface Props {
21
19
  open?: boolean;
22
20
  notifications?: Notification[];
@@ -30,7 +28,7 @@
30
28
  notifications = [],
31
29
  onClose = () => {},
32
30
  onMarkRead = undefined,
33
- onMarkAllRead = undefined
31
+ onMarkAllRead = undefined,
34
32
  }: Props = $props();
35
33
 
36
34
  let filter: 'all' | NotifType = $state('all');
@@ -51,7 +49,9 @@
51
49
  update: 'Update',
52
50
  };
53
51
 
54
- function setFilter(f: string) { filter = f as 'all' | NotifType; }
52
+ function setFilter(f: string) {
53
+ filter = f as 'all' | NotifType;
54
+ }
55
55
 
56
56
  let unread = $derived(notifications.filter((n) => !n.read).length);
57
57
  let filtered = $derived(filter === 'all' ? notifications : notifications.filter((n) => n.type === filter));
@@ -95,46 +95,56 @@
95
95
 
96
96
  <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
97
97
  <div
98
- class="np-overlay{open ? ' np-overlay--show' : ''}"
98
+ data-open={open ? 'true' : undefined}
99
+ class="fixed inset-0 z-[9100] hidden bg-black/45 backdrop-blur-sm data-[open=true]:block"
99
100
  onclick={onClose}
100
101
  aria-hidden="true"
101
102
  ></div>
102
103
  <div
103
104
  bind:this={panelEl}
104
- class="np-panel{open ? ' np-panel--show' : ''}"
105
+ data-open={open ? 'true' : undefined}
106
+ class="fixed right-0 top-0 z-[9200] flex h-full w-[380px] max-w-[90vw] translate-x-full flex-col overflow-hidden border-l border-border-1 bg-bg-1 shadow-[0_0_40px_rgba(0,0,0,0.6)] transition-transform duration-200 data-[open=true]:translate-x-0 motion-reduce:transition-none"
105
107
  role="dialog"
106
108
  aria-modal="true"
107
109
  aria-labelledby={titleId}
108
- tabindex="-1"
110
+ tabindex={-1}
109
111
  >
110
- <div class="np-header">
111
- <div class="np-title-row">
112
- <h2 id={titleId} class="np-title">
112
+ <div class="border-b border-border-1 px-4 py-3">
113
+ <div class="flex items-center justify-between gap-2">
114
+ <h2 id={titleId} class="m-0 flex items-center gap-2 text-15 font-semibold text-text-1">
113
115
  Notifications
114
116
  {#if unread > 0}
115
- <span class="np-badge" aria-label="{unread} unread">{unread}</span>
117
+ <span class="inline-flex h-5 min-w-[20px] items-center justify-center rounded-full bg-brand-05 px-1.5 text-11 font-bold text-white tabular-nums" aria-label={`${unread} unread`}>{unread}</span>
116
118
  {/if}
117
119
  </h2>
118
- <div class="np-header-actions">
120
+ <div class="flex items-center gap-1.5">
119
121
  {#if unread > 0 && onMarkAllRead}
120
- <button type="button" class="np-mark-all" onclick={onMarkAllRead}>
121
- Mark all read
122
- </button>
122
+ <button
123
+ type="button"
124
+ class="cursor-pointer rounded border-0 bg-transparent px-1.5 py-0.5 text-11 text-brand-05 hover:text-status-info focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
125
+ onclick={onMarkAllRead}
126
+ >Mark all read</button>
123
127
  {/if}
124
- <button type="button" class="icon-btn" onclick={onClose} aria-label="Close notifications">
128
+ <button
129
+ type="button"
130
+ aria-label="Close notifications"
131
+ class="inline-flex h-7 w-7 cursor-pointer items-center justify-center rounded border-0 bg-transparent text-text-4 hover:bg-row-hover hover:text-text-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
132
+ onclick={onClose}
133
+ >
125
134
  <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
126
135
  <path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.75.75 0 1 1 1.06 1.06L9.06 8l3.22 3.22a.75.75 0 1 1-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 0 1-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06z"/>
127
136
  </svg>
128
137
  </button>
129
138
  </div>
130
139
  </div>
131
- <div class="np-filters" role="tablist" aria-label="Filter by type">
140
+ <div class="mt-2 flex gap-1" role="tablist" aria-label="Filter by type">
132
141
  {#each ['all', 'alarm', 'system', 'update'] as f}
133
142
  <button
134
143
  type="button"
135
144
  role="tab"
136
145
  aria-selected={filter === f}
137
- class="np-filter{filter === f ? ' np-filter--active' : ''}"
146
+ data-active={filter === f ? 'true' : undefined}
147
+ class="cursor-pointer rounded border border-border-2 bg-transparent px-2.5 py-0.5 text-11 text-text-3 hover:bg-row-hover hover:text-text-1 data-[active=true]:border-brand-05 data-[active=true]:bg-brand-05/[0.10] data-[active=true]:text-brand-05 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
138
148
  onclick={() => setFilter(f)}
139
149
  >
140
150
  {TYPE_LABEL[f] ?? f}
@@ -143,39 +153,42 @@
143
153
  </div>
144
154
  </div>
145
155
  <div
146
- class="np-list"
156
+ class="flex-1 overflow-y-auto"
147
157
  role="log"
148
158
  aria-live="polite"
149
159
  aria-label="Notifications"
150
160
  aria-relevant="additions"
151
161
  >
152
162
  {#if filtered.length === 0}
153
- <div class="np-empty">No notifications</div>
163
+ <div class="p-6 text-center text-13 text-text-4">No notifications</div>
154
164
  {:else}
155
165
  {#each filtered as n (n.id)}
156
- <div class="np-item{n.read ? ' np-item--read' : ''}">
166
+ <div
167
+ data-read={n.read ? 'true' : undefined}
168
+ class="flex items-start gap-2 border-b border-border-1 px-4 py-3 last:border-b-0 data-[read=true]:opacity-60"
169
+ >
157
170
  <span
158
- class="np-dot"
171
+ class="mt-1.5 inline-block h-2 w-2 shrink-0 rounded-full"
159
172
  style="background:{SEV_COLOR[n.severity ?? 'info']};"
160
173
  aria-hidden="true"
161
174
  ></span>
162
- <div class="np-item-body">
163
- <div class="np-item-top">
164
- <span class="np-item-title">{n.title}</span>
165
- <span class="np-item-time">{n.time}</span>
175
+ <div class="min-w-0 flex-1">
176
+ <div class="flex items-baseline justify-between gap-2">
177
+ <span class="truncate text-13 font-medium text-text-1">{n.title}</span>
178
+ <span class="shrink-0 text-11 text-text-4 tabular-nums">{n.time}</span>
166
179
  </div>
167
180
  {#if n.description}
168
- <p class="np-item-desc">{n.description}</p>
181
+ <p class="m-0 mt-0.5 text-12 text-text-3">{n.description}</p>
169
182
  {/if}
170
- <span class="np-type-badge">{TYPE_LABEL[n.type]}</span>
183
+ <span class="mt-1 inline-block rounded bg-row-active px-1.5 py-0.5 text-[10px] uppercase tracking-[0.05em] text-text-4">{TYPE_LABEL[n.type]}</span>
171
184
  </div>
172
185
  {#if !n.read && onMarkRead}
173
186
  <button
174
187
  type="button"
175
- class="np-read-btn"
176
- onclick={() => onMarkRead?.(n.id)}
177
- aria-label="Mark as read: {n.title}"
188
+ aria-label={`Mark as read: ${n.title}`}
178
189
  title="Mark as read"
190
+ class="inline-flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center rounded border-0 bg-transparent text-status-success hover:bg-status-success/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
191
+ onclick={() => onMarkRead?.(n.id)}
179
192
  >&#10003;</button>
180
193
  {/if}
181
194
  </div>
@@ -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?: number;
@@ -30,10 +28,10 @@
30
28
  class: className = '',
31
29
  onchange,
32
30
  }: Props = $props();
33
-
34
31
 
35
32
  const uid = `dash-ui-ni-${++counter}`;
36
33
  let inputId = $derived(id ?? uid);
34
+
37
35
  function clamp(n: number): number {
38
36
  let v = n;
39
37
  if (min !== undefined) v = Math.max(min, v);
@@ -62,19 +60,22 @@
62
60
 
63
61
  let canDecrement = $derived(!disabled && (min === undefined || value > min));
64
62
  let canIncrement = $derived(!disabled && (max === undefined || value < max));
63
+
64
+ const BTN_CLS =
65
+ 'inline-flex h-full w-[30px] shrink-0 cursor-pointer items-center justify-center border-0 bg-transparent text-base text-text-4 transition-[background-color,color] duration-100 hover:bg-row-hover hover:text-text-1 disabled:cursor-not-allowed disabled:opacity-40 focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-brand-05';
65
66
  </script>
66
67
 
67
- <div class="number-input {className}">
68
+ <div class="inline-flex flex-col gap-1 {className}">
68
69
  {#if label}
69
- <label for={inputId} class="number-input__label">{label}</label>
70
+ <label for={inputId} class="text-12 text-text-4">{label}</label>
70
71
  {/if}
71
- <div class="number-input__control">
72
+ <div class="inline-flex h-[34px] items-stretch overflow-hidden rounded-md border border-border-2 bg-bg-page transition-colors duration-100 focus-within:border-brand-05">
72
73
  <button
73
74
  type="button"
74
- class="number-input__btn"
75
75
  aria-label="Decrement"
76
- tabindex="-1"
76
+ tabindex={-1}
77
77
  disabled={!canDecrement}
78
+ class={BTN_CLS}
78
79
  onclick={() => commit(value - step)}
79
80
  >−</button>
80
81
  <!-- svelte-ignore a11y_no_redundant_roles - role kept explicit for screen reader compat -->
@@ -88,19 +89,19 @@
88
89
  aria-valuetext={suffix ? `${value} ${suffix}` : undefined}
89
90
  {value}
90
91
  {disabled}
91
- class="number-input__field"
92
+ class="w-[6ch] min-w-[40px] border-0 bg-transparent px-1 text-center text-13 text-text-1 outline-none [appearance:textfield] [-moz-appearance:textfield] [&::-webkit-outer-spin-button]:m-0 [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:m-0 [&::-webkit-inner-spin-button]:appearance-none"
92
93
  onchange={handleChange}
93
94
  onkeydown={handleKey}
94
95
  />
95
96
  {#if suffix}
96
- <span class="number-input__suffix" aria-hidden="true">{suffix}</span>
97
+ <span aria-hidden="true" class="flex items-center whitespace-nowrap pl-0.5 pr-2 text-12 text-text-4">{suffix}</span>
97
98
  {/if}
98
99
  <button
99
100
  type="button"
100
- class="number-input__btn"
101
101
  aria-label="Increment"
102
- tabindex="-1"
102
+ tabindex={-1}
103
103
  disabled={!canIncrement}
104
+ class={BTN_CLS}
104
105
  onclick={() => commit(value + step)}
105
106
  >+</button>
106
107
  </div>