@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
@@ -0,0 +1,19 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react'
2
+
3
+ export interface FormRowProps extends HTMLAttributes<HTMLDivElement> {
4
+ children?: ReactNode
5
+ }
6
+
7
+ /**
8
+ * Two-column form grid: `1fr 1fr` with the canonical 14 px gap and 6 px top
9
+ * offset that every Settings card uses. Typed equivalent of
10
+ * `<div className="form-row">` from `@w5-ui/tokens/dashboard.css`. Use inside
11
+ * wider `<Card>` tiles for dense editors.
12
+ */
13
+ export function FormRow({ className = '', children, ...rest }: FormRowProps) {
14
+ return (
15
+ <div className={`form-row ${className}`.trim()} {...rest}>
16
+ {children}
17
+ </div>
18
+ )
19
+ }
@@ -0,0 +1,21 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react'
2
+
3
+ export interface GridProps extends HTMLAttributes<HTMLDivElement> {
4
+ children?: ReactNode
5
+ }
6
+
7
+ /**
8
+ * The canonical 12-column page grid: `repeat(12, 1fr)` with a 12 px gap and
9
+ * `16px 24px` padding, reserving room for the bottom panel when present.
10
+ *
11
+ * Typed equivalent of `<div className="grid">` from `@w5-ui/tokens/dashboard.css`.
12
+ * Use as a direct child of the scrolling `.content` region; place `<Card>` /
13
+ * `<Stat>` tiles inside with `span={4 | 6 | 8 | 12}`.
14
+ */
15
+ export function Grid({ className = '', children, ...rest }: GridProps) {
16
+ return (
17
+ <div className={`grid ${className}`.trim()} {...rest}>
18
+ {children}
19
+ </div>
20
+ )
21
+ }
@@ -0,0 +1,32 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react'
2
+
3
+ export interface H1Props extends HTMLAttributes<HTMLHeadingElement> {
4
+ children?: ReactNode
5
+ }
6
+
7
+ /**
8
+ * Page title - the 38 px / bold / `--lh-snug` rhythm with
9
+ * `--tracking-display` (-0.02em) used for the top-of-page heading on
10
+ * marketing surfaces (login, onboarding splash, top-level landing).
11
+ * Renders `<h1 class="dash-ui-h1">` so the `.dash-ui-h1` rule in
12
+ * `@w5-ui/tokens/tokens.css` pins the canonical size, weight,
13
+ * line-height, letter-spacing, and `--text-1` colour. Stops userland
14
+ * reaching for a one-off `<h1 style={{ fontSize: 36 }}>` (or worse,
15
+ * leaving an unstyled browser default `<h1>` at 32 px / 700 / 1.2
16
+ * with no negative tracking, which is the documented "headings break
17
+ * awkwardly across two lines" drift fixed by `--tracking-display`).
18
+ *
19
+ * Typed equivalent of `<h1 className="dash-ui-h1">`. This is the
20
+ * userland-facing semantic class documented in **Foundations / Type**.
21
+ * Use `<H1>` for marketing-grade page titles. In-app pages should reach
22
+ * for `<PageHeader>` (which already styles the page title) and use
23
+ * `<H2>` for section heads, `<H3>` for sub-sections, `<H4>` for card
24
+ * titles.
25
+ */
26
+ export function H1({ className = '', children, ...rest }: H1Props) {
27
+ return (
28
+ <h1 className={`dash-ui-h1 ${className}`.trim()} {...rest}>
29
+ {children}
30
+ </h1>
31
+ )
32
+ }
@@ -0,0 +1,32 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react'
2
+
3
+ export interface H2Props extends HTMLAttributes<HTMLHeadingElement> {
4
+ children?: ReactNode
5
+ }
6
+
7
+ /**
8
+ * Page section heading - the 30 px / semibold / `--lh-snug` rhythm used
9
+ * for top-of-section labels on a dashboard page (above a grid of cards,
10
+ * above a table, above a settings group). Renders
11
+ * `<h2 class="dash-ui-h2">` so the `.dash-ui-h2` rule in
12
+ * `@w5-ui/tokens/tokens.css` pins the canonical size, weight,
13
+ * line-height, and `--text-1` colour. Stops userland reaching for a
14
+ * one-off `<h2 style={{ fontSize: 30, fontWeight: 600 }}>` (or worse,
15
+ * leaving an unstyled browser default `<h2>` at ~24 px with 700 weight
16
+ * and 1.2 line-height, which is the documented "section headings feel
17
+ * cramped on one line" drift).
18
+ *
19
+ * Typed equivalent of `<h2 className="dash-ui-h2">`. This is the
20
+ * userland-facing semantic class documented in **Foundations / Type**.
21
+ * Use `<H2>` for the page section head ("Devices", "Wi-Fi", "Settings")
22
+ * sitting above a content block. For card titles prefer `<H4>` (or just
23
+ * `<Card title="…">`, which injects `.dash-ui-h4` for you); for full
24
+ * page titles in marketing pages prefer `<H1>`.
25
+ */
26
+ export function H2({ className = '', children, ...rest }: H2Props) {
27
+ return (
28
+ <h2 className={`dash-ui-h2 ${className}`.trim()} {...rest}>
29
+ {children}
30
+ </h2>
31
+ )
32
+ }
@@ -0,0 +1,32 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react'
2
+
3
+ export interface H3Props extends HTMLAttributes<HTMLHeadingElement> {
4
+ children?: ReactNode
5
+ }
6
+
7
+ /**
8
+ * Sub-section heading - the 24 px / semibold / `--lh-snug` rhythm used
9
+ * for sub-section labels on a dashboard page (a heading inside a card
10
+ * region, a settings sub-group, a divider above a secondary table).
11
+ * Renders `<h3 class="dash-ui-h3">` so the `.dash-ui-h3` rule in
12
+ * `@w5-ui/tokens/tokens.css` pins the canonical size, weight,
13
+ * line-height, and `--text-1` colour. Stops userland reaching for a
14
+ * one-off `<h3 style={{ fontSize: 24, fontWeight: 600 }}>` (or worse,
15
+ * leaving an unstyled browser default `<h3>` at ~18.7 px with 700
16
+ * weight and 1.2 line-height, which is the documented "sub-section
17
+ * headings drift away from the reference" case).
18
+ *
19
+ * Typed equivalent of `<h3 className="dash-ui-h3">`. This is the
20
+ * userland-facing semantic class documented in **Foundations / Type**.
21
+ * Use `<H3>` for a sub-section head sitting below an `<H2>` page
22
+ * section head. For card titles prefer `<H4>` (or just
23
+ * `<Card title="…">`, which injects `.dash-ui-h4` for you); for page
24
+ * section heads prefer `<H2>`.
25
+ */
26
+ export function H3({ className = '', children, ...rest }: H3Props) {
27
+ return (
28
+ <h3 className={`dash-ui-h3 ${className}`.trim()} {...rest}>
29
+ {children}
30
+ </h3>
31
+ )
32
+ }
@@ -0,0 +1,31 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react'
2
+
3
+ export interface H4Props extends HTMLAttributes<HTMLHeadingElement> {
4
+ children?: ReactNode
5
+ }
6
+
7
+ /**
8
+ * Card / tile title - the 18 px / semibold / `--lh-snug` rhythm used
9
+ * for the heading that sits inside a card, a stat tile, or a settings
10
+ * sub-panel. Renders `<h4 class="dash-ui-h4">` so the `.dash-ui-h4`
11
+ * rule in `@w5-ui/tokens/tokens.css` pins the canonical size, weight,
12
+ * line-height, and `--text-1` colour. Stops userland reaching for a
13
+ * one-off `<h3 style={{ fontSize: 20 }}>` or `text-xl` (the documented
14
+ * "card title size doesn't match the reference" case), and stops them
15
+ * leaving an unstyled browser default `<h4>` at ~13.3 px / 700 / 1.2
16
+ * which reads as caption text instead of a title.
17
+ *
18
+ * Typed equivalent of `<h4 className="dash-ui-h4">`. This is the
19
+ * userland-facing semantic class documented in **Foundations / Type**.
20
+ * Use `<H4>` when you build a card-like tile by hand and need the
21
+ * canonical 18 / 600 title. If you reach for `<Card title="…">` you
22
+ * get the same class injected for you - prefer that. For page section
23
+ * heads prefer `<H2>`; for sub-section heads prefer `<H3>`.
24
+ */
25
+ export function H4({ className = '', children, ...rest }: H4Props) {
26
+ return (
27
+ <h4 className={`dash-ui-h4 ${className}`.trim()} {...rest}>
28
+ {children}
29
+ </h4>
30
+ )
31
+ }
@@ -0,0 +1,30 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react'
2
+
3
+ export interface LabelProps extends HTMLAttributes<HTMLSpanElement> {
4
+ children?: ReactNode
5
+ }
6
+
7
+ /**
8
+ * Small categorical label - the line above a KPI number ("Active devices"),
9
+ * the cell heading on a hand-rolled form row, the tag on a definition list
10
+ * term, anywhere a 13 px / medium-weight tag belongs without the full form
11
+ * field machinery. Renders `<span class="dash-ui-label">` so the
12
+ * `.dash-ui-label` rule in `@w5-ui/tokens/tokens.css` applies the canonical
13
+ * 13 px / 500 weight, 1.25 line-height, and `--text-2` colour. Stops
14
+ * userland reaching for a one-off `font-size: 13px; font-weight: 500;` duo
15
+ * on every KPI label or hand-rolled form caption.
16
+ *
17
+ * Typed equivalent of `<span className="dash-ui-label">`. This is the
18
+ * userland-facing semantic class documented in **Foundations / Type**.
19
+ * For real form fields prefer `<Field label="…">` - it wires `htmlFor`,
20
+ * required, hint, and error semantics for you. For KPI tiles prefer the
21
+ * `<Stat>` label slot. Reach for `<Label>` only when hand-rolling a tile
22
+ * or row where the higher-level component doesn't fit.
23
+ */
24
+ export function Label({ className = '', children, ...rest }: LabelProps) {
25
+ return (
26
+ <span className={`dash-ui-label ${className}`.trim()} {...rest}>
27
+ {children}
28
+ </span>
29
+ )
30
+ }
@@ -0,0 +1,25 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react'
2
+
3
+ export interface MacProps extends HTMLAttributes<HTMLSpanElement> {
4
+ children?: ReactNode
5
+ }
6
+
7
+ /**
8
+ * Inline monospace label for MAC addresses, IPs, SKUs, serial numbers and
9
+ * other fixed-width identifiers that read inside a table cell or a property
10
+ * row. Renders a `<span class="mac">` so the `.mac` rule in
11
+ * `@w5-ui/tokens/dashboard.css` applies the canonical JetBrains Mono family,
12
+ * 12 px size, and muted colour. Stops userland reaching for inline
13
+ * `font-family` / `font-size` overrides on bare identifiers.
14
+ *
15
+ * Typed equivalent of `<span className="mac">` (and `<td className="mac">`
16
+ * inside a table — apply the class directly to the `<td>` there, or wrap the
17
+ * cell contents with `<Mac>` when the cell carries other markup too).
18
+ */
19
+ export function Mac({ className = '', children, ...rest }: MacProps) {
20
+ return (
21
+ <span className={`mac ${className}`.trim()} {...rest}>
22
+ {children}
23
+ </span>
24
+ )
25
+ }
@@ -2,18 +2,32 @@ import { useEffect, useId, useRef, type ReactNode } from 'react'
2
2
  import { CloseIcon } from '../icons.js'
