@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
@@ -0,0 +1,26 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ class?: string;
4
+ children?: import('svelte').Snippet;
5
+ }
6
+
7
+ let { class: className = '', children }: Props = $props();
8
+ </script>
9
+
10
+ <!--
11
+ Longer-form prose paragraph - settings descriptions, onboarding copy,
12
+ empty-state explanations, anywhere the default 14 px body feels too
13
+ cramped for multi-sentence reading. Renders <p class="dash-ui-body-lg">
14
+ so the .dash-ui-body-lg rule in @w5-ui/tokens/tokens.css applies the
15
+ canonical 16 px / regular weight, 1.55 line-height, and --text-2
16
+ colour pair. Stops userland reaching for a one-off font-size: 16px;
17
+ line-height: 1.6; duo on every settings description. Typed equivalent
18
+ of <p class="dash-ui-body-lg"> (the userland-facing semantic class
19
+ documented in Foundations / Type). Reach for <BodyLg> when the cliff
20
+ between 1.25 (heading) and 1.4 (default body) feels too dense for a
21
+ 2-3 sentence paragraph; keep the default .dash-ui-body (14 / 1.4) for
22
+ table cells, table footnotes, and single-line labels.
23
+ -->
24
+ <p class="dash-ui-body-lg {className}">
25
+ {@render children?.()}
26
+ </p>
@@ -1,34 +1,41 @@
1
1
  <script module lang="ts">
2
- export interface BreadcrumbItem { label: string; href?: string; }
2
+ export interface BreadcrumbItem { label: string; href?: string }
3
3
  </script>
4
4
 
5
5
  <script lang="ts">
6
-
7
6
  interface Props {
8
7
  items?: BreadcrumbItem[];
9
8
  class?: string;
10
9
  onnavigate?: (payload: number) => void;
11
10
  }
12
11
 
13
- let { items = [], class: className = '',
14
- onnavigate,
15
- }: Props = $props();
12
+ let { items = [], class: className = '', onnavigate }: Props = $props();
16
13
  </script>
17
14
 
