@tonycodes-ui/mortar 0.11.0 → 0.13.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.
@@ -3,6 +3,7 @@ import { type ReactNode } from "react";
3
3
  import { type Tone } from "./primitives.js";
4
4
  import { type SignInProvider } from "./auth.js";
5
5
  import { type HeadingLevel } from "./heading.js";
6
+ import { type FlyoutItem } from "./internal.js";
6
7
  /**
7
8
  * A navigation link; `icon` replaces the default marker in the sidebar. With
8
9
  * `children` the item becomes a disclosure: its own `href` is the first link
@@ -55,29 +56,41 @@ export interface StackedShellProps {
55
56
  brand: string;
56
57
  logo?: ReactNode;
57
58
  homeHref?: string;
58
- navigation: LinkItem[];
59
+ /** Top-level links, or labelled groups that open a dropdown panel. */
60
+ navigation: NavbarItem[];
59
61
  actions?: ReactNode;
60
62
  /** Page heading shown above the content, e.g. a PageHeading. */
61
63
  header?: ReactNode;
62
64
  width?: "md" | "lg" | "xl" | "full";
65
+ /** The bar's surface: default, inverse (the other appearance) or accent (the brand's accent fill). */
66
+ tone?: NavbarTone;
67
+ /** A second row of page tabs under the bar, rendered as a TabNav named by `label`. */
68
+ tabs?: ItemGroup<TabNavItem>;
63
69
  children: ReactNode;
64
70
  className?: string;
65
71
  }
66
72
  /** Top navigation with a centred page column: Tailwind's stacked application layout. */
67
- export declare function StackedShell({ brand, logo, homeHref, navigation, actions, header, width, children, className, }: StackedShellProps): import("react").JSX.Element;
73
+ export declare function StackedShell({ brand, logo, homeHref, navigation, actions, header, width, tone, tabs, children, className, }: StackedShellProps): import("react").JSX.Element;
74
+ /** A labelled group of links that opens a dropdown panel from the bar. */
75
+ export type NavbarGroup = ItemGroup<FlyoutItem>;
76
+ /** A top-level link, or a group; a group holding the current page is marked current. */
77
+ export type NavbarItem = LinkItem | NavbarGroup;
78
+ export type NavbarTone = "default" | "inverse" | "accent";
68
79
  export interface NavbarProps {
69
80
  brand: string;
70
81
  logo?: ReactNode;
71
82
  homeHref?: string;
72
- /** Top-level links; the navbar has no sub-menus. */
73
- items: LinkItem[];
83
+ /** Top-level links, or labelled groups that open a dropdown panel. */
84
+ items: NavbarItem[];
74
85
  actions?: ReactNode;
86
+ /** The bar's surface: default, inverse (the other appearance) or accent (the brand's accent fill). */
87
+ tone?: NavbarTone;
75
88
  /** Accessible name of the navigation landmark. */
76
89
  label?: string;
77
90
  className?: string;
78
91
  }
79
92
  /** Horizontal application navigation that collapses into a menu on narrow screens. */
