@w5-ui/svelte 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +116 -4
  2. package/package.json +2 -2
  3. package/src/lib/components/Accordion.svelte +1 -1
  4. package/src/lib/components/AccordionItem.svelte +6 -7
  5. package/src/lib/components/ActionMenu.svelte +17 -9
  6. package/src/lib/components/ActivityFeed.svelte +20 -14
  7. package/src/lib/components/Alert.svelte +22 -4
  8. package/src/lib/components/AppLayout.svelte +34 -0
  9. package/src/lib/components/Avatar.svelte +28 -11
  10. package/src/lib/components/AvatarGroup.svelte +21 -4
  11. package/src/lib/components/Badge.svelte +22 -6
  12. package/src/lib/components/Banner.svelte +36 -7
  13. package/src/lib/components/Body.svelte +29 -0
  14. package/src/lib/components/BodyLg.svelte +26 -0
  15. package/src/lib/components/Breadcrumb.svelte +19 -12
  16. package/src/lib/components/Button.svelte +30 -1
  17. package/src/lib/components/CIDRInput.svelte +17 -9
  18. package/src/lib/components/Callout.svelte +29 -8
  19. package/src/lib/components/Caption.svelte +26 -0
  20. package/src/lib/components/Card.svelte +11 -3
  21. package/src/lib/components/CardMore.svelte +20 -0
  22. package/src/lib/components/CardTitle.svelte +20 -0
  23. package/src/lib/components/Carousel.svelte +27 -20
  24. package/src/lib/components/Checkbox.svelte +24 -3
  25. package/src/lib/components/Code.svelte +24 -0
  26. package/src/lib/components/CodeBlock.svelte +8 -7
  27. package/src/lib/components/ColorPicker.svelte +13 -13
  28. package/src/lib/components/ColumnToggle.svelte +10 -9
  29. package/src/lib/components/Combobox.svelte +31 -17
  30. package/src/lib/components/CommandPalette.svelte +42 -26
  31. package/src/lib/components/ConfirmDialog.svelte +21 -15
  32. package/src/lib/components/ContextMenu.svelte +12 -12
  33. package/src/lib/components/ContextualHelp.svelte +11 -6
  34. package/src/lib/components/CopyButton.svelte +2 -1
  35. package/src/lib/components/DatePicker.svelte +27 -23
  36. package/src/lib/components/DateRangePicker.svelte +46 -39
  37. package/src/lib/components/Delta.svelte +25 -0
  38. package/src/lib/components/Display1.svelte +29 -0
  39. package/src/lib/components/Display2.svelte +29 -0
  40. package/src/lib/components/Drawer.svelte +6 -5
  41. package/src/lib/components/DurationInput.svelte +16 -8
  42. package/src/lib/components/EmptyState.svelte +16 -7
  43. package/src/lib/components/ExpandableRow.svelte +8 -15
  44. package/src/lib/components/Eyebrow.svelte +25 -0
  45. package/src/lib/components/Field.svelte +7 -7
  46. package/src/lib/components/FieldStack.svelte +18 -0
  47. package/src/lib/components/FileUpload.svelte +8 -10
  48. package/src/lib/components/FilterBuilder.svelte +52 -28
  49. package/src/lib/components/FormActions.svelte +18 -0
  50. package/src/lib/components/FormRow.svelte +17 -0
  51. package/src/lib/components/Grid.svelte +17 -0
  52. package/src/lib/components/GroupedList.svelte +35 -21
  53. package/src/lib/components/H1.svelte +29 -0
  54. package/src/lib/components/H2.svelte +29 -0
  55. package/src/lib/components/H3.svelte +29 -0
  56. package/src/lib/components/H4.svelte +29 -0
  57. package/src/lib/components/HealthBar.svelte +6 -3
  58. package/src/lib/components/HoverCard.svelte +16 -5
  59. package/src/lib/components/IPInput.svelte +14 -8
  60. package/src/lib/components/IconButton.svelte +1 -2
  61. package/src/lib/components/InlineEdit.svelte +25 -14
  62. package/src/lib/components/Input.svelte +13 -4
  63. package/src/lib/components/InputGroup.svelte +14 -3
  64. package/src/lib/components/JsonViewer.svelte +7 -2
  65. package/src/lib/components/JsonViewerNode.svelte +38 -32
  66. package/src/lib/components/KVTable.svelte +10 -4
  67. package/src/lib/components/KanbanBoard.svelte +29 -27
  68. package/src/lib/components/Kbd.svelte +8 -5
  69. package/src/lib/components/Label.svelte +28 -0
  70. package/src/lib/components/LogViewer.svelte +24 -20
  71. package/src/lib/components/MACInput.svelte +14 -8
  72. package/src/lib/components/Mac.svelte +23 -0
  73. package/src/lib/components/Menubar.svelte +30 -13
  74. package/src/lib/components/Modal.svelte +15 -6
  75. package/src/lib/components/Mono.svelte +28 -0
  76. package/src/lib/components/MultiSelect.svelte +43 -23
  77. package/src/lib/components/Muted.svelte +24 -0
  78. package/src/lib/components/NameCell.svelte +35 -0
  79. package/src/lib/components/NotifDot.svelte +23 -0
  80. package/src/lib/components/NotificationPanel.svelte +44 -31
  81. package/src/lib/components/NumberInput.svelte +13 -12
  82. package/src/lib/components/OTPInput.svelte +14 -8
  83. package/src/lib/components/PageHeader.svelte +25 -0
  84. package/src/lib/components/Pagination.svelte +16 -12
  85. package/src/lib/components/PasswordInput.svelte +8 -7
  86. package/src/lib/components/Pill.svelte +34 -6
  87. package/src/lib/components/Popover.svelte +91 -16
  88. package/src/lib/components/ProgressBar.svelte +10 -7
  89. package/src/lib/components/RadioGroup.svelte +22 -7
  90. package/src/lib/components/RangeSlider.svelte +17 -12
  91. package/src/lib/components/RankedList.svelte +12 -18
  92. package/src/lib/components/ResizablePanel.svelte +25 -14
  93. package/src/lib/components/RowToggle.svelte +6 -4
  94. package/src/lib/components/SearchBox.svelte +6 -2
  95. package/src/lib/components/SegmentedControl.svelte +16 -2
  96. package/src/lib/components/Select.svelte +20 -8
  97. package/src/lib/components/SelectionToolbar.svelte +13 -9
  98. package/src/lib/components/Sidebar.svelte +32 -11
  99. package/src/lib/components/Signal.svelte +3 -2
  100. package/src/lib/components/Skeleton.svelte +21 -9
  101. package/src/lib/components/SkipLink.svelte +5 -2
  102. package/src/lib/components/Slider.svelte +16 -9
  103. package/src/lib/components/SortHeader.svelte +13 -4
  104. package/src/lib/components/SortableList.svelte +17 -18
  105. package/src/lib/components/Sparkline.svelte +20 -14
  106. package/src/lib/components/Spinner.svelte +16 -1
  107. package/src/lib/components/SplitButton.svelte +27 -8
  108. package/src/lib/components/Spoiler.svelte +8 -6
  109. package/src/lib/components/SrOnly.svelte +23 -0
  110. package/src/lib/components/StackedProgress.svelte +17 -9
  111. package/src/lib/components/StarRating.svelte +35 -10
  112. package/src/lib/components/Stat.svelte +19 -5
  113. package/src/lib/components/StatNumber.svelte +22 -0
  114. package/src/lib/components/StatusIndicator.svelte +9 -2
  115. package/src/lib/components/StatusRing.svelte +36 -0
  116. package/src/lib/components/Stepper.svelte +30 -8
  117. package/src/lib/components/Submeta.svelte +19 -0
  118. package/src/lib/components/SwitchPortGrid.svelte +20 -7
  119. package/src/lib/components/TablePager.svelte +22 -0
  120. package/src/lib/components/TableShell.svelte +19 -0
  121. package/src/lib/components/Tabs.svelte +16 -5
  122. package/src/lib/components/Tag.svelte +5 -3
  123. package/src/lib/components/TagInput.svelte +9 -10
  124. package/src/lib/components/Textarea.svelte +7 -1
  125. package/src/lib/components/TimePicker.svelte +14 -8
  126. package/src/lib/components/TimeRange.svelte +20 -22
  127. package/src/lib/components/Timeline.svelte +20 -9
  128. package/src/lib/components/Toast.svelte +24 -5
  129. package/src/lib/components/Toggle.svelte +10 -4
  130. package/src/lib/components/ToggleGroup.svelte +15 -5
  131. package/src/lib/components/Tooltip.svelte +20 -5
  132. package/src/lib/components/Topbar.svelte +108 -27
  133. package/src/lib/components/TopbarActions.svelte +52 -0
  134. package/src/lib/components/TransferList.svelte +30 -22
  135. package/src/lib/components/TreeItem.svelte +11 -13
  136. package/src/lib/components/Unit.svelte +22 -0
  137. package/src/lib/components/VirtualList.svelte +12 -9
  138. package/src/lib/index.ts +35 -0