18
- <nav aria-label="Breadcrumb" class="breadcrumb {className}">
19
- <ol class="breadcrumb__list">
15
+ <nav aria-label="Breadcrumb" class={className}>
16
+ <ol class="m-0 flex flex-wrap items-center gap-x-1.5 gap-y-0.5 p-0 text-12 [list-style:none]">
20
17
  {#each items as item, i}
21
18
  {@const isLast = i === items.length - 1}
22
- <li class="breadcrumb__item">
19
+ <li class="flex items-center gap-1.5">
23
20
  {#if isLast}
24
- <span aria-current="page" class="breadcrumb__current">{item.label}</span>
21
+ <span
22
+ aria-current="page"
23
+ class="font-medium text-text-2"
24
+ >{item.label}</span>
25
25
  {:else if item.href}
26
- <a href={item.href} class="breadcrumb__link">{item.label}</a>
26
+ <a
27
+ href={item.href}
28
+ class="cursor-pointer text-text-4 no-underline transition-colors duration-100 hover:text-text-2 focus-visible:rounded-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
29
+ >{item.label}</a>
27
30
  {:else}
28
- <button type="button" class="breadcrumb__link" onclick={() => onnavigate?.(i)}>{item.label}</button>
31
+ <button
32
+ type="button"
33
+ class="cursor-pointer border-0 bg-transparent p-0 font-[inherit] text-text-4 transition-colors duration-100 hover:text-text-2 focus-visible:rounded-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
34
+ onclick={() => onnavigate?.(i)}
35
+ >{item.label}</button>
29
36
  {/if}
30
37
  {#if !isLast}
31
- <span aria-hidden="true" class="breadcrumb__sep">/</span>
38
+ <span class="select-none text-border-3" aria-hidden="true">/</span>
32
39
  {/if}
33
40
  </li>
34
41
  {/each}
@@ -12,6 +12,14 @@
12
12
  class?: string;
13
13
  style?: string;
14
14
  'aria-label'?: string | undefined;
15
+ /** ARIA popup trigger: `'dialog'` for popover-style buttons that open
16
+ * an inline calendar / picker; mirrors the React `<Button>` API and the
17
+ * actual prop usage in DatePicker / DateRangePicker / TimeRange. */
18
+ 'aria-haspopup'?: 'dialog' | 'menu' | 'listbox' | 'tree' | 'grid' | boolean | undefined;
19
+ /** Expanded state for popup triggers. */
20
+ 'aria-expanded'?: boolean | undefined;
21
+ /** ID of the controlled popup/dialog. */
22
+ 'aria-controls'?: string | undefined;
15
23
  onclick?: (event: MouseEvent) => void;
16
24
  children?: Snippet;
17
25
  }
@@ -26,17 +34,38 @@
26
34
  class: className = '',
27
35
  style = '',
28
36
  'aria-label': ariaLabel = undefined,
37
+ 'aria-haspopup': ariaHasPopup = undefined,
38
+ 'aria-expanded': ariaExpanded = undefined,
39
+ 'aria-controls': ariaControls = undefined,
29
40
  onclick,
30
41
  children,
31
42
  }: Props = $props();
43
+
44
+ // Pre-compose variant strings so Tailwind's static scanner picks them up.
45
+ // Motif-aware:
46
+ // primary: brand-blue, always white text (works against blue in both motifs).
47
+ // ghost: transparent surface with motif-aware text/border + hover.
48
+ // danger: semantic status-danger text + faded border.
49
+ const VARIANT: Record<NonNullable<Props['variant']>, string> = {
50
+ primary: 'bg-brand-05 text-white hover:bg-brand-06 border-transparent',
51
+ ghost:
52
+ 'bg-transparent text-text-2 border-border-2 hover:bg-row-hover hover:text-text-1',
53
+ danger: 'bg-transparent text-status-danger border-status-danger/30',
54
+ };
32
55
  </script>
33
56
 
34
57
  <button
35
58
  {type}
36
59
  {title}
37
60
  aria-label={ariaLabel}
61
+ aria-haspopup={ariaHasPopup}
62
+ aria-expanded={ariaExpanded}
63
+ aria-controls={ariaControls}
38
64
  {onclick}
39
- class="btn btn-{variant} {iconOnly ? 'btn-icon' : ''} {className}"
65
+ class="inline-flex h-[30px] cursor-pointer items-center gap-1.5 whitespace-nowrap rounded-md border px-3 text-13 font-medium transition-all duration-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05 disabled:cursor-not-allowed disabled:opacity-55
66
+ {VARIANT[variant]}
67
+ {iconOnly ? 'w-[30px] justify-center p-0' : ''}
68
+ {className}"
40
69
  {style}
41
70
  disabled={disabled || loading}
42
71
  aria-busy={loading ? true : undefined}
@@ -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;
@@ -97,13 +95,23 @@
97
95
  setTimeout(() => focusAt(4), 0);
98
96
  }
99
97
  }
98
+
99
+ const OCTET_CLS =
100
+ 'w-9 border-0 bg-transparent p-0 text-center font-mono text-13 text-text-1 outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:m-0 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:m-0 [&::-webkit-outer-spin-button]:appearance-none';
101
+ const PREFIX_CLS =
102
+ 'w-7 border-0 bg-transparent p-0 text-center font-mono text-13 text-text-1 outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:m-0 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:m-0 [&::-webkit-outer-spin-button]:appearance-none';
100
103
  </script>
101
104
 
