@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
@@ -8,11 +8,13 @@
8
8
  let { label, class: className = '', onclick }: Props = $props();
9
9
  </script>
10
10
 
11
- <span class="tag {className}">
12
- <span class="tag__label">{label}</span>
11
+ <span
12
+ class="inline-flex h-6 items-center gap-1 whitespace-nowrap rounded-full border border-brand-05/25 bg-brand-05/14 pl-2.5 pr-1 text-12 font-medium text-status-info {className}"
13
+ >
14
+ <span class="leading-none">{label}</span>
13
15
  <button
14
16
  type="button"
15
- class="tag__remove"
17
+ class="inline-flex h-[18px] w-[18px] shrink-0 cursor-pointer items-center justify-center rounded-full border-0 bg-transparent p-0 text-status-info transition-[background,color] duration-100 hover:bg-brand-05/25 hover:text-text-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
16
18
  aria-label="Remove {label} filter"
17
19
  {onclick}
18
20
  >
@@ -3,8 +3,6 @@
3
3
  </script>
4
4
 
5
5
  <script lang="ts">
6
-
7
-
8
6
  interface Props {
9
7
  label?: string | undefined;
10
8
  value?: string[];
@@ -56,26 +54,27 @@
56
54
  }
57
55
  </script>
58
56
 