@@ -22,14 +22,16 @@
22
22
  label = 'Resize panels',
23
23
  style = '',
24
24
  first,
25
- second
25
+ second,
26
26
  }: Props = $props();
27
27
 
28
28
  // svelte-ignore state_referenced_locally
29
29
  let size = $state(defaultSize);
30
30
  let container = $state<HTMLDivElement | undefined>(undefined);
31
31
 
32
- function clamp(v: number) { return Math.min(max, Math.max(min, v)); }
32
+ function clamp(v: number) {
33
+ return Math.min(max, Math.max(min, v));
34
+ }
33
35
 
34
36
  function onMouseDown(e: MouseEvent) {
35
37
  e.preventDefault();
@@ -37,9 +39,10 @@
37
39
  function onMove(me: MouseEvent) {
38
40
  if (!container) return;
39
41
  const rect = container.getBoundingClientRect();
40
- const pct = orientation === 'vertical'
41
- ? ((me.clientX - rect.left) / rect.width) * 100
42
- : ((me.clientY - rect.top) / rect.height) * 100;
42
+ const pct =
43
+ orientation === 'vertical'
44
+ ? ((me.clientX - rect.left) / rect.width) * 100
45
+ : ((me.clientY - rect.top) / rect.height) * 100;
43
46
  size = clamp(Math.round(pct));
44
47
  }
45
48
 
@@ -65,29 +68,37 @@
65
68
  if (e.key === 'End') { e.preventDefault(); size = max; }
66
69
  }