102
- <div class="cidr-input-wrapper {className}">
105
+ <div class={className}>
103
106
  {#if label}
104
- <label for="{uid}-0" class="cidr-input__label">{label}</label>
107
+ <label for="{uid}-0" class="text-12 text-text-4">{label}</label>
105
108
  {/if}
106
- <div class="cidr-input{disabled ? ' cidr-input--disabled' : ''}" role="group" aria-label={label ?? 'CIDR'}>
109
+ <div
110
+ role="group"
111
+ aria-label={label ?? 'CIDR'}
112
+ data-disabled={disabled ? 'true' : undefined}
113
+ 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"
114
+ >
107
115
  {#each [0, 1, 2, 3] as i (i)}
108
116
  <input
109
117
  bind:this={inputs[i]}
@@ -112,20 +120,20 @@
112
120
  inputmode="numeric"
113
121
  pattern="[0-9]*"
114
122
  role="spinbutton"
115
- aria-label="Octet {i + 1} of 4"
123
+ aria-label={`Octet ${i + 1} of 4`}
116
124
  aria-valuenow={fields[i] === '' ? 0 : Number(fields[i])}
117
125
  aria-valuemin={0}
118
126
  aria-valuemax={255}
119
127
  value={fields[i]}
120
128
  {disabled}
121
129
  maxlength={3}
122
- class="cidr-input__octet"
130
+ class={OCTET_CLS}
123
131
  oninput={(e) => commitOctet(i, e.currentTarget.value)}
124
132
  onkeydown={(e) => handleKey(i, e)}
125
133
  onpaste={handlePaste}
126
134
  onfocus={(e) => e.currentTarget.select()}
127
135
  />
128
- <span class="cidr-input__dot" aria-hidden="true">{i < 3 ? '.' : '/'}</span>
136
+ <span aria-hidden="true" class="select-none text-13 leading-none text-text-4">{i < 3 ? '.' : '/'}</span>
129
137
  {/each}
130
138
  <input
131
139
  bind:this={inputs[4]}
@@ -140,7 +148,7 @@
140
148
  value={fields[4]}
141
149
  {disabled}
142
150
  maxlength={2}
143
- class="cidr-input__prefix"
151
+ class={PREFIX_CLS}
144
152
  oninput={(e) => commitPrefix(e.currentTarget.value)}
145
153
  onkeydown={(e) => handleKey(4, e)}
146
154
  onpaste={handlePaste}
@@ -10,33 +10,54 @@
10
10
  }
11
11
 
12
12
  let { variant = 'info', title = undefined, children }: Props = $props();
13
+
14
+ // Per-variant chrome ramp (bg + left border + body text colour) and a
15
+ // separate title colour. Pre-composed so Tailwind v4's static scanner
16
+ // picks them up.
17
+ const VARIANT: Record<CalloutVariant, string> = {
18
+ info: 'bg-brand-05/[0.08] border-l-brand-05 text-status-info',
19
+ success: 'bg-status-success/[0.08] border-l-status-success text-status-success',
20
+ warn: 'bg-status-warning/[0.08] border-l-status-warning text-status-warning',
21
+ danger: 'bg-status-danger/[0.08] border-l-status-danger text-status-danger',
22
+ tip: 'bg-brand-04/[0.08] border-l-brand-04 text-status-info',
23
+ };
24
+ const TITLE_COLOUR: Record<CalloutVariant, string> = {
25
+ info: 'text-status-info',
26
+ success: 'text-status-success',
27
+ warn: 'text-status-warning',
28
+ danger: 'text-status-danger',
29
+ tip: 'text-status-info',
30
+ };
13
31
  </script>
14
32
 
15
- <div class="callout callout--{variant}" role="note">
33
+ <div
34
+ role="note"
35
+ class="flex gap-3 rounded-lg border-l-[3px] px-4 py-3 text-13 leading-[1.5] {VARIANT[variant]}"
36
+ >
16
37
  {#if variant === 'info'}
17
- <svg class="callout-icon" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
38
+ <svg class="mt-px h-4 w-4 shrink-0" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
18
39
  <path fill-rule="evenodd" d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1ZM7.25 5.5a.75.75 0 1 1 1.5 0 .75.75 0 0 1-1.5 0ZM7 7.5h2v4H7v-4Z" />
19
40
  </svg>
20
41
  {:else if variant === 'success'}
21
- <svg class="callout-icon" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
42
+ <svg class="mt-px h-4 w-4 shrink-0" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
22
43
  <path fill-rule="evenodd" d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1Zm3.78 5.47-4.25 4.5a.75.75 0 0 1-1.06.03L4.72 9.22a.75.75 0 1 1 1.06-1.06l1.22 1.22 3.72-3.94a.75.75 0 1 1 1.06 1.03Z" />
23
44
  </svg>
24
45
  {:else if variant === 'warn'}
25
- <svg class="callout-icon" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
46
+ <svg class="mt-px h-4 w-4 shrink-0" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
26
47
  <path fill-rule="evenodd" d="M8.9 1.6a1 1 0 0 0-1.8 0L.6 13a1 1 0 0 0 .9 1.5h13a1 1 0 0 0 .9-1.5L8.9 1.6ZM7.25 6.5a.75.75 0 1 1 1.5 0v3a.75.75 0 0 1-1.5 0v-3Zm.75 6a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Z" />
27
48
  </svg>
28
49
  {:else if variant === 'danger'}
29
- <svg class="callout-icon" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
50
+ <svg class="mt-px h-4 w-4 shrink-0" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
30
51
  <path fill-rule="evenodd" d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1ZM5.97 5.97a.75.75 0 0 1 1.06 0L8 6.94l.97-.97a.75.75 0 1 1 1.06 1.06L9.06 8l.97.97a.75.75 0 0 1-1.06 1.06L8 9.06l-.97.97a.75.75 0 0 1-1.06-1.06L6.94 8l-.97-.97a.75.75 0 0 1 0-1.06Z" />
31
52
  </svg>
32
53
  {:else}
33
- <svg class="callout-icon" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
54
+ <svg class="mt-px h-4 w-4 shrink-0" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
34
55
  <path d="M8 1a5 5 0 0 1 3 9.01V11H5v-.99A5 5 0 0 1 8 1Zm-1 12h2v1.5a.5.5 0 0 1-.5.5h-1a.5.5 0 0 1-.5-.5V13Z" />
35
56
  </svg>
36
57
  {/if}
37
- <div class="callout-body">
58
+ <div class="flex-1">
38
59
  {#if title}
39
- <div class="callout-title">{title}</div>
60
+ <div class="mb-1 font-semibold {TITLE_COLOUR[variant]}">{title}</div>
40
61
  {/if}
41
62
  <div>{@render children?.()}</div>
42
63
  </div>
@@ -0,0 +1,26 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ class?: string;
4
+ children?: import('svelte').Snippet;
5
+ }
6
+
7
+ let { class: className = '', children }: Props = $props();
8
+ </script>
9
+
10
+ <!--
11
+ Small helper / metadata text - "last updated 5 min ago", table footnote,
12
+ timestamp under a chart, byline beneath a section heading. Renders
13
+ <span class="dash-ui-caption"> so the .dash-ui-caption rule in
14
+ @w5-ui/tokens/tokens.css applies the canonical 12 px / regular weight,
15
+ 1.25 line-height, and the muted --text-3 colour. Stops userland reaching
16
+ for a one-off font-size: 12px; color: #a4a7b5; trio on every caption.
17
+ Typed equivalent of <span class="dash-ui-caption"> (the userland-facing
18
+ semantic class documented in Foundations / Type). Use <Caption> for
19
+ non-essential metadata only: on --depthBg-3 (popovers, drawers) the
20
+ --text-3 contrast slides toward 4.0:1, so keep primary copy on
21
+ .dash-ui-body / --text-2. Pair with <Muted> when you want the same
22
+ colour at the default 14 px body size.
23
+ -->
24
+ <span class="dash-ui-caption {className}">
25
+ {@render children?.()}
26
+ </span>
@@ -1,5 +1,4 @@
1
1
  <script lang="ts">
2
-
3
2
  interface Props {
4
3
  span?: number | undefined;
5
4
  class?: string;
@@ -11,11 +10,20 @@
11
10
  span = undefined,
12
11
  class: className = '',
13
12
  style = '',
14
- children
13
+ children,
15
14
  }: Props = $props();
16
15
  let spanStyle = $derived(span !== undefined ? `grid-column: span ${span};` : '');
17
16
  </script>
18
17
 
19
- <div class="card {className}" style="{spanStyle} {style}">
18
+ <!--
19
+ Motif-aware card surface: bg via `var(--depthBg-1)` (= `#141415` dark,
20
+ `#ffffff` light) and border via `var(--border-1)`. The `h3` child
21
+ styling lives in the consumer slot — Tailwind utilities at the call
22
+ site, not bundled here.
23
+ -->
24
+ <div
25
+ class="flex min-w-0 flex-col gap-2 rounded-[8px] border border-border-1 bg-bg-1 px-4 py-3.5 {className}"
26
+ style="{spanStyle} {style}"
27
+ >
20
28
  {@render children?.()}
21
29
  </div>
@@ -0,0 +1,20 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ href?: string;
4
+ class?: string;
5
+ children?: import('svelte').Snippet;
6
+ }
7
+
8
+ let { href = undefined, class: className = '', children }: Props = $props();
9
+ </script>
10
+
11
+ <!--
12
+ Trailing "View all" / "More →" affordance inside a <Card> title row. Renders
13
+ an <a class="more"> so the `.card h3 .more` rule in
14
+ @w5-ui/tokens/dashboard.css styles it (muted colour, pointer cursor) and the
15
+ parent <h3> flex pushes it to the right edge of the title. Pair with <Card>
16
+ and a plain <h3> for the canonical card title pattern.
17
+ -->
18
+ <a {href} class="more {className}">
19
+ {@render children?.()}
20
+ </a>
@@ -0,0 +1,20 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ class?: string;
4
+ children?: import('svelte').Snippet;
5
+ }
6
+
7
+ let { class: className = '', children }: Props = $props();
8
+ </script>
9
+
10
+ <!--
11
+ Title row inside a <Card> tile. Renders a plain <h3> so the `.card h3` rule
12
+ in @w5-ui/tokens/dashboard.css sets the canonical 12 px size, weight 500,
13
+ --text-3 (muted) colour, and flex layout (title on the left, any trailing
14
+ <Pill> / <CardMore> pushed to the right edge). Stops userland reaching for
15
+ an <h2> or a custom <div class="title"> that would skip the rule and read
16
+ broken inside the tile. Pair with <CardMore> for a "View all →" affordance.
17
+ -->
18
+ <h3 class={className}>
19
+ {@render children?.()}
20
+ </h3>
@@ -6,7 +6,6 @@
6
6
  }
