Portal: full mock-driven surfaces, demonolithed components, backend-ready mocks (#6686)

This commit is contained in:
Reece Browne
2026-06-16 12:20:35 +01:00
committed by GitHub
parent 6716398ccb
commit 96accea984
146 changed files with 12221 additions and 286 deletions
+58 -92
View File
@@ -1,10 +1,8 @@
import { useEffect, useId, useRef, type ReactNode } from "react";
import { useEffect, useId, type ReactNode } from "react";
import { createPortal } from "react-dom";
import { FocusTrap } from "@mantine/core";
import "@shared/components/Drawer.css";
const FOCUSABLE_SELECTOR =
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
export type DrawerSide = "right" | "left";
export type DrawerWidth = "sm" | "md" | "lg";
@@ -26,9 +24,10 @@ export interface DrawerProps {
}
/**
* Side drawer — sibling to {@link Modal}. Slides in from `side`, locks body
* scroll, closes on backdrop click or Escape. PipelineDetailDrawer, doc
* detail drawers, etc. should all sit on top of this primitive.
* Side drawer — sibling to {@link Modal}, with our own brand shell. Tab focus
* trapping, initial focus, and focus restoration on close are delegated to
* Mantine's <FocusTrap>; we keep the slide-in chrome, scroll lock, and ESC /
* backdrop dismissal.
*/
export function Drawer({
open,
@@ -44,7 +43,6 @@ export function Drawer({
ariaLabel,
children,
}: DrawerProps) {
const dialogRef = useRef<HTMLElement | null>(null);
const titleId = useId();
useEffect(() => {
@@ -65,40 +63,6 @@ export function Drawer({
};
}, [open]);
useEffect(() => {
if (!open) return;
const previouslyFocused = document.activeElement as HTMLElement | null;
const dialog = dialogRef.current;
const first = dialog?.querySelector<HTMLElement>(FOCUSABLE_SELECTOR);
first?.focus();
function onKey(e: KeyboardEvent) {
if (e.key !== "Tab" || !dialog) return;
const focusables =
dialog.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR);
if (focusables.length === 0) {
e.preventDefault();
return;
}
const firstEl = focusables[0];
const lastEl = focusables[focusables.length - 1];
const active = document.activeElement;
if (e.shiftKey && active === firstEl) {
e.preventDefault();
lastEl.focus();
} else if (!e.shiftKey && active === lastEl) {
e.preventDefault();
firstEl.focus();
}
}
document.addEventListener("keydown", onKey);
return () => {
document.removeEventListener("keydown", onKey);
previouslyFocused?.focus?.();
};
}, [open]);
if (!open) return null;
function onBackdropClick() {
@@ -114,57 +78,59 @@ export function Drawer({
onClick={onBackdropClick}
role="presentation"
/>
<aside
ref={dialogRef}
className={[
"sui-drawer",
`sui-drawer--${side}`,
`sui-drawer--${width}`,
className ?? "",
]
.filter(Boolean)
.join(" ")}
role="dialog"
aria-modal="true"
aria-labelledby={hasTitle ? titleId : undefined}
aria-label={!hasTitle ? ariaLabel : undefined}
>
{(title || subtitle) && (
<header className="sui-drawer__header">
<div className="sui-drawer__header-text">
{title && (
<div id={titleId} className="sui-drawer__title">
{title}
</div>
)}
{subtitle && <div className="sui-drawer__sub">{subtitle}</div>}
</div>
<button
type="button"
className="sui-drawer__close"
onClick={onClose}
aria-label="Close"
>
<svg
viewBox="0 0 24 24"
width="16"
height="16"
fill="none"
stroke="currentColor"
strokeWidth={1.75}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
<FocusTrap active>
<aside
className={[
"sui-drawer",
`sui-drawer--${side}`,
`sui-drawer--${width}`,
className ?? "",
]
.filter(Boolean)
.join(" ")}
role="dialog"
aria-modal="true"
aria-labelledby={hasTitle ? titleId : undefined}
aria-label={!hasTitle ? ariaLabel : undefined}
tabIndex={-1}
>
{(title || subtitle) && (
<header className="sui-drawer__header">
<div className="sui-drawer__header-text">
{title && (
<div id={titleId} className="sui-drawer__title">
{title}
</div>
)}
{subtitle && <div className="sui-drawer__sub">{subtitle}</div>}
</div>
<button
type="button"
className="sui-drawer__close"
onClick={onClose}
aria-label="Close"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</header>
)}
<div className="sui-drawer__body">{children}</div>
{footer && <footer className="sui-drawer__footer">{footer}</footer>}
</aside>
<svg
viewBox="0 0 24 24"
width="16"
height="16"
fill="none"
stroke="currentColor"
strokeWidth={1.75}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</header>
)}
<div className="sui-drawer__body">{children}</div>
{footer && <footer className="sui-drawer__footer">{footer}</footer>}
</aside>
</FocusTrap>
</>,
document.body,
);
@@ -0,0 +1,11 @@
.sui-metric-strip {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.75rem;
}
@media (max-width: 50rem) {
.sui-metric-strip {
grid-template-columns: repeat(2, 1fr);
}
}
@@ -0,0 +1,24 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { MetricStrip } from "@shared/components/MetricStrip";
import { MetricCard } from "@shared/components/MetricCard";
const meta: Meta<typeof MetricStrip> = {
title: "Layout/MetricStrip",
component: MetricStrip,
tags: ["autodocs"],
parameters: { layout: "padded" },
};
export default meta;
type Story = StoryObj<typeof MetricStrip>;
/** Four-up KPI row; collapses to two columns below 50rem. */
export const Default: Story = {
render: () => (
<MetricStrip>
<MetricCard label="Docs / 30d" value="48,210" delta={0.12} />
<MetricCard label="Pipelines" value="12" delta={0.16} />
<MetricCard label="Agents active" value="7" delta={0.4} />
<MetricCard label="Eval pass rate" value="94.6%" delta={0.02} />
</MetricStrip>
),
};
@@ -0,0 +1,24 @@
import type { ReactNode } from "react";
import "@shared/components/MetricStrip.css";
export interface MetricStripProps {
children: ReactNode;
className?: string;
}
/**
* Responsive grid wrapper for a row of {@link MetricCard}s — the prototype's
* "metric strip" (Home, Sources, Usage, Infrastructure all use it). Four-up on
* wide screens, two-up below 50rem.
*/
export function MetricStrip({ children, className }: MetricStripProps) {
return (
<div
className={["sui-metric-strip", className ?? ""]
.filter(Boolean)
.join(" ")}
>
{children}
</div>
);
}
+55 -91
View File
@@ -1,5 +1,6 @@
import { useEffect, useId, useRef, type ReactNode } from "react";
import { useEffect, useId, type ReactNode } from "react";
import { createPortal } from "react-dom";
import { FocusTrap } from "@mantine/core";
import "@shared/components/Modal.css";
export type ModalWidth = "sm" | "md" | "lg" | "xl";
@@ -25,16 +26,12 @@ export interface ModalProps {
children?: ReactNode;
}
const FOCUSABLE_SELECTOR =
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
/**
* Portal-rendered modal. Backdrop fades in, dialog scales in. ESC and
* click-on-backdrop close by default. Each instance is self-contained — the
* caller owns the open state and the close handler.
*
* Focus is trapped inside the dialog while open and returned to the
* previously-focused element on close.
* Portal-rendered modal with our own brand shell (header / body / footer,
* width presets, backdrop). The hard part — trapping Tab focus inside the
* dialog, initial focus, and restoring focus to the opener on close — is
* delegated to Mantine's <FocusTrap> rather than hand-rolled. ESC and
* backdrop click close by default; the caller owns the open state.
*/
export function Modal({
open,
@@ -49,7 +46,6 @@ export function Modal({
className,
children,
}: ModalProps) {
const dialogRef = useRef<HTMLDivElement | null>(null);
const titleId = useId();
useEffect(() => {
@@ -70,40 +66,6 @@ export function Modal({
};
}, [open]);
useEffect(() => {
if (!open) return;
const previouslyFocused = document.activeElement as HTMLElement | null;
const dialog = dialogRef.current;
const first = dialog?.querySelector<HTMLElement>(FOCUSABLE_SELECTOR);
first?.focus();
function onKey(e: KeyboardEvent) {
if (e.key !== "Tab" || !dialog) return;
const focusables =
dialog.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR);
if (focusables.length === 0) {
e.preventDefault();
return;
}
const firstEl = focusables[0];
const lastEl = focusables[focusables.length - 1];
const active = document.activeElement;
if (e.shiftKey && active === firstEl) {
e.preventDefault();
lastEl.focus();
} else if (!e.shiftKey && active === lastEl) {
e.preventDefault();
firstEl.focus();
}
}
document.addEventListener("keydown", onKey);
return () => {
document.removeEventListener("keydown", onKey);
previouslyFocused?.focus?.();
};
}, [open]);
if (!open) return null;
function onBackdropClick() {
@@ -118,53 +80,55 @@ export function Modal({
onClick={onBackdropClick}
role="presentation"
>
<div
ref={dialogRef}
className={["sui-modal", `sui-modal--${width}`, className ?? ""]
.filter(Boolean)
.join(" ")}
onClick={(e) => e.stopPropagation()}
role="dialog"
aria-modal="true"
aria-labelledby={hasTitle ? titleId : undefined}
aria-label={!hasTitle ? ariaLabel : undefined}
>
{(title || subtitle) && (
<header className="sui-modal__header">
<div className="sui-modal__header-text">
{title && (
<div id={titleId} className="sui-modal__title">
{title}
</div>
)}
{subtitle && <div className="sui-modal__sub">{subtitle}</div>}
</div>
<button
type="button"
className="sui-modal__close"
onClick={onClose}
aria-label="Close"
>
<svg
viewBox="0 0 24 24"
width="16"
height="16"
fill="none"
stroke="currentColor"
strokeWidth={1.75}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
<FocusTrap active>
<div
className={["sui-modal", `sui-modal--${width}`, className ?? ""]
.filter(Boolean)
.join(" ")}
onClick={(e) => e.stopPropagation()}
role="dialog"
aria-modal="true"
aria-labelledby={hasTitle ? titleId : undefined}
aria-label={!hasTitle ? ariaLabel : undefined}
tabIndex={-1}
>
{(title || subtitle) && (
<header className="sui-modal__header">
<div className="sui-modal__header-text">
{title && (
<div id={titleId} className="sui-modal__title">
{title}
</div>
)}
{subtitle && <div className="sui-modal__sub">{subtitle}</div>}
</div>
<button
type="button"
className="sui-modal__close"
onClick={onClose}
aria-label="Close"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</header>
)}
<div className="sui-modal__body">{children}</div>
{footer && <footer className="sui-modal__footer">{footer}</footer>}
</div>
<svg
viewBox="0 0 24 24"
width="16"
height="16"
fill="none"
stroke="currentColor"
strokeWidth={1.75}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</header>
)}
<div className="sui-modal__body">{children}</div>
{footer && <footer className="sui-modal__footer">{footer}</footer>}
</div>
</FocusTrap>
</div>,
document.body,
);
+39
View File
@@ -0,0 +1,39 @@
.sui-stat {
display: flex;
flex-direction: column;
gap: 0.25rem;
min-width: 0;
}
.sui-stat__label {
font-size: 0.6875rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--color-text-5);
font-weight: 600;
}
.sui-stat__value {
font-size: 0.9375rem;
font-weight: 600;
color: var(--color-text-1);
font-variant-numeric: tabular-nums;
}
.sui-stat__value--success {
color: var(--color-green-dark);
}
.sui-stat__value--warning {
color: var(--color-amber-dark);
}
.sui-stat__value--danger {
color: var(--color-red);
}
/* Inline code in a value (masked keys, model ids, URLs) reads as mono meta. */
.sui-stat__value code {
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--color-text-2);
word-break: break-all;
}
@@ -0,0 +1,31 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { StatTile } from "@shared/components/StatTile";
const meta: Meta<typeof StatTile> = {
title: "Primitives/StatTile",
component: StatTile,
tags: ["autodocs"],
parameters: { layout: "padded" },
args: { label: "Error rate", value: "0.4%", tone: "default" },
argTypes: {
tone: {
control: "inline-radio",
options: ["default", "success", "warning", "danger"],
},
},
};
export default meta;
type Story = StoryObj<typeof StatTile>;
export const Playground: Story = {};
export const ToneRow: Story = {
render: () => (
<div style={{ display: "flex", gap: 32 }}>
<StatTile label="Uptime" value="99.98%" tone="success" />
<StatTile label="Error rate" value="1.4%" tone="warning" />
<StatTile label="Error rate" value="6.2%" tone="danger" />
<StatTile label="P95 latency" value="412 ms" />
</div>
),
};
+38
View File
@@ -0,0 +1,38 @@
import type { ReactNode } from "react";
import "@shared/components/StatTile.css";
export type StatTileTone = "default" | "success" | "warning" | "danger";
export interface StatTileProps {
label: ReactNode;
value: ReactNode;
/** Colours the value (e.g. error rate over threshold). */
tone?: StatTileTone;
className?: string;
}
/**
* Compact label-over-value stat used inside detail panels, cards, and metric
* grids (the small sibling of {@link MetricCard}). Value uses tabular figures
* so columns of stats stay aligned.
*/
export function StatTile({
label,
value,
tone = "default",
className,
}: StatTileProps) {
return (
<div className={["sui-stat", className ?? ""].filter(Boolean).join(" ")}>
<span className="sui-stat__label">{label}</span>
<span
className={
"sui-stat__value" +
(tone !== "default" ? ` sui-stat__value--${tone}` : "")
}
>
{value}
</span>
</div>
);
}
+61
View File
@@ -0,0 +1,61 @@
.sui-table-wrap {
width: 100%;
overflow-x: auto;
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-surface);
}
.sui-table {
width: 100%;
border-collapse: collapse;
font-size: 0.8125rem;
}
.sui-table__th {
text-align: left;
font-weight: 600;
color: var(--color-text-4);
font-size: 0.6875rem;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 0.625rem 0.875rem;
border-bottom: 1px solid var(--color-border);
white-space: nowrap;
}
.sui-table__th--right,
.sui-table__td--right {
text-align: right;
}
.sui-table__th--center,
.sui-table__td--center {
text-align: center;
}
.sui-table__td {
padding: 0.625rem 0.875rem;
color: var(--color-text-2);
border-bottom: 1px solid var(--color-border-light);
vertical-align: middle;
}
.sui-table tbody tr:last-child .sui-table__td {
border-bottom: none;
}
.sui-table__row--interactive {
cursor: pointer;
transition: background var(--motion-fast);
}
.sui-table__row--interactive:hover {
background: var(--color-bg-hover);
}
.sui-table__row--interactive:focus-visible {
outline: 0.125rem solid var(--color-blue);
outline-offset: -0.125rem;
}
.sui-table__empty {
padding: 2rem;
text-align: center;
color: var(--color-text-4);
}
@@ -0,0 +1,109 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Table, type TableColumn } from "@shared/components/Table";
import { StatusBadge } from "@shared/components/StatusBadge";
interface Region {
id: string;
name: string;
code: string;
status: "healthy" | "degraded";
docs: number;
latency: string;
}
const REGIONS: Region[] = [
{
id: "1",
name: "US East",
code: "us-east-1",
status: "healthy",
docs: 12481,
latency: "41 ms",
},
{
id: "2",
name: "US West",
code: "us-west-2",
status: "healthy",
docs: 8210,
latency: "63 ms",
},
{
id: "3",
name: "EU West",
code: "eu-west-1",
status: "degraded",
docs: 3044,
latency: "190 ms",
},
];
const COLUMNS: TableColumn<Region>[] = [
{ key: "name", header: "Region", render: (r) => r.name },
{
key: "code",
header: "Code",
render: (r) => (
<code style={{ fontFamily: "var(--font-mono)" }}>{r.code}</code>
),
},
{
key: "status",
header: "Status",
render: (r) => (
<StatusBadge
tone={r.status === "healthy" ? "success" : "warning"}
size="sm"
>
{r.status}
</StatusBadge>
),
},
{
key: "docs",
header: "Docs 24h",
align: "right",
render: (r) => r.docs.toLocaleString(),
},
{ key: "latency", header: "P95", align: "right", render: (r) => r.latency },
];
const meta: Meta<typeof Table> = {
title: "Compound/Table",
component: Table,
tags: ["autodocs"],
parameters: { layout: "padded" },
};
export default meta;
type Story = StoryObj<typeof Table>;
/** Presentational table — columns own their cell renderers; pass pre-sorted rows. */
export const Basic: Story = {
render: () => (
<Table<Region> columns={COLUMNS} rows={REGIONS} rowKey={(r) => r.id} />
),
};
/** With `onRowClick`, rows become focusable + hoverable (keyboard: Enter/Space). */
export const Interactive: Story = {
render: () => (
<Table<Region>
columns={COLUMNS}
rows={REGIONS}
rowKey={(r) => r.id}
onRowClick={() => {}}
/>
),
};
/** Empty body slot. */
export const Empty: Story = {
render: () => (
<Table<Region>
columns={COLUMNS}
rows={[]}
rowKey={(r) => r.id}
empty="No regions deployed yet."
/>
),
};
+106
View File
@@ -0,0 +1,106 @@
import type { ReactNode } from "react";
import "@shared/components/Table.css";
export interface TableColumn<T> {
/** Stable column id. */
key: string;
header: ReactNode;
/** Cell renderer for a row. */
render: (row: T) => ReactNode;
align?: "left" | "right" | "center";
/** Optional fixed/min width (any CSS length). */
width?: string;
}
export interface TableProps<T> {
columns: TableColumn<T>[];
rows: T[];
/** Stable key per row. */
rowKey: (row: T) => string;
/** Makes rows interactive (hover + click + keyboard). */
onRowClick?: (row: T) => void;
/** Rendered in place of the body when there are no rows. */
empty?: ReactNode;
className?: string;
}
/**
* Minimal data table primitive. Columns own their own cell renderers, so the
* table stays presentational — callers pre-sort/filter and pass the rows they
* want shown. Rows become focusable buttons-in-disguise when `onRowClick` is
* set.
*/
export function Table<T>({
columns,
rows,
rowKey,
onRowClick,
empty,
className,
}: TableProps<T>) {
const interactive = Boolean(onRowClick);
return (
<div
className={["sui-table-wrap", className ?? ""].filter(Boolean).join(" ")}
>
<table className="sui-table">
<thead>
<tr>
{columns.map((c) => (
<th
key={c.key}
scope="col"
className={`sui-table__th sui-table__th--${c.align ?? "left"}`}
style={c.width ? { width: c.width } : undefined}
>
{c.header}
</th>
))}
</tr>
</thead>
<tbody>
{rows.length === 0 ? (
<tr>
<td className="sui-table__empty" colSpan={columns.length}>
{empty ?? "No data"}
</td>
</tr>
) : (
rows.map((row) => (
<tr
key={rowKey(row)}
className={
interactive
? "sui-table__row sui-table__row--interactive"
: "sui-table__row"
}
onClick={onRowClick ? () => onRowClick(row) : undefined}
tabIndex={interactive ? 0 : undefined}
role={interactive ? "button" : undefined}
onKeyDown={
interactive
? (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onRowClick?.(row);
}
}
: undefined
}
>
{columns.map((c) => (
<td
key={c.key}
className={`sui-table__td sui-table__td--${c.align ?? "left"}`}
>
{c.render(row)}
</td>
))}
</tr>
))
)}
</tbody>
</table>
</div>
);
}
+3
View File
@@ -14,6 +14,8 @@ export * from "@shared/components/Modal";
// Layout
export * from "@shared/components/Stack";
export * from "@shared/components/Inline";
export * from "@shared/components/MetricStrip";
export * from "@shared/components/StatTile";
// Feedback
export * from "@shared/components/Spinner";
@@ -28,6 +30,7 @@ export * from "@shared/components/Toast";
export * from "@shared/components/Tabs";
export * from "@shared/components/Dropdown";
export * from "@shared/components/Drawer";
export * from "@shared/components/Table";
// Forms
export * from "@shared/components/FormField";