80
- export declare function Navbar({ brand, logo, homeHref, items, actions, label, className, }: NavbarProps): import("react").JSX.Element;
93
+ export declare function Navbar({ brand, logo, homeHref, items, actions, tone, label, className, }: NavbarProps): import("react").JSX.Element;
81
94
  export interface BreadcrumbItem {
82
95
  label: string;
83
96
  href?: string;
@@ -198,6 +211,18 @@ export interface DataTableColumn<T> {
198
211
  width?: string;
199
212
  /** Clip long values to one line with an ellipsis. */
200
213
  truncate?: boolean;
214
+ /**
215
+ * How long the column stays as the table narrows (its own width, not the
216
+ * screen's): `medium` hides below 480px, `low` below 768px. The first
217
+ * column always shows. Hidden columns leave the accessibility tree too, so
218
+ * keep what people need in visible columns or on the detail page.
219
+ */
220
+ priority?: "high" | "medium" | "low";
221
+ /**
222
+ * `end` pins the column to the trailing edge while the table scrolls
223
+ * sideways, so a row-actions or status column stays reachable.
224
+ */
225
+ pin?: "end";
201
226
  }
202
227
  export interface DataTableProps<T extends {
203
228
  id: string;
@@ -5,7 +5,7 @@ import { Button, EmptyState, Input, Notice } from "./primitives.js";
5
5
  import { BrandMark, Divider } from "./layout.js";
6
6
  import { AuthHeader, ProviderButtons } from "./auth.js";
7
7
  import { ArrowDown, ArrowUp, Check, SortArrows } from "./icons.js";
8
- import { LinkContent, NavGroups, NavTreeLinks, MobileNavDialog, TermList, cx, componentRoot, classedIntroSlots, useDomId, } from "./internal.js";
8
+ import { Flyout, LinkContent, NavGroups, NavTreeLinks, MobileNavDialog, TermList, cx, componentRoot, classedIntroSlots, sectionTone, useDomId, } from "./internal.js";
9
9
  function SidebarContent({ brand, logo, workspace, homeHref, navigation, sections = [], footer, label = "Primary", onNavigate, }) {
10
10
  return (_jsxs(_Fragment, { children: [brand && (_jsxs(KitLink, { className: "m-workspace", href: homeHref || navigation[0]?.href || "#", children: [_jsx(BrandMark, { logo: logo }), _jsxs("span", { children: [_jsx("strong", { children: brand }), workspace && _jsx("small", { children: workspace })] })] })), _jsxs("nav", { "aria-label": label, children: [_jsx(NavTreeLinks, { items: navigation, marker: true, onNavigate: onNavigate }), _jsx(NavGroups, { groups: sections, marker: true, onNavigate: onNavigate })] }), footer && _jsx("div", { className: "m-user", children: footer })] }));
11
11
  }
@@ -29,15 +29,16 @@ export function AppShell({ brand, logo, workspace, homeHref, footer, navigation,
29
29
  return (_jsxs("div", { ...componentRoot("AppShell"), className: cx("m-app-shell", className), children: [_jsx(KitLink, { className: "m-skip", href: `#${contentId}`, children: "Skip to content" }), _jsx("aside", { className: "m-sidebar", children: _jsx(SidebarContent, { ...sidebar }) }), _jsxs("div", { className: "m-app-main", children: [_jsxs("header", { className: "m-app-header", children: [_jsx(Button, { variant: "ghost", className: "m-menu-trigger", "aria-haspopup": "dialog", "aria-expanded": open, onClick: () => setOpen(true), children: "Menu" }), _jsx("div", { className: "m-header-context", children: header || workspace || brand }), _jsx("div", { className: "m-actions", children: actions })] }), aside ? (_jsxs("div", { className: "m-app-columns", children: [main, _jsx("aside", { className: "m-app-aside", "aria-label": asideLabel, children: aside })] })) : (main)] }), _jsx(MobileNavDialog, { open: open, onClose: () => setOpen(false), title: brand, children: _jsx(SidebarContent, { ...sidebar, onNavigate: () => setOpen(false) }) })] }));
30
30
  }
31
31
  /** Top navigation with a centred page column: Tailwind's stacked application layout. */
32
- export function StackedShell({ brand, logo, homeHref, navigation, actions, header, width = "lg", children, className, }) {
32
+ export function StackedShell({ brand, logo, homeHref, navigation, actions, header, width = "lg", tone, tabs, children, className, }) {
33
33
  const contentId = useDomId("m-stacked-");
34
- return (_jsxs("div", { ...componentRoot("StackedShell"), className: cx("m-stacked-shell", className), children: [_jsx(KitLink, { className: "m-skip", href: `#${contentId}`, children: "Skip to content" }), _jsx(Navbar, { brand: brand, logo: logo, homeHref: homeHref, items: navigation, actions: actions }), _jsxs("main", { id: contentId, tabIndex: -1, "data-width": width, children: [header && _jsx("div", { className: "m-stacked-header", children: header }), children] })] }));
34
+ return (_jsxs("div", { ...componentRoot("StackedShell"), className: cx("m-stacked-shell", className), children: [_jsx(KitLink, { className: "m-skip", href: `#${contentId}`, children: "Skip to content" }), _jsx(Navbar, { brand: brand, logo: logo, homeHref: homeHref, items: navigation, actions: actions, tone: tone }), tabs && (_jsx("div", { className: "m-stacked-tabs", children: _jsx(TabNav, { label: tabs.label, items: tabs.items }) })), _jsxs("main", { id: contentId, tabIndex: -1, "data-width": width, children: [header && _jsx("div", { className: "m-stacked-header", children: header }), children] })] }));
35
35
  }
36
36
  /** Horizontal application navigation that collapses into a menu on narrow screens. */
37
- export function Navbar({ brand, logo, homeHref = "/", items, actions, label = "Primary", className, }) {
37
+ export function Navbar({ brand, logo, homeHref = "/", items, actions, tone, label = "Primary", className, }) {
38
38
  const [open, setOpen] = useState(false);
39
39
  const id = useId();
40
- return (_jsxs("header", { ...componentRoot("Navbar"), className: cx("m-navbar", className), children: [_jsxs(KitLink, { className: "m-brand", href: homeHref, children: [_jsx(BrandMark, { logo: logo }), brand] }), _jsx(Button, { variant: "ghost", className: "m-navbar-toggle", "aria-expanded": open, "aria-controls": id, onClick: () => setOpen(!open), children: "Menu" }), _jsx("nav", { id: id, "aria-label": label, "data-open": open || undefined, children: items.map((item, index) => (_jsx(KitLink, { href: item.href, "aria-current": item.current ? "page" : undefined, onClick: () => setOpen(false), children: _jsx(LinkContent, { prefix: "nav", icon: item.icon, label: item.label, count: item.count }) }, `${item.label}:${item.href}:${index}`))) }), actions && _jsx("div", { className: "m-actions", children: actions })] }));
40
+ const close = () => setOpen(false);
41
+ return (_jsxs("header", { ...componentRoot("Navbar"), className: cx("m-navbar", className), "data-tone": sectionTone(tone), children: [_jsxs(KitLink, { className: "m-brand", href: homeHref, children: [_jsx(BrandMark, { logo: logo }), brand] }), _jsx(Button, { variant: "ghost", className: "m-navbar-toggle", "aria-expanded": open, "aria-controls": id, onClick: () => setOpen(!open), children: "Menu" }), _jsx("nav", { id: id, "aria-label": label, "data-open": open || undefined, children: items.map((item, index) => "items" in item ? (_jsx(Flyout, { label: item.label, items: item.items, onNavigate: close }, `${item.label}:group:${index}`)) : (_jsx(KitLink, { href: item.href, "aria-current": item.current ? "page" : undefined, onClick: close, children: _jsx(LinkContent, { prefix: "nav", icon: item.icon, label: item.label, count: item.count }) }, `${item.label}:${item.href}:${index}`))) }), actions && _jsx("div", { className: "m-actions", children: actions })] }));
41
42
  }
42
43
  /** Trail to the current page; the item without an href is the current page. */
43
44
  export function Breadcrumbs({ items, label = "Breadcrumb", separator = "slash", className, }) {
@@ -86,6 +87,15 @@ export function MetricGroup({ label, items, variant = "shared", className, }) {
86
87
  details: (_jsxs(_Fragment, { children: [_jsx("strong", { children: item.value }), item.previous && (_jsxs("span", { className: "m-metric-previous", children: ["from ", item.previous] })), item.change && (_jsxs("span", { className: "m-metric-change", "data-tone": item.tone && item.tone !== "neutral" ? item.tone : undefined, "data-trend": item.trend, children: [item.trend && (_jsxs(_Fragment, { children: [item.trend === "up" ? _jsx(ArrowUp, {}) : _jsx(ArrowDown, {}), _jsx("span", { className: "m-sr-only", children: item.trend === "up" ? "Increased by " : "Decreased by " })] })), item.change] }))] })),
87
88
  })) }));
88
89
  }