7
7
 
8
8
  let { slides = [], label, class: klass = '' }: Props = $props();
9
-
10
9
 
11
10
  let index = $state(0);
12
11
 
@@ -19,49 +18,57 @@
19
18
  </script>
20
19
 
21
20
  <section
22
- class="carousel {klass}"
21
+ class="relative overflow-hidden rounded-lg border border-border-1 bg-divider {klass}"
23
22
  aria-roledescription="carousel"
24
23
  aria-label={label}
25
24
  >
26
- <div class="carousel-track" aria-live="polite" aria-atomic="false">
25
+ <div class="relative min-h-[140px]" aria-live="polite" aria-atomic="false">
27
26
  {#each slides as slide, i (slide.id)}
28
27
  <div
29
28
  role="group"
30
29
  aria-roledescription="slide"
31
- aria-label="{i + 1} of {slides.length}"
32
- class="carousel-slide{i === index ? ' is-active' : ''}"
30
+ aria-label={`${i + 1} of ${slides.length}`}
31
+ data-active={i === index ? 'true' : undefined}
32
+ class="absolute inset-0 flex items-stretch gap-3 p-4 opacity-0 transition-opacity duration-200 data-[active=true]:opacity-100 data-[active=true]:relative"
33
33
  aria-hidden={i !== index ? true : undefined}
34
34
  >
35
35
  {#if slide.color}
36
- <div class="carousel-slide-accent" style="background:{slide.color}" aria-hidden="true"></div>
36
+ <div class="w-1 shrink-0 rounded" style="background:{slide.color}" aria-hidden="true"></div>
37
37
  {/if}
38
- <div class="carousel-slide-body">
39
- <div class="carousel-slide-title">{slide.title}</div>
38
+ <div class="min-w-0 flex-1">
39
+ <div class="text-15 font-semibold text-text-1">{slide.title}</div>
40
40
  {#if slide.description}
41
- <div class="carousel-slide-desc">{slide.description}</div>
41
+ <div class="mt-1 text-13 text-text-3">{slide.description}</div>
42
42
  {/if}
43
43
  </div>
44
44
  </div>
45
45
  {/each}
46
46
  </div>
47
- <div class="carousel-controls">
48
- <button type="button" class="carousel-btn" aria-label="Previous slide" onclick={prev}
49
- >&#8249;</button
50
- >
51
- <div role="tablist" aria-label="Choose slide to display" class="carousel-indicators">
47
+ <div class="flex items-center justify-between border-t border-border-1 bg-bg-page px-2 py-1.5">
48
+ <button
49
+ type="button"
50
+ aria-label="Previous slide"
51
+ 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"
52
+ onclick={prev}
53
+ >&#8249;</button>
54
+ <div role="tablist" aria-label="Choose slide to display" class="flex items-center gap-1.5">
52
55
  {#each slides as slide, i (slide.id)}
53
56
  <button
54
57
  type="button"
55
58
  role="tab"
56
59
  aria-selected={i === index}
57
- aria-label="Slide {i + 1}: {slide.title}"
58
- class="carousel-dot{i === index ? ' is-active' : ''}"
60
+ aria-label={`Slide ${i + 1}: ${slide.title}`}
61
+ data-active={i === index ? 'true' : undefined}
62
+ class="h-1.5 w-1.5 cursor-pointer rounded-full border-0 bg-border-3 transition-all duration-150 hover:bg-text-3 data-[active=true]:w-4 data-[active=true]:bg-brand-05"
59
63
  onclick={() => (index = i)}
60
- ></button>
64
+ ></button>
61
65
  {/each}
62
66
  </div>
63
- <button type="button" class="carousel-btn" aria-label="Next slide" onclick={next}
64
- >&#8250;</button
65
- >
67
+ <button
68
+ type="button"
69
+ aria-label="Next slide"
70
+ 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"
71
+ onclick={next}
72
+ >&#8250;</button>
66
73
  </div>
67
74
  </section>
@@ -38,16 +38,36 @@
38
38
  el.indeterminate = indeterminate;
39
39
  }
40
40
  });
41
+
42
+ // The ✓ + indeterminate dash glyphs ship as inline SVG data URLs so the
43
+ // checkbox stays a single `<input type="checkbox">` (no wrapping span,
44
+ // native AT behaviour, native focus ring), with the marks drawn by
45
+ // `background-image`. Tailwind v4 supports arbitrary-value `bg-[url(...)]`
46
+ // but the SVG payload would have to live inline in a class string; we
47
+ // keep these as `var(...)` references via a tiny inline style so the
48
+ // class string stays scannable + the icons stay editable.
49
+ const CHECK_SVG = `url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3 8.5l3 3 6.5-6' stroke='%23fff' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round' fill='none'/%3E%3C/svg%3E")`;
50
+ const DASH_SVG = `url("data:image/svg+xml,%3Csvg viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4 8h8' stroke='%23fff' stroke-width='2' stroke-linecap='round' fill='none'/%3E%3C/svg%3E")`;
51
+
52
+ // Compose the background image conditionally for the input style.
53
+ let bgImage = $derived(indeterminate ? DASH_SVG : checked ? CHECK_SVG : 'none');
54
+
55
+ const INPUT_CLS =
56
+ 'relative inline-block h-4 w-4 min-w-4 cursor-pointer appearance-none rounded-sm border-[1.5px] border-border-3 bg-bg-1 align-middle bg-[length:100%_100%] bg-center bg-no-repeat transition-[border-color,background-color] duration-100 hover:border-text-3 checked:border-brand-05 checked:bg-brand-05 indeterminate:border-brand-05 indeterminate:bg-brand-05 disabled:cursor-not-allowed disabled:opacity-40 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05';
41
57
  </script>
42
58
 
43
59
  {#if label}
44
- <label class="checkbox-label" for={inputId}>
60
+ <label
61
+ class="inline-flex cursor-pointer select-none items-center gap-2 text-13 text-text-2 has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-40"
62
+ for={inputId}
63
+ >
45
64
  <input
46
65
  bind:this={el}
47
66
  type="checkbox"
48
67
  id={inputId}
49
68
  {disabled}
50
- class="checkbox"
69
+ class={INPUT_CLS}
70
+ style="background-image: {bgImage};"
51
71
  {onchange}
52
72
  {...rest}
53
73
  />
@@ -59,7 +79,8 @@
59
79
  type="checkbox"
60
80
  id={inputId}
61
81
  {disabled}
62
- class="checkbox"
82
+ class={INPUT_CLS}
83
+ style="background-image: {bgImage};"
63
84
  {onchange}
64
85
  {...rest}
65
86
  />
@@ -0,0 +1,24 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ class?: string;
4
+ children?: import('svelte').Snippet;
5
+ }
6
+
7
+ let { class: className = '', children }: Props = $props();
8
+ </script>
9
+
10
+ <!--
11
+ Inline code snippet for key names, config paths, environment variables
12
+ and other short literal strings that read inside body copy. Renders a
13
+ <code class="dash-ui-code"> so the .dash-ui-code rule in
14
+ @w5-ui/tokens/css applies the canonical JetBrains Mono family, 12 px
15
+ size, muted text colour, and the inset --depthBg-2 chip background
16
+ with a 1 px border. Stops userland reaching for bare <code> (which
17
+ falls back to a serif monospace on #fff) or inline font-family /
18
+ background overrides on every snippet. Typed equivalent of
19
+ <code class="dash-ui-code"> (reach for <CodeBlock> when the snippet
20
+ wraps to multiple lines; this primitive is for the inline case).
21
+ -->
22
+ <code class="dash-ui-code {className}">
23
+ {@render children?.()}
24
+ </code>
@@ -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>