67
70
 
68
- let firstStyle = $derived(orientation === 'vertical'
69
- ? `flex: 0 0 ${size}%; min-width: 0;`
70
- : `flex: 0 0 ${size}%; min-height: 0;`);
71
+ let firstStyle = $derived(
72
+ orientation === 'vertical'
73
+ ? `flex: 0 0 ${size}%; min-width: 0;`
74
+ : `flex: 0 0 ${size}%; min-height: 0;`,
75
+ );
71
76
  </script>
72
77
 
73
- <div bind:this={container} class="rp rp--{orientation}" {style}>
74
- <div class="rp-pane" style={firstStyle}>
78
+ <div
79
+ bind:this={container}
80
+ data-orientation={orientation}
81
+ class="flex h-full w-full overflow-hidden data-[orientation=horizontal]:flex-col"
82
+ {style}
83
+ >
84
+ <div class="overflow-hidden" style={firstStyle}>
75
85
  {@render first?.()}
76
86
  </div>
77
87
  <!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_noninteractive_element_interactions -->
78
88
  <div
79
89
  role="separator"
80
- tabindex="0"
90
+ tabindex={0}
81
91
  aria-orientation={orientation}
82
92
  aria-valuenow={size}
83
93
  aria-valuemin={min}
84
94
  aria-valuemax={max}
85
95
  aria-label={label}
86
- class="rp-handle"
96
+ data-orientation={orientation}
97
+ class="shrink-0 cursor-col-resize bg-row-active hover:bg-brand-05/40 data-[orientation=horizontal]:cursor-row-resize data-[orientation=vertical]:w-1 data-[orientation=horizontal]:h-1 focus-visible:bg-brand-05 focus-visible:outline-none"
87
98
  onmousedown={onMouseDown}
88
99
  onkeydown={onKeyDown}
89
- ></div>
90
- <div class="rp-pane rp-pane--second">
100
+ ></div>
101
+ <div class="min-w-0 flex-1 overflow-hidden">
91
102
  {@render second?.()}
92
103
  </div>
93
104
  </div>
@@ -4,6 +4,7 @@
4
4
 
5
5
  <script lang="ts">
6
6
  import Toggle from './Toggle.svelte';
7
+
7
8
  interface Props {
8
9
  title: string;
9
10
  description: string;
@@ -17,13 +18,14 @@
17
18
  on = $bindable(false),
18
19
  ontoggle,
19
20
  }: Props = $props();
21
+
20
22
  const descId = `dash-ui-rowtoggle-${++counter}`;
21
23
  </script>
22
24
 
23
- <div class="row-toggle">
24
- <div class="info">
25
- <div class="t">{title}</div>
26
- <div class="d" id={descId}>{description}</div>
25
+ <div class="flex items-center gap-3.5 border-b border-border-1 py-2.5">
26
+ <div class="flex-1">
27
+ <div class="text-13 font-medium text-text-1">{title}</div>
28
+ <div class="mt-0.5 text-11 text-text-4" id={descId}>{description}</div>
27
29
  </div>
28
30
  <Toggle bind:on ariaLabel={title} ariaDescribedBy={descId} {ontoggle} />
29
31
  </div>
@@ -18,8 +18,11 @@
18
18
  }: Props = $props();
19
19
  </script>
20
20
 
21
- <div class="search" role="search">
22
- <span aria-hidden="true" style="display:inline-flex">
21
+ <div
22
+ class="flex h-[30px] w-60 items-center gap-1.5 rounded-md border border-border-1 bg-bg-1 px-2.5 focus-within:border-brand-05"
23
+ role="search"
24
+ >
25
+ <span aria-hidden="true" class="inline-flex">
23
26
  <SearchIcon size={14} style="color:#6E7079" />
24
27
  </span>
25
28
  <input
@@ -29,5 +32,6 @@
29
32
  bind:value
30
33
  {oninput}
31
34
  {onchange}
35
+ class="min-w-0 flex-1 border-0 bg-transparent text-13 text-text-1 outline-none placeholder:text-text-4"
32
36
  />
33
37
  </div>
@@ -36,9 +36,23 @@
36
36
  root?.querySelector<HTMLButtonElement>('button[tabindex="0"]')?.focus();
37
37
  }
38
38
  }
39
+
40
+ const BTN_BASE =
41
+ 'flex-shrink-0 grow-0 inline-flex h-7 cursor-pointer select-none items-center justify-center whitespace-nowrap rounded-md border-0 bg-transparent px-3 text-12 font-medium leading-none text-text-3 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';
42
+ // Active button uses the elevated `--depthBg-3` so the active pill
43
+ // is legible on both motifs (a darker step on dark, a softer grey on light).
44
+ const BTN_ACTIVE =
45
+ 'bg-bg-3 text-text-1 shadow-[0_1px_3px_rgba(0,0,0,0.18)] dark:shadow-[0_1px_3px_rgba(0,0,0,0.4)]';
39
46
  </script>