90
+ /** Responsive attributes shared by a column's `col`, header and cells. */
91
+ function columnLayout(col, index) {
92
+ return {
93
+ "data-priority": index > 0 && col.priority && col.priority !== "high"
94
+ ? col.priority
95
+ : undefined,
96
+ "data-pin": col.pin,
97
+ };
98
+ }
89
99
  const collator = new Intl.Collator(undefined, {
90
100
  numeric: true,
91
101
  sensitivity: "base",
@@ -171,12 +181,14 @@ export function DataTable({ caption, columns, rows, selectedIds = [], onSelectio
171
181
  ? selectedIds.filter((rowId) => rowId !== row.id)
172
182
  : [...selectedIds, row.id]) }) })), columns.map((col, index) => {
173
183
  const content = col.truncate ? (_jsx("span", { className: "m-truncate", children: col.render(row) })) : (col.render(row));
174
- return index === 0 ? (_jsx("th", { scope: "row", "data-align": columnAlign(col.align), "data-truncate": col.truncate || undefined, children: href ? (_jsx(KitLink, { className: "m-row-link", href: href, children: content })) : (content) }, col.key)) : (_jsx("td", { "data-align": columnAlign(col.align), "data-truncate": col.truncate || undefined, children: content }, col.key));
184
+ return index === 0 ? (_jsx("th", { scope: "row", ...columnLayout(col, index), "data-align": columnAlign(col.align), "data-truncate": col.truncate || undefined, children: href ? (_jsx(KitLink, { className: "m-row-link", href: href, children: content })) : (content) }, col.key)) : (_jsx("td", { ...columnLayout(col, index), "data-align": columnAlign(col.align), "data-truncate": col.truncate || undefined, children: content }, col.key));
175
185
  })] }, row.id));
176
186
  };
