@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.
- package/README.md +116 -4
- package/package.json +2 -2
- package/src/lib/actions/portal.ts +55 -0
- package/src/lib/components/Accordion.svelte +1 -1
- package/src/lib/components/AccordionItem.svelte +6 -7
- package/src/lib/components/ActionMenu.svelte +17 -9
- package/src/lib/components/ActivityFeed.svelte +20 -14
- package/src/lib/components/Alert.svelte +22 -4
- package/src/lib/components/AppLayout.svelte +34 -0
- package/src/lib/components/Avatar.svelte +28 -11
- package/src/lib/components/AvatarGroup.svelte +21 -4
- package/src/lib/components/Badge.svelte +22 -6
- package/src/lib/components/Banner.svelte +36 -7
- package/src/lib/components/Body.svelte +29 -0
- package/src/lib/components/BodyLg.svelte +26 -0
- package/src/lib/components/Breadcrumb.svelte +19 -12
- package/src/lib/components/Button.svelte +30 -1
- package/src/lib/components/CIDRInput.svelte +17 -9
- package/src/lib/components/Callout.svelte +29 -8
- package/src/lib/components/Caption.svelte +26 -0
- package/src/lib/components/Card.svelte +11 -3
- package/src/lib/components/CardMore.svelte +20 -0
- package/src/lib/components/CardTitle.svelte +20 -0
- package/src/lib/components/Carousel.svelte +27 -20
- package/src/lib/components/Checkbox.svelte +24 -3
- package/src/lib/components/Code.svelte +24 -0
- package/src/lib/components/CodeBlock.svelte +8 -7
- package/src/lib/components/ColorPicker.svelte +13 -13
- package/src/lib/components/ColumnToggle.svelte +10 -9
- package/src/lib/components/Combobox.svelte +31 -17
- package/src/lib/components/CommandPalette.svelte +42 -26
- package/src/lib/components/ConfirmDialog.svelte +21 -15
- package/src/lib/components/ContextMenu.svelte +12 -12
- package/src/lib/components/ContextualHelp.svelte +11 -6
- package/src/lib/components/CopyButton.svelte +2 -1
- package/src/lib/components/DatePicker.svelte +27 -23
- package/src/lib/components/DateRangePicker.svelte +46 -39
- package/src/lib/components/Delta.svelte +25 -0
- package/src/lib/components/Display1.svelte +29 -0
- package/src/lib/components/Display2.svelte +29 -0
- package/src/lib/components/Drawer.svelte +6 -5
- package/src/lib/components/DurationInput.svelte +16 -8
- package/src/lib/components/EmptyState.svelte +16 -7
- package/src/lib/components/ExpandableRow.svelte +8 -15
- package/src/lib/components/Eyebrow.svelte +25 -0
- package/src/lib/components/Field.svelte +7 -7
- package/src/lib/components/FieldStack.svelte +18 -0
- package/src/lib/components/FileUpload.svelte +8 -10
- package/src/lib/components/FilterBuilder.svelte +52 -28
- package/src/lib/components/FormActions.svelte +18 -0
- package/src/lib/components/FormRow.svelte +17 -0
- package/src/lib/components/Grid.svelte +17 -0
- package/src/lib/components/GroupedList.svelte +35 -21
- package/src/lib/components/H1.svelte +29 -0
- package/src/lib/components/H2.svelte +29 -0
- package/src/lib/components/H3.svelte +29 -0
- package/src/lib/components/H4.svelte +29 -0
- package/src/lib/components/HealthBar.svelte +6 -3
- package/src/lib/components/HoverCard.svelte +16 -5
- package/src/lib/components/IPInput.svelte +14 -8
- package/src/lib/components/IconButton.svelte +1 -2
- package/src/lib/components/InlineEdit.svelte +25 -14
- package/src/lib/components/Input.svelte +13 -4
- package/src/lib/components/InputGroup.svelte +14 -3
- package/src/lib/components/JsonViewer.svelte +7 -2
- package/src/lib/components/JsonViewerNode.svelte +38 -32
- package/src/lib/components/KVTable.svelte +10 -4
- package/src/lib/components/KanbanBoard.svelte +29 -27
- package/src/lib/components/Kbd.svelte +8 -5
- package/src/lib/components/Label.svelte +28 -0
- package/src/lib/components/LogViewer.svelte +24 -20
- package/src/lib/components/MACInput.svelte +14 -8
- package/src/lib/components/Mac.svelte +23 -0
- package/src/lib/components/Menubar.svelte +30 -13
- package/src/lib/components/Modal.svelte +15 -6
- package/src/lib/components/Mono.svelte +28 -0
- package/src/lib/components/MultiSelect.svelte +43 -23
- package/src/lib/components/Muted.svelte +24 -0
- package/src/lib/components/NameCell.svelte +35 -0
- package/src/lib/components/NotifDot.svelte +23 -0
- package/src/lib/components/NotificationPanel.svelte +44 -31
- package/src/lib/components/NumberInput.svelte +13 -12
- package/src/lib/components/OTPInput.svelte +14 -8
- package/src/lib/components/PageHeader.svelte +25 -0
- package/src/lib/components/Pagination.svelte +16 -12
- package/src/lib/components/PasswordInput.svelte +8 -7
- package/src/lib/components/Pill.svelte +34 -6
- package/src/lib/components/Popover.svelte +101 -17
- package/src/lib/components/ProgressBar.svelte +10 -7
- package/src/lib/components/RadioGroup.svelte +22 -7
- package/src/lib/components/RangeSlider.svelte +17 -12
- package/src/lib/components/RankedList.svelte +12 -18
- package/src/lib/components/ResizablePanel.svelte +25 -14
- package/src/lib/components/RowToggle.svelte +6 -4
- package/src/lib/components/SearchBox.svelte +6 -2
- package/src/lib/components/SegmentedControl.svelte +16 -2
- package/src/lib/components/Select.svelte +20 -8
- package/src/lib/components/SelectionToolbar.svelte +13 -9
- package/src/lib/components/Sidebar.svelte +32 -11
- package/src/lib/components/Signal.svelte +3 -2
- package/src/lib/components/Skeleton.svelte +21 -9
- package/src/lib/components/SkipLink.svelte +5 -2
- package/src/lib/components/Slider.svelte +16 -9
- package/src/lib/components/SortHeader.svelte +13 -4
- package/src/lib/components/SortableList.svelte +17 -18
- package/src/lib/components/Sparkline.svelte +20 -14
- package/src/lib/components/Spinner.svelte +16 -1
- package/src/lib/components/SplitButton.svelte +27 -8
- package/src/lib/components/Spoiler.svelte +8 -6
- package/src/lib/components/SrOnly.svelte +23 -0
- package/src/lib/components/StackedProgress.svelte +17 -9
- package/src/lib/components/StarRating.svelte +35 -10
- package/src/lib/components/Stat.svelte +19 -5
- package/src/lib/components/StatNumber.svelte +22 -0
- package/src/lib/components/StatusIndicator.svelte +9 -2
- package/src/lib/components/StatusRing.svelte +36 -0
- package/src/lib/components/Stepper.svelte +30 -8
- package/src/lib/components/Submeta.svelte +19 -0
- package/src/lib/components/SwitchPortGrid.svelte +20 -7
- package/src/lib/components/TablePager.svelte +22 -0
- package/src/lib/components/TableShell.svelte +19 -0
- package/src/lib/components/Tabs.svelte +16 -5
- package/src/lib/components/Tag.svelte +5 -3
- package/src/lib/components/TagInput.svelte +9 -10
- package/src/lib/components/Textarea.svelte +7 -1
- package/src/lib/components/TimePicker.svelte +14 -8
- package/src/lib/components/TimeRange.svelte +20 -22
- package/src/lib/components/Timeline.svelte +20 -9
- package/src/lib/components/Toast.svelte +24 -5
- package/src/lib/components/Toggle.svelte +10 -4
- package/src/lib/components/ToggleGroup.svelte +15 -5
- package/src/lib/components/Tooltip.svelte +20 -5
- package/src/lib/components/Topbar.svelte +161 -28
- package/src/lib/components/TopbarActions.svelte +52 -0
- package/src/lib/components/TransferList.svelte +30 -22
- package/src/lib/components/TreeItem.svelte +11 -13
- package/src/lib/components/Unit.svelte +22 -0
- package/src/lib/components/VirtualList.svelte +12 -9
- package/src/lib/index.ts +37 -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
|
-
```
|
|
6
|
-
|
|
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.
|
|
3
|
+
"version": "0.4.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.
|
|
51
|
+
"svelte": "^5.55.8",
|
|
52
52
|
"typescript": "^5.5.0"
|
|
53
53
|
},
|
|
54
54
|
"files": [
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Action } from 'svelte/action'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Move a DOM node into `target` (default: `document.body`) for the
|
|
5
|
+
* lifetime of the action. Used by floating UI primitives so
|
|
6
|
+
* `position: fixed` + `z-index` always escape:
|
|
7
|
+
*
|
|
8
|
+
* - ancestor `overflow: hidden` / `overflow: clip` (would clip
|
|
9
|
+
* a non-portalled child even at `position: fixed` if the
|
|
10
|
+
* ancestor establishes a containing block)
|
|
11
|
+
* - ancestor `transform`, `filter`, `perspective`, `contain: paint`,
|
|
12
|
+
* `will-change: transform` (any of which create a containing
|
|
13
|
+
* block for `position: fixed` descendants, defeating viewport
|
|
14
|
+
* positioning)
|
|
15
|
+
* - ancestor stacking contexts with a lower `z-index` than the
|
|
16
|
+
* popover's `z-index`, which would otherwise trap it underneath
|
|
17
|
+
* sibling chrome
|
|
18
|
+
*
|
|
19
|
+
* The node still belongs to the Svelte component that owns it: props,
|
|
20
|
+
* reactivity, lifecycle, and event handlers continue to work; only
|
|
21
|
+
* the DOM position changes. The action removes the node on destroy
|
|
22
|
+
* so cleanup is automatic.
|
|
23
|
+
*
|
|
24
|
+
* SSR-safe: actions don't run during server rendering.
|
|
25
|
+
*/
|
|
26
|
+
export const portal: Action<HTMLElement, HTMLElement | string | undefined> = (
|
|
27
|
+
node,
|
|
28
|
+
target = 'body',
|
|
29
|
+
) => {
|
|
30
|
+
const resolveTarget = (value: HTMLElement | string | undefined): HTMLElement | null => {
|
|
31
|
+
if (value === undefined) return document.body
|
|
32
|
+
if (typeof value === 'string') {
|
|
33
|
+
return document.querySelector<HTMLElement>(value)
|
|
34
|
+
}
|
|
35
|
+
return value
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const move = (value: HTMLElement | string | undefined) => {
|
|
39
|
+
const parent = resolveTarget(value)
|
|
40
|
+
if (parent !== null && parent !== node.parentNode) {
|
|
41
|
+
parent.appendChild(node)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
move(target)
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
update(next: HTMLElement | string | undefined) {
|
|
49
|
+
move(next)
|
|
50
|
+
},
|
|
51
|
+
destroy() {
|
|
52
|
+
node.remove()
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -14,31 +14,30 @@
|
|
|
14
14
|
const panelId = `acc-panel-${uid}`;
|
|
15
15
|
</script>
|
|
16
16
|
|
|
17
|
-
<div class="
|
|
18
|
-
<h3 class="
|
|
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}
|
|
41
|
-
<div class="
|
|
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 {
|
|
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="
|
|
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
|
|
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=
|
|
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="
|
|
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="
|
|
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
|
-
|
|
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=
|
|
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="
|
|
58
|
-
<div class="
|
|
59
|
-
<div class="
|
|
60
|
-
<span id="af-title-{item.id}" class="
|
|
61
|
-
<span class="
|
|
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="
|
|
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="
|
|
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="
|
|
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="
|
|
31
|
+
<span class="flex-1">{@render children?.()}</span>
|
|
19
32
|
{#if onDismiss}
|
|
20
|
-
<
|
|
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
|
-
</
|
|
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
|
-
|
|
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
|
|
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
|
|
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="
|
|
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="
|
|
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="
|
|
42
|
+
<span class="relative inline-flex {klass}">
|
|
32
43
|
{@render children?.()}
|
|
33
44
|
{#if show}
|
|
34
|
-
<span
|
|
35
|
-
|
|
36
|
-
|
|
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>
|