@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,6 +8,8 @@
8
8
 
9
9
  <script lang="ts">
10
10
  import CloseIcon from '../icons/CloseIcon.svelte';
11
+ import IconButton from './IconButton.svelte';
12
+
11
13
  interface Props {
12
14
  variant?: BannerVariant;
13
15
  title?: string | undefined;
@@ -21,28 +23,55 @@
21
23
  title = undefined,
22
24
  action = undefined,
23
25
  onDismiss = undefined,
24
- children
26
+ children,
25
27
  }: Props = $props();
26
28
 
27
29
  let isUrgent = $derived(variant === 'danger' || variant === 'warn');
30
+
31
+ // Pre-composed variant pairs (banner + title tint). Backgrounds at 10%,
32
+ // top border solid in the base colour, both matching dashboard.css.
33
+ const BANNER: Record<BannerVariant, string> = {
34
+ info: 'bg-status-info/10 border-t-brand-05 text-status-info',
35
+ success: 'bg-status-success/10 border-t-status-success text-status-success',
36
+ warn: 'bg-status-warning/10 border-t-status-warning text-status-warning',
37
+ danger: 'bg-status-danger/10 border-t-status-danger text-status-danger',
38
+ };
39
+ const TITLE: Record<BannerVariant, string> = {
40
+ info: 'text-status-info',
41
+ success: 'text-status-success',
42
+ warn: 'text-status-warning',
43
+ danger: 'text-status-danger',
44
+ };
28
45
  </script>
29
46
 
30
47
  <div
31
- class="banner banner--{variant}"
48
+ class="box-border flex w-full items-center gap-2.5 border-t-[3px] px-4 py-2.5 text-13 leading-[1.4]
49
+ {BANNER[variant]}"
32
50
  role={isUrgent ? 'alert' : 'status'}
33
51
  aria-live={isUrgent ? 'assertive' : 'polite'}
34
52
  aria-atomic="true"
35
53
  >
36
- <div class="banner__body">
37
- {#if title}<span class="banner__title">{title}</span>{/if}
54
+ <div class="flex flex-1 flex-wrap items-baseline gap-x-2 gap-y-0.5">
55
+ {#if title}
56
+ <span class="mr-0.5 font-semibold {TITLE[variant]}">{title}</span>
57
+ {/if}
38
58
  <span>{@render children?.()}</span>
39
59
  </div>
40
60
  {#if action}
41
- <button type="button" class="banner__action" onclick={action.onClick}>{action.label}</button>
61
+ <button
62
+ type="button"
63
+ class="h-6 shrink-0 cursor-pointer rounded border border-current bg-transparent px-2.5 text-12 text-[inherit] opacity-85 hover:opacity-100"
64
+ onclick={action.onClick}
65
+ >{action.label}</button>
42
66
  {/if}
43
67
  {#if onDismiss}
44
- <button type="button" class="banner__dismiss icon-btn" onclick={onDismiss} aria-label="Dismiss banner">
68
+ <IconButton
69
+ title="Dismiss banner"
70
+ onclick={onDismiss}
71
+ class="ml-1 h-6 w-6 shrink-0"
72
+ aria-label="Dismiss banner"
73
+ >
45
74
  <CloseIcon />
46
- </button>
75
+ </IconButton>
47
76
  {/if}
48
77
  </div>
@@ -0,0 +1,29 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ class?: string;
4
+ children?: import('svelte').Snippet;
5
+ }
6
+
7
+ let { class: className = '', children }: Props = $props();
8
+ </script>
9
+
10
+ <!--
11
+ Default body paragraph - the 14 px / regular / 1.4 / --text-2 rhythm
12
+ that runs across table cells, paragraphs, and most user-readable copy
13
+ in the dashboard. Renders <p class="dash-ui-body"> so the
14
+ .dash-ui-body rule in @w5-ui/tokens/tokens.css pins the canonical
15
+ size, weight, line-height, and colour. Stops userland reaching for a
16
+ one-off font-size: 14px; line-height: 1.4; duo (or worse, leaving the
17
+ 16 px browser default in place and getting the #1 documented spacing
18
+ drift: "page text looks too big vs the reference dashboard"). Typed
19
+ equivalent of <p class="dash-ui-body"> (the userland-facing semantic
20
+ class documented in Foundations / Type). Body copy is "almost always
21
+ inherited, almost never overridden" inside .card / .table-shell /
22
+ page chrome - reach for <Body> when wrapping standalone prose in
23
+ userland (empty states, onboarding paragraphs, info banners) or when
24
+ a parent stylesheet has reset the default size and you need to opt
25
+ back in explicitly. For 16 px settings descriptions prefer <BodyLg>.
26
+ -->
27
+ <p class="dash-ui-body {className}">
28
+ {@render children?.()}
29
+ </p>
@@ -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>