177
- return (_jsxs("section", { id: id, ...componentRoot("DataTable"), className: cx("m-data-table", className), "aria-label": caption, "data-density": density === "default" ? undefined : density, "data-striped": striped || undefined, children: [(toolbar || selectedIds.length > 0) && (_jsx("div", { className: "m-table-toolbar", children: selectedIds.length > 0 ? (_jsxs(_Fragment, { children: [_jsxs("strong", { children: [selectedIds.length, " selected"] }), _jsxs("div", { className: "m-actions", children: [bulkActions, _jsx(Button, { variant: "ghost", onClick: () => onSelectionChange?.([]), children: "Clear selection" })] })] })) : (toolbar) })), _jsx("p", { className: loading ? "m-table-state" : "m-sr-only", role: "status", "data-loading": loading || undefined, children: loading ? "Loading records…" : "" }), loading ? null : error ? (_jsx(Notice, { title: "Unable to load records", tone: "danger", announce: true, children: error })) : !rows.length ? (_jsx(EmptyState, { title: empty.title, description: empty.description, action: empty.action, headingLevel: headingLevel })) : (_jsx("div", { className: "m-table-scroll", role: "region", "aria-label": `${caption} scroll area`, tabIndex: 0, style: maxHeight ? { maxHeight } : undefined, "data-sticky": maxHeight ? true : undefined, children: _jsxs("table", { children: [_jsx("caption", { className: "m-sr-only", children: caption }), columns.some((col) => col.width) && (_jsxs("colgroup", { children: [onSelectionChange && _jsx("col", { className: "m-table-select-col" }), columns.map((col) => (_jsx("col", { style: col.width ? { width: col.width } : undefined }, col.key)))] })), _jsx("thead", { children: _jsxs("tr", { children: [onSelectionChange && (_jsx("th", { scope: "col", children: _jsx(SelectAll, { checked: allHere, partial: selectedHere.length > 0 && !allHere, onChange: toggleAll }) })), columns.map((col) => {
187
+ return (_jsxs("section", { id: id, ...componentRoot("DataTable"), className: cx("m-data-table", className), "aria-label": caption, "data-density": density === "default" ? undefined : density, "data-striped": striped || undefined, "data-prioritised": columns.some((col, index) => columnLayout(col, index)["data-priority"])
188
+ ? true
189
+ : undefined, children: [(toolbar || selectedIds.length > 0) && (_jsx("div", { className: "m-table-toolbar", children: selectedIds.length > 0 ? (_jsxs(_Fragment, { children: [_jsxs("strong", { children: [selectedIds.length, " selected"] }), _jsxs("div", { className: "m-actions", children: [bulkActions, _jsx(Button, { variant: "ghost", onClick: () => onSelectionChange?.([]), children: "Clear selection" })] })] })) : (toolbar) })), _jsx("p", { className: loading ? "m-table-state" : "m-sr-only", role: "status", "data-loading": loading || undefined, children: loading ? "Loading records…" : "" }), loading ? null : error ? (_jsx(Notice, { title: "Unable to load records", tone: "danger", announce: true, children: error })) : !rows.length ? (_jsx(EmptyState, { title: empty.title, description: empty.description, action: empty.action, headingLevel: headingLevel })) : (_jsx("div", { className: "m-table-scroll", role: "region", "aria-label": `${caption} scroll area`, tabIndex: 0, style: maxHeight ? { maxHeight } : undefined, "data-sticky": maxHeight ? true : undefined, children: _jsxs("table", { children: [_jsx("caption", { className: "m-sr-only", children: caption }), columns.some((col) => col.width) && (_jsxs("colgroup", { children: [onSelectionChange && _jsx("col", { className: "m-table-select-col" }), columns.map((col, index) => (_jsx("col", { ...columnLayout(col, index), style: col.width ? { width: col.width } : undefined }, col.key)))] })), _jsx("thead", { children: _jsxs("tr", { children: [onSelectionChange && (_jsx("th", { scope: "col", children: _jsx(SelectAll, { checked: allHere, partial: selectedHere.length > 0 && !allHere, onChange: toggleAll }) })), columns.map((col, index) => {
178
190
  const direction = sort?.key === col.key ? sort.direction : undefined;
179
- return (_jsx("th", { scope: "col", "data-align": columnAlign(col.align), "aria-sort": col.sortable ? direction || "none" : undefined, children: col.sortable ? (_jsxs("button", { type: "button", className: "m-sort-button", onClick: () => toggleSort(col.key), children: [col.label, _jsx(SortArrows, { direction: direction })] })) : (col.label) }, col.key));
191
+ return (_jsx("th", { scope: "col", ...columnLayout(col, index), "data-align": columnAlign(col.align), "aria-sort": col.sortable ? direction || "none" : undefined, children: col.sortable ? (_jsxs("button", { type: "button", className: "m-sort-button", onClick: () => toggleSort(col.key), children: [col.label, _jsx(SortArrows, { direction: direction })] })) : (col.label) }, col.key));
180
192
  })] }) }), groupBy ? (groupRows(rows, visibleRows, groupBy).map(([label, group]) => (_jsxs("tbody", { children: [_jsx(TableGroupRow, { label: label, span: span }), group.map(renderRow)] }, label)))) : (_jsx("tbody", { children: visibleRows.map(renderRow) }))] }) })), footer && _jsx("div", { className: "m-table-footer", children: footer })] }));
181
193
  }
182
194
  /** Plain semantic table for documents, settings summaries and comparisons without row actions. */
@@ -0,0 +1,47 @@
1
+ /** A point in the drawing, in the illustration's viewBox units. */
2
+ export interface NetworkNode {
3
+ id: string;
4
+ x: number;
5
+ y: number;
6
+ /** Short text drawn under the node. */
7
+ label?: string;
8
+ /** accent: the brand's accent fill, to pick out a node. */
9
+ tone?: "default" | "accent";
10
+ }
11
+ /** A straight connection between two node ids. */
12
+ export interface NetworkConnection {
13
+ from: string;
14
+ to: string;
15
+ /** Whether a signal travels from `from` to `to`. Default true. */
16
+ signal?: boolean;
17
+ /** Seconds for this signal to cross; defaults to the illustration's duration. */
18
+ duration?: number;
19
+ /** Seconds before this signal starts; defaults to an even stagger. */
20
+ delay?: number;
21
+ }
22
+ /**
23
+ * A lightweight network drawing: nodes and connections supplied as data, with
24
+ * optional signals travelling along the connections. Decorative unless it has
25
+ * a label. The animation is illustrative: it never represents live activity.
26
+ */
27
+ export declare function NetworkIllustration({ nodes, connections, width, height, label, description, motion, duration, paused, onPausedChange, id, className, }: {
28
+ nodes: NetworkNode[];
29
+ connections: NetworkConnection[];
30
+ /** Width of the viewBox; the drawing scales to its container. */
31
+ width?: number;
32
+ /** Height of the viewBox. */
33
+ height?: number;
34
+ /** Makes the drawing informative: an image with this accessible name. Omitted, it is decorative and hidden from assistive technology. */
35
+ label?: string;
36
+ /** With a label, a longer description of what the drawing shows. */
37
+ description?: string;
38
+ /** auto: signals travel where connections carry one, with a pause control. static: the same drawing without motion. */
39
+ motion?: "auto" | "static";
40
+ /** Seconds for a signal to cross a connection. */
41
+ duration?: number;
42
+ /** Whether the user has paused the animation; pass with onPausedChange to control it or share it between illustrations. */
43
+ paused?: boolean;
44
+ onPausedChange?: (paused: boolean) => void;
45
+ id?: string;
46
+ className?: string;
47
+ }): import("react").JSX.Element;
@@ -0,0 +1,47 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef } from "react";
3
+ import { componentRoot, cx, useDomId, usePlayback } from "./internal.js";
4
+ const NODE_RADIUS = 8;
5
+ const SIGNAL_RADIUS = 4;
6
+ /**
7
+ * A lightweight network drawing: nodes and connections supplied as data, with
8
+ * optional signals travelling along the connections. Decorative unless it has
9
+ * a label. The animation is illustrative: it never represents live activity.
10
+ */
11
+ export function NetworkIllustration({ nodes, connections, width = 480, height = 320, label, description, motion = "auto", duration = 3, paused, onPausedChange, id, className, }) {
12
+ const ref = useRef(null);
13
+ const titleId = useDomId("m-network-title");
14
+ const descId = useDomId("m-network-desc");
15
+ const byId = new Map(nodes.map((node) => [node.id, node]));
16
+ const links = connections.flatMap((connection) => {
17
+ const from = byId.get(connection.from);
18
+ const to = byId.get(connection.to);
19
+ return from && to ? [{ ...connection, a: from, b: to }] : [];
20
+ });
21
+ const carriers = links.filter((link) => link.signal !== false);
22
+ const animated = motion === "auto" && carriers.length > 0;
23
+ const { reason, reduced, userPaused, toggle } = usePlayback(ref, {
24
+ paused,
25
+ onPausedChange,
26
+ enabled: animated,
27
+ });
28
+ const informative = Boolean(label);
29
+ return (_jsxs("div", { ref: ref, id: id, className: cx("m-network", className), ...componentRoot("NetworkIllustration"), "data-motion": motion === "static" ? "static" : undefined, "data-paused": animated ? (reason ?? undefined) : undefined, children: [_jsxs("svg", { viewBox: `0 0 ${width} ${height}`, ...(informative
30
+ ? {
31
+ role: "img",
32
+ "aria-labelledby": titleId,
33
+ "aria-describedby": description ? descId : undefined,
34
+ }
35
+ : { "aria-hidden": true, focusable: false }), children: [informative && _jsx("title", { id: titleId, children: label }), informative && description && _jsx("desc", { id: descId, children: description }), links.map((link, index) => (_jsx("line", { className: "m-network-link", x1: link.a.x, y1: link.a.y, x2: link.b.x, y2: link.b.y }, `${link.from}:${link.to}:${index}`))), animated &&
36
+ !reduced &&
37
+ carriers.map((link, index) => {
38
+ const time = link.duration ?? duration;
39
+ const delay = link.delay ?? -((index * time) / carriers.length);
40
+ return (_jsx("circle", { className: "m-network-signal", r: SIGNAL_RADIUS, style: {
41
+ "--m-signal-from": `${link.a.x}px ${link.a.y}px`,
42
+ "--m-signal-to": `${link.b.x}px ${link.b.y}px`,
43
+ animationDuration: `${time}s`,
44
+ animationDelay: delay ? `${delay}s` : undefined,
45
+ } }, `${link.from}:${link.to}:${index}`));
46
+ }), nodes.map((node) => (_jsx("circle", { className: "m-network-node", cx: node.x, cy: node.y, r: NODE_RADIUS, "data-tone": node.tone === "accent" ? "accent" : undefined }, node.id))), nodes.map((node) => node.label && (_jsx("text", { className: "m-network-label", x: node.x, y: node.y + NODE_RADIUS + 16, textAnchor: "middle", children: node.label }, node.id)))] }), animated && !reduced && (_jsx("button", { type: "button", className: "m-network-toggle", onClick: toggle, children: userPaused ? "Play animation" : "Pause animation" }))] }));
47
+ }
package/dist/index.d.ts CHANGED
@@ -4,10 +4,10 @@ export type { ThemeProviderProps, Appearance, Brand, Density, } from "./foundati
4
4
  export { Button, ButtonGroup, ActionLink, Input, Select, TextArea, Checkbox, Fieldset, RadioGroup, Switch, Badge, Tabs, Modal, Dropdown, Notice, EmptyState, ErrorSummary, } from "./primitives.js";
5
5
  export type { ButtonProps, ButtonGroupItem, DropdownItem, ErrorSummaryItem, InputProps, LinkAction, Tone, } from "./primitives.js";
6
6
  export { AppShell, PageHeading, MetricGroup, DataTable, Table, Pagination, SettingsSection, ActivityFeed, SignInPanel, Sidebar, Navbar, StackedShell, Breadcrumbs, ProgressSteps, TabNav, ActionPanel, } from "./application.js";
7
- export type { AppShellProps, NavigationItem, DataTableProps, DataTableColumn, TableSort, SortDirection, SignInProvider, SidebarProps, BreadcrumbItem, ProgressStep, TabNavItem, StackedShellProps, NavbarProps, BreadcrumbsProps, TabNavProps, ProgressStepsProps, ActionPanelProps, PageHeadingProps, MetricGroupProps, TableProps, PaginationProps, SettingsSectionProps, ActivityFeedProps, SignInPanelProps, } from "./application.js";
7
+ export type { AppShellProps, NavigationItem, DataTableProps, DataTableColumn, TableSort, SortDirection, SignInProvider, SidebarProps, BreadcrumbItem, ProgressStep, TabNavItem, StackedShellProps, NavbarProps, NavbarItem, NavbarGroup, NavbarTone, BreadcrumbsProps, TabNavProps, ProgressStepsProps, ActionPanelProps, PageHeadingProps, MetricGroupProps, TableProps, PaginationProps, SettingsSectionProps, ActivityFeedProps, SignInPanelProps, } from "./application.js";
8
8
  export { AuthLayout, ProviderButtons, PasswordInput, CodeInput, } from "./auth.js";
9
9
  export type { AuthLayoutProps, ProviderButtonsProps, PasswordInputProps, PasswordRequirement, PasswordStrength, CodeInputProps, } from "./auth.js";
10
- export { MarketingHeader, Hero, FeatureGrid, FeatureSection, ProcessSteps, Testimonial, LogoCloud, Pricing, FAQ, CallToAction, Footer, HeaderSection, ContentSection, } from "./marketing.js";
10
+ export { MarketingHeader, MarketingPage, Hero, FeatureGrid, FeatureSection, ProcessSteps, Testimonial, LogoCloud, Pricing, FAQ, CallToAction, Footer, HeaderSection, ContentSection, } from "./marketing.js";
11
11
  export type { PricingPlan, PricingExtra, HeaderLink, FlyoutItem, } from "./marketing.js";
12
12
  export { SegmentedControl, CodeBlock, PreviewFrame } from "./inspection.js";
13
13
  export { Avatar, AvatarGroup, Kbd, ProgressBar, FilterChip, CheckList, DescriptionList, GridList, AttachmentList, } from "./display.js";
@@ -32,3 +32,5 @@ export type { ProductImage, ProductCard, ProductOption, CartItem, FilterFacet, R
32
32
  export { CommentComposer } from "./composer.js";
33
33
  export type { LinkItem, ItemGroup } from "./navigation.js";
34
34
  export type { HeadingLevel } from "./heading.js";
35
+ export { NetworkIllustration } from "./illustration.js";
36
+ export type { NetworkNode, NetworkConnection } from "./illustration.js";
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ export { ThemeProvider, AppearanceControl } from "./foundation.js";
2
2
  export { Button, ButtonGroup, ActionLink, Input, Select, TextArea, Checkbox, Fieldset, RadioGroup, Switch, Badge, Tabs, Modal, Dropdown, Notice, EmptyState, ErrorSummary, } from "./primitives.js";
3
3
  export { AppShell, PageHeading, MetricGroup, DataTable, Table, Pagination, SettingsSection, ActivityFeed, SignInPanel, Sidebar, Navbar, StackedShell, Breadcrumbs, ProgressSteps, TabNav, ActionPanel, } from "./application.js";
4
4
  export { AuthLayout, ProviderButtons, PasswordInput, CodeInput, } from "./auth.js";
5
- export { MarketingHeader, Hero, FeatureGrid, FeatureSection, ProcessSteps, Testimonial, LogoCloud, Pricing, FAQ, CallToAction, Footer, HeaderSection, ContentSection, } from "./marketing.js";
5
+ export { MarketingHeader, MarketingPage, Hero, FeatureGrid, FeatureSection, ProcessSteps, Testimonial, LogoCloud, Pricing, FAQ, CallToAction, Footer, HeaderSection, ContentSection, } from "./marketing.js";
6
6
  export { SegmentedControl, CodeBlock, PreviewFrame } from "./inspection.js";
7
7
  export { Avatar, AvatarGroup, Kbd, ProgressBar, FilterChip, CheckList, DescriptionList, GridList, AttachmentList, } from "./display.js";
8
8
  export { Stack, Container, Text, Section, Divider, Card, MediaObject, SectionHeading, Prose, } from "./layout.js";
@@ -15,3 +15,4 @@ export { Listbox, Combobox } from "./choice.js";
15
15
  export { Calendar } from "./calendar.js";
16
16
  export { ProductList, ProductOverview, ShoppingCart, OrderSummary, ProductQuickView, ProductFilters, ProductReviews, OrderHistory, } from "./ecommerce.js";
17
17
  export { CommentComposer } from "./composer.js";
18
+ export { NetworkIllustration } from "./illustration.js";
@@ -1,9 +1,25 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useLayoutEffect, useRef } from "react";
2
3
  import { KitLink } from "./navigation.js";
3
4
  import { componentRoot, cx, useComponentName } from "./internal.js";
4
5
  export function SegmentedControl({ label, value, options, onChange, className, }) {
5
6
  const name = useComponentName("SegmentedControl");
6
- return (_jsx("div", { ...componentRoot(name), className: cx("m-segmented", className), role: "group", "aria-label": label, children: options.map((option) => (_jsx("button", { type: "button", "aria-pressed": value === option.value, onClick: () => onChange(option.value), children: option.label }, option.value))) }));
7
+ const group = useRef(null);
8
+ // When the options scroll (narrow screens), keep the selected one in view,
9
+ // on mount and on change. Only the control scrolls, never the page.
10
+ useLayoutEffect(() => {
11
+ const root = group.current;
12
+ const selected = root?.querySelector('[aria-pressed="true"]');
13
+ if (!root || !selected || root.scrollWidth <= root.clientWidth)
14
+ return;
15
+ const outer = root.getBoundingClientRect();
16
+ const inner = selected.getBoundingClientRect();
17
+ if (inner.left < outer.left)
18
+ root.scrollLeft -= outer.left - inner.left;
19
+ else if (inner.right > outer.right)
20
+ root.scrollLeft += inner.right - outer.right;
21
+ }, [value]);
22
+ return (_jsx("div", { ref: group, ...componentRoot(name), className: cx("m-segmented", className), role: "group", "aria-label": label, children: options.map((option) => (_jsx("button", { type: "button", "aria-pressed": value === option.value, onClick: () => onChange(option.value), children: option.label }, option.value))) }));
7
23
  }
8
24
  /** A scrollable code sample in a figure; the caption names the figure. */
9
25
  export function CodeBlock({ code, label = "Code", className, }) {
@@ -1,5 +1,5 @@
1
- import { type HTMLAttributes, type ReactNode } from "react";
2
- import { type ItemGroup } from "./navigation.js";
1
+ import { type HTMLAttributes, type RefObject, type ReactNode } from "react";
2
+ import { type ItemGroup, type LinkItem } from "./navigation.js";
3
3
  import { type LinkAction } from "./primitives.js";
4
4
  import { type HeadingLevel } from "./heading.js";
5
5
  /**
@@ -36,6 +36,8 @@ export interface IntroProps {
36
36
  eyebrow?: string;
37
37
  /** Omit to render no heading. */
38
38
  title?: string;
39
+ /** A phrase in `title` to emphasise; its first occurrence renders as `<em>`. */
40
+ emphasis?: string;
39
41
  description?: string;
40
42
  /** Id for the heading, e.g. the target of the section's aria-labelledby. */
41
43
  id?: string;
@@ -49,7 +51,7 @@ export interface IntroProps {
49
51
  * in the same parent: React derives useId values from each child's position,
50
52
  * so keeping the slots flat keeps the ids of that content unchanged.
51
53
  */
52
- export declare function introSlots({ eyebrow, title, description, id, headingLevel, descriptionClassName, }: IntroProps): ReactNode[];
54
+ export declare function introSlots({ eyebrow, title, emphasis, description, id, headingLevel, descriptionClassName, }: IntroProps): ReactNode[];
53
55
  /** The section intro as one element, for parents where nothing user-supplied follows it. */
54
56
  export declare function SectionIntro(props: IntroProps): import("react").JSX.Element;
55
57
  /** A row of link actions: the first primary, the rest secondary. */
@@ -367,6 +369,26 @@ export declare function LinkContent({ prefix, label, icon, count, marker, }: {
367
369
  }): import("react").FunctionComponentElement<import("react").FragmentProps>;
368
370
  /** A generated id that is safe in CSS selectors and URL fragments. */
369
371
  export declare function useDomId(prefix: string): string;
372
+ /** Why an animation is not running; null while it plays. */
373
+ export type PauseReason = "user" | "motion" | "hidden" | "offscreen" | null;
374
+ /**
375
+ * Shared pause and resume for decorative motion. It plays only while the
376
+ * user has not paused it, reduced motion is not requested, the document is
377
+ * visible and `ref` is on screen. The user's pause is kept apart from the
378
+ * automatic reasons, so returning to the page never overrides it. `paused`
379
+ * and `onPausedChange` make the user's choice controlled, for example one
380
+ * state shared by several animations. Every listener is removed on unmount.
381
+ */
382
+ export declare function usePlayback(ref: RefObject<Element | null>, { paused, onPausedChange, enabled, }: {
383
+ paused?: boolean;
384
+ onPausedChange?: (paused: boolean) => void;
385
+ enabled?: boolean;
386
+ }): {
387
+ reason: PauseReason;
388
+ reduced: boolean;
389
+ userPaused: boolean;
390
+ toggle: () => void;
391
+ };
370
392
  /**
371
393
  * Next index for arrow, Home and End keys in a horizontal set (wrapping), or
372
394
  * null for any other key.
@@ -490,3 +512,17 @@ export declare function classedIntroSlots(props: IntroProps, titleClassName: str
490
512
  * or the one before it, searching up through the ancestors that remain.
491
513
  */
492
514
  export declare function focusAfterRemoval(element: HTMLElement): void;
515
+ /** An item in a flyout panel; `current` marks the page you are on. */
516
+ export type FlyoutItem = Pick<LinkItem, "label" | "href" | "description" | "icon" | "current">;
517
+ /**
518
+ * A labelled group of links in a header that opens an anchored panel
519
+ * (MarketingHeader and Navbar). The group is marked current when one of its
520
+ * items is the current page.
521
+ */
522
+ export declare function Flyout({ label, items, footer, layout, onNavigate, }: {
523
+ label: string;
524
+ items: FlyoutItem[];
525
+ footer?: LinkAction[];
526
+ layout?: "list" | "columns" | "full";
527
+ onNavigate: () => void;
528
+ }): import("react").JSX.Element;
package/dist/internal.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // Shared building blocks for the kit's components. Internal: nothing here is
3
3
  // exported from the package entry, so these can change without a release note.
4
- import { Fragment, createContext, createElement, useContext, useId, cloneElement, isValidElement, } from "react";
5
- import { Dialog, DialogPanel, DialogTitle } from "@headlessui/react";
4
+ import { Fragment, createContext, createElement, useContext, useEffect, useId, useState, cloneElement, isValidElement, } from "react";
5
+ import { CloseButton, Dialog, DialogPanel, DialogTitle, Popover, PopoverButton, PopoverPanel, } from "@headlessui/react";
6
6
  import { KitLink } from "./navigation.js";
7
+ import { ChevronDown } from "./icons.js";
7
8
  import { ActionLink, Button } from "./primitives.js";
8
9
  import { headingTag } from "./heading.js";
9
10
  /**
@@ -46,14 +47,21 @@ export function cx(...classes) {
46
47
  * in the same parent: React derives useId values from each child's position,
47
48
  * so keeping the slots flat keeps the ids of that content unchanged.
48
49
  */
49
- export function introSlots({ eyebrow, title, description, id, headingLevel = 2, descriptionClassName = "m-section-description", }) {
50
+ export function introSlots({ eyebrow, title, emphasis, description, id, headingLevel = 2, descriptionClassName = "m-section-description", }) {
50
51
  const Heading = headingTag(headingLevel);
51
52
  return [
52
53
  eyebrow && _jsx("p", { className: "m-eyebrow", children: eyebrow }),
53
- title !== undefined && _jsx(Heading, { id: id, children: title }),
54
+ title !== undefined && (_jsx(Heading, { id: id, children: emphasise(title, emphasis) })),
54
55
  description && (_jsx("p", { className: descriptionClassName ?? undefined, children: description })),
55
56
  ];
56
57
  }
58
+ /** `title` with the first occurrence of `phrase` in `<em>`; unchanged when absent. */
59
+ function emphasise(title, phrase) {
60
+ const at = phrase ? title.indexOf(phrase) : -1;
61
+ if (!phrase || at < 0)
62
+ return title;
63
+ return (_jsxs(_Fragment, { children: [title.slice(0, at), _jsx("em", { className: "m-emphasis", children: phrase }), title.slice(at + phrase.length)] }));
64
+ }
57
65
  /** The section intro as one element, for parents where nothing user-supplied follows it. */
58
66
  export function SectionIntro(props) {
59
67
  return _jsxs(_Fragment, { children: [...introSlots(props)] });
@@ -84,6 +92,58 @@ export function LinkContent({ prefix, label, icon, count, marker = false, }) {
84
92
  export function useDomId(prefix) {
85
93
  return prefix + useId().replace(/[^a-zA-Z0-9-]/g, "");
86
94
  }
95
+ /**
96
+ * Shared pause and resume for decorative motion. It plays only while the
97
+ * user has not paused it, reduced motion is not requested, the document is
98
+ * visible and `ref` is on screen. The user's pause is kept apart from the
99
+ * automatic reasons, so returning to the page never overrides it. `paused`
100
+ * and `onPausedChange` make the user's choice controlled, for example one
101
+ * state shared by several animations. Every listener is removed on unmount.
102
+ */
103
+ export function usePlayback(ref, { paused, onPausedChange, enabled = true, }) {
104
+ const [ownPaused, setOwnPaused] = useState(false);
105
+ const [reduced, setReduced] = useState(false);
106
+ const [hidden, setHidden] = useState(false);
107
+ const [offscreen, setOffscreen] = useState(false);
108
+ const userPaused = paused ?? ownPaused;
109
+ useEffect(() => {
110
+ if (!enabled)
111
+ return;
112
+ const media = window.matchMedia("(prefers-reduced-motion: reduce)");
113
+ const onMedia = () => setReduced(media.matches);
114
+ const onVisibility = () => setHidden(document.visibilityState === "hidden");
115
+ onMedia();
116
+ onVisibility();
117
+ media.addEventListener("change", onMedia);
118
+ document.addEventListener("visibilitychange", onVisibility);
119
+ const observer = ref.current && typeof IntersectionObserver !== "undefined"
120
+ ? new IntersectionObserver(([entry]) => setOffscreen(!entry.isIntersecting))
121
+ : null;
122
+ if (observer && ref.current)
123
+ observer.observe(ref.current);
124
+ return () => {
125
+ media.removeEventListener("change", onMedia);
126
+ document.removeEventListener("visibilitychange", onVisibility);
127
+ observer?.disconnect();
128
+ };
129
+ }, [enabled, ref]);
130
+ const reason = userPaused
131
+ ? "user"
132
+ : reduced
133
+ ? "motion"
134
+ : hidden
135
+ ? "hidden"
136
+ : offscreen
137
+ ? "offscreen"
138
+ : null;
139
+ const toggle = () => {
140
+ const next = !userPaused;
141
+ if (paused === undefined)
142
+ setOwnPaused(next);
143
+ onPausedChange?.(next);
144
+ };
145
+ return { reason, reduced, userPaused, toggle };
146
+ }
87
147
  /**
88
148
  * Next index for arrow, Home and End keys in a horizontal set (wrapping), or
89
149
  * null for any other key.
@@ -271,3 +331,13 @@ export function focusAfterRemoval(element) {
271
331
  }
272
332
  });
273
333
  }
334
+ /**
335
+ * A labelled group of links in a header that opens an anchored panel
336
+ * (MarketingHeader and Navbar). The group is marked current when one of its
337
+ * items is the current page.
338
+ */
339
+ export function Flyout({ label, items, footer = [], layout = "list", onNavigate, }) {
340
+ const dataLayout = layout === "list" ? undefined : layout;
341
+ const current = items.some((item) => item.current);
342
+ return (_jsxs(Popover, { className: "m-flyout", "data-current": current || undefined, "data-layout": dataLayout, children: [_jsxs(PopoverButton, { className: "m-flyout-trigger", children: [label, _jsx(ChevronDown, {})] }), _jsxs(PopoverPanel, { anchor: "bottom", className: "m-flyout-panel", "data-layout": dataLayout, children: [_jsx("ul", { children: items.map((item, index) => (_jsx("li", { children: _jsxs(CloseButton, { as: KitLink, href: item.href, "aria-current": item.current ? "page" : undefined, onClick: onNavigate, children: [item.icon && (_jsx("span", { className: "m-flyout-icon", "aria-hidden": "true", children: item.icon })), _jsxs("span", { className: "m-flyout-text", children: [_jsx("strong", { children: item.label }), item.description && _jsx("span", { children: item.description })] })] }) }, `${item.label}:${item.href}:${index}`))) }), footer.length > 0 && (_jsx("div", { className: "m-flyout-footer", children: footer.map((link, index) => (_jsx(CloseButton, { as: KitLink, href: link.href, onClick: onNavigate, children: link.label }, `${link.label}:${link.href}:${index}`))) }))] })] }));
343
+ }
@@ -1,17 +1,20 @@
1
- import { type IconLink, type ItemGroup, type LinkItem } from "./navigation.js";
1
+ import { type IconLink, type ItemGroup } from "./navigation.js";
2
2
  import { type ReactNode } from "react";
3
3
  import { type LinkAction } from "./primitives.js";
4
4
  import { type SectionTone } from "./layout.js";
5
5
  import { type HeadingLevel } from "./heading.js";
6
- export type FlyoutItem = Pick<LinkItem, "label" | "href" | "description" | "icon">;
6
+ import { type FlyoutItem } from "./internal.js";
7
+ export type { FlyoutItem } from "./internal.js";
7
8
  /** A header link, or a labelled group that opens a flyout panel. */
8
9
  export type HeaderLink = LinkAction | (ItemGroup<FlyoutItem> & {
9
10
  footer?: LinkAction[];
10
11
  /** list: one column. columns: a wider panel in two columns. full: as wide as the viewport allows, up to four columns. */
11
12
  layout?: "list" | "columns" | "full";
12
13
  });
13
- export declare function MarketingHeader({ brand, homeHref, logo, links, action, controls, layout, mobileMenu, className, id, }: {
14
+ export declare function MarketingHeader({ brand, tagline, homeHref, logo, links, action, controls, layout, mobileMenu, className, id, }: {
14
15
  brand: string;
16
+ /** A short descriptor beside the wordmark, outside the home link. Hidden on narrow screens. */
17
+ tagline?: string;
15
18
  logo?: ReactNode;
16
19
  homeHref?: string;
17
20
  /** Links, or groups of items that open a flyout panel. */
@@ -25,16 +28,37 @@ export declare function MarketingHeader({ brand, homeHref, logo, links, action,
25
28
  className?: string;
26
29
  id?: string;
27
30
  }): import("react").JSX.Element;
28
- export declare function Hero({ id, className, eyebrow, title, description, actions, children, layout, headingLevel, background, images, tone, }: {
31
+ /**
32
+ * A public page: the header, a main region that grows to fill the viewport, and
33
+ * the footer. Content is never clipped; a longer page simply scrolls.
34
+ */
35
+ export declare function MarketingPage({ header, footer, children, align, id, className, }: {
36
+ /** Usually a MarketingHeader. */
37
+ header?: ReactNode;
38
+ /** Usually a Footer. */
39
+ footer?: ReactNode;
40
+ children: ReactNode;
41
+ /** start: content from the top. center: content centred in the space between header and footer, for a single hero. */
42
+ align?: "start" | "center";
43
+ id?: string;
44
+ className?: string;
45
+ }): import("react").JSX.Element;
46
+ export declare function Hero({ id, className, eyebrow, title, emphasis, description, actions, note, children, layout, wide, headingLevel, background, images, tone, }: {
29
47
  id?: string;
30
48
  className?: string;
31
49
  eyebrow?: string;
32
50
  title: string;
51
+ /** A phrase in the title to emphasise. Its first occurrence renders as `<em>` inside the one heading; a phrase not in the title is ignored. */
52
+ emphasis?: string;
33
53
  description: string;
34
54
  actions?: LinkAction[];
55
+ /** Short supporting content below the actions, such as a note or a Notice. */
56
+ note?: ReactNode;
35
57
  children?: ReactNode;
36
58
  /** center: copy above the preview. split: copy beside the preview. angled: copy beside an image that fills the end half to the edge, cut on a slant (pass one img or video as children). */
37
59
  layout?: "center" | "split" | "angled";
60
+ /** Split only: the column that takes more of the width. Omitted, the columns are even. */
61
+ wide?: "copy" | "preview";
38
62
  headingLevel?: HeadingLevel;
39
63
  /** Decorative full-cover image or video behind the copy. The hero switches to the inverse tone with a scrim so text keeps its contrast. */
40
64
  background?: ReactNode;
@@ -194,18 +218,18 @@ export declare function Footer({ id, className, brand, logo, description, note,
194
218
  className?: string;
195
219
  brand: string;
196
220
  logo?: ReactNode;
197
- description: string;
221
+ description?: string;
198
222
  note?: ReactNode;
199
- /** Link columns; each label is the column's visible heading and its navigation name. Centred, the label only names the navigation. */
200
- columns: ItemGroup<LinkAction>[];
223
+ /** Link columns; each label is the column's visible heading and its navigation name. Centred or inline, the label only names the navigation. */
224
+ columns?: ItemGroup<LinkAction>[];
201
225
  copyright: string;
202
226
  legal?: LinkAction[];
203
227
  /** Level of the column headings. */
204
228
  itemHeadingLevel?: HeadingLevel;
205
229
  /** Social profiles, usually icon-only; each label names its link. */
206
230
  social?: IconLink[];
207
- /** columns: the brand beside headed link columns. center: brand, a row of links and social links, all centred. */
208
- layout?: "columns" | "center";
231
+ /** columns: the brand beside headed link columns. center: brand, a row of links and social links, all centred. inline: one compact row of brand, notes, links and copyright that wraps in reading order. */
232
+ layout?: "columns" | "center" | "inline";
209
233
  /** A signup row above the copyright, usually a Newsletter; the footer removes its section padding. */
210
234
  newsletter?: ReactNode;
211
235
  }): import("react").JSX.Element;