40
47
 
41
- <div bind:this={root} role="radiogroup" aria-label={label} class="seg-ctrl" tabindex="-1" onkeydown={handleKeyDown}>
48
+ <div
49
+ bind:this={root}
50
+ role="radiogroup"
51
+ aria-label={label}
52
+ class="inline-flex gap-px rounded-lg bg-bg-2 p-0.5"
53
+ tabindex={-1}
54
+ onkeydown={handleKeyDown}
55
+ >
42
56
  {#each options as opt (opt.value)}
43
57
  <button
44
58
  type="button"
@@ -46,7 +60,7 @@
46
60
  aria-checked={value === opt.value}
47
61
  disabled={opt.disabled}
48
62
  tabindex={value === opt.value ? 0 : -1}
49
- class="seg-ctrl__btn{value === opt.value ? ' seg-ctrl__btn--active' : ''}"
63
+ class="{BTN_BASE} {value === opt.value ? BTN_ACTIVE : ''}"
50
64
  onclick={() => { value = opt.value; }}
51
65
  >
52
66
  {opt.label}
@@ -1,6 +1,9 @@
1
1
  <script module lang="ts">
2
2
  let counter = 0;
3
- export interface SelectOption { value: string; label: string; }
3
+ export interface SelectOption {
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-sel-${++counter}`;
33
35
  let triggerId = $derived(id ?? uid);
@@ -83,7 +85,7 @@
83
85
  onDestroy(() => document.removeEventListener('mousedown', handleOutside));
84
86
  </script>
85
87
 
86
- <div class="select-wrapper {className}" bind:this={wrapperEl}>
88
+ <div class="relative inline-block {className}" bind:this={wrapperEl}>
87
89
  {#if label}<label for={triggerId} class="sr-only">{label}</label>{/if}
88
90
  <button
89
91
  bind:this={triggerEl}
@@ -94,24 +96,34 @@
94
96
  aria-controls={listboxId}
95
97
  aria-label={label}
96
98
  {disabled}
97
- class="select-trigger"
99
+ type="button"
100
+ class="inline-flex h-[34px] cursor-pointer select-none items-center gap-1.5 whitespace-nowrap rounded-md border border-border-2 bg-bg-page pl-3 pr-2.5 text-13 leading-none text-text-1 transition-colors duration-100 hover:border-border-3 aria-expanded:border-brand-05 aria-expanded:shadow-[0_0_0_2px_rgba(0,111,255,0.2)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05 disabled:cursor-not-allowed disabled:opacity-40"
98
101
  onclick={toggle}
99
102
  onkeydown={onKeyDown}
100
- type="button"
101
103
  >
102
104
  <span>{displayLabel}</span>
103
- <svg class="select-chevron" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
105
+ <svg
106
+ viewBox="0 0 16 16"
107
+ aria-hidden="true"
108
+ focusable="false"
109
+ class="h-3.5 w-3.5 shrink-0 text-text-4 transition-transform duration-100 motion-reduce:transition-none {open ? 'rotate-180' : ''}"
110
+ >
104
111
  <path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none" />
105
112
  </svg>
106
113
  </button>
107
114
  {#if open}
108
- <ul id={listboxId} role="listbox" aria-label={label} class="select-listbox">
115
+ <ul
116
+ id={listboxId}
117
+ role="listbox"
118
+ aria-label={label}
119
+ class="absolute left-0 top-[calc(100%+4px)] z-[9000] m-0 min-w-full list-none rounded-lg border border-border-3 bg-bg-2 p-1 shadow-[0_8px_24px_rgba(0,0,0,0.5)]"
120
+ >
109
121
  {#each options as opt, idx (opt.value)}
110
122
  <li
111
123
  role="option"
112
124
  aria-selected={opt.value === value}
113
125
  data-active={idx === activeIdx ? 'true' : undefined}
114
- class="select-option"
126
+ class="flex cursor-pointer items-center 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 aria-selected:font-medium aria-selected:text-brand-05"
115
127
  onmousedown={(e) => { e.preventDefault(); (() => pick(opt.value))(); }}
116
128
  onmouseenter={() => { activeIdx = idx; }}
117
129
  >{opt.label}</li>
@@ -9,7 +9,6 @@
9
9
  <script lang="ts">
10
10
  import CloseIcon from '../icons/CloseIcon.svelte';
11
11
 
12
-
13
12
  interface Props {
14
13
  count: number;
15
14
  actions?: SelectionToolbarAction[];
@@ -23,20 +22,25 @@
23
22
  actions = [],
24
23
  onClear,
25
24
  ariaLabel = 'Selection actions',
26
- children
25
+ children,
27
26
  }: Props = $props();
28
27
  </script>
29
28
 
30
29
  {#if count > 0}
31
- <div class="sel-toolbar" role="toolbar" aria-label={ariaLabel}>
32
- <span class="sel-toolbar__count" aria-live="polite" aria-atomic="true">
33
- <strong>{count}</strong> selected
30
+ <div
31
+ role="toolbar"
32
+ aria-label={ariaLabel}
33
+ class="flex items-center gap-3 rounded-lg border border-brand-05/25 bg-brand-05/[0.08] px-4 py-2 text-13 text-text-2"
34
+ >
35
+ <span class="whitespace-nowrap" aria-live="polite" aria-atomic="true">
36
+ <strong class="text-text-1">{count}</strong> selected
34
37
  </span>
35
- <div class="sel-toolbar__actions">
38
+ <div class="flex flex-1 items-center gap-2">
36
39
  {#each actions as action}
37
40
  <button
38
41
  type="button"
39
- class="sel-toolbar__btn{action.variant === 'danger' ? ' sel-toolbar__btn--danger' : ''}"
42
+ data-danger={action.variant === 'danger' ? 'true' : undefined}
43
+ class="inline-flex h-7 cursor-pointer items-center rounded border border-border-2 bg-row-hover px-2.5 text-12 font-medium text-text-2 transition-colors duration-100 hover:border-border-3 hover:bg-row-active hover:text-text-1 data-[danger=true]:border-status-danger/30 data-[danger=true]:text-status-danger data-[danger=true]:hover:bg-status-danger/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
40
44
  onclick={action.onClick}
41
45
  >
42
46
  {action.label}
@@ -46,9 +50,9 @@
46
50
  </div>
47
51
  <button
48
52
  type="button"
49
- class="sel-toolbar__clear icon-btn"
50
- onclick={onClear}
51
53
  aria-label="Clear selection"
54
+ class="inline-flex h-7 w-7 shrink-0 cursor-pointer items-center justify-center rounded border-0 bg-transparent text-text-4 hover:bg-row-active hover:text-text-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
55
+ onclick={onClear}
52
56
  >
53
57
  <CloseIcon />
54
58
  </button>
@@ -14,34 +14,55 @@
14
14
  </script>
15
15
 
16
16
  <script lang="ts">
17
-
18
17
  interface Props {
19
18
  sections?: SidebarSectionDef[];
20
19
  activeId: string;
21
20
  onchange?: (payload: string) => void;
22
21
  }
23
22
 
24
- let { sections = [], activeId = $bindable(),
25
- onchange,
26
- }: Props = $props();
23
+ let { sections = [], activeId = $bindable(), onchange }: Props = $props();
27
24
  </script>
28
25
 
29
- <nav class="sidebar" aria-label="Primary">
26
+ <!--
27
+ Motif-aware chrome: bg via `var(--bg-page)`, border via `var(--border-1)`,
28
+ text via the standard `text-1/2/3/4` ramp. Active row uses `--row-active`
29
+ (brand-blue tint) so it stays visible in both motifs.
30
+ -->
31
+ <nav
32
+ class="overflow-auto border-r border-border-1 bg-bg-page py-2"
33
+ aria-label="Primary"
34
+ >
30
35
  {#each sections as sec (sec.title)}
31
- <h2 class="sb-section">{sec.title}</h2>
32
- <ul class="sb-list">
36
+ <h2
37
+ class="m-0 px-4 pb-1.5 pt-3.5 text-[10px] font-semibold uppercase leading-[1.4] tracking-[0.06em] text-text-4"
38
+ >{sec.title}</h2>
39
+ <ul class="m-0 flex list-none flex-col gap-px px-2">
33
40
  {#each sec.items as it (it.id)}
34
41
  <li>
35
42
  <button
36
43
  type="button"
37
- class="sb-item {it.id === activeId ? 'active' : ''}"
44
+ class="flex w-full cursor-pointer select-none items-center gap-2.5 rounded-md px-2.5 py-1.5 text-left font-[inherit] text-13 leading-[18px] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05
45
+ {it.id === activeId
46
+ ? '[&_.sb-ico]:text-brand-05 bg-row-active text-text-1'
47
+ : 'bg-transparent text-text-2 hover:bg-row-hover hover:text-text-1'}"
38
48
  aria-current={it.id === activeId ? 'page' : undefined}
39
49
  onclick={() => { activeId = it.id; onchange?.(it.id); }}
40
50
  >
41
- <span class="sb-ico"><it.icon /></span>
51
+ <span class="sb-ico shrink-0 text-text-3 [&_svg]:h-4 [&_svg]:w-4"
52
+ ><it.icon /></span>
42
53
  {it.label}
43
- {#if it.count !== undefined}<span class="sb-count">{it.count}</span>{/if}
44
- {#if it.pill !== undefined}<span class="sb-pill">{it.pill}<span class="sr-only"> alert{it.pill !== 1 ? 's' : ''}</span></span>{/if}
54
+ {#if it.count !== undefined}
55
+ <span
56
+ class="ml-auto text-[11px] tabular-nums {it.id === activeId
57
+ ? 'text-text-3'
58
+ : 'text-text-4'}"
59
+ >{it.count}</span>
60
+ {/if}
61
+ {#if it.pill !== undefined}
62
+ <span
63
+ class="ml-auto rounded-full bg-brand-05 px-1.5 py-px text-[10px] font-semibold text-white"
64
+ >{it.pill}<span class="sr-only"> alert{it.pill !== 1 ? 's' : ''}</span></span>
65
+ {/if}
45
66
  </button>
46
67
  </li>
47
68
  {/each}
@@ -8,9 +8,10 @@
8
8
  </script>
9
9
 
10
10
  <span
11
- class="signal {weak ? 'weak' : ''}"
11
+ data-weak={weak ? 'true' : undefined}
12
+ class="inline-flex h-3 items-end gap-px [&>i]:block [&>i]:w-0.5 [&>i]:rounded-[0.5px] [&>i]:bg-status-success data-[weak=true]:[&>i:nth-child(3)]:bg-bg-3 data-[weak=true]:[&>i:nth-child(4)]:bg-bg-3"
12
13
  role="img"
13
14
  aria-label={label ?? (weak ? 'Signal: weak' : 'Signal: strong')}
14
15
  >
15
- <i></i><i></i><i></i><i></i>
16
+ <i class="h-[3px]"></i><i class="h-1.5"></i><i class="h-[9px]"></i><i class="h-3"></i>
16
17
  </span>
@@ -1,5 +1,4 @@
1
1
  <script lang="ts">
2
-
3
2
  interface Props {
4
3
  variant?: 'text' | 'title' | 'stat' | 'circle' | 'block';
5
4
  width?: string | number | undefined;
@@ -13,20 +12,33 @@
13
12
  width = undefined,
14
13
  height = undefined,
15
14
  class: className = '',
16
- ariaLabel = 'Loading'
15
+ ariaLabel = 'Loading',
17
16
  }: Props = $props();
18
17
 
19
- let variantClass = $derived(variant === 'block' ? '' : variant);
20
18
  let w = $derived(width ?? (variant === 'circle' ? height : undefined));
21
- let style = $derived([
22
- w !== undefined ? `width:${typeof w === 'number' ? w + 'px' : w}` : '',
23
- height !== undefined ? `height:${typeof height === 'number' ? height + 'px' : height}` : '',
24
- ].filter(Boolean).join(';'));
19
+ let inlineStyle = $derived(
20
+ [
21
+ w !== undefined ? `width:${typeof w === 'number' ? w + 'px' : w}` : '',
22
+ height !== undefined ? `height:${typeof height === 'number' ? height + 'px' : height}` : '',
23
+ ]
24
+ .filter(Boolean)
25
+ .join(';'),
26
+ );
27
+
28
+ // Pre-composed variant strings so Tailwind's static scanner picks them up.
29
+ // Mirrors the legacy `.skeleton.text/.title/.stat/.circle` ramp.
30
+ const VARIANT: Record<NonNullable<Props['variant']>, string> = {
31
+ text: 'h-3 rounded-[3px]',
32
+ title: 'h-[18px] rounded',
33
+ stat: 'h-7 rounded',
34
+ circle: 'rounded-full',
35
+ block: 'rounded',
36
+ };
25
37
  </script>
26
38
 
27
39
  <span
28
- class="skeleton {variantClass} {className}"
29
- {style}
40
+ class="block animate-shimmer bg-[length:200%_100%] bg-[linear-gradient(90deg,rgba(255,255,255,0.04)_0%,rgba(255,255,255,0.1)_50%,rgba(255,255,255,0.04)_100%)] motion-reduce:animate-none {VARIANT[variant]} {className}"
41
+ style={inlineStyle}
30
42
  role="status"
31
43
  aria-label={ariaLabel}
32
44
  aria-busy="true"
@@ -6,9 +6,12 @@
6
6
  }
7
7
 
8
8
  let { href = '#main-content', class: className = '', children }: Props = $props();
9
-
10
9
  </script>
11
10
 
12
- <a class="skip-link {className}" {href}>
11
+ <a
12
+ class="absolute left-0 top-0 z-[100] -translate-y-full rounded-br-md bg-brand-05 px-3 py-2 text-13 font-semibold text-white no-underline focus:translate-y-0 focus:outline-2 focus:-outline-offset-4 focus:outline-white
13
+ {className}"
14
+ {href}
15
+ >
13
16
  {#if children}{@render children()}{:else}Skip to main content{/if}
14
17
  </a>
@@ -1,6 +1,4 @@
1
1
  <script lang="ts">
2
-
3
-
4
2
  interface Props {
5
3
  label?: string | undefined;
6
4
  value?: number;
@@ -34,27 +32,36 @@
34
32
  value = Number((e.target as HTMLInputElement).value);
35
33
  onchange?.(value);
36
34
  }
35
+
36
+ // The native <input type="range"> track gets a gradient-fill background
37
+ // composed inline (so the fill % is reactive). Thumb chrome lives in
38
+ // arbitrary-value pseudo-element utilities — Tailwind v4 handles them
39
+ // statically.
40
+ const TRACK_CLS =
41
+ 'h-1 w-full cursor-pointer appearance-none rounded-sm outline-none transition-opacity duration-100 disabled:cursor-not-allowed disabled:opacity-40 ' +
42
+ '[&::-webkit-slider-thumb]:[-webkit-appearance:none] [&::-webkit-slider-thumb]:h-3.5 [&::-webkit-slider-thumb]:w-3.5 [&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-brand-05 [&::-webkit-slider-thumb]:bg-white ' +
43
+ '[&::-moz-range-thumb]:h-3.5 [&::-moz-range-thumb]:w-3.5 [&::-moz-range-thumb]:cursor-pointer [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-brand-05 [&::-moz-range-thumb]:bg-white';
37
44
  </script>
38
45
 
39
- <div class="slider">
46
+ <div class="flex flex-col gap-1.5">
40
47
  {#if label}
41
- <div class="slider__header">
42
- <label for={inputId} class="slider__label">{label}</label>
43
- <span class="slider__value" aria-live="polite">
44
- {value}{#if suffix}<span class="slider__suffix">{suffix}</span>{/if}
48
+ <div class="flex items-center justify-between gap-2">
49
+ <label for={inputId} class="text-12 text-text-4">{label}</label>
50
+ <span class="flex items-baseline gap-px text-12 font-medium text-text-1 tabular-nums" aria-live="polite">
51
+ {value}{#if suffix}<span class="ml-px text-11 text-text-4">{suffix}</span>{/if}
45
52
  </span>
46
53
  </div>
47
54
  {/if}
48
55
  <input
49
56
  {id}
50
57
  type="range"
51
- class="slider__track"
58
+ class={TRACK_CLS}
52
59
  {min}
53
60
  {max}
54
61
  {step}
55
62
  {value}
56
63
  {disabled}
57
- style="--slider-fill:{pct}%"
64
+ style={`background: linear-gradient(to right, var(--brand-05) 0%, var(--brand-05) ${pct}%, rgba(255,255,255,0.12) ${pct}%, rgba(255,255,255,0.12) 100%);`}
58
65
  aria-label={label}
59
66
  aria-valuetext={suffix ? `${value} ${suffix}` : undefined}
60
67
  oninput={handleChange}
@@ -1,5 +1,4 @@
1
1
  <script lang="ts">
2
-
3
2
  interface Props {
4
3
  sortKey: string;
5
4
  activeKey?: string | null;
@@ -15,14 +14,24 @@
15
14
  children,
16
15
  onsort,
17
16
  }: Props = $props();
17
+
18
18
  let active = $derived(activeKey === sortKey);
19
- let ariaSort = $derived((active ? (dir === 'asc' ? 'ascending' : 'descending') : 'none') as 'ascending' | 'descending' | 'none');
19
+ let ariaSort = $derived(
20
+ (active ? (dir === 'asc' ? 'ascending' : 'descending') : 'none') as
21
+ | 'ascending'
22
+ | 'descending'
23
+ | 'none',
24
+ );
20
25
  </script>
21
26
 
22
27
  <th scope="col" aria-sort={ariaSort}>
23
- <button type="button" class="sort-header-btn" onclick={() => onsort?.(sortKey)}>
28
+ <button
29
+ type="button"
30
+ class="inline-flex w-full cursor-pointer items-center gap-1.5 whitespace-nowrap border-0 bg-transparent px-3 py-2 font-inherit text-inherit hover:text-text-2 focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-brand-05"
31
+ onclick={() => onsort?.(sortKey)}
32
+ >
24
33
  {@render children?.()}
25
- <svg class="sort-icon" viewBox="0 0 8 11" aria-hidden="true" fill="currentColor">
34
+ <svg class="h-[11px] w-2 shrink-0" viewBox="0 0 8 11" aria-hidden="true" fill="currentColor">
26
35
  <path d="M4 0L7 4H1Z" opacity={active && dir === 'asc' ? 1 : 0.3} />
27
36
  <path d="M4 11L1 7H7Z" opacity={active && dir === 'desc' ? 1 : 0.3} />
28
37
  </svg>
@@ -7,7 +7,6 @@
7
7
  </script>
8
8
 
9
9
  <script lang="ts">
10
-
11
10
  interface Props {
12
11
  items?: SortableItem[];
13
12
  onChange?: (items: SortableItem[]) => void;
@@ -22,8 +21,8 @@
22
21
 
23
22
  function reorder(fromId: string, toId: string) {
24
23
  if (fromId === toId) return;
25
- const from = items.findIndex(i => i.id === fromId);
26
- const to = items.findIndex(i => i.id === toId);
24
+ const from = items.findIndex((i) => i.id === fromId);
25
+ const to = items.findIndex((i) => i.id === toId);
27
26
  const arr = [...items];
28
27
  const [item] = arr.splice(from, 1);
29
28
  arr.splice(to, 0, item);
@@ -32,7 +31,7 @@
32
31
  }
33
32
 
34
33
  function move(id: string, dir: -1 | 1) {
35
- const idx = items.findIndex(i => i.id === id);
34
+ const idx = items.findIndex((i) => i.id === id);
36
35
  const next = idx + dir;
37
36
  if (next < 0 || next >= items.length) return;
38
37
  const arr = [...items];
@@ -53,25 +52,25 @@
53
52
  }
54
53
  </script>
55
54
 
56
- <ol class="sortable-list" aria-label={ariaLabel}>
55
+ <ol class="m-0 flex list-none flex-col gap-1 p-0" aria-label={ariaLabel}>
57
56
  {#each items as item, idx (item.id)}
58
57
  <li
59
- class="sortable-list__item"
60
- class:is-grabbed={grabbed === item.id}
61
- class:is-drag-over={dragOver === item.id}
58
+ data-grabbed={grabbed === item.id ? 'true' : undefined}
59
+ data-dragover={dragOver === item.id ? 'true' : undefined}
62
60
  draggable="true"
63
- ondragstart={e => { e.dataTransfer?.setData('text/plain', item.id); if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move'; dragSrc = item.id; }}
61
+ class="flex items-center gap-2 rounded-md border border-border-1 bg-divider px-2 py-1.5 transition-colors duration-100 data-[grabbed=true]:border-brand-05 data-[grabbed=true]:bg-brand-05/[0.08] data-[dragover=true]:border-brand-05"
62
+ ondragstart={(e) => { e.dataTransfer?.setData('text/plain', item.id); if (e.dataTransfer) e.dataTransfer.effectAllowed = 'move'; dragSrc = item.id; }}
64
63
  ondragend={() => { dragSrc = null; dragOver = null; }}
65
- ondragover={e => { e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'; dragOver = item.id; }}
64
+ ondragover={(e) => { e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = 'move'; dragOver = item.id; }}
66
65
  ondragleave={() => { dragOver = null; }}
67
- ondrop={e => { e.preventDefault(); if (dragSrc) reorder(dragSrc, item.id); dragSrc = null; dragOver = null; }}
66
+ ondrop={(e) => { e.preventDefault(); if (dragSrc) reorder(dragSrc, item.id); dragSrc = null; dragOver = null; }}
68
67
  >
69
68
  <button
70
69
  type="button"
71
- class="sortable-list__handle"
72
- aria-label="{grabbed === item.id ? 'Release' : 'Grab'} {item.label}. Use arrow keys to reorder."
70
+ aria-label={`${grabbed === item.id ? 'Release' : 'Grab'} ${item.label}. Use arrow keys to reorder.`}
73
71
  aria-pressed={grabbed === item.id}
74
- onkeydown={e => handleKeyDown(e, item.id)}
72
+ class="flex h-6 w-6 cursor-grab items-center justify-center rounded border-0 bg-transparent p-0 text-text-4 hover:bg-row-hover hover:text-text-1 active:cursor-grabbing focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
73
+ onkeydown={(e) => handleKeyDown(e, item.id)}
75
74
  >
76
75
  <svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor" aria-hidden="true" focusable="false">
77
76
  <rect x="3" y="2" width="2" height="2" rx="1" />
@@ -82,13 +81,13 @@
82
81
  <rect x="9" y="10" width="2" height="2" rx="1" />
83
82
  </svg>
84
83
  </button>
85
- <span class="sortable-list__content">
86
- <span class="sortable-list__label">{item.label}</span>
84
+ <span class="flex min-w-0 flex-1 flex-col">
85
+ <span class="truncate text-13 text-text-1">{item.label}</span>
87
86
  {#if item.meta}
88
- <span class="sortable-list__meta">{item.meta}</span>
87
+ <span class="truncate text-11 text-text-4">{item.meta}</span>
89
88
  {/if}
90
89
  </span>
91
- <span class="sortable-list__index" aria-hidden="true">{idx + 1}</span>
90
+ <span class="shrink-0 text-11 text-text-4 tabular-nums" aria-hidden="true">{idx + 1}</span>
92
91
  </li>
93
92
  {/each}
94
93
  </ol>
@@ -10,7 +10,7 @@
10
10
  bars = 36,
11
11
  active = false,
12
12
  seed = 1,
13
- ariaLabel = 'Activity sparkline'
13
+ ariaLabel = 'Activity sparkline',
14
14
  }: Props = $props();
15
15
 
16
16
  function rng(s0: number) {
@@ -21,21 +21,27 @@
21
21
  };
22
22
  }
23
23
 
24
- let heights = $derived((() => {
25
- const r = rng(seed);
26
- const out: number[] = [];
27
- let prev = 12;
28
- for (let i = 0; i < bars; i++) {
29
- const v = Math.max(4, Math.min(40, prev + (r() * 10 - 5)));
30
- prev = v;
31
- out.push(v);
32
- }
33
- return out;
34
- })());
24
+ let heights = $derived(
25
+ (() => {
26
+ const r = rng(seed);
27
+ const out: number[] = [];
28
+ let prev = 12;
29
+ for (let i = 0; i < bars; i++) {
30
+ const v = Math.max(4, Math.min(40, prev + (r() * 10 - 5)));
31
+ prev = v;
32
+ out.push(v);
33
+ }
34
+ return out;
35
+ })(),
36
+ );
35
37
  </script>
36
38
 
37
- <div class="spark" role="img" aria-label={ariaLabel}>
39
+ <div class="flex h-12 items-end gap-px" role="img" aria-label={ariaLabel}>
38
40
  {#each heights as h, i}
39
- <div class="b" aria-hidden="true" style="height:{h}px;opacity:{active ? 0.4 + i / (bars * 1.4) : 0.5 + i / (bars * 1.6)}"></div>
41
+ <div
42
+ class="flex-1 rounded-t-[1px] bg-gradient-to-b from-brand-06 to-brand-04"
43
+ aria-hidden="true"
44
+ style="height:{h}px;opacity:{active ? 0.4 + i / (bars * 1.4) : 0.5 + i / (bars * 1.6)}"
45
+ ></div>
40
46
  {/each}
41
47
  </div>