@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
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { onDestroy } from 'svelte';
3
+
3
4
  interface Props {
4
5
  code?: string;
5
6
  label?: string;
@@ -11,9 +12,8 @@
11
12
  code = '',
12
13
  label = 'Code block',
13
14
  language = '',
14
- class: className = ''
15
+ class: className = '',
15
16
  }: Props = $props();
16
-
17
17
 
18
18
  let copied = $state(false);
19
19
  let timer: ReturnType<typeof setTimeout> | null = null;
@@ -29,17 +29,18 @@
29
29
  onDestroy(() => { if (timer) clearTimeout(timer); });
30
30
  </script>
31
31
 
32
- <div role="region" aria-label={label} class="code-block {className}">
33
- <div class="code-block__header">
32
+ <div role="region" aria-label={label} class="overflow-hidden rounded-lg border border-border-2 bg-bg-page {className}">
33
+ <div class="flex min-h-8 items-center justify-between border-b border-border-1 bg-bg-page py-1.5 pl-3 pr-2">
34
34
  {#if language}
35
- <span class="code-block__lang">{language}</span>
35
+ <span class="font-mono text-11 uppercase tracking-[0.05em] text-text-4">{language}</span>
36
36
  {:else}
37
37
  <span></span>
38
38
  {/if}
39
39
  <button
40
40
  type="button"
41
- class="copy-btn{copied ? ' copy-btn--done' : ''}"
41
+ data-copied={copied ? 'true' : undefined}
42
42
  aria-label={copied ? 'Copied!' : 'Copy code'}
43
+ 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 data-[copied=true]:hover:text-status-success"
43
44
  onclick={handleCopy}
44
45
  >
45
46
  {#if copied}
@@ -54,5 +55,5 @@
54
55
  {/if}
55
56
  </button>
56
57
  </div>
57
- <pre class="code-block__pre"><code class="code-block__code">{code}</code></pre>
58
+ <pre class="m-0 overflow-x-auto px-3.5 py-3"><code class="whitespace-pre font-mono text-12 leading-[1.6] text-text-2">{code}</code></pre>
58
59
  </div>
@@ -35,7 +35,7 @@
35
35
  value = $bindable(COLOR_SWATCHES[0].value),
36
36
  swatches = COLOR_SWATCHES,
37
37
  disabled = false,
38
- onChange = undefined
38
+ onChange = undefined,
39
39
  }: Props = $props();
40
40
 
41
41
  const groupId = `dash-ui-cp-${++counter}`;
@@ -48,8 +48,7 @@
48
48
  const idx = cells.findIndex((c) => c.value === value);
49
49
  let next = idx;
50
50
  if (e.key === 'ArrowRight' || e.key === 'ArrowDown') next = (idx + 1) % cells.length;
51
- else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp')
52
- next = (idx - 1 + cells.length) % cells.length;
51
+ else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') next = (idx - 1 + cells.length) % cells.length;
53
52
  else if (e.key === 'Home') next = 0;
54
53
  else if (e.key === 'End') next = cells.length - 1;
55
54
  else return;
@@ -62,28 +61,29 @@
62
61
  let hasMatch = $derived(swatches.some((s) => s.value === value));
63
62
  </script>
64
63
 
65
- <div class="color-picker{disabled ? ' color-picker--disabled' : ''}">
66
- <div id="{groupId}-label" class="color-picker__label{srOnlyLabel ? ' sr-only' : ''}"
67
- >{label}</div
68
- >
64
+ <div
65
+ data-disabled={disabled ? 'true' : undefined}
66
+ class="inline-flex flex-col gap-1.5 data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-40"
67
+ >
68
+ <div id="{groupId}-label" class={srOnlyLabel ? 'sr-only' : 'text-12 text-text-4'}>{label}</div>
69
69
  <div
70
70
  role="radiogroup"
71
71
  aria-labelledby="{groupId}-label"
72
- class="color-picker__grid"
73
- tabindex="-1"
72
+ class="flex flex-wrap gap-1.5"
73
+ tabindex={-1}
74
74
  onkeydown={handleKeyDown}
75
75
  >
76
76
  {#each swatches as sw, i}
77
77
  {@const checked = sw.value === value}
78
78
  {@const tabIdx = checked || (i === 0 && !hasMatch) ? 0 : -1}
79
- <label class="color-picker__swatch" title={sw.label}>
79
+ <label class="group/sw inline-flex cursor-pointer" title={sw.label}>
80
80
  <input
81
81
  type="radio"
82
82
  name={groupId}
83
83
  value={sw.value}
84
84
  {checked}
85
85
  {disabled}
86
- class="sr-only"
86
+ class="peer sr-only"
87
87
  tabindex={tabIdx}
88
88
  aria-label={sw.label}
89
89
  onchange={() => {
@@ -92,10 +92,10 @@
92
92
  }}
93
93
  />
94
94
  <span
95
- class="color-picker__circle{checked ? ' color-picker__circle--selected' : ''}"
95
+ class="inline-block h-5 w-5 rounded-full transition-[transform,box-shadow] duration-100 hover:scale-110 motion-reduce:hover:scale-100 peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-brand-05 {checked ? 'shadow-[0_0_0_2px_#0a0a0b,0_0_0_4px_#fff]' : ''}"
96
96
  style="background:{sw.color}"
97
97
  aria-hidden="true"
98
- ></span>
98
+ ></span>
99
99
  </label>
100
100
  {/each}
101
101
  </div>
@@ -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}