@w5-ui/react 0.2.1 → 0.5.1
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 +118 -4
- package/package.json +2 -2
- package/src/components/AppLayout.tsx +38 -0
- package/src/components/AppShell.tsx +41 -49
- package/src/components/Body.tsx +32 -0
- package/src/components/BodyLg.tsx +29 -0
- package/src/components/Caption.tsx +29 -0
- package/src/components/Card.tsx +13 -0
- package/src/components/CardMore.tsx +22 -0
- package/src/components/Code.tsx +27 -0
- package/src/components/ColumnToggle.tsx +1 -0
- package/src/components/CommandPalette.tsx +1 -0
- package/src/components/ContextMenu.tsx +5 -1
- package/src/components/DateRangePicker.tsx +16 -9
- package/src/components/Delta.tsx +29 -0
- package/src/components/Display1.tsx +33 -0
- package/src/components/Display2.tsx +32 -0
- package/src/components/ExpandableRow.tsx +1 -0
- package/src/components/Eyebrow.tsx +28 -0
- package/src/components/FieldStack.tsx +19 -0
- package/src/components/FormActions.tsx +20 -0
- package/src/components/FormRow.tsx +19 -0
- package/src/components/Grid.tsx +21 -0
- package/src/components/H1.tsx +32 -0
- package/src/components/H2.tsx +32 -0
- package/src/components/H3.tsx +32 -0
- package/src/components/H4.tsx +31 -0
- package/src/components/Label.tsx +30 -0
- package/src/components/Mac.tsx +25 -0
- package/src/components/Modal.tsx +16 -2
- package/src/components/Mono.tsx +31 -0
- package/src/components/Muted.tsx +27 -0
- package/src/components/NameCell.tsx +45 -0
- package/src/components/NotifDot.tsx +24 -0
- package/src/components/NumberInput.tsx +1 -0
- package/src/components/PageHeader.tsx +20 -0
- package/src/components/PasswordInput.tsx +1 -0
- package/src/components/RadioGroup.tsx +2 -1
- package/src/components/SrOnly.tsx +25 -0
- package/src/components/StatNumber.tsx +25 -0
- package/src/components/StatusRing.tsx +38 -0
- package/src/components/Submeta.tsx +23 -0
- package/src/components/TablePager.tsx +27 -0
- package/src/components/TableShell.tsx +23 -0
- package/src/components/TagInput.tsx +7 -2
- package/src/components/TopbarActions.tsx +70 -0
- package/src/components/Unit.tsx +25 -0
- package/src/index.ts +33 -0
package/README.md
CHANGED
|
@@ -1,9 +1,123 @@
|
|
|
1
1
|
# @w5-ui/react
|
|
2
2
|
|
|
3
|
-
React component library for the Dash UI design system. Pairs with `@w5-ui/tokens` for
|
|
4
|
-
`@w5-ui/assets` for brand SVGs.
|
|
3
|
+
React 18 component library for the Dash UI design system. Pairs with `@w5-ui/tokens` for design
|
|
4
|
+
tokens and `@w5-ui/assets` for brand SVGs.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
yarn add @w5-ui/react
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Quick start
|
|
5
11
|
|
|
6
12
|
```tsx
|
|
7
|
-
import '@w5-ui/react/styles.css'
|
|
8
|
-
import {
|
|
13
|
+
import '@w5-ui/react/styles.css' // ships @w5-ui/tokens/dashboard.css
|
|
14
|
+
import { AppLayout, Topbar, Sidebar, PageHeader, Card, Stat, Pill } from '@w5-ui/react'
|
|
15
|
+
|
|
16
|
+
export function App() {
|
|
17
|
+
return (
|
|
18
|
+
<AppLayout
|
|
19
|
+
pageLabel="Dashboard"
|
|
20
|
+
topbar={<Topbar siteName="HQ" activeApp="system" />}
|
|
21
|
+
sidebar={<Sidebar sections={SECTIONS} activeId={page} onChange={setPage} />}
|
|
22
|
+
>
|
|
23
|
+
<PageHeader title="Dashboard" />
|
|
24
|
+
<div className="grid">
|
|
25
|
+
<Stat span={4} label="Internet" value="847" unit="Mbps" />
|
|
26
|
+
<Stat span={4} label="Clients" value="124" />
|
|
27
|
+
<Stat span={4} label="Throughput" value="12.4" unit="Gbps" />
|
|
28
|
+
<Card span={8}>
|
|
29
|
+
<h3>
|
|
30
|
+
Traffic <Pill variant="success">healthy</Pill>
|
|
31
|
+
</h3>
|
|
32
|
+
</Card>
|
|
33
|
+
<Card span={4}>
|
|
34
|
+
<h3>Top clients</h3>
|
|
35
|
+
</Card>
|
|
36
|
+
</div>
|
|
37
|
+
</AppLayout>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
9
40
|
```
|
|
41
|
+
|
|
42
|
+
That single skeleton (`AppLayout` then `PageHeader` then `<div className="grid">` then
|
|
43
|
+
`<Card span={n}>`) carries every padding, gap, and column value the reference dashboards use.
|
|
44
|
+
Reproduce it verbatim and spacing falls into place automatically.
|
|
45
|
+
|
|
46
|
+
## The contract (read this before styling anything)
|
|
47
|
+
|
|
48
|
+
The reference dashboards (`apps/dashboard-react`, rendered in Storybook under **Dashboard**) all
|
|
49
|
+
share one skeleton. Userland looking different almost always comes down to one of these:
|
|
50
|
+
|
|
51
|
+
| Don't | Do |
|
|
52
|
+
| -------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
53
|
+
| Wrap `.grid` in your own `<div style={{ padding }}>` | Render `.grid` as a direct child of `<main>`. It already pads 16px x 24px. |
|
|
54
|
+
| `display: flex; gap: ...` on the page root | Render `<Banner>`, `<PageHeader>`, `.grid` as siblings. They own their spacing. |
|
|
55
|
+
| Re-implement `.card` with a `<div>` and a custom border-radius | Use `<Card>` or `<Stat>`. The radius, padding, and `<h3>` rhythm ship together. |
|
|
56
|
+
| `<h3>` outside a `<Card>` | Keep titles inside `<Card>`. The `.card h3` selector sizes and colours them. |
|
|
57
|
+
| `<span style={{ color: 'green' }}>online</span>` | Use `<Pill variant="success">online</Pill>` (also `<Badge>`, `<StatusIndicator>`). |
|
|
58
|
+
| Mix `span={5}` and `span={7}` | Use 4 / 6 / 8 / 12 so rows divide cleanly into the 12-column grid. |
|
|
59
|
+
| Skip `AppLayout` and roll your own `.app` grid | Use `AppLayout` once at the root. `.app`, `.workspace`, and `<main>` line up. |
|
|
60
|
+
| Hand-pick hex colours | Use tokens via the `data-motif` CSS variables. See `@w5-ui/tokens`. |
|
|
61
|
+
|
|
62
|
+
## Grid math
|
|
63
|
+
|
|
64
|
+
`.grid` is `repeat(12, 1fr)` with a 12px gap and 16px x 24px outer padding.
|
|
65
|
+
|
|
66
|
+
| Tile width | `span` | Typical use |
|
|
67
|
+
| ---------- | ------ | ------------------------------------ |
|
|
68
|
+
| Third | `4` | Single-stat card, donut |
|
|
69
|
+
| Half | `6` | Two-column comparison |
|
|
70
|
+
| Two-thirds | `8` | Wide table, line chart |
|
|
71
|
+
| Full | `12` | Time series, sankey, page-wide table |
|
|
72
|
+
|
|
73
|
+
4+4+4, 8+4, 6+6, and 12 all snap cleanly. Avoid 5 and 7. They leave dead pixels and force the next
|
|
74
|
+
row to wrap unevenly.
|
|
75
|
+
|
|
76
|
+
## What lives where
|
|
77
|
+
|
|
78
|
+
| Package | Description |
|
|
79
|
+
| --------------- | -------------------------------------------------------- |
|
|
80
|
+
| `@w5-ui/react` | ~170 React components (this package) |
|
|
81
|
+
| `@w5-ui/svelte` | The same component surface for Svelte |
|
|
82
|
+
| `@w5-ui/tokens` | CSS variables (`data-motif="dark"` / `"light"`) + JS map |
|
|
83
|
+
| `@w5-ui/assets` | Brand mark, wordmark, app-rail SVGs |
|
|
84
|
+
|
|
85
|
+
`@w5-ui/react/styles.css` re-exports `@w5-ui/tokens/dashboard.css`, so importing it once is enough
|
|
86
|
+
for tokens, the `.app` / `.workspace` / `.grid` / `.card` classes, and component styles.
|
|
87
|
+
|
|
88
|
+
## Themes
|
|
89
|
+
|
|
90
|
+
Two motifs ship in tokens:
|
|
91
|
+
|
|
92
|
+
```html
|
|
93
|
+
<html data-motif="dark">
|
|
94
|
+
<!-- the default -->
|
|
95
|
+
</html>
|
|
96
|
+
<html data-motif="light"></html>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Set the attribute on `<html>` (or any ancestor of the app root) to switch.
|
|
100
|
+
|
|
101
|
+
## CSS hooks you can rely on
|
|
102
|
+
|
|
103
|
+
Stable class names that ship from `@w5-ui/tokens/dashboard.css`:
|
|
104
|
+
|
|
105
|
+
- `.app`, `.workspace`, `.content`: outer chrome, wired up by `AppLayout`
|
|
106
|
+
- `.grid`: 12-column page grid with the canonical gutter
|
|
107
|
+
- `.card`, `.card h3`: tile shell and title rhythm. Use through `<Card>` or `<Stat>`.
|
|
108
|
+
- `.ph-bar`, `.ph-title`, `.ph-actions`: page header primitives. Use through `<PageHeader>`.
|
|
109
|
+
- `.field-stack`, `.form-row`, `.form-actions`: form rhythm primitives
|
|
110
|
+
- `.mac`, `.name-cell`: monospace and avatar-style cell layouts
|
|
111
|
+
|
|
112
|
+
Anything else (raw colours, ad-hoc paddings) should come from tokens, not bespoke values.
|
|
113
|
+
|
|
114
|
+
## Reference
|
|
115
|
+
|
|
116
|
+
- **Storybook**: full component catalogue plus every Foundations page
|
|
117
|
+
- **Foundations / Page layout**: the canonical recipe in full, with anatomy and pitfalls
|
|
118
|
+
- **Foundations / Spacing**: the 12-token spacing scale and rhythm rules
|
|
119
|
+
- **Foundations / Colours**, **Type**, **Icons**, **Forms**, **Tables**, **Charts**, **Status &
|
|
120
|
+
feedback**, **Navigation**, **Overlays**, **Actions**: pick-by-question tables for every primitive
|
|
121
|
+
|
|
122
|
+
The Storybook **Dashboard** section renders the reference pages 1:1. When your page looks different,
|
|
123
|
+
open the matching story and diff the markup.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@w5-ui/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "React 18 components for the Dash UI design system - ~170 dashboard-focused components with full type defs and SSR support. Part of Webber's design systems.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@w5-ui/assets": "^0.2.1",
|
|
42
|
-
"@w5-ui/tokens": "^0.
|
|
42
|
+
"@w5-ui/tokens": "^0.5.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/react": "^18.3.0",
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { SkipLink } from './SkipLink.js'
|
|
3
|
+
|
|
4
|
+
export interface AppLayoutProps {
|
|
5
|
+
/** Rendered above the workspace, typically `<Topbar />`. */
|
|
6
|
+
topbar: ReactNode
|
|
7
|
+
/** Rendered to the left of the main content, typically `<Sidebar />`. */
|
|
8
|
+
sidebar: ReactNode
|
|
9
|
+
/** Page title for the visually-hidden `<h1>` that labels `<main>` (WCAG 2.4.6). */
|
|
10
|
+
pageLabel: string
|
|
11
|
+
children?: ReactNode
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Outer dashboard chrome: `<SkipLink>`, `<Topbar>` slot, `<Sidebar>` slot, and
|
|
16
|
+
* a `<main className="content">` region with the page-title `<h1>` wired up.
|
|
17
|
+
*
|
|
18
|
+
* Reproducing this skeleton in userland is the #1 source of spacing drift —
|
|
19
|
+
* `<AppLayout>` guarantees the `.app` grid, the `.workspace` columns, and the
|
|
20
|
+
* sticky scroll container line up the same way the reference dashboards do.
|
|
21
|
+
*/
|
|
22
|
+
export function AppLayout({ topbar, sidebar, pageLabel, children }: AppLayoutProps) {
|
|
23
|
+
return (
|
|
24
|
+
<div className="app">
|
|
25
|
+
<SkipLink />
|
|
26
|
+
{topbar}
|
|
27
|
+
<div className="workspace">
|
|
28
|
+
{sidebar}
|
|
29
|
+
<main className="content" id="main-content" tabIndex={-1} aria-labelledby="page-title">
|
|
30
|
+
<h1 id="page-title" className="sr-only">
|
|
31
|
+
{pageLabel}
|
|
32
|
+
</h1>
|
|
33
|
+
{children}
|
|
34
|
+
</main>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { appLogos, logos } from '@w5-ui/assets'
|
|
2
2
|
import { Fragment, type ReactNode } from 'react'
|
|
3
|
-
import { CaretIcon
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { CaretIcon } from '../icons.js'
|
|
4
|
+
import { StatusRing, type StatusRingStatus } from './StatusRing.js'
|
|
5
|
+
import { TopbarActions } from './TopbarActions.js'
|
|
6
6
|
|
|
7
7
|
void logos
|
|
8
8
|
|
|
@@ -25,6 +25,17 @@ export interface TopbarProps {
|
|
|
25
25
|
onAppChange?: (id: string) => void
|
|
26
26
|
initials?: string
|
|
27
27
|
notificationCount?: number
|
|
28
|
+
/** Health colour of the site-name status ring. Defaults to `'ok'` (green).
|
|
29
|
+
* Drive from userland health state to surface degraded / down conditions
|
|
30
|
+
* without replacing the canonical site-switcher chrome. */
|
|
31
|
+
status?: StatusRingStatus
|
|
32
|
+
/** When false, the site label renders as a static element instead of a
|
|
33
|
+
* site-switcher dropdown trigger. Use for single-site dashboards. */
|
|
34
|
+
siteSwitchable?: boolean
|
|
35
|
+
/** Custom right-side actions. When provided, replaces the default
|
|
36
|
+
* Search/Updates/Notifications/Help/Avatar block in full. Use to slot in
|
|
37
|
+
* app-specific controls (theme toggle, status pill, etc.). */
|
|
38
|
+
actions?: ReactNode
|
|
28
39
|
}
|
|
29
40
|
|
|
30
41
|
export function Topbar({
|
|
@@ -34,21 +45,35 @@ export function Topbar({
|
|
|
34
45
|
onAppChange,
|
|
35
46
|
initials = 'MS',
|
|
36
47
|
notificationCount = 1,
|
|
48
|
+
status = 'ok',
|
|
49
|
+
siteSwitchable = true,
|
|
50
|
+
actions,
|
|
37
51
|
}: TopbarProps) {
|
|
52
|
+
const siteLabel = (
|
|
53
|
+
<>
|
|
54
|
+
<StatusRing status={status} />
|
|
55
|
+
<span className="site-name" aria-hidden={siteSwitchable ? 'true' : undefined}>
|
|
56
|
+
{siteName}
|
|
57
|
+
</span>
|
|
58
|
+
{siteSwitchable && <CaretIcon className="caret" aria-hidden="true" />}
|
|
59
|
+
</>
|
|
60
|
+
)
|
|
38
61
|
return (
|
|
39
62
|
<header className="topbar">
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
<
|
|
51
|
-
|
|
63
|
+
{siteSwitchable ? (
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
className="site-switch"
|
|
67
|
+
aria-label={`Switch site: ${siteName}`}
|
|
68
|
+
aria-haspopup="menu"
|
|
69
|
+
>
|
|
70
|
+
{siteLabel}
|
|
71
|
+
</button>
|
|
72
|
+
) : (
|
|
73
|
+
<div className="site-switch" role="presentation">
|
|
74
|
+
{siteLabel}
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
52
77
|
<nav className="app-tabs" aria-label="Apps">
|
|
53
78
|
{apps.map((a) => (
|
|
54
79
|
<button
|
|
@@ -65,40 +90,7 @@ export function Topbar({
|
|
|
65
90
|
</nav>
|
|
66
91
|
<div className="topbar-spacer" />
|
|
67
92
|
<div className="topbar-right">
|
|
68
|
-
<
|
|
69
|
-
<SearchIcon />
|
|
70
|
-
</IconButton>
|
|
71
|
-
<IconButton aria-label="Updates" title="Updates">
|
|
72
|
-
<UpdatesIcon />
|
|
73
|
-
</IconButton>
|
|
74
|
-
<IconButton
|
|
75
|
-
aria-label={
|
|
76
|
-
notificationCount > 0 ? `Notifications, ${notificationCount} new` : 'Notifications'
|
|
77
|
-
}
|
|
78
|
-
title="Notifications"
|
|
79
|
-
style={{ position: 'relative' }}
|
|
80
|
-
>
|
|
81
|
-
<BellIcon />
|
|
82
|
-
{notificationCount > 0 && (
|
|
83
|
-
<span
|
|
84
|
-
aria-hidden="true"
|
|
85
|
-
style={{
|
|
86
|
-
position: 'absolute',
|
|
87
|
-
top: 6,
|
|
88
|
-
right: 6,
|
|
89
|
-
width: 6,
|
|
90
|
-
height: 6,
|
|
91
|
-
background: '#F03A3A',
|
|
92
|
-
borderRadius: '50%',
|
|
93
|
-
border: '1.5px solid #0A0A0B',
|
|
94
|
-
}}
|
|
95
|
-
/>
|
|
96
|
-
)}
|
|
97
|
-
</IconButton>
|
|
98
|
-
<IconButton aria-label="Help" title="Help">
|
|
99
|
-
<HelpIcon />
|
|
100
|
-
</IconButton>
|
|
101
|
-
<Avatar initials={initials} size="sm" alt={`Account, ${initials}`} />
|
|
93
|
+
{actions ?? <TopbarActions initials={initials} notificationCount={notificationCount} />}
|
|
102
94
|
</div>
|
|
103
95
|
</header>
|
|
104
96
|
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface BodyProps extends HTMLAttributes<HTMLParagraphElement> {
|
|
4
|
+
children?: ReactNode
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Default body paragraph - the 14 px / regular / 1.4 / `--text-2` rhythm
|
|
9
|
+
* that runs across table cells, paragraphs, and most user-readable copy
|
|
10
|
+
* in the dashboard. Renders `<p class="dash-ui-body">` so the
|
|
11
|
+
* `.dash-ui-body` rule in `@w5-ui/tokens/tokens.css` pins the canonical
|
|
12
|
+
* size, weight, line-height, and colour. Stops userland reaching for a
|
|
13
|
+
* one-off `font-size: 14px; line-height: 1.4;` duo (or worse, leaving the
|
|
14
|
+
* 16 px browser default in place and getting the #1 documented spacing
|
|
15
|
+
* drift: "page text looks too big vs the reference dashboard").
|
|
16
|
+
*
|
|
17
|
+
* Typed equivalent of `<p className="dash-ui-body">`. This is the
|
|
18
|
+
* userland-facing semantic class documented in **Foundations / Type**.
|
|
19
|
+
* Body copy is "almost always inherited, almost never overridden" inside
|
|
20
|
+
* `.card` / `.table-shell` / page chrome - reach for `<Body>` when
|
|
21
|
+
* wrapping standalone prose in userland (empty states, onboarding
|
|
22
|
+
* paragraphs, info banners) or when a parent stylesheet has reset the
|
|
23
|
+
* default size and you need to opt back in explicitly. For 16 px
|
|
24
|
+
* settings descriptions prefer `<BodyLg>`.
|
|
25
|
+
*/
|
|
26
|
+
export function Body({ className = '', children, ...rest }: BodyProps) {
|
|
27
|
+
return (
|
|
28
|
+
<p className={`dash-ui-body ${className}`.trim()} {...rest}>
|
|
29
|
+
{children}
|
|
30
|
+
</p>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface BodyLgProps extends HTMLAttributes<HTMLParagraphElement> {
|
|
4
|
+
children?: ReactNode
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Longer-form prose paragraph - settings descriptions, onboarding copy,
|
|
9
|
+
* empty-state explanations, anywhere the default 14 px body feels too
|
|
10
|
+
* cramped for multi-sentence reading. Renders `<p class="dash-ui-body-lg">`
|
|
11
|
+
* so the `.dash-ui-body-lg` rule in `@w5-ui/tokens/tokens.css` applies the
|
|
12
|
+
* canonical 16 px / regular weight, 1.55 line-height, and `--text-2`
|
|
13
|
+
* colour pair. Stops userland reaching for a one-off `font-size: 16px;
|
|
14
|
+
* line-height: 1.6;` duo on every settings description.
|
|
15
|
+
*
|
|
16
|
+
* Typed equivalent of `<p className="dash-ui-body-lg">`. This is the
|
|
17
|
+
* userland-facing semantic class documented in **Foundations / Type**.
|
|
18
|
+
* Reach for `<BodyLg>` when the cliff between 1.25 (heading) and 1.4
|
|
19
|
+
* (default body) feels too dense for a 2-3 sentence paragraph; keep the
|
|
20
|
+
* default `.dash-ui-body` (14 / 1.4) for table cells, table footnotes,
|
|
21
|
+
* and single-line labels.
|
|
22
|
+
*/
|
|
23
|
+
export function BodyLg({ className = '', children, ...rest }: BodyLgProps) {
|
|
24
|
+
return (
|
|
25
|
+
<p className={`dash-ui-body-lg ${className}`.trim()} {...rest}>
|
|
26
|
+
{children}
|
|
27
|
+
</p>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface CaptionProps extends HTMLAttributes<HTMLSpanElement> {
|
|
4
|
+
children?: ReactNode
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Small helper / metadata text - "last updated 5 min ago", table footnote,
|
|
9
|
+
* timestamp under a chart, byline beneath a section heading. Renders
|
|
10
|
+
* `<span class="dash-ui-caption">` so the `.dash-ui-caption` rule in
|
|
11
|
+
* `@w5-ui/tokens/tokens.css` applies the canonical 12 px / regular weight,
|
|
12
|
+
* 1.25 line-height, and the muted `--text-3` colour. Stops userland
|
|
13
|
+
* reaching for a one-off `font-size: 12px; color: #a4a7b5;` trio on every
|
|
14
|
+
* caption.
|
|
15
|
+
*
|
|
16
|
+
* Typed equivalent of `<span className="dash-ui-caption">`. This is the
|
|
17
|
+
* userland-facing semantic class documented in **Foundations / Type**.
|
|
18
|
+
* Use `<Caption>` for non-essential metadata only - on `--depthBg-3`
|
|
19
|
+
* (popovers, drawers) the `--text-3` contrast slides toward 4.0:1, so
|
|
20
|
+
* keep primary copy on `.dash-ui-body` / `--text-2`. Pair with `<Muted>`
|
|
21
|
+
* when you want the same colour at the default 14 px body size.
|
|
22
|
+
*/
|
|
23
|
+
export function Caption({ className = '', children, ...rest }: CaptionProps) {
|
|
24
|
+
return (
|
|
25
|
+
<span className={`dash-ui-caption ${className}`.trim()} {...rest}>
|
|
26
|
+
{children}
|
|
27
|
+
</span>
|
|
28
|
+
)
|
|
29
|
+
}
|
package/src/components/Card.tsx
CHANGED
|
@@ -18,6 +18,19 @@ export interface CardTitleProps extends HTMLAttributes<HTMLHeadingElement> {
|
|
|
18
18
|
children?: ReactNode
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Title row inside a `<Card>` tile. Renders a plain `<h3>` so the `.card h3`
|
|
23
|
+
* rule in `@w5-ui/tokens/dashboard.css` sets the canonical 12 px size, weight
|
|
24
|
+
* 500, `--text-3` (muted) colour, and flex layout (title on the left, any trailing
|
|
25
|
+
* `<Pill>` / `<CardMore>` pushed to the right edge). Stops userland reaching
|
|
26
|
+
* for an `<h2>` or a custom `<div className="title">` that would skip the
|
|
27
|
+
* rule and read broken inside the tile.
|
|
28
|
+
*
|
|
29
|
+
* Use the typed wrapper instead of a raw `<h3>` when the markup is awkward
|
|
30
|
+
* (codegen, MDX, mixed JSX trees); otherwise a plain `<h3>` works equally
|
|
31
|
+
* well. Pair with `<CardMore>` for a trailing "View all →" affordance and
|
|
32
|
+
* `<Pill>` for inline status.
|
|
33
|
+
*/
|
|
21
34
|
export function CardTitle({ children, className = '', ...rest }: CardTitleProps) {
|
|
22
35
|
return (
|
|
23
36
|
<h3 className={className} {...rest}>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AnchorHTMLAttributes, ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface CardMoreProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
4
|
+
children?: ReactNode
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Trailing "View all" / "More →" affordance inside a `<Card>` title row.
|
|
9
|
+
* Renders an `<a class="more">` so the `.card h3 .more` rule in
|
|
10
|
+
* `@w5-ui/tokens/dashboard.css` styles it (muted colour, pointer cursor) and
|
|
11
|
+
* the parent `<h3>` flex pushes it to the right edge of the title.
|
|
12
|
+
*
|
|
13
|
+
* Typed equivalent of `<a className="more">` inside `<h3>` for the canonical
|
|
14
|
+
* card title pattern; pair with `<Card>` / `<CardTitle>` for full ergonomics.
|
|
15
|
+
*/
|
|
16
|
+
export function CardMore({ className = '', children, ...rest }: CardMoreProps) {
|
|
17
|
+
return (
|
|
18
|
+
<a className={`more ${className}`.trim()} {...rest}>
|
|
19
|
+
{children}
|
|
20
|
+
</a>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface CodeProps extends HTMLAttributes<HTMLElement> {
|
|
4
|
+
children?: ReactNode
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Inline code snippet for key names, config paths, environment variables
|
|
9
|
+
* and other short literal strings that read inside body copy. Renders a
|
|
10
|
+
* `<code class="dash-ui-code">` so the `.dash-ui-code` rule in
|
|
11
|
+
* `@w5-ui/tokens/css` applies the canonical JetBrains Mono family, 12 px
|
|
12
|
+
* size, muted text colour, and the inset `--depthBg-2` chip background
|
|
13
|
+
* with a 1 px border. Stops userland reaching for bare `<code>` (which
|
|
14
|
+
* falls back to a serif monospace on `#fff`) or inline `font-family` /
|
|
15
|
+
* `background` overrides on every snippet.
|
|
16
|
+
*
|
|
17
|
+
* Typed equivalent of `<code className="dash-ui-code">`. Reach for
|
|
18
|
+
* `<CodeBlock>` when the snippet wraps to multiple lines and wants a
|
|
19
|
+
* full block surface; this primitive is for the inline case.
|
|
20
|
+
*/
|
|
21
|
+
export function Code({ className = '', children, ...rest }: CodeProps) {
|
|
22
|
+
return (
|
|
23
|
+
<code className={`dash-ui-code ${className}`.trim()} {...rest}>
|
|
24
|
+
{children}
|
|
25
|
+
</code>
|
|
26
|
+
)
|
|
27
|
+
}
|
|
@@ -87,6 +87,7 @@ export function ColumnToggle({ columns, visible, onChange }: ColumnToggleProps)
|
|
|
87
87
|
<label className="col-toggle__label">
|
|
88
88
|
<input
|
|
89
89
|
type="checkbox"
|
|
90
|
+
aria-label={col.label}
|
|
90
91
|
className="col-toggle__check"
|
|
91
92
|
checked={visible.has(col.key)}
|
|
92
93
|
disabled={col.required}
|
|
@@ -4,7 +4,10 @@ import type { KeyboardEvent } from 'react'
|
|
|
4
4
|
export interface ContextMenuItem {
|
|
5
5
|
id: string
|
|
6
6
|
label: string
|
|
7
|
+
/** Red tone (destructive). Mutually exclusive with `warning`. */
|
|
7
8
|
danger?: boolean
|
|
9
|
+
/** Yellow tone (caution / heads-up). Mutually exclusive with `danger`. */
|
|
10
|
+
warning?: boolean
|
|
8
11
|
disabled?: boolean
|
|
9
12
|
}
|
|
10
13
|
|
|
@@ -100,7 +103,7 @@ export function ContextMenu({
|
|
|
100
103
|
>
|
|
101
104
|
{items.map((entry, i) => {
|
|
102
105
|
if ('separator' in entry) {
|
|
103
|
-
return <li key={i} role="separator" className="ctx-menu-sep" />
|
|
106
|
+
return <li key={i} role="separator" aria-hidden="true" className="ctx-menu-sep" />
|
|
104
107
|
}
|
|
105
108
|
const ai = actionItems.indexOf(entry)
|
|
106
109
|
return (
|
|
@@ -111,6 +114,7 @@ export function ContextMenu({
|
|
|
111
114
|
aria-disabled={entry.disabled || undefined}
|
|
112
115
|
data-active={ai === activeIdx ? 'true' : undefined}
|
|
113
116
|
data-danger={entry.danger || undefined}
|
|
117
|
+
data-warning={entry.warning || undefined}
|
|
114
118
|
className="ctx-menu-item"
|
|
115
119
|
onMouseEnter={() => setActiveIdx(ai)}
|
|
116
120
|
onMouseDown={(e) => {
|
|
@@ -145,8 +145,23 @@ export function DateRangePicker({
|
|
|
145
145
|
setPicking(null)
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
|
+
// Escape closes the dialog and restores focus to the trigger. Handled
|
|
149
|
+
// at the document level (rather than a JSX onKeyDown on the dialog) so
|
|
150
|
+
// it works regardless of focus position inside the popover.
|
|
151
|
+
function onKey(e: KeyboardEvent) {
|
|
152
|
+
if (e.key === 'Escape') {
|
|
153
|
+
e.preventDefault()
|
|
154
|
+
setOpen(false)
|
|
155
|
+
setPicking(null)
|
|
156
|
+
triggerRef.current?.focus()
|
|
157
|
+
}
|
|
158
|
+
}
|
|
148
159
|
document.addEventListener('mousedown', handler)
|
|
149
|
-
|
|
160
|
+
document.addEventListener('keydown', onKey)
|
|
161
|
+
return () => {
|
|
162
|
+
document.removeEventListener('mousedown', handler)
|
|
163
|
+
document.removeEventListener('keydown', onKey)
|
|
164
|
+
}
|
|
150
165
|
}, [open])
|
|
151
166
|
|
|
152
167
|
function renderCalendar(grid: Date[], gridMonth: number, gridYear: number) {
|
|
@@ -302,14 +317,6 @@ export function DateRangePicker({
|
|
|
302
317
|
aria-modal="true"
|
|
303
318
|
aria-label="Select date range"
|
|
304
319
|
className="drp-cal"
|
|
305
|
-
onKeyDown={(e) => {
|
|
306
|
-
if (e.key === 'Escape') {
|
|
307
|
-
e.preventDefault()
|
|
308
|
-
setOpen(false)
|
|
309
|
-
setPicking(null)
|
|
310
|
-
triggerRef.current?.focus()
|
|
311
|
-
}
|
|
312
|
-
}}
|
|
313
320
|
>
|
|
314
321
|
<div className="drp-months">
|
|
315
322
|
{renderCalendar(leftGrid, viewMonth, viewYear)}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface DeltaProps extends HTMLAttributes<HTMLSpanElement> {
|
|
4
|
+
dir: 'up' | 'down' | 'flat'
|
|
5
|
+
children?: ReactNode
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Trend indicator span used inside `<Submeta>` or any inline copy where a delta
|
|
10
|
+
* needs to be colour-coded (success/green when up, danger/red when down,
|
|
11
|
+
* neutral when flat). Emits a visually-hidden `Trend: ` prefix so screen
|
|
12
|
+
* readers announce the direction independent of the arrow glyph the caller
|
|
13
|
+
* supplies in the copy.
|
|
14
|
+
*
|
|
15
|
+
* Typed equivalent of `<span className="delta-up | delta-down">` from
|
|
16
|
+
* `@w5-ui/tokens/dashboard.css`.
|
|
17
|
+
*/
|
|
18
|
+
export function Delta({ dir, className = '', children, ...rest }: DeltaProps) {
|
|
19
|
+
const cls = dir === 'flat' ? '' : `delta-${dir}`
|
|
20
|
+
const merged = `${cls} ${className}`.trim()
|
|
21
|
+
return (
|
|
22
|
+
<>
|
|
23
|
+
<span className="sr-only">Trend: </span>
|
|
24
|
+
<span className={merged || undefined} {...rest}>
|
|
25
|
+
{children}
|
|
26
|
+
</span>
|
|
27
|
+
</>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface Display1Props extends HTMLAttributes<HTMLHeadingElement> {
|
|
4
|
+
children?: ReactNode
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Display-1 - the 64 px / bold / `--lh-tight` / `--tracking-display`
|
|
9
|
+
* (-0.02em) rhythm reserved for onboarding splashes and marketing
|
|
10
|
+
* heroes (the top of the type scale, one step above the 48 px
|
|
11
|
+
* `<Display2>`). Renders `<h1 class="dash-ui-display-1">` so the
|
|
12
|
+
* `.dash-ui-display-1` rule in `@w5-ui/tokens/tokens.css` pins the
|
|
13
|
+
* canonical size, weight, line-height, letter-spacing, and `--text-1`
|
|
14
|
+
* colour.
|
|
15
|
+
*
|
|
16
|
+
* Stops userland reaching for an ad-hoc `<h1 style={{ fontSize: 64 }}>`
|
|
17
|
+
* that almost always forgets `--lh-tight` (1.1) and the negative
|
|
18
|
+
* tracking, which is the documented "headings break awkwardly across
|
|
19
|
+
* two lines" drift in **Foundations / Type**.
|
|
20
|
+
*
|
|
21
|
+
* Typed equivalent of `<h1 className="dash-ui-display-1">`. Use for
|
|
22
|
+
* onboarding splashes or large marketing heroes. In-app pages should
|
|
23
|
+
* reach for `<PageHeader>` (page title) and `<H2>` (section heads);
|
|
24
|
+
* never substitute `<Display1>` for an in-app screen — its 64 px
|
|
25
|
+
* rhythm dwarfs dashboard chrome.
|
|
26
|
+
*/
|
|
27
|
+
export function Display1({ className = '', children, ...rest }: Display1Props) {
|
|
28
|
+
return (
|
|
29
|
+
<h1 className={`dash-ui-display-1 ${className}`.trim()} {...rest}>
|
|
30
|
+
{children}
|
|
31
|
+
</h1>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface Display2Props extends HTMLAttributes<HTMLHeadingElement> {
|
|
4
|
+
children?: ReactNode
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Display-2 - the 48 px / bold / `--lh-tight` / `--tracking-display`
|
|
9
|
+
* (-0.02em) rhythm used for login and large marketing surfaces (the
|
|
10
|
+
* step below the 64 px Display-1 hero). Renders
|
|
11
|
+
* `<h1 class="dash-ui-display-2">` so the `.dash-ui-display-2` rule in
|
|
12
|
+
* `@w5-ui/tokens/tokens.css` pins the canonical size, weight,
|
|
13
|
+
* line-height, letter-spacing, and `--text-1` colour.
|
|
14
|
+
*
|
|
15
|
+
* Stops userland reaching for an ad-hoc `<h1 style={{ fontSize: 48 }}>`
|
|
16
|
+
* that almost always forgets `--lh-tight` (1.1) and the negative
|
|
17
|
+
* tracking, which is the documented "headings break awkwardly across
|
|
18
|
+
* two lines" drift in **Foundations / Type**.
|
|
19
|
+
*
|
|
20
|
+
* Typed equivalent of `<h1 className="dash-ui-display-2">`. Use for
|
|
21
|
+
* login-page titles or large marketing heroes one step down from
|
|
22
|
+
* `<Display1>`. In-app pages should reach for `<PageHeader>` (page
|
|
23
|
+
* title) and `<H2>` (section heads); never substitute `<Display2>` for
|
|
24
|
+
* an in-app screen — its 48 px rhythm dominates dashboard chrome.
|
|
25
|
+
*/
|
|
26
|
+
export function Display2({ className = '', children, ...rest }: Display2Props) {
|
|
27
|
+
return (
|
|
28
|
+
<h1 className={`dash-ui-display-2 ${className}`.trim()} {...rest}>
|
|
29
|
+
{children}
|
|
30
|
+
</h1>
|
|
31
|
+
)
|
|
32
|
+
}
|