@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
@@ -9,7 +9,6 @@
9
9
  <script lang="ts">
10
10
  import { onMount } from 'svelte';
11
11
 
12
-
13
12
  interface Props {
14
13
  columns?: ColumnDef[];
15
14
  visible?: Set<string>;
@@ -56,14 +55,15 @@
56
55
  });
57
56
  </script>
58
57
 
59
- <div class="col-toggle">
58
+ <div class="relative inline-block">
60
59
  <button
61
60
  bind:this={btnEl}
62
61
  type="button"
63
- class="col-toggle__btn{open ? ' col-toggle__btn--active' : ''}"
62
+ data-open={open ? 'true' : undefined}
64
63
  aria-haspopup="dialog"
65
64
  aria-expanded={open}
66
65
  aria-controls={panelId}
66
+ class="inline-flex h-7 cursor-pointer items-center gap-1.5 rounded-md border border-border-2 bg-transparent px-2.5 text-12 font-medium text-text-3 transition-colors duration-100 hover:bg-row-hover hover:text-text-1 data-[open=true]:border-brand-05 data-[open=true]:bg-brand-05/[0.10] data-[open=true]:text-brand-05 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
67
67
  onclick={() => (open = !open)}
68
68
  >
69
69
  <svg viewBox="0 0 14 14" width="14" height="14" fill="none" aria-hidden="true" focusable="false">
@@ -77,18 +77,19 @@
77
77
  <div
78
78
  bind:this={panelEl}
79
79
  id={panelId}
80
- class="col-toggle__panel"
81
80
  role="dialog"
82
81
  aria-label="Toggle columns"
82
+ class="absolute right-0 top-[calc(100%+4px)] z-[9100] min-w-[200px] rounded-lg border border-border-3 bg-bg-2 p-1 shadow-[0_8px_24px_rgba(0,0,0,0.5)]"
83
83
  >