59
- <div class="tag-input-wrapper {klass}">
57
+ <div class={klass}>
60
58
  {#if label}
61
- <label for={uid} class="tag-input__label">{label}</label>
59
+ <label for={uid} class="text-12 text-text-4">{label}</label>
62
60
  {/if}
63
61
  <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
64
62
  <!-- svelte-ignore a11y_no_noninteractive_element_interactions - group container focuses input on click -->
65
63
  <div
66
64
  role="group"
67
65
  aria-label={label ?? 'Tags'}
68
- class="tag-input{disabled ? ' tag-input--disabled' : ''}"
66
+ data-disabled={disabled ? 'true' : undefined}
67
+ class="flex min-h-[34px] cursor-text flex-wrap items-center gap-1 rounded-md border border-border-2 bg-bg-page py-[3px] pl-2 pr-2 transition-colors duration-100 hover:border-border-3 focus-within:border-brand-05 focus-within:shadow-[0_0_0_2px_rgba(0,111,255,0.2)] data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-40"
69
68
  onclick={() => inputEl?.focus()}
70
69
  >
71
70
  {#each value as tag, i}
72
- <span class="tag">
73
- <span class="tag__label">{tag}</span>
71
+ <span class="inline-flex h-6 items-center gap-1 whitespace-nowrap rounded-full border border-brand-05/25 bg-brand-05/[0.14] pl-2.5 pr-1 text-12 font-medium text-status-info">
72
+ <span class="leading-none">{tag}</span>
74
73
  {#if !disabled}
75
74
  <button
76
75
  type="button"
77
- class="tag__remove"
78
- aria-label="Remove {tag}"
76
+ aria-label={`Remove ${tag}`}
77
+ class="inline-flex h-[18px] w-[18px] shrink-0 cursor-pointer items-center justify-center rounded-full border-0 bg-transparent p-0 text-status-info transition-colors duration-100 hover:bg-brand-05/25 hover:text-text-1 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-brand-05"
79
78
  onclick={(e) => { e.stopPropagation(); (() => removeTag(i))(); }}
80
79
  >
81
80
  <svg viewBox="0 0 10 10" width="10" height="10" fill="none" aria-hidden="true" focusable="false">
@@ -89,12 +88,12 @@
89
88
  bind:this={inputEl}
90
89
  id={uid}
91
90
  type="text"
92
- class="tag-input__field"
93
91
  bind:value={draft}
94
92
  {disabled}
95
93
  placeholder={value.length === 0 ? placeholder : ''}
96
94
  aria-label={label ?? 'Add tag'}
97
95
  autocomplete="off"
96
+ class="h-6 min-w-[80px] flex-1 border-0 bg-transparent px-1 text-13 leading-none text-text-1 outline-none placeholder:text-text-4"
98
97
  onkeydown={handleKeyDown}
99
98
  onblur={handleBlur}
100
99
  />
@@ -37,9 +37,15 @@
37
37
  let inputId = $derived(id ?? uid);
38
38
  </script>
39
39
 
40
+ <!--
41
+ Textarea reuses the dashboard.css/Input layout (.input + .textarea sit
42
+ together; the `auto` height + min-height: 80px + resize: vertical + 8px
43
+ vertical padding + 1.5 line-height overrides come from .textarea). All
44
+ inline now.
45
+ -->
40
46
  <textarea
41
47
  id={inputId}
42
- class="input textarea {className}"
48
+ class="block min-h-20 w-full resize-y rounded-md border border-border-2 bg-bg-page px-3 py-2 font-[inherit] text-13 leading-[1.5] 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)] {className}"
43
49
  {style}
44
50
  {rows}
45
51
  {placeholder}
@@ -3,8 +3,6 @@
3
3
  </script>
4
4
 
5
5
  <script lang="ts">
6
-
7
-
8
6
  interface Props {
9
7
  label?: string | undefined;
10
8
  value?: string;
@@ -73,13 +71,21 @@
73
71
  field === 0 ? commit(clamped, minutes) : commit(hours, clamped);
74
72
  if (raw.length === 2 && field === 0) setTimeout(() => inputs[1]?.focus(), 0);
75
73
  }
74
+
75
+ const FIELD_CLS =
76
+ 'w-9 border-0 bg-transparent p-0 text-center font-mono text-13 text-text-1 outline-none';
76
77
  </script>
77
78
 
78
- <div class="time-picker-wrapper {className}">
79
+ <div class={className}>
79
80
  {#if label}
80
- <label for="{uid}-h" class="time-picker__label">{label}</label>
81
+ <label for="{uid}-h" class="text-12 text-text-4">{label}</label>
81
82
  {/if}
82
- <div class="time-picker{disabled ? ' time-picker--disabled' : ''}" role="group" aria-label={label ?? 'Time'}>
83
+ <div
84
+ role="group"
85
+ aria-label={label ?? 'Time'}
86
+ data-disabled={disabled ? 'true' : undefined}
87
+ 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"
88
+ >
83
89
  <input
84
90
  bind:this={inputs[0]}
85
91
  id="{uid}-h"
@@ -95,12 +101,12 @@
95
101
  value={pad(hours)}
96
102
  {disabled}
97
103
  maxlength={2}
98
- class="time-picker__field"
104
+ class={FIELD_CLS}
99
105
  oninput={(e) => handleInput(0, e)}
100
106
  onkeydown={(e) => handleKey(0, e)}
101
107
  onfocus={(e) => e.currentTarget.select()}
102
108
  />
103
- <span class="time-picker__sep" aria-hidden="true">:</span>
109
+ <span aria-hidden="true" class="select-none text-13 leading-none text-text-4">:</span>
104
110
  <input
105
111
  bind:this={inputs[1]}
106
112
  type="text"
@@ -115,7 +121,7 @@
115
121
  value={pad(minutes)}
116
122
  {disabled}
117
123
  maxlength={2}
118
- class="time-picker__field"
124
+ class={FIELD_CLS}
119
125
  oninput={(e) => handleInput(1, e)}
120
126
  onkeydown={(e) => handleKey(1, e)}
121
127
  onfocus={(e) => e.currentTarget.select()}
@@ -12,21 +12,19 @@
12
12
 
13
13
  <script lang="ts">
14
14
  import { onMount, onDestroy } from 'svelte';
15
+ import Button from './Button.svelte';
15
16
 
16
17
  interface Props {
17
18
  value?: TimeRangeId;
18
19
  onchange?: (payload: TimeRangeId) => void;
19
20
  }
20
21
 
21
- let { value = '1h',
22
- onchange,
23
- }: Props = $props();
22
+ let { value = '1h', onchange }: Props = $props();
24
23
  const uid = `dash-ui-tr-${++counter}`;
25
24
  const listId = `${uid}-list`;
26
25
 
27
26
  let open = $state(false);
28
27
  let activeIdx = $state(0);
29
- let triggerEl = $state<HTMLButtonElement | undefined>(undefined);
30
28
  let wrapperEl = $state<HTMLDivElement | undefined>(undefined);
31
29
 
32
30
  let selected = $derived(PRESETS.find((p) => p.id === value) ?? PRESETS[0]);
@@ -39,7 +37,6 @@
39
37
  function select(id: TimeRangeId) {
40
38
  onchange?.(id);
41
39
  open = false;
42
- triggerEl?.focus();
43
40
  }
44
41
 
45
42
  function onKeyDown(e: KeyboardEvent) {
@@ -51,7 +48,7 @@
51
48
  }
52
49
  return;
53
50
  }
54
- if (e.key === 'Escape') { e.preventDefault(); open = false; triggerEl?.focus(); }
51
+ if (e.key === 'Escape') { e.preventDefault(); open = false; }
55
52
  else if (e.key === 'ArrowDown') { e.preventDefault(); activeIdx = Math.min(activeIdx + 1, PRESETS.length - 1); }
56
53
  else if (e.key === 'ArrowUp') { e.preventDefault(); activeIdx = Math.max(activeIdx - 1, 0); }
57
54
  else if (e.key === 'Home') { e.preventDefault(); activeIdx = 0; }
@@ -68,31 +65,32 @@
68
65
  onDestroy(() => document.removeEventListener('mousedown', handleOutside));
69
66
  </script>
70
67
 
71
- <div class="time-range-root" bind:this={wrapperEl}>
72
- <button
73
- bind:this={triggerEl}
74
- type="button"
75
- class="btn time-range-trigger"
76
- aria-haspopup="listbox"
77
- aria-expanded={open}
78
- aria-controls={open ? listId : undefined}
79
- onclick={toggle}
80
- onkeydown={onKeyDown}
81
- >
68
+ <div
69
+ class="relative inline-block"
70
+ bind:this={wrapperEl}
71
+ onkeydown={onKeyDown}
72
+ role="presentation"
73
+ >
74
+ <Button variant="ghost" aria-haspopup="listbox" aria-expanded={open} aria-controls={open ? listId : undefined} onclick={toggle}>
82
75
  {selected.short}
83
- <svg class="time-range-chevron" width="10" height="6" viewBox="0 0 10 6" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
76
+ <svg width="10" height="6" viewBox="0 0 10 6" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="shrink-0 transition-transform duration-100 motion-reduce:transition-none {open ? 'rotate-180' : ''}">
84
77
  <path d="M1 1l4 4 4-4" />
85
78
  </svg>
86
- </button>
79
+ </Button>
87
80
  {#if open}
88
- <ul id={listId} role="listbox" aria-label="Time range" class="time-range-list">
81
+ <ul
82
+ id={listId}
83
+ role="listbox"
84
+ aria-label="Time range"
85
+ class="absolute right-0 top-[calc(100%+4px)] z-[9100] m-0 min-w-[152px] list-none rounded-lg border border-border-3 bg-bg-2 p-1 shadow-[0_8px_24px_rgba(0,0,0,0.5)]"
86
+ >
89
87
  {#each PRESETS as p, idx (p.id)}
90
88
  <li
91
89
  role="option"
92
90
  aria-selected={p.id === value}
93
- tabindex="-1"
91
+ tabindex={-1}
94
92
  data-active={idx === activeIdx ? 'true' : undefined}
95
- class="time-range-option"
93
+ class="flex cursor-pointer items-center justify-between gap-2 whitespace-nowrap rounded-[5px] px-2.5 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 aria-selected:text-status-info"
96
94
  onmouseenter={() => { activeIdx = idx; }}
97
95
  onmousedown={(e) => { e.preventDefault(); (() => select(p.id))(); }}
98
96
  >
@@ -15,20 +15,31 @@
15
15
  }
16
16
 
17
17
  let { events, class: className = '' }: Props = $props();
18
-
18
+
19
+ type V = NonNullable<TimelineEvent['variant']>;
20
+ const VARIANT: Record<V, string> = {
21
+ info: 'bg-brand-05',
22
+ success: 'bg-status-success',
23
+ warn: 'bg-status-warning',
24
+ danger: 'bg-status-danger',
25
+ neutral: 'bg-status-neutral',
26
+ };
19
27
  </script>
20
28
 
21
- <ol class="timeline {className}" aria-label="Event timeline">
29
+ <ol class="relative m-0 list-none p-0 {className}" aria-label="Event timeline">
22
30
  {#each events as e (e.id)}
23
- <li class="timeline__item timeline__item--{e.variant ?? 'neutral'}">
24
- <span class="timeline__dot" aria-hidden="true"></span>
25
- <div class="timeline__content">
26
- <div class="timeline__header">
27
- <span class="timeline__title">{e.title}</span>
28
- <time class="timeline__time">{e.time}</time>
31
+ <li class="relative flex gap-3 pb-3 pl-4 last:pb-0 [&:not(:last-child)]:before:absolute [&:not(:last-child)]:before:bottom-0 [&:not(:last-child)]:before:left-[5px] [&:not(:last-child)]:before:top-4 [&:not(:last-child)]:before:w-px [&:not(:last-child)]:before:bg-row-active [&:not(:last-child)]:before:content-['']">
32
+ <span
33
+ class="absolute left-0 top-1.5 inline-block h-2.5 w-2.5 shrink-0 rounded-full {VARIANT[e.variant ?? 'neutral']}"
34
+ aria-hidden="true"
35
+ ></span>
36
+ <div class="min-w-0 flex-1">
37
+ <div class="flex items-baseline justify-between gap-2">
38
+ <span class="font-medium text-text-1">{e.title}</span>
39
+ <time class="shrink-0 text-11 text-text-4 tabular-nums">{e.time}</time>
29
40
  </div>
30
41
  {#if e.description}
31
- <p class="timeline__desc">{e.description}</p>
42
+ <p class="m-0 mt-0.5 text-12 text-text-3">{e.description}</p>
32
43
  {/if}
33
44
  </div>
34
45
  </li>
@@ -30,16 +30,35 @@
30
30
  </script>
31
31
 
32
32
  <script lang="ts">
33
- // store and dismiss are in module scope — accessible in template via $store
33
+ // store + dismiss live in module scope; template uses $store
34
+ // Pre-composed variant classes for Tailwind's static scanner.
35
+ // Backgrounds at 16% (matches dashboard.css), left border solid, text
36
+ // colour the legibility-tuned literal hue.
37
+ const TOAST: Record<ToastVariant, string> = {
38
+ success: 'bg-status-success/16 border-l-status-success text-status-success',
39
+ warn: 'bg-status-warning/16 border-l-status-warning text-status-warning',
40
+ danger: 'bg-status-danger/16 border-l-status-danger text-status-danger',
41
+ info: 'bg-status-info/16 border-l-brand-05 text-status-info',
42
+ };
34
43
  </script>
35
44
 
36
- <div class="toaster" role="log" aria-label="Notifications" aria-live="polite" aria-atomic="false">
45
+ <div
46
+ class="pointer-events-none fixed bottom-6 right-6 z-[9900] flex min-w-[240px] max-w-[360px] flex-col gap-2"
47
+ role="log"
48
+ aria-label="Notifications"
49
+ aria-live="polite"
50
+ aria-atomic="false"
51
+ >
37
52
  {#each $store as item (item.id)}
38
- <div class="toast toast-{item.variant}" role={item.variant === 'danger' ? 'alert' : 'status'}>
39
- <span class="toast-msg">{item.message}</span>
53
+ <div
54
+ class="pointer-events-auto flex items-center gap-2.5 rounded-lg border-l-[3px] px-3.5 py-2.5 text-13 leading-[1.4] [animation:toast-in_150ms_ease] [box-shadow:0_4px_16px_rgba(0,0,0,0.5)]
55
+ {TOAST[item.variant]}"
56
+ role={item.variant === 'danger' ? 'alert' : 'status'}
57
+ >
58
+ <span class="flex-1">{item.message}</span>
40
59
  <button
41
60
  type="button"
42
- class="toast-dismiss icon-btn"
61
+ class="ml-auto inline-flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center rounded-md border-0 bg-transparent text-current hover:bg-row-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
43
62
  onclick={() => dismiss(item.id)}
44
63
  aria-label="Dismiss notification"
45
64
  >
@@ -1,5 +1,4 @@
1
1
  <script lang="ts">
2
-
3
2
  interface Props {
4
3
  on?: boolean;
5
4
  ariaLabel?: string | undefined;
@@ -7,7 +6,10 @@
7
6
  ontoggle?: (payload: boolean) => void;
8
7
  }
9
8
 
10
- let { on = $bindable(false), ariaLabel = undefined, ariaDescribedBy = undefined,
9
+ let {
10
+ on = $bindable(false),
11
+ ariaLabel = undefined,
12
+ ariaDescribedBy = undefined,
11
13
  ontoggle,
12
14
  }: Props = $props();
13
15
  function flip() {
@@ -22,8 +24,12 @@
22
24
  aria-checked={on}
23
25
  aria-label={ariaLabel}
24
26
  aria-describedby={ariaDescribedBy}
25
- class="toggle-track {on ? 'on' : 'off'}"
27
+ class="relative inline-block h-[18px] w-8 shrink-0 cursor-pointer appearance-none rounded-full border-0 p-0 font-[inherit] text-[inherit] transition-colors duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05
28
+ {on ? 'bg-brand-05' : 'bg-text-4'}"
26
29
  onclick={flip}
27
30
  >
28
- <span class="knob"></span>
31
+ <span
32
+ class="absolute top-0.5 h-3.5 w-3.5 rounded-full bg-white transition-[left] duration-150
33
+ {on ? 'left-4' : 'left-0.5'}"
34
+ ></span>
29
35
  </button>
@@ -1,6 +1,4 @@
1
1
  <script lang="ts">
2
-
3
-
4
2
  interface Props {
5
3
  options?: { value: string; label: string; disabled?: boolean }[];
6
4
  value?: string[];
@@ -18,20 +16,32 @@
18
16
  class: klass = '',
19
17
  onchange,
20
18
  }: Props = $props();
19
+
21
20
  function toggle(v: string) {
22
21
  const next = value.includes(v) ? value.filter((x) => x !== v) : [...value, v];
23
22
  value = next;
24
23
  onchange?.(next);
25
24
  }
25
+
26
+ // Pre-composed size strings — Tailwind's static scanner must see literals.
27
+ const SIZE: Record<NonNullable<Props['size']>, string> = {
28
+ sm: 'h-6 px-[9px] text-11',
29
+ md: 'h-7 px-3 text-12',
30
+ };
26
31
  </script>
27
32
 
28
- <div role="group" aria-label={ariaLabel} class="tg-group tg-group--{size}{klass ? ` ${klass}` : ''}">
33
+ <div
34
+ role="group"
35
+ aria-label={ariaLabel}
36
+ class="inline-flex flex-wrap gap-px rounded-lg bg-row-hover p-0.5 {klass}"
37
+ >
29
38
  {#each options as opt}
39
+ {@const on = value.includes(opt.value)}
30
40
  <button
31
41
  type="button"
32
- aria-pressed={value.includes(opt.value)}
42
+ aria-pressed={on}
33
43
  disabled={opt.disabled}
34
- class="tg-btn{value.includes(opt.value) ? ' tg-btn--on' : ''}"
44
+ class="flex-shrink-0 grow-0 inline-flex cursor-pointer select-none items-center justify-center whitespace-nowrap rounded-md border-0 bg-transparent font-medium leading-none text-text-3 transition-[background-color,color] duration-100 hover:bg-row-hover hover:text-text-1 disabled:cursor-not-allowed disabled:opacity-40 focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-brand-05 {SIZE[size]} {on ? 'bg-bg-3 text-text-1 shadow-[0_1px_3px_rgba(0,0,0,0.18)] dark:shadow-[0_1px_3px_rgba(0,0,0,0.4)]' : ''}"
35
45
  onclick={() => toggle(opt.value)}
36
46
  >
37
47
  {opt.label}
@@ -14,13 +14,28 @@
14
14
  label,
15
15
  placement = 'top',
16
16
  class: className = '',
17
- children
17
+ children,
18
18
  }: Props = $props();
19
-
19
+
20
20
  const id = `dash-ui-tooltip-${++counter}`;
21
+
22
+ // Pre-composed placement strings so Tailwind's scanner picks each up.
23
+ // The wrapper carries `group` so the content shows on group-hover or
24
+ // group-focus-within (replaces dashboard.css's `:hover .tooltip-content`).
25
+ const PLACEMENT: Record<NonNullable<Props['placement']>, string> = {
26
+ top: 'bottom-[calc(100%+6px)] left-1/2 -translate-x-1/2',
27
+ bottom: 'top-[calc(100%+6px)] left-1/2 -translate-x-1/2',
28
+ left: 'right-[calc(100%+6px)] top-1/2 -translate-y-1/2',
29
+ right: 'left-[calc(100%+6px)] top-1/2 -translate-y-1/2',
30
+ };
21
31
  </script>
22
32
 
23
- <span class="tooltip-wrapper tooltip-{placement} {className}">
24
- <span class="tooltip-trigger" aria-describedby={id}>{@render children?.()}</span>
25
- <span {id} role="tooltip" class="tooltip-content">{label}</span>
33
+ <span class="group relative inline-flex {className}">
34
+ <span class="inline-flex" aria-describedby={id}>{@render children?.()}</span>
35
+ <span
36
+ {id}
37
+ role="tooltip"
38
+ class="pointer-events-none absolute z-[9999] whitespace-nowrap rounded border border-border-1 bg-bg-2 px-2 py-1 text-[11px] leading-[1.3] text-text-2 opacity-0 transition-opacity duration-100 group-hover:opacity-100 group-focus-within:opacity-100 motion-reduce:transition-none
39
+ {PLACEMENT[placement]}"
40
+ >{label}</span>
26
41
  </span>