@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
@@ -37,6 +37,7 @@
37
37
 
38
38
  <script lang="ts">
39
39
  import { onMount, onDestroy, tick } from 'svelte';
40
+ import Button from './Button.svelte';
40
41
 
41
42
  interface Props {
42
43
  value?: Date | null;
@@ -45,7 +46,10 @@
45
46
  onchange?: (payload: Date) => void;
46
47
  }
47
48
 
48
- let { value = null, placeholder = 'Pick a date', disabled = false,
49
+ let {
50
+ value = null,
51
+ placeholder = 'Pick a date',
52
+ disabled = false,
49
53
  onchange,
50
54
  }: Props = $props();
51
55
  const uid = `dash-ui-dp-${++counter}`;
@@ -58,7 +62,7 @@
58
62
  // svelte-ignore state_referenced_locally
59
63
  let viewMonth = $state(value?.getMonth() ?? today.getMonth());
60
64
  let focusIdx = $state(0);
61
- let triggerEl = $state<HTMLButtonElement | undefined>(undefined);
65
+ let triggerEl = $state<HTMLElement | undefined>(undefined);
62
66
  let calEl = $state<HTMLDivElement | undefined>(undefined);
63
67
  let dayEls: (HTMLButtonElement | null)[] = $state(Array(42).fill(null));
64
68
 
@@ -125,25 +129,25 @@
125
129
 
126
130
  onMount(() => document.addEventListener('mousedown', handleOutside));
127
131
  onDestroy(() => document.removeEventListener('mousedown', handleOutside));
132
+
133
+ function dayCls(outside: boolean, isToday: boolean, isSelected: boolean) {
134
+ let cls = 'flex h-8 w-8 cursor-pointer items-center justify-center rounded border-0 bg-transparent text-12 leading-none transition-colors duration-100 hover:bg-row-active focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-brand-05';
135
+ if (outside) cls += ' text-text-4';
136
+ else cls += ' text-text-2';
137
+ if (isToday && !isSelected) cls += ' ring-1 ring-inset ring-brand-05';
138
+ if (isSelected) cls += ' bg-brand-05 text-white hover:bg-brand-06';
139
+ return cls;
140
+ }
128
141
  </script>
129
142
 
130
- <div class="dp-root">
131
- <button
132
- bind:this={triggerEl}
133
- type="button"
134
- class="btn dp-trigger"
135
- aria-haspopup="dialog"
136
- aria-expanded={open}
137
- aria-controls={open ? calId : undefined}
138
- {disabled}
139
- onclick={() => open ? closeCal() : openCal()}
140
- >
143
+ <div class="relative inline-block" bind:this={triggerEl}>
144
+ <Button variant="ghost" aria-haspopup="dialog" aria-expanded={open} aria-controls={open ? calId : undefined} disabled={disabled} onclick={() => open ? closeCal() : openCal()}>
141
145
  <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
142
146
  <rect x="1" y="2" width="12" height="11" rx="1.5" />
143
147
  <path d="M1 6h12M4 1v2M10 1v2" />
144
148
  </svg>
145
149
  {value ? formatDate(value) : placeholder}
146
- </button>
150
+ </Button>
147
151
 
148
152
  {#if open}
149
153
  <div
@@ -152,27 +156,27 @@
152
156
  role="dialog"
153
157
  aria-modal="true"
154
158
  aria-label={`Choose date, ${label}`}
155
- class="dp-cal"
159
+ class="absolute left-0 top-[calc(100%+4px)] z-[9100] rounded-lg border border-border-3 bg-bg-2 p-2 shadow-[0_8px_24px_rgba(0,0,0,0.5)]"
156
160
  >
157
- <div class="dp-header">
158
- <button type="button" class="dp-nav" onclick={prevMonth} aria-label="Previous month">
161
+ <div class="flex items-center justify-between px-1 py-1">
162
+ <button type="button" 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" onclick={prevMonth} aria-label="Previous month">
159
163
  <svg width="8" height="12" viewBox="0 0 8 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
160
164
  <path d="M7 1L2 6l5 5" />
161
165
  </svg>
162
166
  </button>
163
- <span class="dp-month-label" aria-live="polite">{label}</span>
164
- <button type="button" class="dp-nav" onclick={nextMonth} aria-label="Next month">
167
+ <span class="text-13 font-medium text-text-1" aria-live="polite">{label}</span>
168
+ <button type="button" 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" onclick={nextMonth} aria-label="Next month">
165
169
  <svg width="8" height="12" viewBox="0 0 8 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
166
170
  <path d="M1 1l5 5-5 5" />
167
171
  </svg>
168
172
  </button>
169
173
  </div>
170
174
 
171
- <table role="grid" class="dp-grid" aria-label={label}>
175
+ <table role="grid" class="mt-1 border-collapse" aria-label={label}>
172
176
  <thead>
173
177
  <tr>
174
178
  {#each DAYS as d}
175
- <th scope="col" abbr={d}>{d}</th>
179
+ <th scope="col" abbr={d} class="p-0 text-center text-11 font-semibold uppercase tracking-[0.05em] text-text-4">{d}</th>
176
180
  {/each}
177
181
  </tr>
178
182
  </thead>
@@ -185,12 +189,12 @@
185
189
  {@const outside = day.getMonth() !== viewMonth}
186
190
  {@const isToday = isSameDay(day, today)}
187
191
  {@const isSelected = value ? isSameDay(day, value) : false}
188
- <td role="gridcell" aria-selected={isSelected}>
192
+ <td role="gridcell" aria-selected={isSelected} class="p-0">
189
193
  <button
190
194
  bind:this={dayEls[idx]}
191
195
  type="button"
192
196
  tabindex={idx === focusIdx ? 0 : -1}
193
- class="dp-day{outside ? ' dp-day--outside' : ''}{isToday && !isSelected ? ' dp-day--today' : ''}{isSelected ? ' dp-day--selected' : ''}"
197
+ class={dayCls(outside, isToday, isSelected)}
194
198
  aria-label={day.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}
195
199
  onkeydown={(e) => onGridKey(e, idx)}
196
200
  onclick={() => select(day)}
@@ -42,6 +42,7 @@
42
42
 
43
43
  <script lang="ts">
44
44
  import { onMount, onDestroy } from 'svelte';
45
+ import Button from './Button.svelte';
45
46
 
46
47
  interface Props {
47
48
  value?: DateRange;
@@ -50,7 +51,10 @@
50
51
  onchange?: (payload: DateRange) => void;
51
52
  }
52
53
 
53
- let { value = { start: null, end: null }, placeholder = 'Select date range', disabled = false,
54
+ let {
55
+ value = { start: null, end: null },
56
+ placeholder = 'Select date range',
57
+ disabled = false,
54
58
  onchange,
55
59
  }: Props = $props();
56
60
  const uid = `dash-ui-drp-${++counter}`;
@@ -62,7 +66,7 @@
62
66
  let hoverDate: Date | null = $state(null);
63
67
  let viewYear = $state(today.getFullYear());
64
68
  let viewMonth = $state(today.getMonth());
65
- let triggerEl = $state<HTMLButtonElement | undefined>(undefined);
69
+ let triggerEl = $state<HTMLDivElement | undefined>(undefined);
66
70
  let dlgEl = $state<HTMLDivElement | undefined>(undefined);
67
71
 
68
72
  let rightMonth = $derived(viewMonth === 11 ? 0 : viewMonth + 1);
@@ -90,7 +94,6 @@
90
94
  hoverDate = null;
91
95
  onchange?.({ start: s, end: e });
92
96
  open = false;
93
- triggerEl?.focus();
94
97
  }
95
98
  }
96
99
 
@@ -128,55 +131,59 @@
128
131
 
129
132
  onMount(() => document.addEventListener('mousedown', handleOutside));
130
133
  onDestroy(() => document.removeEventListener('mousedown', handleOutside));
134
+
135
+ function dayCls(outside: boolean, isTodayDay: boolean, start: boolean, end: boolean, inRange: boolean) {
136
+ let cls = 'flex h-8 w-8 cursor-pointer items-center justify-center rounded border-0 bg-transparent text-12 leading-none transition-colors duration-100 hover:bg-row-active focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-brand-05';
137
+ if (outside) cls += ' text-text-4';
138
+ else cls += ' text-text-2';
139
+ if (isTodayDay && !start && !end) cls += ' ring-1 ring-inset ring-brand-05';
140
+ if (start || end) cls += ' bg-brand-05 text-white hover:bg-brand-06';
141
+ else if (inRange) cls += ' bg-brand-05/20 text-text-1';
142
+ return cls;
143
+ }
144
+
145
+ const NAV_BTN =
146
+ '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';
131
147
  </script>
132
148
 
133
- <div class="drp-root">
134
- <button
135
- bind:this={triggerEl}
136
- type="button"
137
- class="btn dp-trigger"
138
- aria-haspopup="dialog"
139
- aria-expanded={open}
140
- aria-controls={open ? dlgId : undefined}
141
- {disabled}
142
- onclick={() => { if (open) { open = false; picking = null; } else open = true; }}
143
- >
149
+ <div class="relative inline-block" bind:this={triggerEl}>
150
+ <Button variant="ghost" aria-haspopup="dialog" aria-expanded={open} aria-controls={open ? dlgId : undefined} disabled={disabled} onclick={() => { if (open) { open = false; picking = null; } else open = true; }}>
144
151
  <svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
145
152
  <rect x="1" y="2" width="12" height="11" rx="1.5" />
146
153
  <path d="M1 6h12M4 1v2M10 1v2" />
147
154
  </svg>
148
155
  {formatRange() ?? placeholder}
149
- </button>
156
+ </Button>
150
157
 
151
158
  {#if open}
152
- <!-- svelte-ignore a11y_no_noninteractive_element_interactions - dialog uses keyboard listener for Escape -->
159
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
153
160
  <div
154
161
  id={dlgId}
155
162
  bind:this={dlgEl}
156
163
  role="dialog"
157
164
  aria-modal="true"
158
165
  aria-label="Select date range"
159
- tabindex="-1"
160
- class="drp-cal"
166
+ tabindex={-1}
167
+ class="absolute left-0 top-[calc(100%+4px)] z-[9100] rounded-lg border border-border-3 bg-bg-2 p-2 shadow-[0_8px_24px_rgba(0,0,0,0.5)]"
161
168
  onkeydown={(e) => {
162
- if (e.key === 'Escape') { e.preventDefault(); open = false; picking = null; triggerEl?.focus(); }
169
+ if (e.key === 'Escape') { e.preventDefault(); open = false; picking = null; }
163
170
  }}
164
171
  >
165
- <div class="drp-months">
172
+ <div class="flex gap-3">
166
173
  <!-- Left calendar -->
167
- <div class="drp-month">
168
- <div class="dp-header">
169
- <button type="button" class="dp-nav" onclick={prevMonth} aria-label="Previous month">
174
+ <div>
175
+ <div class="flex items-center justify-between px-1 py-1">
176
+ <button type="button" class={NAV_BTN} onclick={prevMonth} aria-label="Previous month">
170
177
  <svg width="8" height="12" viewBox="0 0 8 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
171
178
  <path d="M7 1L2 6l5 5" />
172
179
  </svg>
173
180
  </button>
174
- <span class="dp-month-label" aria-live="polite">{MONTHS[viewMonth]} {viewYear}</span>
175
- <span style="width:28px;display:inline-block;" aria-hidden="true"></span>
181
+ <span class="text-13 font-medium text-text-1" aria-live="polite">{MONTHS[viewMonth]} {viewYear}</span>
182
+ <span class="inline-block w-7" aria-hidden="true"></span>
176
183
  </div>
177
- <table role="grid" class="dp-grid" aria-label="{MONTHS[viewMonth]} {viewYear}">
184
+ <table role="grid" class="mt-1 border-collapse" aria-label={`${MONTHS[viewMonth]} ${viewYear}`}>
178
185
  <thead>
179
- <tr>{#each DAYS as d}<th scope="col" abbr={d}>{d}</th>{/each}</tr>
186
+ <tr>{#each DAYS as d}<th scope="col" abbr={d} class="p-0 text-center text-11 font-semibold uppercase tracking-[0.05em] text-text-4">{d}</th>{/each}</tr>
180
187
  </thead>
181
188
  <tbody>
182
189
  {#each Array(6) as _, row}
@@ -189,10 +196,10 @@
189
196
  {@const start = isStart(day)}
190
197
  {@const end = isEnd(day)}
191
198
  {@const inRange = isInRange(day)}
192
- <td role="gridcell" aria-selected={start || end}>
199
+ <td role="gridcell" aria-selected={start || end} class="p-0">
193
200
  <button
194
201
  type="button"
195
- class="dp-day{outside ? ' dp-day--outside' : ''}{isTodayDay && !start && !end ? ' dp-day--today' : ''}{start ? ' dp-day--start' : ''}{end ? ' dp-day--end' : ''}{inRange ? ' dp-day--in-range' : ''}"
202
+ class={dayCls(outside, isTodayDay, start, end, inRange)}
196
203
  aria-label={day.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}
197
204
  onclick={() => handleDayClick(day)}
198
205
  onmouseenter={() => { if (picking) hoverDate = day; }}
@@ -206,19 +213,19 @@
206
213
  </table>
207
214
  </div>
208
215
  <!-- Right calendar -->
209
- <div class="drp-month">
210
- <div class="dp-header">
211
- <span style="width:28px;display:inline-block;" aria-hidden="true"></span>
212
- <span class="dp-month-label" aria-live="polite">{MONTHS[rightMonth]} {rightYear}</span>
213
- <button type="button" class="dp-nav" onclick={nextMonth} aria-label="Next month">
216
+ <div>
217
+ <div class="flex items-center justify-between px-1 py-1">
218
+ <span class="inline-block w-7" aria-hidden="true"></span>
219
+ <span class="text-13 font-medium text-text-1" aria-live="polite">{MONTHS[rightMonth]} {rightYear}</span>
220
+ <button type="button" class={NAV_BTN} onclick={nextMonth} aria-label="Next month">
214
221
  <svg width="8" height="12" viewBox="0 0 8 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
215
222
  <path d="M1 1l5 5-5 5" />
216
223
  </svg>
217
224
  </button>
218
225
  </div>
219
- <table role="grid" class="dp-grid" aria-label="{MONTHS[rightMonth]} {rightYear}">
226
+ <table role="grid" class="mt-1 border-collapse" aria-label={`${MONTHS[rightMonth]} ${rightYear}`}>
220
227
  <thead>
221
- <tr>{#each DAYS as d}<th scope="col" abbr={d}>{d}</th>{/each}</tr>
228
+ <tr>{#each DAYS as d}<th scope="col" abbr={d} class="p-0 text-center text-11 font-semibold uppercase tracking-[0.05em] text-text-4">{d}</th>{/each}</tr>
222
229
  </thead>
223
230
  <tbody>
224
231
  {#each Array(6) as _, row}
@@ -231,10 +238,10 @@
231
238
  {@const start = isStart(day)}
232
239
  {@const end = isEnd(day)}
233
240
  {@const inRange = isInRange(day)}
234
- <td role="gridcell" aria-selected={start || end}>
241
+ <td role="gridcell" aria-selected={start || end} class="p-0">
235
242
  <button
236
243
  type="button"
237
- class="dp-day{outside ? ' dp-day--outside' : ''}{isTodayDay && !start && !end ? ' dp-day--today' : ''}{start ? ' dp-day--start' : ''}{end ? ' dp-day--end' : ''}{inRange ? ' dp-day--in-range' : ''}"
244
+ class={dayCls(outside, isTodayDay, start, end, inRange)}
238
245
  aria-label={day.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}
239
246
  onclick={() => handleDayClick(day)}
240
247
  onmouseenter={() => { if (picking) hoverDate = day; }}
@@ -249,7 +256,7 @@
249
256
  </div>
250
257
  </div>
251
258
  {#if picking}
252
- <p class="drp-hint" aria-live="polite">Pick the end date</p>
259
+ <p class="mt-2 px-1 text-12 text-status-info" aria-live="polite">Pick the end date</p>
253
260
  {/if}
254
261
  </div>
255
262
  {/if}
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ dir: 'up' | 'down' | 'flat';
4
+ class?: string;
5
+ children?: import('svelte').Snippet;
6
+ }
7
+
8
+ let { dir, class: className = '', children }: Props = $props();
9
+
10
+ let deltaClass = $derived(dir === 'flat' ? '' : `delta-${dir}`);
11
+ let merged = $derived(`${deltaClass} ${className}`.trim());
12
+ </script>
13
+
14
+ <!--
15
+ Trend indicator span used inside <Submeta> or any inline copy where a delta
16
+ needs to be colour-coded (success when up, danger when down, neutral when
17
+ flat). Emits a visually-hidden "Trend: " prefix so screen readers announce
18
+ the direction independent of any arrow glyph supplied by the caller. Typed
19
+ equivalent of `<span class="delta-up | delta-down">` from
20
+ @w5-ui/tokens/dashboard.css.
21
+ -->
22
+ <span class="sr-only">Trend: </span>
23
+ <span class={merged || undefined}>
24
+ {@render children?.()}
25
+ </span>
@@ -0,0 +1,29 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ class?: string;
4
+ children?: import('svelte').Snippet;
5
+ }
6
+
7
+ let { class: className = '', children }: Props = $props();
8
+ </script>
9
+
10
+ <!--
11
+ Display-1 - the 64 px / bold / --lh-tight / --tracking-display
12
+ (-0.02em) rhythm reserved for onboarding splashes and marketing
13
+ heroes (the top of the type scale, one step above the 48 px
14
+ <Display2>). Renders <h1 class="dash-ui-display-1"> so the
15
+ .dash-ui-display-1 rule in @w5-ui/tokens/tokens.css pins the canonical
16
+ size, weight, line-height, letter-spacing, and --text-1 colour. Stops
17
+ userland reaching for an ad-hoc <h1 style="font-size: 64px"> that
18
+ almost always forgets --lh-tight (1.1) and the negative tracking,
19
+ which is the documented "headings break awkwardly across two lines"
20
+ drift in Foundations / Type. Typed equivalent of
21
+ <h1 class="dash-ui-display-1"> (the userland-facing semantic class
22
+ documented in Foundations / Type). Use for onboarding splashes or
23
+ large marketing heroes. In-app pages should reach for <PageHeader>
24
+ (page title) and <H2> (section heads); never substitute <Display1>
25
+ for an in-app screen - its 64 px rhythm dwarfs dashboard chrome.
26
+ -->
27
+ <h1 class="dash-ui-display-1 {className}">
28
+ {@render children?.()}
29
+ </h1>
@@ -0,0 +1,29 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ class?: string;
4
+ children?: import('svelte').Snippet;
5
+ }
6
+
7
+ let { class: className = '', children }: Props = $props();
8
+ </script>
9
+
10
+ <!--
11
+ Display-2 - the 48 px / bold / --lh-tight / --tracking-display
12
+ (-0.02em) rhythm used for login and large marketing surfaces (the
13
+ step below the 64 px Display-1 hero). Renders
14
+ <h1 class="dash-ui-display-2"> so the .dash-ui-display-2 rule in
15
+ @w5-ui/tokens/tokens.css pins the canonical size, weight, line-
16
+ height, letter-spacing, and --text-1 colour. Stops userland reaching
17
+ for an ad-hoc <h1 style="font-size: 48px"> that almost always
18
+ forgets --lh-tight (1.1) and the negative tracking, which is the
19
+ documented "headings break awkwardly across two lines" drift in
20
+ Foundations / Type. Typed equivalent of <h1 class="dash-ui-display-2">
21
+ (the userland-facing semantic class documented in Foundations /
22
+ Type). Use for login-page titles or large marketing heroes one step
23
+ down from <Display1>. In-app pages should reach for <PageHeader>
24
+ (page title) and <H2> (section heads); never substitute <Display2>
25
+ for an in-app screen - its 48 px rhythm dominates dashboard chrome.
26
+ -->
27
+ <h1 class="dash-ui-display-2 {className}">
28
+ {@render children?.()}
29
+ </h1>
@@ -44,24 +44,25 @@
44
44
  <svelte:window onkeydown={onKeydown} />
45
45
 
46
46
  <div
47
- class="drawer-overlay {open ? 'show' : ''}"
47
+ class="fixed inset-0 z-[60] bg-black/40 {open ? 'block' : 'hidden'}"
48
48
  onclick={() => (open = false)}
49
49
  aria-hidden="true"
50
50
  role="presentation"
51
51
  ></div>
52
52
  <div
53
53
  bind:this={panelEl}
54
- class="drawer-panel {open ? 'show' : ''}"
54
+ class="fixed right-0 top-0 z-[61] flex h-full w-[380px] max-w-[90vw] flex-col border-l border-border-2 bg-bg-1 transition-transform duration-200 ease-out [box-shadow:-24px_0_64px_rgba(0,0,0,0.6)]
55
+ {open ? 'translate-x-0' : 'translate-x-full'}"
55
56
  role="dialog"
56
57
  aria-modal="true"
57
58
  aria-labelledby={titleId}
58
59
  tabindex="-1"
59
60
  >
60
- <div class="drawer-h">
61
- <h2 id={titleId}>{title}</h2>
61
+ <div class="flex shrink-0 items-center justify-between border-b border-border-1 px-5 py-4">
62
+ <h2 id={titleId} class="m-0 text-16 font-semibold">{title}</h2>
62
63
  <IconButton title="Close" onclick={() => (open = false)}>
63
64
  <CloseIcon />
64
65
  </IconButton>
65
66
  </div>
66
- <div class="drawer-b">{@render children?.()}</div>
67
+ <div class="flex-1 overflow-y-auto px-5 py-4">{@render children?.()}</div>
67
68
  </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;
@@ -39,7 +37,9 @@
39
37
 
40
38
  let [hours, minutes, seconds] = $derived(toHMS(value));
41
39
 
42
- let inputs: [HTMLInputElement | null, HTMLInputElement | null, HTMLInputElement | null] = $state([null, null, null]);
40
+ let inputs: [HTMLInputElement | null, HTMLInputElement | null, HTMLInputElement | null] = $state([
41
+ null, null, null,
42
+ ]);
43
43
 
44
44
  function focusAt(i: number) {
45
45
  const el = inputs[i];
@@ -91,13 +91,21 @@
91
91
 
92
92
  let fieldVals = $derived([hours, minutes, seconds]);
93
93
  let fieldMaxes = $derived([maxHours, 59, 59]);
94
+
95
+ const FIELD_CLS =
96
+ 'w-9 border-0 bg-transparent p-0 text-center font-mono text-13 text-text-1 outline-none';
94
97
  </script>
95
98
 
96
- <div class="dur-input-wrapper {className}">
99
+ <div class={className}>
97
100
  {#if label}
98
- <label for="{uid}-h" class="dur-input__label">{label}</label>
101
+ <label for="{uid}-h" class="text-12 text-text-4">{label}</label>
99
102
  {/if}
100
- <div class="dur-input{disabled ? ' dur-input--disabled' : ''}" role="group" aria-label={label ?? 'Duration'}>
103
+ <div
104
+ role="group"
105
+ aria-label={label ?? 'Duration'}
106
+ data-disabled={disabled ? 'true' : undefined}
107
+ 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"
108
+ >
101
109
  {#each FIELDS as { idx, ariaLabel }, i}
102
110
  <input
103
111
  bind:this={inputs[idx]}
@@ -114,13 +122,13 @@
114
122
  value={pad(fieldVals[idx])}
115
123
  {disabled}
116
124
  maxlength={2}
117
- class="dur-input__field"
125
+ class={FIELD_CLS}
118
126
  oninput={(e) => handleInput(idx, e)}
119
127
  onkeydown={(e) => handleKey(idx, e)}
120
128
  onfocus={(e) => e.currentTarget.select()}
121
129
  />
122
130
  {#if i < 2}
123
- <span class="dur-input__sep" aria-hidden="true">:</span>
131
+ <span aria-hidden="true" class="select-none text-13 leading-none text-text-4">:</span>
124
132
  {/if}
125
133
  {/each}
126
134
  </div>
@@ -10,22 +10,31 @@
10
10
  title,
11
11
  description = undefined,
12
12
  class: className = '',
13
- action
13
+ action,
14
14
  }: Props = $props();
15
-
16
15
  </script>
17
16
 
18
- <div role="status" aria-live="polite" class="empty-state {className}">
19
- <svg class="empty-state__icon" viewBox="0 0 48 48" fill="none" aria-hidden="true" focusable="false">
17
+ <div
18
+ role="status"
19
+ aria-live="polite"
20
+ class="flex flex-col items-center justify-center gap-2.5 px-6 py-12 text-center {className}"
21
+ >
22
+ <svg
23
+ class="h-12 w-12 shrink-0 text-border-3"
24
+ viewBox="0 0 48 48"
25
+ fill="none"
26
+ aria-hidden="true"
27
+ focusable="false"
28
+ >
20
29
  <rect x="6" y="11" width="36" height="28" rx="4" stroke="currentColor" stroke-width="1.5"/>
21
30
  <path d="M6 30h10l3 5h10l3-5h10" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/>
22
31
  <path d="M15 20h18M15 25h12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" opacity=".4"/>
23
32
  </svg>
24
- <p class="empty-state__title">{title}</p>
33
+ <p class="m-0 text-14 font-medium text-text-3">{title}</p>
25
34
  {#if description}
26
- <p class="empty-state__desc">{description}</p>
35
+ <p class="m-0 max-w-[280px] text-12 leading-[1.5] text-text-4">{description}</p>
27
36
  {/if}
28
37
  {#if action}
29
- <div class="empty-state__action">{@render action?.()}</div>
38
+ <div class="mt-1.5">{@render action?.()}</div>
30
39
  {/if}
31
40
  </div>
@@ -6,35 +6,29 @@
6
6
  detail?: import('svelte').Snippet;
7
7
  }
8
8
 
9
- let {
10
- colSpan,
11
- defaultExpanded = false,
12
- row,
13
- detail
14
- }: Props = $props();
9
+ let { colSpan, defaultExpanded = false, row, detail }: Props = $props();
15
10
 
16
11
  // svelte-ignore state_referenced_locally
17
12
  let open = $state(defaultExpanded);
18
13
  const detailId = `exp-${Math.random().toString(36).slice(2, 9)}`;
19
14
  </script>
20
15
 
21
- <tbody class="exp-tbody">
22
- <tr class="exp-row">
23
- <td class="exp-toggle-cell">
16
+ <tbody>
17
+ <tr>
18
+ <td class="w-8 px-1">
24
19
  <button
25
20
  type="button"
26
- class="exp-toggle"
27
21
  aria-expanded={open}
28
22
  aria-controls={detailId}
29
23
  aria-label={open ? 'Collapse row' : 'Expand row'}
24
+ class="inline-flex h-6 w-6 cursor-pointer items-center justify-center rounded border-0 bg-transparent p-0 text-text-4 transition-colors duration-100 hover:text-text-2 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
30
25
  onclick={() => (open = !open)}
31
26
  >
32
27
  <svg
33
- class="exp-chevron"
34
- class:exp-chevron--open={open}
35
28
  viewBox="0 0 10 6"
36
29
  fill="none"
37
30
  aria-hidden="true"
31
+ class="h-1.5 w-2.5 transition-transform duration-100 motion-reduce:transition-none {open ? 'rotate-180' : ''}"
38
32
  >
39
33
  <path d="M1 1l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
40
34
  </svg>
@@ -44,11 +38,10 @@
44
38
  </tr>
45
39
  <tr
46
40
  id={detailId}
47
- class="exp-detail"
48
- class:exp-detail--open={open}
49
41
  aria-hidden={!open}
42
+ class="{open ? 'table-row' : 'hidden'}"
50
43
  >
51
- <td colspan={colSpan + 1} class="exp-detail-cell">
44
+ <td colspan={colSpan + 1} class="border-b border-border-1 bg-divider pb-3 pl-9 pr-3 pt-2">
52
45
  {@render detail?.()}
53
46
  </td>
54
47
  </tr>
@@ -0,0 +1,25 @@
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
+ Uppercase, tracked overline used as a section eyebrow / category tag
12
+ above a heading or beside a primary label. Renders
13
+ <span class="dash-ui-eyebrow"> so the .dash-ui-eyebrow rule in
14
+ @w5-ui/tokens/css applies the canonical 11 px / semibold weight,
15
+ 0.08em letter-spacing, uppercase text-transform and the muted
16
+ --text-3 colour. Stops userland reaching for a one-off
17
+ text-transform: uppercase + letter-spacing + font-size: 11px trio
18
+ on every overline. Typed equivalent of
19
+ <span class="dash-ui-eyebrow"> (the contrast pair is AA against
20
+ every motif background, verified for --text-3 over
21
+ --depthBg-0/--depthBg-1/--depthBg-2 in both dark and light motifs).
22
+ -->
23
+ <span class="dash-ui-eyebrow {className}">
24
+ {@render children?.()}
25
+ </span>
@@ -20,7 +20,7 @@
20
20
  error = undefined,
21
21
  required = false,
22
22
  hint = undefined,
23
- children
23
+ children,
24
24
  }: Props = $props();
25
25
  // svelte-ignore state_referenced_locally
26
26
  const inputId = id ?? `dash-ui-field-${++counter}`;
@@ -29,18 +29,18 @@
29
29
  let describedBy = $derived([hintId, errorId].filter(Boolean).join(' ') || undefined);
30
30
  </script>
31
31
 
32
- <div class="field">
33
- <label for={inputId}>
34
- {label}{#if required}<span class="req" aria-hidden="true"> *</span>{/if}
32
+ <div class="mb-3 flex flex-col gap-1.5">
33
+ <label for={inputId} class="text-12 font-medium text-text-3">
34
+ {label}{#if required}<span class="text-status-danger" aria-hidden="true"> *</span>{/if}
35
35
  </label>
36
36
  {#if hint}
37
- <div class="field-hint" id={hintId}>{hint}</div>
37
+ <div class="text-12 text-text-3" id={hintId}>{hint}</div>
38
38
  {/if}
39
39
  {#if children}
40
40
  {@render children?.()}
41
41
  {:else}
42
42
  <input
43
- class="input"
43
+ class="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)]"
44
44
  id={inputId}
45
45
  bind:value
46
46
  {required}
@@ -49,6 +49,6 @@
49
49
  />
50
50
  {/if}
51
51
  {#if error}
52
- <div class="field-error" id={errorId} role="alert">{error}</div>
52
+ <div class="text-12 text-status-danger" id={errorId} role="alert">{error}</div>
53
53
  {/if}
54
54
  </div>