84
- <div class="col-toggle__header">Columns</div>
85
- <ul class="col-toggle__list" role="list">
84
+ <div class="px-2.5 pb-1.5 pt-2 text-11 font-semibold uppercase tracking-[0.06em] text-text-4">Columns</div>
85
+ <ul class="m-0 list-none p-0" role="list">
86
86
  {#each columns as col (col.key)}
87
- <li class="col-toggle__item">
88
- <label class="col-toggle__label">
87
+ <li>
88
+ <label class="flex cursor-pointer items-center gap-2 rounded-[5px] px-2.5 py-1.5 text-13 text-text-2 hover:bg-row-active hover:text-text-1 has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50">
89
89
  <input
90
90
  type="checkbox"
91
- class="col-toggle__check"
91
+ class="h-3.5 w-3.5 cursor-pointer appearance-none rounded-sm border-[1.5px] border-border-3 bg-bg-1 checked:border-brand-05 checked:bg-brand-05 disabled:cursor-not-allowed"
92
+ style="background-image: var(--check-img, none);"
92
93
  checked={visible.has(col.key)}
93
94
  disabled={col.required}
94
95
  onchange={() => { if (!col.required) toggle(col.key); }}
@@ -1,6 +1,9 @@
1
1
  <script module lang="ts">
2
2
  let counter = 0;
3
- export interface ComboboxOption { value: string; label: string; }
3
+ export interface ComboboxOption {
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-cb-${++counter}`;
33
35
  let inputId = $derived(id ?? uid);
@@ -40,13 +42,15 @@
40
42
 
41
43
  let selectedOption = $derived(options.find((o) => o.value === value));
42
44
 
43
- let filtered = $derived(query.trim() === ''
44
- ? options
45
- : options.filter((o) => o.label.toLowerCase().includes(query.toLowerCase())));
45
+ let filtered = $derived(
46
+ query.trim() === ''
47
+ ? options
48
+ : options.filter((o) => o.label.toLowerCase().includes(query.toLowerCase())),
49
+ );
46
50
 
47
- let activeOptionId = $derived(activeIdx >= 0 && filtered[activeIdx]
48
- ? `${listboxId}-opt-${activeIdx}`
49
- : undefined);
51
+ let activeOptionId = $derived(
52
+ activeIdx >= 0 && filtered[activeIdx] ? `${listboxId}-opt-${activeIdx}` : undefined,
53
+ );
50
54
 
51
55
  let displayValue = $derived(open ? query : (selectedOption?.label ?? ''));
52
56
 
@@ -93,9 +97,9 @@
93
97
  onDestroy(() => document.removeEventListener('mousedown', handleOutside));
94
98
  </script>
95
99
 
96
- <div class="combobox-wrapper {className}" bind:this={wrapperEl}>
100
+ <div class="relative block {className}" bind:this={wrapperEl}>
97
101
  {#if label}<label for={inputId} class="sr-only">{label}</label>{/if}
98
- <div class="combobox-field">
102
+ <div class="relative flex items-center">
99
103
  <input
100
104
  bind:this={inputEl}
101
105
  id={inputId}
@@ -113,24 +117,34 @@
113
117
  oninput={onInputChange}
114
118
  onfocus={openList}
115
119
  onkeydown={onKeyDown}
116
- class="combobox-input"
120
+ class="box-border h-[34px] w-full rounded-md border border-border-2 bg-bg-page py-0 pl-3 pr-9 text-13 leading-none text-text-1 transition-colors duration-100 hover:border-border-3 focus:border-brand-05 focus:outline-none focus:shadow-[0_0_0_2px_rgba(0,111,255,0.2)] disabled:cursor-not-allowed disabled:opacity-40"
117
121
  />
118
122
  <button
119
123
  type="button"
120
- tabindex="-1"
124
+ tabindex={-1}
121
125
  aria-hidden="true"
122
- class="combobox-chevron-btn"
126
+ class="absolute right-2 flex cursor-pointer items-center border-0 bg-transparent p-0.5 leading-none text-text-4"
123
127
  onclick={() => { if (open) { open = false; query = ''; } else { inputEl?.focus(); openList(); } }}
124
128
  >
125
- <svg class="select-chevron{open ? ' combobox-chevron-open' : ''}" viewBox="0 0 16 16" aria-hidden="true" focusable="false">
129
+ <svg
130
+ viewBox="0 0 16 16"
131
+ aria-hidden="true"
132
+ focusable="false"
133
+ class="h-3.5 w-3.5 transition-transform duration-100 motion-reduce:transition-none {open ? 'rotate-180' : ''}"
134
+ >
126
135
  <path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none" />
127
136
  </svg>
128
137
  </button>
129
138
  </div>
130
139
  {#if open}
131
- <ul id={listboxId} role="listbox" aria-label={label} class="select-listbox combobox-listbox">
140
+ <ul
141
+ id={listboxId}
142
+ role="listbox"
143
+ aria-label={label}
144
+ class="absolute left-0 top-[calc(100%+4px)] z-[9000] m-0 max-h-60 min-w-full list-none overflow-y-auto rounded-lg border border-border-3 bg-bg-2 p-1 shadow-[0_8px_24px_rgba(0,0,0,0.5)]"
145
+ >
132
146
  {#if filtered.length === 0}
133
- <li class="combobox-empty">No results</li>
147
+ <li class="list-none p-2.5 text-center text-13 text-text-4">No results</li>
134
148
  {:else}
135
149
  {#each filtered as opt, idx (opt.value)}
136
150
  <li
@@ -138,7 +152,7 @@
138
152
  role="option"
139
153
  aria-selected={opt.value === value}
140
154
  data-active={idx === activeIdx ? 'true' : undefined}
141
- class="select-option"
155
+ 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"
142
156
  onmousedown={(e) => { e.preventDefault(); (() => pick(opt))(); }}
143
157
  onmouseenter={() => { activeIdx = idx; }}
144
158
  >{opt.label}</li>
@@ -19,7 +19,10 @@
19
19
  onclose?: () => void;
20
20
  }
21
21
 
22
- let { open = false, items = [], placeholder = 'Search pages and actions…',
22
+ let {
23
+ open = false,
24
+ items = [],
25
+ placeholder = 'Search pages and actions…',
23
26
  onselect,
24
27
  onclose,
25
28
  }: Props = $props();
@@ -28,21 +31,24 @@
28
31
  let inputEl = $state<HTMLInputElement | undefined>(undefined);
29
32
  const listboxId = `cp-list-${Math.random().toString(36).slice(2)}`;
30
33
 
31
- let filtered =
32
- $derived(query.trim() === ''
34
+ let filtered = $derived(
35
+ query.trim() === ''
33
36
  ? items
34
- : items.filter((i) => i.label.toLowerCase().includes(query.toLowerCase())));
37
+ : items.filter((i) => i.label.toLowerCase().includes(query.toLowerCase())),
38
+ );
35
39
 
36
- let grouped = $derived(filtered.reduce(
37
- (acc, item) => {
38
- const g = item.group ?? '';
39
- const found = acc.find((a: { group: string }) => a.group === g);
40
- if (found) found.items.push(item);
41
- else acc.push({ group: g, items: [item] });
42
- return acc;
43
- },
44
- [] as { group: string; items: CommandItem[] }[],
45
- ));
40
+ let grouped = $derived(
41
+ filtered.reduce(
42
+ (acc, item) => {
43
+ const g = item.group ?? '';
44
+ const found = acc.find((a: { group: string }) => a.group === g);
45
+ if (found) found.items.push(item);
46
+ else acc.push({ group: g, items: [item] });
47
+ return acc;
48
+ },
49
+ [] as { group: string; items: CommandItem[] }[],
50
+ ),
51
+ );
46
52
 
47
53
  $effect(() => {
48
54
  if (open) {
@@ -86,16 +92,21 @@
86
92
 
87
93
  {#if open}
88
94
  <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
89
- <div class="cp-backdrop" role="presentation" onclick={handleBackdrop} onkeydown={handleKey}>
95
+ <div
96
+ class="fixed inset-0 z-[9300] flex items-start justify-center bg-black/60 pt-[15vh] backdrop-blur-md"
97
+ role="presentation"
98
+ onclick={handleBackdrop}
99
+ onkeydown={handleKey}
100
+ >
90
101
  <div
91
102
  role="dialog"
92
103
  aria-modal="true"
93
104
  aria-label="Command palette"
94
- class="cp-panel"
105
+ class="w-[520px] max-w-[90vw] overflow-hidden rounded-xl border border-border-3 bg-bg-1 shadow-[0_24px_64px_rgba(0,0,0,0.6)]"
95
106
  >
96
- <div class="cp-search">
107
+ <div class="flex items-center gap-2.5 border-b border-border-1 px-4 py-3">
97
108
  <svg
98
- class="cp-search-icon"
109
+ class="shrink-0 text-text-4"
99
110
  width="16"
100
111
  height="16"
101
112
  viewBox="0 0 16 16"
@@ -112,7 +123,6 @@
112
123
  </svg>
113
124
  <input
114
125
  bind:this={inputEl}
115
- class="cp-input"
116
126
  type="text"
117
127
  role="combobox"
118
128
  aria-expanded={filtered.length > 0}
@@ -121,33 +131,39 @@
121
131
  {placeholder}
122
132
  bind:value={query}
123
133
  onkeydown={handleKey}
134
+ class="min-w-0 flex-1 border-0 bg-transparent text-[15px] leading-none text-text-1 outline-none placeholder:text-text-4"
124
135
  />
125
- <span class="cp-kbd">Esc</span>
136
+ <span class="shrink-0 whitespace-nowrap rounded border border-border-2 bg-row-active px-1.5 py-0.5 text-[10px] text-text-4">Esc</span>
126
137
  </div>
127
- <ul id={listboxId} role="listbox" aria-label="Results" class="cp-list">
138
+ <ul
139
+ id={listboxId}
140
+ role="listbox"
141
+ aria-label="Results"
142
+ class="m-0 max-h-[360px] list-none overflow-y-auto p-1.5"
143
+ >
128
144
  {#if filtered.length === 0}
129
- <li class="cp-empty">No results for &ldquo;{query}&rdquo;</li>
145
+ <li class="list-none p-6 text-center text-13 text-text-4">No results for &ldquo;{query}&rdquo;</li>
130
146
  {:else}
131
147
  {#each grouped as { group, items: groupItems }}
132
148
  <li role="presentation">
133
149
  {#if group}
134
- <div class="cp-group-label">{group}</div>
150
+ <div class="px-2.5 pb-1 pt-2 text-[10px] font-semibold uppercase tracking-[0.06em] text-text-4">{group}</div>
135
151
  {/if}
136
152
  {#each groupItems as item}
137
153
  {@const idx = filtered.indexOf(item)}
138
154
  <!-- svelte-ignore a11y_click_events_have_key_events -->
139
155
  <div
140
156
  role="option"
141
- tabindex="-1"
157
+ tabindex={-1}
142
158
  aria-selected={idx === activeIdx}
143
159
  data-active={idx === activeIdx ? 'true' : undefined}
144
- class="cp-item"
160
+ class="flex cursor-pointer list-none items-center gap-2.5 rounded-md px-2.5 py-2 text-13 text-text-2 outline-none data-[active=true]:bg-brand-05/15 data-[active=true]:text-text-1"
145
161
  onclick={() => commit(item.id)}
146
162
  onmouseenter={() => (activeIdx = idx)}
147
163
  >
148
164
  {item.label}
149
165
  {#if item.shortcut}
150
- <span class="cp-item-shortcut" aria-label={item.shortcut.replace(/\+/g, ' then ')}>
166
+ <span class="ml-auto shrink-0" aria-label={item.shortcut.replace(/\+/g, ' then ')}>
151
167
  <Kbd keys={item.shortcut} />
152
168
  </span>
153
169
  {/if}
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { onDestroy } from 'svelte';
3
+ import Button from './Button.svelte';
3
4
 
4
5
  interface Props {
5
6
  open?: boolean;
@@ -26,10 +27,17 @@
26
27
  const FOCUSABLE =
27
28
  'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
28
29
 
29
- const VARIANT_CLASS: Record<string, string> = {
30
- danger: 'btn cd-confirm cd-confirm--danger',
31
- warning: 'btn cd-confirm cd-confirm--warning',
32
- info: 'btn cd-confirm cd-confirm--info',
30
+ // Map confirm-variant to the migrated `Button` variants. `warning` doesn't
31
+ // exist on `Button`, so we mark it as primary + override colours inline.
32
+ const CONFIRM_VARIANT: Record<NonNullable<Props['variant']>, 'primary' | 'danger'> = {
33
+ danger: 'danger',
34
+ warning: 'primary',
35
+ info: 'primary',
36
+ };
37
+ const CONFIRM_EXTRA: Record<NonNullable<Props['variant']>, string> = {
38
+ danger: '!bg-status-danger !text-white hover:!bg-[#ff5e5e] !border-status-danger',
39
+ warning: '!bg-status-warning !text-neutral-00 hover:!bg-[#ffba42] !border-status-warning',
40
+ info: '',
33
41
  };
34
42
 
35
43
  let dialogEl = $state<HTMLDivElement | undefined>(undefined);
@@ -59,8 +67,6 @@
59
67
  }
60
68
  }
61
69
 
62
- // Replaces the old `$: if (open) { ... }` + `afterUpdate` combo. `$effect`
63
- // runs after the DOM commits, so `dialogEl` is bound before we query it.
64
70
  $effect(() => {
65
71
  if (open) {
66
72
  prev = document.activeElement as HTMLElement | null;
@@ -86,7 +92,7 @@
86
92
  </script>
87
93
 
88
94
  <div
89
- class="backdrop {open ? 'show' : ''}"
95
+ class="fixed inset-0 z-[60] items-center justify-center bg-black/55 backdrop-blur-lg {open ? 'flex' : 'hidden'}"
90
96
  role="presentation"
91
97
  onclick={(e) => {
92
98
  if (e.target === e.currentTarget) onCancel();
@@ -94,22 +100,22 @@
94
100
  >
95
101
  <div
96
102
  bind:this={dialogEl}
97
- class="modal cd-dialog"
103
+ class="w-[400px] max-w-[90vw] overflow-hidden rounded-xl border border-border-2 bg-bg-1 shadow-modal"
98
104
  role="alertdialog"
99
105
  aria-modal="true"
100
106
  aria-labelledby="cd-title"
101
107
  aria-describedby={description ? 'cd-desc' : undefined}
102
- tabindex="-1"
108
+ tabindex={-1}
103
109
  >
104
- <div class="modal-h">
105
- <h2 id="cd-title">{title}</h2>
110
+ <div class="flex items-center justify-between border-b border-border-1 px-5 py-4">
111
+ <h2 id="cd-title" class="m-0 text-16 font-semibold">{title}</h2>
106
112
  </div>
107
113
  {#if description}
108
- <div id="cd-desc" class="cd-body">{description}</div>
114
+ <div id="cd-desc" class="px-5 py-3.5 text-13 leading-[1.5] text-text-3">{description}</div>
109
115
  {/if}
110
- <div class="modal-f">
111
- <button type="button" class="btn btn-ghost" onclick={onCancel}>{cancelLabel}</button>
112
- <button type="button" class={VARIANT_CLASS[variant]} onclick={onConfirm}>{confirmLabel}</button>
116
+ <div class="flex justify-end gap-2 border-t border-border-1 bg-bg-0 px-5 py-3.5">
117
+ <Button variant="ghost" onclick={onCancel}>{cancelLabel}</Button>
118
+ <Button variant={CONFIRM_VARIANT[variant]} class={CONFIRM_EXTRA[variant]} onclick={onConfirm}>{confirmLabel}</Button>
113
119
  </div>
114
120
  </div>
115
121
  </div>
@@ -33,10 +33,12 @@
33
33
  let menuEl = $state<HTMLUListElement | undefined>(undefined);
34
34
  let activeIdx = $state(0);
35
35
 
36
- let actionItems = $derived(items.reduce<ContextMenuItem[]>((acc, e) => {
37
- if (!('separator' in e)) acc.push(e);
38
- return acc;
39
- }, []));
36
+ let actionItems = $derived(
37
+ items.reduce<ContextMenuItem[]>((acc, e) => {
38
+ if (!('separator' in e)) acc.push(e);
39
+ return acc;
40
+ }, []),
41
+ );
40
42
 
41
43
  const menuW = 168;
42
44
  let menuH = $derived(items.length * 30 + 8);
@@ -92,28 +94,26 @@
92
94
  bind:this={menuEl}
93
95
  role="menu"
94
96
  aria-label={label}
95
- tabindex="-1"
96
- class="ctx-menu"
97
+ tabindex={-1}
98
+ class="fixed z-[9200] m-0 min-w-[168px] list-none rounded-lg border border-border-3 bg-bg-2 p-1 shadow-[0_8px_24px_rgba(0,0,0,0.5)] focus:outline-none"
97
99
  style="left:{cx}px;top:{cy}px"
98
100
  onkeydown={handleKeyDown}
99
101
  >
100
102
  {#each items as entry, i (i)}
101
103
  {#if 'separator' in entry}
102
- <li role="separator" class="ctx-menu-sep"></li>
104
+ <li role="separator" class="my-1 h-px bg-row-active"></li>
103
105
  {:else}
104
106
  <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
105
107
  <li
106
108
  role="menuitem"
107
- tabindex="-1"
109
+ tabindex={-1}
108
110
  aria-disabled={entry.disabled || undefined}
109
111
  data-active={actionIndex(entry) === activeIdx ? 'true' : undefined}
110
112
  data-danger={entry.danger || undefined}
111
- class="ctx-menu-item"
113
+ class="flex cursor-pointer items-center whitespace-nowrap rounded-[5px] px-3 py-1.5 text-13 text-text-2 outline-none hover:bg-row-active hover:text-text-1 data-[active=true]:bg-row-active data-[active=true]:text-text-1 data-[danger=true]:text-status-danger data-[danger=true]:hover:bg-status-danger/10 aria-disabled:cursor-not-allowed aria-disabled:opacity-40"
112
114
  onmouseenter={() => (activeIdx = actionIndex(entry))}
113
115
  onmousedown={(e) => { e.preventDefault(); (() => { if (!entry.disabled) activate(entry.id); })(); }}
114
- >
115
- {entry.label}
116
- </li>
116
+ >{entry.label}</li>
117
117
  {/if}
118
118
  {/each}
119
119
  </ul>
@@ -50,16 +50,21 @@
50
50
  window.removeEventListener('keydown', onKey);
51
51
  document.removeEventListener('pointerdown', onPointer);
52
52
  });
53
+
54
+ const PLACEMENT: Record<NonNullable<Props['placement']>, string> = {
55
+ top: 'bottom-[calc(100%+8px)] left-1/2 -translate-x-1/2',
56
+ bottom: 'top-[calc(100%+8px)] left-1/2 -translate-x-1/2',
57
+ };
53
58
  </script>
54
59
 
55
- <span class="ch-root" bind:this={rootEl}>
60
+ <span class="relative inline-flex items-center" bind:this={rootEl}>
56
61
  <button
57
62
  bind:this={triggerEl}
58
63
  type="button"
59
- class="ch-trigger"
60
64
  aria-label="Help"
61
65
  aria-haspopup="dialog"
62
66
  aria-expanded={open}
67
+ class="inline-flex h-4 w-4 shrink-0 cursor-pointer items-center justify-center rounded-full border border-border-3 bg-transparent p-0 text-[10px] font-bold leading-none text-text-4 transition-[border-color,color] duration-100 hover:border-text-3 hover:text-text-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
63
68
  onclick={toggle}
64
69
  >?</button>
65
70
  {#if open}
@@ -67,11 +72,11 @@
67
72
  bind:this={panelEl}
68
73
  role="dialog"
69
74
  aria-labelledby={titleId}
70
- class="ch-panel ch-panel--{placement}"
71
- tabindex="-1"
75
+ class="absolute z-[9200] min-w-[220px] max-w-[300px] rounded-[10px] border border-border-3 bg-bg-2 shadow-[0_8px_32px_rgba(0,0,0,0.5)] focus:outline-none {PLACEMENT[placement]}"
76
+ tabindex={-1}
72
77
  >
73
- <div id={titleId} class="ch-title">{title}</div>
74
- <div class="ch-body">{body}</div>
78
+ <div id={titleId} class="border-b border-border-1 px-3.5 pb-2 pt-2.5 text-12 font-semibold text-text-2">{title}</div>
79
+ <div class="px-3.5 py-2.5 text-12 leading-[1.5] text-text-3">{body}</div>
75
80
  </div>
76
81
  {/if}
77
82
  </span>
@@ -20,8 +20,9 @@
20
20
 
21
21
  <button
22
22
  type="button"
23
- class="copy-btn {copied ? 'copy-btn--done' : ''}"
23
+ data-copied={copied ? 'true' : undefined}
24
24
  aria-label={copied ? 'Copied!' : label}
25
+ class="inline-flex h-[22px] w-[22px] shrink-0 cursor-pointer items-center justify-center rounded border-0 bg-transparent text-text-4 transition-[color,background-color] duration-100 hover:bg-row-hover hover:text-text-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05 data-[copied=true]:text-status-success data-[copied=true]:hover:bg-status-success/10"
25
26
  onclick={handleCopy}
26
27
  >
27
28
  {#if copied}
@@ -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)}