@w5-ui/react 0.2.1 → 0.5.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 (40) hide show
  1. package/README.md +118 -4
  2. package/package.json +2 -2
  3. package/src/components/AppLayout.tsx +38 -0
  4. package/src/components/AppShell.tsx +41 -49
  5. package/src/components/Body.tsx +32 -0
  6. package/src/components/BodyLg.tsx +29 -0
  7. package/src/components/Caption.tsx +29 -0
  8. package/src/components/Card.tsx +13 -0
  9. package/src/components/CardMore.tsx +22 -0
  10. package/src/components/Code.tsx +27 -0
  11. package/src/components/ContextMenu.tsx +4 -0
  12. package/src/components/Delta.tsx +29 -0
  13. package/src/components/Display1.tsx +33 -0
  14. package/src/components/Display2.tsx +32 -0
  15. package/src/components/Eyebrow.tsx +28 -0
  16. package/src/components/FieldStack.tsx +19 -0
  17. package/src/components/FormActions.tsx +20 -0
  18. package/src/components/FormRow.tsx +19 -0
  19. package/src/components/Grid.tsx +21 -0
  20. package/src/components/H1.tsx +32 -0
  21. package/src/components/H2.tsx +32 -0
  22. package/src/components/H3.tsx +32 -0
  23. package/src/components/H4.tsx +31 -0
  24. package/src/components/Label.tsx +30 -0
  25. package/src/components/Mac.tsx +25 -0
  26. package/src/components/Modal.tsx +16 -2
  27. package/src/components/Mono.tsx +31 -0
  28. package/src/components/Muted.tsx +27 -0
  29. package/src/components/NameCell.tsx +45 -0
  30. package/src/components/NotifDot.tsx +24 -0
  31. package/src/components/PageHeader.tsx +20 -0
  32. package/src/components/SrOnly.tsx +25 -0
  33. package/src/components/StatNumber.tsx +25 -0
  34. package/src/components/StatusRing.tsx +38 -0
  35. package/src/components/Submeta.tsx +23 -0
  36. package/src/components/TablePager.tsx +27 -0
  37. package/src/components/TableShell.tsx +23 -0
  38. package/src/components/TopbarActions.tsx +70 -0
  39. package/src/components/Unit.tsx +25 -0
  40. 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 tokens and
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 { Card, Pill, Button, Topbar, Sidebar } from '@w5-ui/react'
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.2.1",
3
+ "version": "0.5.0",
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.2.1"
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, SearchIcon, UpdatesIcon, BellIcon, HelpIcon } from '../icons.js'
4
- import { Avatar } from './Avatar.js'
5
- import { IconButton } from './Button.js'
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
- <button
41
- type="button"
42
- className="site-switch"
43
- aria-label={`Switch site: ${siteName}`}
44
- aria-haspopup="menu"
45
- >
46
- <span className="status-ring" />
47
- <span className="site-name" aria-hidden="true">
48
- {siteName}
49
- </span>
50
- <CaretIcon className="caret" aria-hidden="true" />
51
- </button>
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
- <IconButton aria-label="Search" title="Search">
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
+ }
@@ -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
+ }
@@ -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
 
@@ -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) => {
@@ -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
+ }
@@ -0,0 +1,28 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react'
2
+
3
+ export interface EyebrowProps extends HTMLAttributes<HTMLSpanElement> {
4
+ children?: ReactNode
5
+ }
6
+
7
+ /**
8
+ * Uppercase, tracked overline used as a section eyebrow / category tag
9
+ * above a heading or beside a primary label. Renders
10
+ * `<span class="dash-ui-eyebrow">` so the `.dash-ui-eyebrow` rule in
11
+ * `@w5-ui/tokens/css` applies the canonical 11 px / semibold weight,
12
+ * 0.08em letter-spacing, uppercase text-transform and the muted
13
+ * `--text-3` colour. Stops userland reaching for a one-off
14
+ * `text-transform: uppercase` + `letter-spacing` + `font-size: 11px`
15
+ * trio on every overline.
16
+ *
17
+ * Typed equivalent of `<span className="dash-ui-eyebrow">`. The
18
+ * contrast pair is AA against every motif background (verified for
19
+ * `--text-3` over `--depthBg-0`/`--depthBg-1`/`--depthBg-2` in both
20
+ * dark and light motifs).
21
+ */
22
+ export function Eyebrow({ className = '', children, ...rest }: EyebrowProps) {
23
+ return (
24
+ <span className={`dash-ui-eyebrow ${className}`.trim()} {...rest}>
25
+ {children}
26
+ </span>
27
+ )
28
+ }
@@ -0,0 +1,19 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react'
2
+
3
+ export interface FieldStackProps extends HTMLAttributes<HTMLDivElement> {
4
+ children?: ReactNode
5
+ }
6
+
7
+ /**
8
+ * Single-column form rhythm: a vertical stack of `<Field>`s with the canonical
9
+ * 16 px gap. Typed equivalent of `<div className="field-stack">` from
10
+ * `@w5-ui/tokens/dashboard.css`. Reach for this inside narrow `<Card>` tiles
11
+ * and `<Modal>` bodies.
12
+ */
13
+ export function FieldStack({ className = '', children, ...rest }: FieldStackProps) {
14
+ return (
15
+ <div className={`field-stack ${className}`.trim()} {...rest}>
16
+ {children}
17
+ </div>
18
+ )
19
+ }
@@ -0,0 +1,20 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react'
2
+
3
+ export interface FormActionsProps extends HTMLAttributes<HTMLDivElement> {
4
+ children?: ReactNode
5
+ }
6
+
7
+ /**
8
+ * Right-aligned action row that closes a form `<Card>` or modal body:
9
+ * `display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px`.
10
+ * Typed equivalent of `<div className="form-actions">` from
11
+ * `@w5-ui/tokens/dashboard.css`. Place destructive / cancel buttons on the
12
+ * left, primary on the right.
13
+ */
14
+ export function FormActions({ className = '', children, ...rest }: FormActionsProps) {
15
+ return (
16
+ <div className={`form-actions ${className}`.trim()} {...rest}>
17
+ {children}
18
+ </div>
19
+ )
20
+ }