@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
package/README.md CHANGED
@@ -1,8 +1,120 @@
1
1
  # @w5-ui/svelte
2
2
 
3
- Svelte component library for the Dash UI design system. Mirrors `@w5-ui/react` 1:1.
3
+ Svelte component library for the Dash UI design system. Mirrors `@w5-ui/react` 1:1. Pairs with
4
+ `@w5-ui/tokens` for design tokens and `@w5-ui/assets` for brand SVGs.
4
5
 
5
- ```ts
6
- import '@w5-ui/svelte/styles.css'
7
- import { Card, Pill, Button, Topbar, Sidebar } from '@w5-ui/svelte'
6
+ ```bash
7
+ yarn add @w5-ui/svelte
8
8
  ```
9
+
10
+ ## Quick start
11
+
12
+ ```svelte
13
+ <script lang="ts">
14
+ import '@w5-ui/svelte/styles.css' // ships @w5-ui/tokens/dashboard.css
15
+ import { AppLayout, Topbar, Sidebar, PageHeader, Card, Stat, Pill } from '@w5-ui/svelte'
16
+ </script>
17
+
18
+ <AppLayout pageLabel="Dashboard">
19
+ <Topbar slot="topbar" siteName="HQ" activeApp="system" />
20
+ <Sidebar slot="sidebar" sections={SECTIONS} activeId={page} on:change={(e) => (page = e.detail)} />
21
+
22
+ <PageHeader title="Dashboard" />
23
+ <div class="grid">
24
+ <Stat span={4} label="Internet" value="847" unit="Mbps" />
25
+ <Stat span={4} label="Clients" value="124" />
26
+ <Stat span={4} label="Throughput" value="12.4" unit="Gbps" />
27
+ <Card span={8}>
28
+ <h3>
29
+ Traffic <Pill variant="success">healthy</Pill>
30
+ </h3>
31
+ </Card>
32
+ <Card span={4}>
33
+ <h3>Top clients</h3>
34
+ </Card>
35
+ </div>
36
+ </AppLayout>
37
+ ```
38
+
39
+ That single skeleton (`AppLayout` then `PageHeader` then `<div class="grid">` then
40
+ `<Card span={n}>`) carries every padding, gap, and column value the reference dashboards use.
41
+ Reproduce it verbatim and spacing falls into place automatically.
42
+
43
+ ## The contract (read this before styling anything)
44
+
45
+ The reference dashboards (`apps/dashboard-svelte`, rendered in Storybook under **Dashboard**) all
46
+ share one skeleton. Userland looking different almost always comes down to one of these:
47
+
48
+ | Don't | Do |
49
+ | -------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
50
+ | Wrap `.grid` in your own `<div style="padding: ...">` | Render `.grid` as a direct child of `<main>`. It already pads 16px x 24px. |
51
+ | `display: flex; gap: ...` on the page root | Render `<Banner>`, `<PageHeader>`, `.grid` as siblings. They own their spacing. |
52
+ | Re-implement `.card` with a `<div>` and a custom border-radius | Use `<Card>` or `<Stat>`. The radius, padding, and `<h3>` rhythm ship together. |
53
+ | `<h3>` outside a `<Card>` | Keep titles inside `<Card>`. The `.card h3` selector sizes and colours them. |
54
+ | `<span style="color: green">online</span>` | Use `<Pill variant="success">online</Pill>` (also `<Badge>`, `<StatusIndicator>`). |
55
+ | Mix `span={5}` and `span={7}` | Use 4 / 6 / 8 / 12 so rows divide cleanly into the 12-column grid. |
56
+ | Skip `AppLayout` and roll your own `.app` grid | Use `AppLayout` once at the root. `.app`, `.workspace`, and `<main>` line up. |
57
+ | Hand-pick hex colours | Use tokens via the `data-motif` CSS variables. See `@w5-ui/tokens`. |
58
+
59
+ ## Grid math
60
+
61
+ `.grid` is `repeat(12, 1fr)` with a 12px gap and 16px x 24px outer padding.
62
+
63
+ | Tile width | `span` | Typical use |
64
+ | ---------- | ------ | ------------------------------------ |
65
+ | Third | `4` | Single-stat card, donut |
66
+ | Half | `6` | Two-column comparison |
67
+ | Two-thirds | `8` | Wide table, line chart |
68
+ | Full | `12` | Time series, sankey, page-wide table |
69
+
70
+ 4+4+4, 8+4, 6+6, and 12 all snap cleanly. Avoid 5 and 7. They leave dead pixels and force the next
71
+ row to wrap unevenly.
72
+
73
+ ## What lives where
74
+
75
+ | Package | Description |
76
+ | --------------- | -------------------------------------------------------- |
77
+ | `@w5-ui/svelte` | ~170 Svelte components (this package) |
78
+ | `@w5-ui/react` | The same component surface for React |
79
+ | `@w5-ui/tokens` | CSS variables (`data-motif="dark"` / `"light"`) + JS map |
80
+ | `@w5-ui/assets` | Brand mark, wordmark, app-rail SVGs |
81
+
82
+ `@w5-ui/svelte/styles.css` re-exports `@w5-ui/tokens/dashboard.css`, so importing it once is enough
83
+ for tokens, the `.app` / `.workspace` / `.grid` / `.card` classes, and component styles.
84
+
85
+ ## Themes
86
+
87
+ Two motifs ship in tokens:
88
+
89
+ ```html
90
+ <html data-motif="dark">
91
+ <!-- the default -->
92
+ </html>
93
+ <html data-motif="light"></html>
94
+ ```
95
+
96
+ Set the attribute on `<html>` (or any ancestor of the app root) to switch.
97
+
98
+ ## CSS hooks you can rely on
99
+
100
+ Stable class names that ship from `@w5-ui/tokens/dashboard.css`:
101
+
102
+ - `.app`, `.workspace`, `.content`: outer chrome, wired up by `AppLayout`
103
+ - `.grid`: 12-column page grid with the canonical gutter
104
+ - `.card`, `.card h3`: tile shell and title rhythm. Use through `<Card>` or `<Stat>`.
105
+ - `.ph-bar`, `.ph-title`, `.ph-actions`: page header primitives. Use through `<PageHeader>`.
106
+ - `.field-stack`, `.form-row`, `.form-actions`: form rhythm primitives
107
+ - `.mac`, `.name-cell`: monospace and avatar-style cell layouts
108
+
109
+ Anything else (raw colours, ad-hoc paddings) should come from tokens, not bespoke values.
110
+
111
+ ## Reference
112
+
113
+ - **Storybook (Svelte)**: `/svelte/` in the published site
114
+ - **Foundations / Page layout**: the canonical recipe in full, with anatomy and pitfalls
115
+ - **Foundations / Spacing**: the 12-token spacing scale and rhythm rules
116
+ - **Foundations / Colours**, **Type**, **Icons**, **Forms**, **Tables**, **Charts**, **Status &
117
+ feedback**, **Navigation**, **Overlays**, **Actions**: pick-by-question tables for every primitive
118
+
119
+ The Storybook **Dashboard** section renders the reference pages 1:1. When your page looks different,
120
+ open the matching story and diff the markup.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@w5-ui/svelte",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Svelte 5 components for the Dash UI design system - mirrors the @w5-ui/react API 1:1. Part of Webber's design systems.",
5
5
  "keywords": [
6
6
  "svelte",
@@ -48,7 +48,7 @@
48
48
  "@w5-ui/tokens": "^0.2.1"
49
49
  },
50
50
  "devDependencies": {
51
- "svelte": "^5.43.5",
51
+ "svelte": "^5.55.8",
52
52
  "typescript": "^5.5.0"
53
53
  },
54
54
  "files": [
@@ -6,6 +6,6 @@
6
6
  let { children }: Props = $props();
7
7
  </script>
8
8
 
9
- <div class="accordion">
9
+ <div class="flex flex-col">
10
10
  {@render children?.()}
11
11
  </div>
@@ -14,31 +14,30 @@
14
14
  const panelId = `acc-panel-${uid}`;
15
15
  </script>
16
16
 
17
- <div class="accordion-item">
18
- <h3 class="accordion-header">
17
+ <div class="border-b border-border-1 last:border-b-0">
18
+ <h3 class="m-0">
19
19
  <button
20
20
  id={btnId}
21
21
  type="button"
22
- class="accordion-trigger"
23
22
  aria-expanded={open}
24
23
  aria-controls={panelId}
24
+ class="flex w-full cursor-pointer items-center justify-between gap-2.5 border-0 bg-transparent py-2.5 text-left text-13 font-medium leading-[1.4] text-text-1 transition-colors duration-100 hover:text-text-link focus-visible:rounded-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05"
25
25
  onclick={() => (open = !open)}
26
26
  >
27
27
  <span>{title}</span>
28
28
  <svg
29
- class="accordion-chevron"
30
- class:open
31
29
  viewBox="0 0 16 16"
32
30
  fill="none"
33
31
  aria-hidden="true"
32
+ class="h-3.5 w-3.5 shrink-0 text-text-4 transition-transform duration-200 motion-reduce:transition-none {open ? 'rotate-180' : ''}"
34
33
  >
35
34
  <path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
36
35
  </svg>
37
36
  </button>
38
37
  </h3>
39
38
  {#if open}
40
- <div id={panelId} role="region" aria-labelledby={btnId} class="accordion-panel">
41
- <div class="accordion-content">
39
+ <div id={panelId} role="region" aria-labelledby={btnId}>
40
+ <div class="pb-2.5">
42
41
  {@render children?.()}
43
42
  </div>
44
43
  </div>
@@ -1,6 +1,11 @@
1
1
  <script module lang="ts">
2
2
  let counter = 0;
3
- export interface ActionMenuItem { id: string; label: string; danger?: boolean; disabled?: boolean; }
3
+ export interface ActionMenuItem {
4
+ id: string;
5
+ label: string;
6
+ danger?: boolean;
7
+ disabled?: boolean;
8
+ }
4
9
  </script>
5
10
 
6
11
  <script lang="ts">
@@ -12,9 +17,7 @@
12
17
  onaction?: (payload: string) => void;
13
18
  }
14
19
 
15
- let { items = [], label = 'Actions',
16
- onaction,
17
- }: Props = $props();
20
+ let { items = [], label = 'Actions', onaction }: Props = $props();
18
21
  const uid = `dash-ui-am-${++counter}`;
19
22
  const menuId = `${uid}-menu`;
20
23
 
@@ -64,15 +67,15 @@
64
67
  onDestroy(() => document.removeEventListener('mousedown', handleOutside));
65
68
  </script>
66
69
 
67
- <div class="action-menu-root" bind:this={wrapperEl}>
70
+ <div class="group/am relative inline-block" bind:this={wrapperEl}>
68
71
  <button
69
72
  bind:this={triggerEl}
70
73
  type="button"
71
- class="icon-btn action-menu-trigger"
72
74
  aria-label={label}
73
75
  aria-haspopup="menu"
74
76
  aria-expanded={open}
75
77
  aria-controls={open ? menuId : undefined}
78
+ class="inline-flex h-7 w-7 cursor-pointer items-center justify-center rounded-md bg-transparent text-text-3 opacity-0 transition-opacity duration-100 hover:bg-row-hover hover:text-text-1 focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-05 aria-expanded:opacity-100 group-hover/am:opacity-100 motion-reduce:transition-none"
76
79
  onclick={toggle}
77
80
  onkeydown={onKeyDown}
78
81
  >
@@ -83,15 +86,20 @@
83
86
  </svg>
84
87
  </button>
85
88
  {#if open}
86
- <ul id={menuId} role="menu" aria-label={label} class="action-menu">
89
+ <ul
90
+ id={menuId}
91
+ role="menu"
92
+ aria-label={label}
93
+ class="absolute right-0 top-[calc(100%+4px)] z-[9100] m-0 min-w-[148px] list-none rounded-lg border border-border-3 bg-bg-2 p-1 shadow-[0_8px_24px_rgba(0,0,0,0.5)]"
94
+ >
87
95
  {#each items as item, idx (item.id)}
88
96
  <li
89
97
  role="menuitem"
90
- tabindex="-1"
98
+ tabindex={-1}
91
99
  aria-disabled={item.disabled}
92
100
  data-active={idx === activeIdx ? 'true' : undefined}
93
101
  data-danger={item.danger ? 'true' : undefined}
94
- class="action-menu-item"
102
+ class="flex cursor-pointer items-center 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 data-[danger=true]:text-status-danger data-[danger=true]:hover:bg-status-danger/10 aria-disabled:cursor-not-allowed aria-disabled:opacity-40"
95
103
  onmouseenter={() => { activeIdx = idx; }}
96
104
  onmousedown={(e) => { e.preventDefault(); (() => { if (!item.disabled) activate(item.id); })(); }}
97
105
  >{item.label}</li>
@@ -28,46 +28,52 @@
28
28
 
29
29
  let endEl = $state<HTMLDivElement | undefined>(undefined);
30
30
 
31
- // Replaces `afterUpdate`. Reading `items.length` makes the effect depend
32
- // on the visible list so we re-scroll whenever an item is appended.
33
31
  $effect(() => {
34
32
  void items.length;
35
33
  if (autoScroll && endEl) endEl.scrollIntoView({ block: 'nearest' });
36
34
  });
35
+
36
+ const SEVERITY_DOT: Record<ActivitySeverity, string> = {
37
+ info: 'bg-brand-05',
38
+ success: 'bg-status-success',
39
+ warn: 'bg-status-warning',
40
+ error: 'bg-status-danger',
41
+ neutral: 'bg-status-neutral',
42
+ };
37
43
  </script>
38
44
 
39
45
  <div
40
46
  role="feed"
41
47
  aria-label={label}
42
48
  aria-busy={busy}
43
- class="activity-feed"
49
+ class="flex flex-col gap-1 overflow-y-auto"
44
50
  style:max-height="{maxHeight}px"
45
- style:overflow-y="auto"
46
51
  >
47
52
  {#each items as item, i (item.id)}
48
- <!-- svelte-ignore a11y_no_noninteractive_tabindex - navigable feed item for keyboard users -->
53
+ {@const sev = item.severity ?? 'neutral'}
54
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
49
55
  <article
50
- class="af-item af-item--{item.severity ?? 'neutral'}"
51
56
  aria-labelledby="af-title-{item.id}"
52
57
  aria-describedby={item.description ? `af-desc-${item.id}` : undefined}
53
58
  aria-posinset={i + 1}
54
59
  aria-setsize={items.length}
55
- tabindex="0"
60
+ tabindex={0}
61
+ class="flex items-start gap-2 rounded-md px-2 py-1.5 text-13 hover:bg-divider focus-visible:bg-row-hover focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-brand-05"
56
62
  >
57
- <div class="af-dot" aria-hidden="true"></div>
58
- <div class="af-body">
59
- <div class="af-header">
60
- <span id="af-title-{item.id}" class="af-title">{item.title}</span>
61
- <span class="af-time">{item.time}</span>
63
+ <div class="mt-1.5 h-2 w-2 shrink-0 rounded-full {SEVERITY_DOT[sev]}" aria-hidden="true"></div>
64
+ <div class="min-w-0 flex-1">
65
+ <div class="flex items-baseline justify-between gap-2">
66
+ <span id="af-title-{item.id}" class="truncate font-medium text-text-1">{item.title}</span>
67
+ <span class="shrink-0 text-11 text-text-4 tabular-nums">{item.time}</span>
62
68
  </div>
63
69
  {#if item.description}
64
- <p id="af-desc-{item.id}" class="af-desc">{item.description}</p>
70
+ <p id="af-desc-{item.id}" class="m-0 mt-0.5 text-12 text-text-3">{item.description}</p>
65
71
  {/if}
66
72
  </div>
67
73
  </article>
68
74
  {/each}
69
75
  {#if items.length === 0}
70
- <div class="af-empty" role="status">No activity</div>
76
+ <div class="py-6 text-center text-12 text-text-4" role="status">No activity</div>
71
77
  {/if}
72
78
  <div bind:this={endEl}></div>
73
79
  </div>
@@ -1,5 +1,7 @@
1
1
  <script lang="ts">
2
2
  import CloseIcon from '../icons/CloseIcon.svelte';
3
+ import IconButton from './IconButton.svelte';
4
+
3
5
  interface Props {
4
6
  variant?: 'success' | 'warn' | 'danger' | 'info';
5
7
  onDismiss?: (() => void) | undefined;
@@ -7,18 +9,34 @@
7
9
  }
8
10
 
9
11
  let { variant = 'info', onDismiss = undefined, children }: Props = $props();
12
+
13
+ // Variant strings statically composed so Tailwind's scanner picks them
14
+ // up. Values match dashboard.css exactly: 10% bg tint + left-border in
15
+ // the variant base colour + hand-tuned readable text colour.
16
+ const VARIANT: Record<NonNullable<Props['variant']>, string> = {
17
+ success: 'bg-status-success/10 border-l-status-success text-status-success',
18
+ warn: 'bg-status-warning/10 border-l-status-warning text-status-warning',
19
+ danger: 'bg-status-danger/10 border-l-status-danger text-status-danger',
20
+ info: 'bg-status-info/10 border-l-brand-05 text-status-info',
21
+ };
10
22
  </script>
11
23
 
12
24
  <div
13
- class="alert alert-{variant}"
25
+ class="flex items-center gap-2.5 rounded-lg border-l-[3px] px-3.5 py-2.5 text-13 leading-[1.4]
26
+ {VARIANT[variant]}"
14
27
  role={variant === 'danger' ? 'alert' : 'status'}
15
28
  aria-live={variant === 'danger' ? 'assertive' : 'polite'}
16
29
  aria-atomic="true"
17
30
  >
18
- <span class="alert-body">{@render children?.()}</span>
31
+ <span class="flex-1">{@render children?.()}</span>
19
32
  {#if onDismiss}
20
- <button type="button" class="alert-dismiss icon-btn" onclick={onDismiss} aria-label="Dismiss alert">
33
+ <IconButton
34
+ title="Dismiss alert"
35
+ onclick={onDismiss}
36
+ class="ml-auto h-6 w-6 shrink-0"
37
+ aria-label="Dismiss alert"
38
+ >
21
39
  <CloseIcon />
22
- </button>
40
+ </IconButton>
23
41
  {/if}
24
42
  </div>
@@ -0,0 +1,34 @@
1
+ <script lang="ts">
2
+ import SkipLink from './SkipLink.svelte';
3
+
4
+ interface Props {
5
+ /** Page title for the visually-hidden h1 that labels main (WCAG 2.4.6). */
6
+ pageLabel: string;
7
+ class?: string;
8
+ /** Topbar snippet, typically a Topbar component. */
9
+ topbar?: import('svelte').Snippet;
10
+ /** Sidebar snippet, typically a Sidebar component. */
11
+ sidebar?: import('svelte').Snippet;
12
+ children?: import('svelte').Snippet;
13
+ }
14
+
15
+ let {
16
+ pageLabel,
17
+ class: className = '',
18
+ topbar,
19
+ sidebar,
20
+ children,
21
+ }: Props = $props();
22
+ </script>
23
+
24
+ <div class="app {className}">
25
+ <SkipLink />
26
+ {@render topbar?.()}
27
+ <div class="workspace">
28
+ {@render sidebar?.()}
29
+ <main class="content" id="main-content" tabindex="-1" aria-labelledby="page-title">
30
+ <h1 id="page-title" class="sr-only">{pageLabel}</h1>
31
+ {@render children?.()}
32
+ </main>
33
+ </div>
34
+ </div>
@@ -10,7 +10,8 @@
10
10
  alt?: string | undefined;
11
11
  size?: AvatarSize;
12
12
  status?: AvatarStatus | undefined;
13
- className?: string;
13
+ /** Extra Tailwind utilities to compose with the avatar root. */
14
+ class?: string;
14
15
  style?: string;
15
16
  }
16
17
 
@@ -20,23 +21,39 @@
20
21
  alt = undefined,
21
22
  size = 'md',
22
23
  status = undefined,
23
- className = '',
24
- style = ''
24
+ class: className = '',
25
+ style = '',
25
26
  }: Props = $props();
27
+
28
+ // Pre-compose class strings so Tailwind's static scanner picks them up.
29
+ const SIZE: Record<AvatarSize, string> = {
30
+ sm: 'h-6 w-6 text-[9px]',
31
+ md: 'h-8 w-8 text-[12px]',
32
+ lg: 'h-10 w-10 text-[16px]',
33
+ };
34
+ const STATUS_BG: Record<AvatarStatus, string> = {
35
+ online: 'bg-status-success',
36
+ offline: 'bg-status-neutral',
37
+ away: 'bg-status-warning',
38
+ };
39
+
40
+ // Hardcoded neutral chrome to match dashboard.css's literal #4a4b53 / #fff,
41
+ // independent of motif. See the comment in Topbar.svelte for rationale.
42
+ let rootClass = $derived(
43
+ `relative inline-flex shrink-0 select-none items-center justify-center overflow-hidden rounded-full bg-neutral-06 font-semibold text-white ${SIZE[size]} ${className}`,
44
+ );
26
45
  </script>
27
46
 
28
- <span
29
- class="avatar avatar-{size}{className ? ` ${className}` : ''}"
30
- role="img"
31
- aria-label={alt ?? (src ? '' : initials)}
32
- {style}
33
- >
47
+ <span class={rootClass} role="img" aria-label={alt ?? (src ? '' : initials)} {style}>
34
48
  {#if src}
35
- <img {src} alt="" />
49
+ <img {src} alt="" class="block h-full w-full object-cover" />
36
50
  {:else}
37
51
  {initials}
38
52
  {/if}
39
53
  {#if status}
40
- <span class="avatar-status avatar-status-{status}" aria-hidden="true"></span>
54
+ <span
55
+ class="absolute bottom-0 right-0 h-2 w-2 rounded-full border-[1.5px] border-bg-page {STATUS_BG[status]}"
56
+ aria-hidden="true"
57
+ ></span>
41
58
  {/if}
42
59
  </span>
@@ -21,23 +21,40 @@
21
21
  avatars = [],
22
22
  max = 5,
23
23
  size = 'md',
24
- ariaLabel = 'Avatar group'
24
+ ariaLabel = 'Avatar group',
25
25
  }: Props = $props();
26
26
 
27
27
  let visible = $derived(avatars.slice(0, max));
28
28
  let overflow = $derived(avatars.length - visible.length);
29
+
30
+ const SIZE: Record<AvatarSize, string> = {
31
+ sm: 'h-6 w-6 text-[9px]',
32
+ md: 'h-8 w-8 text-[12px]',
33
+ lg: 'h-10 w-10 text-[16px]',
34
+ };
35
+ // Group avatars overlap by 8px to the left and add a 2px bg-coloured ring
36
+ // to read clearly when stacked. Ring uses the chrome's literal
37
+ // #0a0a0b (neutral-10) to match dashboard.css.
38
+ const OVERLAP =
39
+ '[&_>*:not(:last-child)>span]:-ml-2 [&_>*>span]:border-2 [&_>*>span]:border-bg-page';
29
40
  </script>
30
41
 
31
- <span class="avatar-group" role="list" aria-label={ariaLabel}>
42
+ <span
43
+ class="inline-flex flex-row-reverse items-center {OVERLAP}"
44
+ role="list"
45
+ aria-label={ariaLabel}
46
+ >
32
47
  {#each visible as av, i (i)}
33
- <span role="listitem" class="avatar-group-item">
48
+ <span role="listitem" class="contents">
34
49
  <Avatar initials={av.initials} src={av.src} alt={av.alt} {size} />
35
50
  </span>
36
51
  {/each}
37
52
  {#if overflow > 0}
38
53
  <span
39
54
  role="listitem"
40
- class="avatar avatar-{size} avatar-overflow"
55
+ class="relative inline-flex shrink-0 select-none items-center justify-center overflow-visible rounded-full border-2 border-bg-page bg-neutral-07 font-bold text-text-3 {SIZE[
56
+ size
57
+ ]}"
41
58
  aria-label="{overflow} more"
42
59
  >+{overflow}</span>
43
60
  {/if}
@@ -20,19 +20,35 @@
20
20
  showZero = false,
21
21
  color = 'danger',
22
22
  class: klass = '',
23
- children
23
+ children,
24
24
  }: Props = $props();
25
-
26
25
 
27
26
  let show = $derived(dot || (count !== undefined && (showZero || count > 0)));
28
27
  let label = $derived(count !== undefined ? (count > max ? `${max}+` : String(count)) : '');
28
+
29
+ // Variant strings pre-composed for Tailwind's static scanner. Warn keeps
30
+ // dashboard.css's `color: #111` (legible on the orange chip).
31
+ const COLOR: Record<BadgeColor, string> = {
32
+ danger: 'bg-status-danger text-white',
33
+ // Status-warning is yellow in both motifs; near-black text stays
34
+ // legible against it. `text-neutral-10` resolves to `#0a0a0b`.
35
+ warn: 'bg-status-warning text-neutral-10',
36
+ info: 'bg-brand-05 text-white',
37
+ success: 'bg-status-success text-white',
38
+ neutral: 'bg-status-neutral text-white',
39
+ };
29
40
  </script>
30
41
 
31
- <span class="badge-wrapper{klass ? ` ${klass}` : ''}">
42
+ <span class="relative inline-flex {klass}">
32
43
  {@render children?.()}
33
44
  {#if show}
34
- <span class="badge badge-{color}{dot ? ' badge-dot' : ''}" aria-hidden="true">
35
- {#if !dot}{label}{/if}
36
- </span>
45
+ <span
46
+ class="pointer-events-none absolute z-10 flex items-center justify-center whitespace-nowrap rounded-lg border-[1.5px] border-bg-page text-[10px] font-bold leading-none
47
+ {COLOR[color]}
48
+ {dot
49
+ ? '-right-0.5 -top-0.5 h-2 w-2 min-w-2 p-0'
50
+ : '-right-1.5 -top-1 h-4 min-w-4 px-1'}"
51
+ aria-hidden="true"
52
+ >{#if !dot}{label}{/if}</span>
37
53
  {/if}
38
54
  </span>
@@ -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>