@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
@@ -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}
@@ -8,12 +8,7 @@
8
8
  placeholder?: string;
9
9
  }
10
10
 
11
- let {
12
- value,
13
- onconfirm,
14
- label,
15
- placeholder = ''
16
- }: Props = $props();
11
+ let { value, onconfirm, label, placeholder = '' }: Props = $props();
17
12
 
18
13
  let editing = $state(false);
19
14
  // svelte-ignore state_referenced_locally
@@ -50,30 +45,46 @@
50
45
  </script>
51
46
 
52
47
  {#if editing}
53
- <span class="ie ie--editing">
48
+ <span class="inline-flex items-center gap-1">
54
49
  <input
55
50
  bind:this={inputEl}
56
- class="ie__input"
57
- aria-label="Edit {label}"
51
+ class="min-w-[80px] max-w-[200px] rounded-[5px] border border-brand-05/50 bg-row-active px-[7px] py-0.5 text-13 leading-none text-text-1 outline-none focus:border-brand-05 focus:bg-brand-05/[0.08]"
52
+ aria-label={`Edit ${label}`}
58
53
  bind:value={draft}
59
54
  {placeholder}
60
55
  onkeydown={handleKeydown}
61
56
  />
62
- <button type="button" class="ie__btn ie__btn--save" aria-label="Save" onclick={confirm}>
57
+ <button
58
+ type="button"
59
+ aria-label="Save"
60
+ class="inline-flex cursor-pointer items-center rounded border-0 bg-transparent px-1 py-0.5 text-status-success transition-[color,background-color] duration-100 hover:bg-status-success/15 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
61
+ onclick={confirm}
62
+ >
63
63
  <svg viewBox="0 0 14 14" width="12" height="12" fill="none" aria-hidden="true" focusable="false">
64
64
  <path d="M2.5 7.5l3.5 3.5L12 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
65
65
  </svg>
66
66
  </button>
67
- <button type="button" class="ie__btn ie__btn--cancel" aria-label="Cancel" onclick={cancel}>
67
+ <button
68
+ type="button"
69
+ aria-label="Cancel"
70
+ class="inline-flex cursor-pointer items-center rounded border-0 bg-transparent px-1 py-0.5 text-text-4 transition-[color,background-color] duration-100 hover:bg-row-active hover:text-text-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
71
+ onclick={cancel}
72
+ >
68
73
  <svg viewBox="0 0 14 14" width="12" height="12" fill="none" aria-hidden="true" focusable="false">
69
74
  <path d="M3 3l8 8M11 3l-8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
70
75
  </svg>
71
76
  </button>
72
77
  </span>
73
78
  {:else}
74
- <span class="ie">
75
- <span class="ie__text">{value}</span>
76
- <button bind:this={editBtnEl} type="button" class="ie__edit" aria-label="Edit {label}" onclick={startEdit}>
79
+ <span class="group/ie inline-flex items-center gap-1">
80
+ <span class="text-13 text-text-1">{value}</span>
81
+ <button
82
+ bind:this={editBtnEl}
83
+ type="button"
84
+ aria-label={`Edit ${label}`}
85
+ class="inline-flex cursor-pointer items-center rounded border-0 bg-transparent px-1 py-0.5 text-text-4 opacity-0 transition-[opacity,color] duration-100 hover:text-text-2 focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05 group-hover/ie:opacity-100"
86
+ onclick={startEdit}
87
+ >
77
88
  <svg viewBox="0 0 14 14" width="12" height="12" fill="none" aria-hidden="true" focusable="false">
78
89
  <path d="M9.5 2l2.5 2.5-8 8H1.5v-2.5l8-8z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
79
90
  </svg>
@@ -1,3 +1,12 @@
1
+ <script module lang="ts">
2
+ // Shared utility-class string for native input elements. Matches the
3
+ // dashboard.css `.input` rule exactly so Field + Input render identically
4
+ // and old markup with `class="input"` keeps working through the
5
+ // unmigrated tail. Update both sites in lockstep if it ever moves.
6
+ export const INPUT_CLASSES =
7
+ "h-[34px] w-full rounded-md border border-border-2 bg-bg-page px-3 text-13 text-text-1 outline-none transition-[border-color] duration-100 focus:border-brand-05 focus:[box-shadow:0_0_0_2px_rgba(0,111,255,0.2)] aria-[invalid='true']:border-status-danger aria-[invalid='true']:focus:[box-shadow:0_0_0_2px_rgba(240,58,58,0.12)]";
8
+ </script>
9
+
1
10
  <script lang="ts">
2
11
  interface Props {
3
12
  value?: string;
@@ -26,7 +35,7 @@
26
35
 
27
36
  {#if type === 'text'}
28
37
  <input
29
- class="input {className}"
38
+ class="{INPUT_CLASSES} {className}"
30
39
  type="text"
31
40
  {placeholder}
32
41
  bind:value
@@ -38,7 +47,7 @@
38
47
  />
39
48
  {:else if type === 'password'}
40
49
  <input
41
- class="input {className}"
50
+ class="{INPUT_CLASSES} {className}"
42
51
  type="password"
43
52
  {placeholder}
44
53
  bind:value
@@ -50,7 +59,7 @@
50
59
  />
51
60
  {:else if type === 'number'}
52
61
  <input
53
- class="input {className}"
62
+ class="{INPUT_CLASSES} {className}"
54
63
  type="number"
55
64
  {placeholder}
56
65
  bind:value
@@ -62,7 +71,7 @@
62
71
  />
63
72
  {:else}
64
73
  <input
65
- class="input {className}"
74
+ class="{INPUT_CLASSES} {className}"
66
75
  {type}
67
76
  {placeholder}
68
77
  {value}
@@ -8,12 +8,23 @@
8
8
  let { prefix = undefined, suffix = undefined, children }: Props = $props();
9
9
  </script>
10
10
 
11
- <div class="input-group">
11
+ <!--
12
+ Input group: prefix / suffix addons flank a slot-rendered `<Input>` and
13
+ trim/round its corners. The `[&_input]:rounded-none` selector targets the
14
+ child input regardless of its own utility classes. Addons inherit the
15
+ same height as the migrated Input (34px).
16
+ -->
17
+ <div
18
+ class="flex items-stretch
19
+ [&_input]:flex-1 [&_input]:min-w-0 [&_input]:rounded-none
20
+ {prefix === undefined ? '[&_input]:rounded-l-md' : ''}
21
+ {suffix === undefined ? '[&_input]:rounded-r-md' : ''}"
22
+ >
12
23
  {#if prefix !== undefined}
13
- <span class="input-group-addon input-group-addon--prefix">{prefix}</span>
24
+ <span class="flex shrink-0 select-none items-center whitespace-nowrap rounded-l-md border border-r-0 border-border-2 bg-row-active px-2.5 text-13 text-text-4">{prefix}</span>
14
25
  {/if}
15
26
  {@render children?.()}
16
27
  {#if suffix !== undefined}
17
- <span class="input-group-addon input-group-addon--suffix">{suffix}</span>
28
+ <span class="flex shrink-0 select-none items-center whitespace-nowrap rounded-r-md border border-l-0 border-border-2 bg-row-active px-2.5 text-13 text-text-4">{suffix}</span>
18
29
  {/if}
19
30
  </div>
@@ -5,6 +5,7 @@
5
5
  <script lang="ts">
6
6
  import JsonViewerNode from './JsonViewerNode.svelte';
7
7
  import type { JsVal } from './JsonViewerNode.svelte';
8
+
8
9
  interface Props {
9
10
  data: unknown;
10
11
  label?: string;
@@ -16,11 +17,15 @@
16
17
  data,
17
18
  label = 'JSON viewer',
18
19
  defaultExpanded = true,
19
- maxDepth = 3
20
+ maxDepth = 3,
20
21
  }: Props = $props();
21
22
  let root = $derived(data as JsVal);
22
23
  </script>
23
24
 
24
- <div class="json-viewer" role="tree" aria-label={label}>
25
+ <div
26
+ class="overflow-auto rounded-md border border-border-1 bg-bg-page py-2 font-mono text-12 leading-[1.8]"
27
+ role="tree"
28
+ aria-label={label}
29
+ >
25
30
  <JsonViewerNode v={root} depth={0} {maxDepth} startOpen={defaultExpanded} />
26
31
  </div>
@@ -4,6 +4,7 @@
4
4
 
5
5
  <script lang="ts">
6
6
  import JsonViewerNode from './JsonViewerNode.svelte';
7
+
7
8
  interface Props {
8
9
  v: JsVal;
9
10
  k?: string | undefined;
@@ -12,64 +13,69 @@
12
13
  startOpen?: boolean;
13
14
  }
14
15
 
15
- let {
16
- v,
17
- k = undefined,
18
- depth = 0,
19
- maxDepth = 3,
20
- startOpen = true
21
- }: Props = $props();
16
+ let { v, k = undefined, depth = 0, maxDepth = 3, startOpen = true }: Props = $props();
22
17
 
23
18
  let isArr = $derived(Array.isArray(v));
24
19
  let isObj = $derived(v !== null && typeof v === 'object');
25
20
  let expandable = $derived(isArr || isObj);
26
21
 
27
- // `open` is initialised from `startOpen`/`depth`/`maxDepth` props but is
28
- // intentionally a one-shot snapshot at construction time — afterwards
29
- // the user controls it via the toggle.
30
22
  // svelte-ignore state_referenced_locally
31
23
  let open = $state(startOpen && depth < maxDepth);
32
24
 
33
- let entries = $derived(isArr
34
- ? (v as JsVal[]).map((item: JsVal, i: number) => ({ ek: String(i), ev: item, arrItem: true }))
35
- : isObj
36
- ? Object.entries(v as { [key: string]: JsVal }).map(([ek, ev]) => ({ ek, ev, arrItem: false }))
37
- : []);
25
+ let entries = $derived(
26
+ isArr
27
+ ? (v as JsVal[]).map((item: JsVal, i: number) => ({ ek: String(i), ev: item, arrItem: true }))
28
+ : isObj
29
+ ? Object.entries(v as { [key: string]: JsVal }).map(([ek, ev]) => ({ ek, ev, arrItem: false }))
30
+ : [],
31
+ );
38
32
  let count = $derived(entries.length);
39
33
  let ob = $derived(isArr ? '[' : '{');
40
34
  let cb = $derived(isArr ? ']' : '}');
41
35
  let indent = $derived(depth * 14);
36
+
37
+ // Syntax-highlight palette is motif-aware: dark uses the familiar
38
+ // VS Code dark-plus accents; light flips to a calmer, slightly
39
+ // higher-contrast set tuned for #ffffff page bg.
40
+ const KEY = 'text-[#9cdcfe] dark:text-[#9cdcfe] light:text-[#0451a5]';
41
+ const COLON = 'text-text-4';
42
+ const NULL = 'text-text-4 italic';
43
+ const STR = 'text-[#ce9178] dark:text-[#ce9178] light:text-[#a31515]';
44
+ const NUM = 'text-[#b5cea8] dark:text-[#b5cea8] light:text-[#098658]';
45
+ const BOOL = 'text-[#569cd6] dark:text-[#569cd6] light:text-[#0000ff]';
46
+ const BRACKET = 'text-text-2';
47
+ const COUNT = 'text-text-4 tabular-nums px-1';
42
48
  </script>
43
49
 
44
50
  {#if !expandable}
45
- <div role="treeitem" aria-selected="false" class="jv-row" style:padding-left="{indent}px">
46
- {#if k != null}<span class="jv-key">"{k}"</span><span class="jv-colon">: </span>{/if}
47
- {#if v === null}<span class="jv-null">null</span>
48
- {:else if typeof v === 'string'}<span class="jv-string">"{v}"</span>
49
- {:else if typeof v === 'number'}<span class="jv-number">{v}</span>
50
- {:else}<span class="jv-boolean">{String(v)}</span>{/if}
51
+ <div role="treeitem" aria-selected="false" class="px-3 py-px" style:padding-left="{indent + 12}px">
52
+ {#if k != null}<span class={KEY}>"{k}"</span><span class={COLON}>: </span>{/if}
53
+ {#if v === null}<span class={NULL}>null</span>
54
+ {:else if typeof v === 'string'}<span class={STR}>"{v}"</span>
55
+ {:else if typeof v === 'number'}<span class={NUM}>{v}</span>
56
+ {:else}<span class={BOOL}>{String(v)}</span>{/if}
51
57
  </div>
52
58
  {:else}
53
- <div role="treeitem" aria-selected="false" aria-expanded={open} class="jv-node">
59
+ <div role="treeitem" aria-selected="false" aria-expanded={open} class="block">
54
60
  <button
55
61
  type="button"
56
- class="jv-toggle-row"
57
- onclick={() => (open = !open)}
58
- aria-label="{open ? 'Collapse' : 'Expand'} {k ?? (isArr ? 'array' : 'object')}"
62
+ aria-label={`${open ? 'Collapse' : 'Expand'} ${k ?? (isArr ? 'array' : 'object')}`}
63
+ class="flex w-full cursor-pointer items-baseline border-0 bg-transparent px-3 py-px text-left font-mono text-12 leading-[1.8] hover:bg-divider focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-brand-05"
59
64
  style:padding-left="{indent}px"
65
+ onclick={() => (open = !open)}
60
66
  >
61
- <span class="jv-caret" aria-hidden="true">{open ? '▾' : '▸'}</span>
62
- {#if k != null}<span class="jv-key">"{k}"</span><span class="jv-colon">: </span>{/if}
63
- <span class="jv-bracket">{ob}</span>
64
- {#if !open}<span class="jv-count">&thinsp;{count}&thinsp;</span><span class="jv-bracket">{cb}</span>{/if}
67
+ <span class="mr-1 inline-block w-3 text-text-4" aria-hidden="true">{open ? '▾' : '▸'}</span>
68
+ {#if k != null}<span class={KEY}>"{k}"</span><span class={COLON}>: </span>{/if}
69
+ <span class={BRACKET}>{ob}</span>
70
+ {#if !open}<span class={COUNT}>&thinsp;{count}&thinsp;</span><span class={BRACKET}>{cb}</span>{/if}
65
71
  </button>
66
72
  {#if open}
67
- <div role="group" class="jv-children">
73
+ <div role="group" class="block">
68
74
  {#each entries as { ek, ev, arrItem } (ek)}
69
75
  <JsonViewerNode v={ev} k={arrItem ? undefined : ek} depth={depth + 1} {maxDepth} {startOpen} />
70
76
  {/each}
71
- <div class="jv-row jv-close" style:padding-left="{indent}px">
72
- <span class="jv-bracket">{cb}</span>
77
+ <div class="px-3 py-px" style:padding-left="{indent + 12}px">
78
+ <span class={BRACKET}>{cb}</span>
73
79
  </div>
74
80
  </div>
75
81
  {/if}
@@ -7,15 +7,21 @@
7
7
  let { rows = [], caption = undefined }: Props = $props();
8
8
  </script>
9
9
 
10
- <table class="kv-table">
10
+ <table class="w-full border-collapse text-13">
11
11
  {#if caption}
12
12
  <caption class="sr-only">{caption}</caption>
13
13
  {/if}
14
14
  <tbody>
15
15
  {#each rows as { label, value, valueColor }}
16
- <tr class="kv-table__row">
17
- <th scope="row" class="kv-table__label">{label}</th>
18
- <td class="kv-table__value" style={valueColor ? `color:${valueColor}` : undefined}>{value}</td>
16
+ <tr class="border-b border-border-1 last:border-b-0">
17
+ <th
18
+ scope="row"
19
+ class="w-2/5 whitespace-nowrap py-1.5 pr-3 text-left align-top font-medium text-text-3"
20
+ >{label}</th>
21
+ <td
22
+ class="py-1.5 align-top text-text-1 break-words"
23
+ style={valueColor ? `color:${valueColor}` : undefined}
24
+ >{value}</td>
19
25
  </tr>
20
26
  {/each}
21
27
  </tbody>
@@ -14,8 +14,6 @@
14
14
  </script>
15
15
 
16
16
  <script lang="ts">
17
-
18
-
19
17
  interface Props {
20
18
  columns: KanbanColumn[];
21
19
  onCardMove?: ((cardId: string, fromColId: string, toColId: string) => void) | undefined;
@@ -35,7 +33,7 @@
35
33
  function say(msg: string) { announce = msg; }
36
34
 
37
35
  function colOf(cardId: string): KanbanColumn | undefined {
38
- return columns.find(c => c.cards.some(k => k.id === cardId));
36
+ return columns.find((c) => c.cards.some((k) => k.id === cardId));
39
37
  }
40
38
 
41
39
  function handleKeyDown(e: KeyboardEvent, card: KanbanCard, col: KanbanColumn) {
@@ -54,7 +52,7 @@
54
52
  say('Move cancelled.');
55
53
  } else if ((e.key === 'ArrowLeft' || e.key === 'ArrowRight') && grabbed?.cardId === card.id) {
56
54
  e.preventDefault();
57
- const idx = columns.findIndex(c => c.id === grabbed!.colId);
55
+ const idx = columns.findIndex((c) => c.id === grabbed!.colId);
58
56
  const nextIdx = e.key === 'ArrowLeft' ? idx - 1 : idx + 1;
59
57
  if (nextIdx < 0 || nextIdx >= columns.length) return;
60
58
  const nextCol = columns[nextIdx];
@@ -79,8 +77,8 @@
79
77
  function handleDrop(e: DragEvent, toColId: string) {
80
78
  e.preventDefault();
81
79
  if (!dragSrc || dragSrc.colId === toColId) { dragSrc = null; dragOverCol = null; return; }
82
- const card = colOf(dragSrc.cardId)?.cards.find(c => c.id === dragSrc!.cardId);
83
- const toCol = columns.find(c => c.id === toColId);
80
+ const card = colOf(dragSrc.cardId)?.cards.find((c) => c.id === dragSrc!.cardId);
81
+ const toCol = columns.find((c) => c.id === toColId);
84
82
  onCardMove?.(dragSrc.cardId, dragSrc.colId, toColId);
85
83
  if (card && toCol) say(`Moved ${card.title} to ${toCol.title}.`);
86
84
  dragSrc = null;
@@ -88,7 +86,7 @@
88
86
  }
89
87
  </script>
90
88
 
91
- <div class="kanban" role="region" aria-label={ariaLabel}>
89
+ <div role="region" aria-label={ariaLabel}>
92
90
  <span
93
91
  id={announceId}
94
92
  role="status"
@@ -97,49 +95,53 @@
97
95
  class="sr-only"
98
96
  >{announce}</span>
99
97
 
100
- <div class="kanban-board" role="list">
98
+ <div class="flex gap-3 overflow-x-auto" role="list">
101
99
  {#each columns as col (col.id)}
102
100
  <div
103
- class="kanban-col"
104
- class:kanban-col--over={dragOverCol === col.id}
101
+ data-over={dragOverCol === col.id ? 'true' : undefined}
105
102
  role="listitem"
106
- ondragover={e => handleDragOver(e, col.id)}
103
+ class="flex min-w-[240px] flex-1 flex-col gap-2 rounded-lg border border-border-1 bg-divider p-2 data-[over=true]:border-brand-05/40 data-[over=true]:bg-brand-05/[0.04]"
104
+ ondragover={(e) => handleDragOver(e, col.id)}
107
105
  ondragleave={() => { dragOverCol = null; }}
108
- ondrop={e => handleDrop(e, col.id)}
106
+ ondrop={(e) => handleDrop(e, col.id)}
109
107
  >
110
- <div class="kanban-col-header">
108
+ <div class="flex items-center gap-2 px-1 pt-0.5">
111
109
  {#if col.color}
112
- <span class="kanban-col-dot" style="background:{col.color}" aria-hidden="true"></span>
110
+ <span class="inline-block h-2 w-2 shrink-0 rounded-full" style="background:{col.color}" aria-hidden="true"></span>
113
111
  {/if}
114
- <span class="kanban-col-title">{col.title}</span>
115
- <span class="kanban-col-count" aria-label="{col.cards.length} card{col.cards.length !== 1 ? 's' : ''}">
112
+ <span class="flex-1 text-12 font-semibold uppercase tracking-[0.05em] text-text-3">{col.title}</span>
113
+ <span
114
+ class="rounded bg-row-active px-1.5 py-0.5 text-11 text-text-4 tabular-nums"
115
+ aria-label={`${col.cards.length} card${col.cards.length !== 1 ? 's' : ''}`}
116
+ >
116
117
  {col.cards.length}
117
118
  </span>
118
119
  </div>
119
120
 
120
- <ul class="kanban-cards" aria-label="{col.title} cards">
121
- {#each col.cards as card (card.id)}
121
+ <ul class="m-0 flex list-none flex-col gap-1.5 p-0" aria-label={`${col.title} cards`}>
122
+ {#each col.cards as card (col.id + '/' + card.id)}
122
123
  {@const isGrabbed = grabbed?.cardId === card.id}
123
- <li class="kanban-card-item" class:is-grabbed={isGrabbed}>
124
+ <li>
124
125
  <button
125
126
  bind:this={cardRefs[card.id]}
126
127
  type="button"
127
- class="kanban-card"
128
128
  draggable={true}
129
129
  aria-pressed={isGrabbed}
130
- aria-label="{card.title}{card.subtitle ? `, ${card.subtitle}` : ''}{card.meta ? `, ${card.meta}` : ''}. {isGrabbed ? 'Moving. Use arrow keys to change column, Space to drop, Escape to cancel.' : 'Press Space to move.'}"
131
- onkeydown={e => handleKeyDown(e, card, col)}
132
- ondragstart={e => handleDragStart(e, card.id, col.id)}
130
+ aria-label={`${card.title}${card.subtitle ? `, ${card.subtitle}` : ''}${card.meta ? `, ${card.meta}` : ''}. ${isGrabbed ? 'Moving. Use arrow keys to change column, Space to drop, Escape to cancel.' : 'Press Space to move.'}`}
131
+ data-grabbed={isGrabbed ? 'true' : undefined}
132
+ class="flex w-full cursor-grab flex-col items-start gap-0.5 rounded-md border border-border-1 bg-bg-1 p-2 text-left active:cursor-grabbing hover:border-border-3 data-[grabbed=true]:border-brand-05 data-[grabbed=true]:bg-brand-05/[0.08] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
133
+ onkeydown={(e) => handleKeyDown(e, card, col)}
134
+ ondragstart={(e) => handleDragStart(e, card.id, col.id)}
133
135
  ondragend={() => { dragSrc = null; dragOverCol = null; }}
134
136
  >
135
- <span class="kanban-card-title">{card.title}</span>
136
- {#if card.subtitle}<span class="kanban-card-sub">{card.subtitle}</span>{/if}
137
- {#if card.meta}<span class="kanban-card-meta">{card.meta}</span>{/if}
137
+ <span class="text-13 font-medium text-text-1">{card.title}</span>
138
+ {#if card.subtitle}<span class="text-12 text-text-3">{card.subtitle}</span>{/if}
139
+ {#if card.meta}<span class="text-11 text-text-4">{card.meta}</span>{/if}
138
140
  </button>
139
141
  </li>
140
142
  {/each}
141
143
  {#if col.cards.length === 0}
142
- <li class="kanban-empty" role="note" aria-label="{col.title} is empty">Empty</li>
144
+ <li class="rounded-md border border-dashed border-border-1 p-3 text-center text-11 text-text-4" role="note" aria-label={`${col.title} is empty`}>Empty</li>
143
145
  {/if}
144
146
  </ul>
145
147
  </div>
@@ -5,16 +5,19 @@
5
5
  }
6
6
 
7
7
  let { keys, class: className = '' }: Props = $props();
8
-
9
8
 
10
9
  let parts = $derived(typeof keys === 'string' ? keys.split('+') : keys);
11
10
  </script>
12
11
 
13
- <kbd class="kbd {className}">
12
+ <kbd class="inline-flex shrink-0 items-center gap-px font-mono text-[11px] leading-none {className}">
14
13
  {#each parts as key, i}
15
- <span class="kbd__segment">
16
- {#if i > 0}<span class="kbd__plus" aria-hidden="true">+</span>{/if}
17
- <kbd class="kbd__key">{key}</kbd>
14
+ <span class="inline-flex items-center gap-0.5">
15
+ {#if i > 0}
16
+ <span class="px-px font-sans text-[10px] leading-none text-text-4" aria-hidden="true">+</span>
17
+ {/if}
18
+ <kbd
19
+ class="inline-flex min-w-[18px] items-center justify-center rounded border border-b-2 border-border-3 bg-row-active px-1.5 py-0.5 font-[inherit] text-text-3 [box-shadow:0_1px_0_rgba(0,0,0,0.4)]"
20
+ >{key}</kbd>
18
21
  </span>
19
22
  {/each}
20
23
  </kbd>
@@ -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
+ Small categorical label - the line above a KPI number ("Active devices"),
12
+ the cell heading on a hand-rolled form row, the tag on a definition list
13
+ term, anywhere a 13 px / medium-weight tag belongs without the full form
14
+ field machinery. Renders <span class="dash-ui-label"> so the
15
+ .dash-ui-label rule in @w5-ui/tokens/tokens.css applies the canonical
16
+ 13 px / 500 weight, 1.25 line-height, and --text-2 colour. Stops
17
+ userland reaching for a one-off font-size: 13px; font-weight: 500; duo
18
+ on every KPI label or hand-rolled form caption. Typed equivalent of
19
+ <span class="dash-ui-label"> (the userland-facing semantic class
20
+ documented in Foundations / Type). For real form fields prefer
21
+ <Field label="..."> - it wires htmlFor, required, hint, and error
22
+ semantics for you. For KPI tiles prefer the <Stat> label slot. Reach
23
+ for <Label> only when hand-rolling a tile or row where the higher-level
24
+ component doesn't fit.
25
+ -->
26
+ <span class="dash-ui-label {className}">
27
+ {@render children?.()}
28
+ </span>
@@ -37,24 +37,28 @@
37
37
  info: '#006FFF',
38
38
  debug: '#4A4B53',
39
39
  };
40
+ const SEV_TXT: Record<string, string> = {
41
+ error: 'text-status-danger',
42
+ warn: 'text-status-warning',
43
+ info: 'text-brand-05',
44
+ debug: 'text-text-4',
45
+ };
40
46
 
41
- // Replaces `afterUpdate`: `$effect` runs after every DOM commit triggered
42
- // by reactive dependencies. Reading `entries` ensures we re-scroll on
43
- // every new batch.
44
47
  $effect(() => {
45
48
  void entries.length;
46
49
  if (follow && bodyEl) bodyEl.scrollTop = bodyEl.scrollHeight;
47
50
  });
48
51
  </script>
49
52
 
50
- <div class="lv {className}">
51
- <div class="lv__toolbar">
52
- <span class="lv__count">{entries.length} entries</span>
53
+ <div class="flex flex-col gap-1 rounded-lg border border-border-1 bg-bg-page {className}">
54
+ <div class="flex items-center justify-between border-b border-border-1 px-3 py-1.5 text-12 text-text-4">
55
+ <span class="tabular-nums">{entries.length} entries</span>
53
56
  <button
54
57
  type="button"
55
- class="lv__follow{follow ? ' lv__follow--on' : ''}"
56
- onclick={() => { follow = !follow; }}
58
+ data-on={follow ? 'true' : undefined}
57
59
  aria-pressed={follow}
60
+ class="inline-flex h-6 cursor-pointer items-center rounded border border-border-2 bg-transparent px-2 text-12 text-text-3 transition-colors duration-100 hover:bg-row-hover hover:text-text-1 data-[on=true]:border-brand-05 data-[on=true]:bg-brand-05/[0.10] data-[on=true]:text-brand-05 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
61
+ onclick={() => { follow = !follow; }}
58
62
  >Follow</button>
59
63
  </div>
60
64
  <div
@@ -62,12 +66,12 @@
62
66
  role="log"
63
67
  aria-live="polite"
64
68
  aria-label={ariaLabel}
65
- class="lv__body"
69
+ class="overflow-auto"
66
70
  style="height:{height}px;"
67
71
  >
68
- <table class="lv__table">
72
+ <table class="w-full border-collapse font-mono text-12">
69
73
  <caption class="sr-only">{ariaLabel}</caption>
70
- <thead>
74
+ <thead class="sr-only">
71
75
  <tr>
72
76
  <th scope="col">Severity</th>
73
77
  <th scope="col">Source</th>
@@ -79,18 +83,18 @@
79
83
  </thead>
80
84
  <tbody>
81
85
  {#each entries as e, i (e.id ?? i)}
82
- <tr>
83
- <td>
84
- <span class="lv__sev lv__sev--{e.level}">
85
- <span class="lv__dot" style="background:{SEV_DOT[e.level] ?? SEV_DOT.info};" aria-hidden="true"></span>
86
+ <tr class="border-b border-divider last:border-b-0 hover:bg-divider">
87
+ <td class="whitespace-nowrap py-1 pl-3 pr-2">
88
+ <span class="inline-flex items-center gap-1 {SEV_TXT[e.level] ?? SEV_TXT.info}">
89
+ <span class="inline-block h-1.5 w-1.5 rounded-full" style="background:{SEV_DOT[e.level] ?? SEV_DOT.info};" aria-hidden="true"></span>
86
90
  {e.level === 'error' ? 'crit' : e.level}
87
91
  </span>
88
92
  </td>
89
- <td class="lv__cat">{e.category ?? ''}</td>
90
- <td class="lv__time">{e.timestamp}</td>
91
- <td class="lv__event">{e.event}</td>
92
- <td class="lv__subject">{e.subject ?? ''}</td>
93
- <td class="lv__detail">{e.detail ?? ''}</td>
93
+ <td class="whitespace-nowrap px-2 py-1 text-text-4">{e.category ?? ''}</td>
94
+ <td class="whitespace-nowrap px-2 py-1 text-text-4 tabular-nums">{e.timestamp}</td>
95
+ <td class="whitespace-nowrap px-2 py-1 text-text-2">{e.event}</td>
96
+ <td class="whitespace-nowrap px-2 py-1 text-text-2">{e.subject ?? ''}</td>
97
+ <td class="px-2 py-1 pr-3 text-text-3">{e.detail ?? ''}</td>
94
98
  </tr>
95
99
  {/each}
96
100
  </tbody>
@@ -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,31 +73,39 @@
75
73
  setTimeout(() => focusAt(5), 0);
76
74
  }
77
75
  }
76
+
77
+ const PAIR_CLS =
78
+ 'w-6 border-0 bg-transparent p-0 text-center font-mono text-13 uppercase text-text-1 outline-none';
78
79
  </script>
79
80
 
80
- <div class="mac-input-wrapper {className}">
81
+ <div class={className}>
81
82
  {#if label}
82
- <label for="{uid}-0" class="mac-input__label">{label}</label>
83
+ <label for="{uid}-0" class="text-12 text-text-4">{label}</label>
83
84
  {/if}
84
- <div class="mac-input{disabled ? ' mac-input--disabled' : ''}" role="group" aria-label={label ?? 'MAC address'}>
85
+ <div
86
+ role="group"
87
+ aria-label={label ?? 'MAC 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 pairs as pair, i}
86
92
  <input
87
93
  bind:this={inputs[i]}
88
94
  id={i === 0 ? `${uid}-0` : undefined}
89
95
  type="text"
90
96
  inputmode="text"
91
- aria-label="Byte {i + 1} of 6"
97
+ aria-label={`Byte ${i + 1} of 6`}
92
98
  value={pair}
93
99
  {disabled}
94
100
  maxlength={2}
95
- class="mac-input__pair"
101
+ class={PAIR_CLS}
96
102
  oninput={(e) => commitPair(i, e.currentTarget.value)}
97
103
  onkeydown={(e) => handleKey(i, e)}
98
104
  onpaste={handlePaste}
99
105
  onfocus={(e) => e.currentTarget.select()}
100
106
  />
101
107
  {#if i < 5}
102
- <span class="mac-input__colon" aria-hidden="true">:</span>
108
+ <span aria-hidden="true" class="select-none text-13 leading-none text-text-4">:</span>
103
109
  {/if}
104
110
  {/each}
105
111
  </div>