3
3
  import { IconButton } from './Button.js'
4
4
 
5
+ export type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | '2xl'
6
+
5
7
  export interface ModalProps {
6
8
  open: boolean
7
9
  title: string
10
+ /** Panel width. `md` (520px) is the default; larger sizes suit
11
+ * data-dense or editor-style dialogs. Always capped at 90vw. */
12
+ size?: ModalSize
8
13
  onClose: () => void
9
14
  children?: ReactNode
10
15
  footer?: ReactNode
11
16
  }
12
17
 
18
+ /** Width modifier appended to `.modal`; `md` is the base rule. */
19
+ const MODAL_SIZE: Record<ModalSize, string> = {
20
+ sm: 'modal--sm',
21
+ md: '',
22
+ lg: 'modal--lg',
23
+ xl: 'modal--xl',
24
+ '2xl': 'modal--2xl',
25
+ }
26
+
13
27
  const FOCUSABLE =
14
28
  'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])'
15
29
 
16
- export function Modal({ open, title, onClose, children, footer }: ModalProps) {
30
+ export function Modal({ open, title, size = 'md', onClose, children, footer }: ModalProps) {
17
31
  const titleId = useId()
18
32
  const modalRef = useRef<HTMLDivElement>(null)
19
33
  const downOnBackdropRef = useRef(false)
@@ -67,7 +81,7 @@ export function Modal({ open, title, onClose, children, footer }: ModalProps) {
67
81
  >
68
82
  <div
69
83
  ref={modalRef}
70
- className="modal"
84
+ className={`modal ${MODAL_SIZE[size]}`.trim()}
71
85
  role="dialog"
72
86
  aria-modal="true"
73
87
  aria-labelledby={titleId}
@@ -0,0 +1,31 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react'
2
+
3
+ export interface MonoProps extends HTMLAttributes<HTMLSpanElement> {
4
+ children?: ReactNode
5
+ }
6
+
7
+ /**
8
+ * Inline monospace identifier for MAC addresses, IPs, hex IDs, ports,
9
+ * firmware versions, config keys, and other machine-shaped values that
10
+ * read inside body copy or a property row. Renders a
11
+ * `<span class="dash-ui-mono">` so the `.dash-ui-mono` rule in
12
+ * `@w5-ui/tokens/tokens.css` applies the canonical JetBrains Mono family,
13
+ * 13 px size, regular weight, 1.4 line-height, and `--text-2` colour. The
14
+ * class also enables `font-variant-numeric: tabular-nums` so a column of
15
+ * MACs / IPs / versions aligns vertically. Stops userland reaching for
16
+ * bare `font-family: monospace` (which falls back to a serif Courier with
17
+ * proportional digits) or one-off `font-size` overrides on identifiers.
18
+ *
19
+ * Typed equivalent of `<span className="dash-ui-mono">`. This is the
20
+ * userland-facing semantic class documented in **Foundations / Type**.
21
+ * Prefer `<Mono>` for new userland code. Reach for `<Mac>` only when you
22
+ * need the dashboard's denser 12 px `.mac` variant (table cells in the
23
+ * reference Devices / Clients pages).
24
+ */
25
+ export function Mono({ className = '', children, ...rest }: MonoProps) {
26
+ return (
27
+ <span className={`dash-ui-mono ${className}`.trim()} {...rest}>
28
+ {children}
29
+ </span>
30
+ )
31
+ }
@@ -0,0 +1,27 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react'
2
+
3
+ export interface MutedProps extends HTMLAttributes<HTMLSpanElement> {
4
+ children?: ReactNode
5
+ }
6
+
7
+ /**
8
+ * Inline span that drops a phrase to the tertiary text colour
9
+ * (`--text-3`). Renders `<span class="t-muted">` so the `.t-muted` rule in
10
+ * `@w5-ui/tokens/dashboard.css` applies the canonical muted colour used by
11
+ * `.submeta`, `.stat .unit` and the `.card h3` title chrome. Use for
12
+ * "X active" counters next to an icon, caption-style metadata sitting
13
+ * beside a primary label, or any sub-line that would otherwise tempt
14
+ * userland to reach for a one-off `color: #...` override.
15
+ *
16
+ * Typed equivalent of `<span className="t-muted">`. The contrast pair is
17
+ * AA against every motif background (verified for `--text-3` over
18
+ * `--depthBg-0`/`--depthBg-1`/`--depthBg-2` in both dark and light
19
+ * motifs).
20
+ */
21
+ export function Muted({ className = '', children, ...rest }: MutedProps) {
22
+ return (
23
+ <span className={`t-muted ${className}`.trim()} {...rest}>
24
+ {children}
25
+ </span>
26
+ )
27
+ }
@@ -0,0 +1,45 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react'
2
+
3
+ export interface NameCellProps extends HTMLAttributes<HTMLDivElement> {
4
+ /**
5
+ * Square 28 px avatar / initial that sits to the left of the name. Pass a
6
+ * letter (`r[0]`), an emoji, or a small SVG. Rendered inside a
7
+ * `<span class="nc-thumb">` so the canonical thumbnail rule applies.
8
+ * Omit if the row has no leading affordance.
9
+ */
10
+ thumb?: ReactNode
11
+ /**
12
+ * Extra `className` for the inner `.nc-thumb` (e.g. `'cam'` for the
13
+ * camera gradient variant). Ignored when `thumb` is not supplied.
14
+ */
15
+ thumbClassName?: string
16
+ children?: ReactNode
17
+ }
18
+
19
+ /**
20
+ * First-column row in a list / table: a 28 px square thumbnail or initial
21
+ * followed by the device / client / alert name. Renders the canonical
22
+ * `<div class="name-cell"><span class="nc-thumb">…</span>…</div>` markup so
23
+ * the `.name-cell` and `.nc-thumb` rules from `@w5-ui/tokens/dashboard.css`
24
+ * apply (28 px thumb, 10 px gap, white name colour, JetBrains Mono initial).
25
+ * Stops userland reaching for ad-hoc flex / padding / colour overrides on the
26
+ * first column.
27
+ *
28
+ * Typed equivalent of the manual `<div className="name-cell"><span
29
+ * className="nc-thumb">{x}</span>{name}</div>` pattern used across the
30
+ * Dashboard, Devices, Wi-Fi, and Alarms reference pages.
31
+ */
32
+ export function NameCell({
33
+ className = '',
34
+ thumb,
35
+ thumbClassName = '',
36
+ children,
37
+ ...rest
38
+ }: NameCellProps) {
39
+ return (
40
+ <div className={`name-cell ${className}`.trim()} {...rest}>
41
+ {thumb !== undefined && <span className={`nc-thumb ${thumbClassName}`.trim()}>{thumb}</span>}
42
+ {children}
43
+ </div>
44
+ )
45
+ }
@@ -0,0 +1,24 @@
1
+ import type { HTMLAttributes } from 'react'
2
+
3
+ export interface NotifDotProps extends HTMLAttributes<HTMLSpanElement> {}
4
+
5
+ /**
6
+ * Tiny red dot that sits on top of a bell / mail / inbox icon to flag unread
7
+ * activity. Renders `<span aria-hidden class="topbar-notif-dot">` so the
8
+ * `.topbar-notif-dot` rule in `@w5-ui/tokens/dashboard.css` paints the canonical
9
+ * 6 px circle with the danger colour and a neutral-10 ring (the ring is what
10
+ * keeps the dot legible over the deep topbar background).
11
+ *
12
+ * The dot is presentational — wrap your bell `<IconButton>` and put the live
13
+ * count in its `aria-label` (e.g. `aria-label="Notifications, 3 new"`) so
14
+ * screen readers announce the number while sighted users see the indicator.
15
+ * The hosting button must be `position: relative` for the dot to anchor in the
16
+ * top-right corner.
17
+ *
18
+ * Typed equivalent of `<span aria-hidden className="topbar-notif-dot" />`.
19
+ * Use when userland slots a custom `<Topbar actions>` block and needs to keep
20
+ * the unread indicator wired up without copying the class name.
21
+ */
22
+ export function NotifDot({ className = '', ...rest }: NotifDotProps) {
23
+ return <span aria-hidden="true" className={`topbar-notif-dot ${className}`.trim()} {...rest} />
24
+ }
@@ -0,0 +1,20 @@
1
+ import type { ReactNode } from 'react'
2
+
3
+ export interface PageHeaderProps {
4
+ title: ReactNode
5
+ actions?: ReactNode
6
+ sticky?: boolean
7
+ className?: string
8
+ }
9
+
10
+ export function PageHeader({ title, actions, sticky = true, className = '' }: PageHeaderProps) {
11
+ return (
12
+ <div
13
+ className={`ph-bar ${className}`.trim()}
14
+ style={sticky ? undefined : { position: 'static' }}
15
+ >
16
+ <h2 className="ph-title">{title}</h2>
17
+ {actions && <div className="ph-actions">{actions}</div>}
18
+ </div>
19
+ )
20
+ }
@@ -0,0 +1,25 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react'
2
+
3
+ export interface SrOnlyProps extends HTMLAttributes<HTMLSpanElement> {
4
+ children?: ReactNode
5
+ }
6
+
7
+ /**
8
+ * Visually-hidden text that stays available to assistive tech. Renders a
9
+ * `<span class="sr-only">` so the `.sr-only` rule in
10
+ * `@w5-ui/tokens/dashboard.css` clips it from the visual layout while keeping
11
+ * it announceable by screen readers. Use it to name regions that the design
12
+ * intentionally leaves unlabelled, the way the reference Tables recipe wires
13
+ * `<caption className="sr-only">` onto every `<table>` (WCAG 1.3.1).
14
+ *
15
+ * Typed equivalent of `<span className="sr-only">`. Apply the class directly
16
+ * to a `<caption>` / `<label>` when the whole element is the hidden text, or
17
+ * wrap an inline phrase with `<SrOnly>` when it sits alongside visible markup.
18
+ */
19
+ export function SrOnly({ className = '', children, ...rest }: SrOnlyProps) {
20
+ return (
21
+ <span className={`sr-only ${className}`.trim()} {...rest}>
22
+ {children}
23
+ </span>
24
+ )
25
+ }
@@ -0,0 +1,25 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react'
2
+
3
+ export interface StatNumberProps extends HTMLAttributes<HTMLDivElement> {
4
+ children?: ReactNode
5
+ }
6
+
7
+ /**
8
+ * Hero-number row inside a `<Card>`: the big tabular-nums value (optionally
9
+ * followed by a trailing `<Unit>`). Renders a `<div class="stat">` so the
10
+ * `.stat` rule in `@w5-ui/tokens/dashboard.css` sets the canonical 30 px size,
11
+ * semibold weight, tabular figures, and primary text colour. Stops userland
12
+ * reaching for inline `font-size` / `font-weight` overrides on bare numbers.
13
+ *
14
+ * Typed equivalent of `<div className="stat">`; pair with `<Unit>` for the
15
+ * suffix label, and `<Submeta>` for the muted line underneath. `<Stat>`
16
+ * already wires all three internally — reach for `<StatNumber>` when you
17
+ * compose `.stat` by hand (e.g. number above a chart inside a `<Card>`).
18
+ */
19
+ export function StatNumber({ className = '', children, ...rest }: StatNumberProps) {
20
+ return (
21
+ <div className={`stat ${className}`.trim()} {...rest}>
22
+ {children}
23
+ </div>
24
+ )
25
+ }
@@ -0,0 +1,38 @@
1
+ import type { HTMLAttributes } from 'react'
2
+
3
+ export type StatusRingStatus = 'ok' | 'warn' | 'danger' | 'neutral'
4
+
5
+ export interface StatusRingProps extends HTMLAttributes<HTMLSpanElement> {
6
+ /** Health colour of the ring. Defaults to `'ok'` (success green). */
7
+ status?: StatusRingStatus
8
+ }
9
+
10
+ const MODIFIER: Record<StatusRingStatus, string> = {
11
+ ok: '',
12
+ warn: 'status-ring--warn',
13
+ danger: 'status-ring--danger',
14
+ neutral: 'status-ring--neutral',
15
+ }
16
+
17
+ /**
18
+ * Pulsing status indicator: a soft translucent ring around a solid coloured
19
+ * inner dot. Renders `<span aria-hidden class="status-ring">` so the
20
+ * `.status-ring` rule in `@w5-ui/tokens/dashboard.css` paints the canonical
21
+ * 18 px ring + 8 px inner dot without userland having to hand-roll the two
22
+ * layers.
23
+ *
24
+ * The `status` prop swaps the ring + dot colour: `'ok'` (default, success
25
+ * green), `'warn'` (amber), `'danger'` (red), `'neutral'` (grey). Use it to
26
+ * drive a live-health signal next to the site name in `<Topbar>` or on a
27
+ * tunnel / session / device row.
28
+ *
29
+ * Decorative — the wrapper sets `aria-hidden`; pair with a label so screen
30
+ * readers announce the state. Typed equivalent of
31
+ * `<span aria-hidden className="status-ring status-ring--<status>" />`. Stops
32
+ * userland copying the class name into bespoke status rows and silently
33
+ * drifting when the ring colour / size tokens change.
34
+ */
35
+ export function StatusRing({ className = '', status = 'ok', ...rest }: StatusRingProps) {
36
+ const cls = ['status-ring', MODIFIER[status], className].filter(Boolean).join(' ')
37
+ return <span aria-hidden="true" className={cls} {...rest} />
38
+ }
@@ -0,0 +1,23 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react'
2
+
3
+ export interface SubmetaProps extends HTMLAttributes<HTMLDivElement> {
4
+ children?: ReactNode
5
+ }
6
+
7
+ /**
8
+ * Secondary one-line caption inside a `<Card>` tile: the muted row that sits
9
+ * under the headline `.stat` number and carries supporting metrics, deltas, or
10
+ * status. Carries the canonical 12 px font size, `--text-2` colour, and the
11
+ * `↑` / `↓` separator rhythm the Dashboard reference tiles use.
12
+ *
13
+ * Typed equivalent of `<div className="submeta">` from
14
+ * `@w5-ui/tokens/dashboard.css`. Pair with `.delta-up` / `.delta-down` spans
15
+ * for trend text so screen readers announce the direction.
16
+ */
17
+ export function Submeta({ className = '', children, ...rest }: SubmetaProps) {
18
+ return (
19
+ <div className={`submeta ${className}`.trim()} {...rest}>
20
+ {children}
21
+ </div>
22
+ )
23
+ }
@@ -0,0 +1,27 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react'
2
+
3
+ export interface TablePagerProps extends HTMLAttributes<HTMLDivElement> {
4
+ children?: ReactNode
5
+ }
6
+
7
+ /**
8
+ * Sibling row that sits directly under a `<TableShell>` (or
9
+ * `<div className="table-shell">`) and carries the rows-per-page control on
10
+ * the left and the `<Pagination>` nav on the right. Renders the canonical
11
+ * `<div class="table-pager">` markup so the flex / space-between / 12 px top
12
+ * margin from `@w5-ui/tokens/dashboard.css` applies and userland drops the
13
+ * inline `style={{ display: 'flex', justifyContent: 'space-between', … }}`
14
+ * wrapper that every reference page used to ship.
15
+ *
16
+ * Typed equivalent of the manual `<div className="table-pager">` pattern
17
+ * documented in **Foundations / Tables**. Render it as a sibling of
18
+ * `<TableShell>` — never inside it — so the focus rings on the pager buttons
19
+ * are not clipped by `overflow: hidden`.
20
+ */
21
+ export function TablePager({ className = '', children, ...rest }: TablePagerProps) {
22
+ return (
23
+ <div className={`table-pager ${className}`.trim()} {...rest}>
24
+ {children}
25
+ </div>
26
+ )
27
+ }
@@ -0,0 +1,23 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react'
2
+
3
+ export interface TableShellProps extends HTMLAttributes<HTMLDivElement> {
4
+ children?: ReactNode
5
+ }
6
+
7
+ /**
8
+ * Rounded, bordered wrapper for any full-width `<table>` on a page. Carries the
9
+ * 8 px radius, `--neutral-09` background, single 1 px border and `overflow:
10
+ * hidden` clip so userland tables read like the reference Devices / Clients
11
+ * pages without inline `border` / `borderRadius` styles.
12
+ *
13
+ * Typed equivalent of `<div className="table-shell">` from
14
+ * `@w5-ui/tokens/dashboard.css`. Render the `<Pagination>` row as a sibling
15
+ * below, not inside, so its focus rings aren't clipped.
16
+ */
17
+ export function TableShell({ className = '', children, ...rest }: TableShellProps) {
18
+ return (
19
+ <div className={`table-shell ${className}`.trim()} {...rest}>
20
+ {children}
21
+ </div>
22
+ )
23
+ }