@tonycodes-ui/mortar 0.0.0-stage → 0.12.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 (107) hide show
  1. package/AGENTS.md +10 -0
  2. package/CHANGELOG.md +268 -0
  3. package/CONTRIBUTING.md +60 -0
  4. package/DESIGN.md +51 -0
  5. package/LICENSE +21 -0
  6. package/README.md +155 -2
  7. package/TESTING.md +83 -0
  8. package/baseline-policy.json +16 -0
  9. package/bin/mortar.mjs +258 -0
  10. package/catalog.json +9900 -0
  11. package/cli/audit.mjs +943 -0
  12. package/cli/data.mjs +176 -0
  13. package/cli/mcp.mjs +216 -0
  14. package/cli/migrations.mjs +128 -0
  15. package/cli/packet.mjs +267 -0
  16. package/cli/search.mjs +246 -0
  17. package/cli/tools.mjs +229 -0
  18. package/design-principles.json +30 -0
  19. package/dist/agent-data.json +1 -0
  20. package/dist/application.d.ts +340 -0
  21. package/dist/application.js +240 -0
  22. package/dist/auth.d.ts +121 -0
  23. package/dist/auth.js +97 -0
  24. package/dist/brand.d.ts +69 -0
  25. package/dist/brand.js +266 -0
  26. package/dist/calendar.d.ts +75 -0
  27. package/dist/calendar.js +310 -0
  28. package/dist/choice.d.ts +36 -0
  29. package/dist/choice.js +36 -0
  30. package/dist/composer.d.ts +28 -0
  31. package/dist/composer.js +47 -0
  32. package/dist/display.d.ts +136 -0
  33. package/dist/display.js +75 -0
  34. package/dist/docs.d.ts +145 -0
  35. package/dist/docs.js +141 -0
  36. package/dist/ecommerce.d.ts +261 -0
  37. package/dist/ecommerce.js +177 -0
  38. package/dist/feedback.d.ts +80 -0
  39. package/dist/feedback.js +98 -0
  40. package/dist/foundation.d.ts +42 -0
  41. package/dist/foundation.js +121 -0
  42. package/dist/heading.d.ts +5 -0
  43. package/dist/heading.js +9 -0
  44. package/dist/icons.d.ts +50 -0
  45. package/dist/icons.js +70 -0
  46. package/dist/illustration.d.ts +47 -0
  47. package/dist/illustration.js +47 -0
  48. package/dist/index.d.ts +36 -0
  49. package/dist/index.js +18 -0
  50. package/dist/inspection.d.ts +24 -0
  51. package/dist/inspection.js +14 -0
  52. package/dist/internal.d.ts +514 -0
  53. package/dist/internal.js +332 -0
  54. package/dist/layout.d.ts +107 -0
  55. package/dist/layout.js +66 -0
  56. package/dist/marketing-sections.d.ts +161 -0
  57. package/dist/marketing-sections.js +58 -0
  58. package/dist/marketing.d.ts +277 -0
  59. package/dist/marketing.js +163 -0
  60. package/dist/navigation.d.ts +35 -0
  61. package/dist/navigation.js +44 -0
  62. package/dist/primitives.d.ts +258 -0
  63. package/dist/primitives.js +244 -0
  64. package/dist/showcase.d.ts +29 -0
  65. package/dist/showcase.js +10 -0
  66. package/dist/workspace.d.ts +80 -0
  67. package/dist/workspace.js +48 -0
  68. package/docs/agents.md +127 -0
  69. package/docs/auth-flows.md +71 -0
  70. package/docs/migrating-to-0.6.md +94 -0
  71. package/docs/theming.md +170 -0
  72. package/package.json +94 -4
  73. package/skills/mortar/SKILL.md +36 -0
  74. package/src/application.css +1487 -0
  75. package/src/application.tsx +1413 -0
  76. package/src/auth.tsx +452 -0
  77. package/src/brand.ts +335 -0
  78. package/src/calendar.css +468 -0
  79. package/src/calendar.tsx +868 -0
  80. package/src/choice.tsx +240 -0
  81. package/src/composer.tsx +158 -0
  82. package/src/data.css +690 -0
  83. package/src/display.tsx +452 -0
  84. package/src/docs.css +704 -0
  85. package/src/docs.tsx +737 -0
  86. package/src/ecommerce.css +810 -0
  87. package/src/ecommerce.tsx +1033 -0
  88. package/src/feedback.tsx +388 -0
  89. package/src/foundation.css +1867 -0
  90. package/src/foundation.tsx +227 -0
  91. package/src/heading.ts +13 -0
  92. package/src/icons.tsx +237 -0
  93. package/src/illustration.tsx +169 -0
  94. package/src/index.ts +233 -0
  95. package/src/inspection.tsx +91 -0
  96. package/src/internal.tsx +700 -0
  97. package/src/layout.tsx +342 -0
  98. package/src/marketing-sections.tsx +621 -0
  99. package/src/marketing.css +2579 -0
  100. package/src/marketing.tsx +1235 -0
  101. package/src/navigation.tsx +118 -0
  102. package/src/overlays.css +350 -0
  103. package/src/primitives.tsx +1135 -0
  104. package/src/showcase.tsx +120 -0
  105. package/src/tokens.css +449 -0
  106. package/src/workspace.tsx +333 -0
  107. package/styles.css +56 -0
@@ -0,0 +1,340 @@
1
+ import { type ItemGroup, type LinkItem } from "./navigation.js";
2
+ import { type ReactNode } from "react";
3
+ import { type Tone } from "./primitives.js";
4
+ import { type SignInProvider } from "./auth.js";
5
+ import { type HeadingLevel } from "./heading.js";
6
+ /**
7
+ * A navigation link; `icon` replaces the default marker in the sidebar. With
8
+ * `children` the item becomes a disclosure: its own `href` is the first link
9
+ * inside it, and it opens when it or a child is current.
10
+ */
11
+ export interface NavigationItem extends LinkItem {
12
+ /** Sub-pages; the item becomes a disclosure, open when it or a child is current. */
13
+ children?: NavigationItem[];
14
+ }
15
+ export interface AppShellProps {
16
+ brand: string;
17
+ /** Logo node shown in place of the placeholder brand mark. */
18
+ logo?: ReactNode;
19
+ workspace?: string;
20
+ /** Destination for the brand link; defaults to the first navigation item. */
21
+ homeHref?: string;
22
+ /** Account menu or other content pinned to the bottom of the sidebar. */
23
+ footer?: ReactNode;
24
+ navigation: NavigationItem[];
25
+ /** Labelled groups of links under the main navigation. */
26
+ sections?: ItemGroup<NavigationItem>[];
27
+ header?: ReactNode;
28
+ actions?: ReactNode;
29
+ /** Secondary column beside the main content (multi-column layout). */
30
+ aside?: ReactNode;
31
+ /** Accessible name for the secondary column. */
32
+ asideLabel?: string;
33
+ children: ReactNode;
34
+ className?: string;
35
+ }
36
+ export interface SidebarProps {
37
+ brand?: string;
38
+ logo?: ReactNode;
39
+ workspace?: string;
40
+ /** Destination for the brand link; defaults to the first navigation item. */
41
+ homeHref?: string;
42
+ navigation: NavigationItem[];
43
+ /** Labelled groups of links under the main navigation. */
44
+ sections?: ItemGroup<NavigationItem>[];
45
+ /** Account menu or other content pinned to the bottom. */
46
+ footer?: ReactNode;
47
+ /** Accessible name of the navigation landmark. */
48
+ label?: string;
49
+ className?: string;
50
+ }
51
+ /** Vertical application navigation; AppShell composes the same content. */
52
+ export declare function Sidebar({ className, ...props }: SidebarProps): import("react").JSX.Element;
53
+ export declare function AppShell({ brand, logo, workspace, homeHref, footer, navigation, sections, header, actions, aside, asideLabel, children, className, }: AppShellProps): import("react").JSX.Element;
54
+ export interface StackedShellProps {
55
+ brand: string;
56
+ logo?: ReactNode;
57
+ homeHref?: string;
58
+ navigation: LinkItem[];
59
+ actions?: ReactNode;
60
+ /** Page heading shown above the content, e.g. a PageHeading. */
61
+ header?: ReactNode;
62
+ width?: "md" | "lg" | "xl" | "full";
63
+ children: ReactNode;
64
+ className?: string;
65
+ }
66
+ /** 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;
68
+ export interface NavbarProps {
69
+ brand: string;
70
+ logo?: ReactNode;
71
+ homeHref?: string;
72
+ /** Top-level links; the navbar has no sub-menus. */
73
+ items: LinkItem[];
74
+ actions?: ReactNode;
75
+ /** Accessible name of the navigation landmark. */
76
+ label?: string;
77
+ className?: string;
78
+ }
79
+ /** 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;
81
+ export interface BreadcrumbItem {
82
+ label: string;
83
+ href?: string;
84
+ }
85
+ export interface BreadcrumbsProps {
86
+ items: BreadcrumbItem[];
87
+ label?: string;
88
+ separator?: "slash" | "chevron";
89
+ className?: string;
90
+ }
91
+ /** Trail to the current page; the item without an href is the current page. */
92
+ export declare function Breadcrumbs({ items, label, separator, className, }: BreadcrumbsProps): import("react").JSX.Element;
93
+ export type TabNavItem = LinkItem;
94
+ export interface TabNavProps {
95
+ label: string;
96
+ items: TabNavItem[];
97
+ /** Underlined tabs (default) or filled pills. */
98
+ variant?: "underline" | "pills";
99
+ /** Stretch the tabs to share the full width. */
100
+ fullWidth?: boolean;
101
+ className?: string;
102
+ }
103
+ /** Tabs that link between pages, e.g. settings sections. Uses links, not ARIA tabs. */
104
+ export declare function TabNav({ label, items, variant, fullWidth, className, }: TabNavProps): import("react").JSX.Element;
105
+ export interface ProgressStep {
106
+ id: string;
107
+ name: string;
108
+ description?: string;
109
+ /** Lets people revisit the step, e.g. a completed form page. */
110
+ href?: string;
111
+ status: "complete" | "current" | "upcoming";
112
+ }
113
+ export interface ProgressStepsProps {
114
+ steps: ProgressStep[];
115
+ label?: string;
116
+ /**
117
+ * simple: bars with step numbers; circles: vertical with descriptions;
118
+ * bullets: "Step 2 of 4" with dots; bar: one track filled up to the current
119
+ * step with the names beneath, e.g. an order's delivery progress.
120
+ */
121
+ variant?: "simple" | "circles" | "bullets" | "bar";
122
+ /**
123
+ * `false` renders a labelled group instead of a navigation landmark, for
124
+ * progress that reports status rather than offering wayfinding, e.g. an
125
+ * order tracker (default true).
126
+ */
127
+ navigation?: boolean;
128
+ className?: string;
129
+ }
130
+ /** Where someone is in a multi-step flow. The current step is marked aria-current="step". */
131
+ export declare function ProgressSteps({ steps, label, variant, navigation, className, }: ProgressStepsProps): import("react").JSX.Element;
132
+ export interface ActionPanelProps {
133
+ title: string;
134
+ description?: string;
135
+ action?: ReactNode;
136
+ children?: ReactNode;
137
+ /** `danger` marks a destructive action, e.g. deleting an account. */
138
+ tone?: "default" | "danger";
139
+ headingLevel?: HeadingLevel;
140
+ id?: string;
141
+ className?: string;
142
+ }
143
+ /** Card with a title, explanation and one decisive action, e.g. deleting an account. */
144
+ export declare function ActionPanel({ title, description, action, children, tone, headingLevel, id, className, }: ActionPanelProps): import("react").JSX.Element;
145
+ export interface PageHeadingProps {
146
+ title: string;
147
+ description?: string;
148
+ eyebrow?: string;
149
+ actions?: ReactNode;
150
+ breadcrumbs?: BreadcrumbItem[];
151
+ /** The page's h1 by default; use 2 when the page already has one. */
152
+ headingLevel?: HeadingLevel;
153
+ /** Facts under the title, e.g. "Full-time", "Remote", "Closes 9 January". */
154
+ meta?: {
155
+ label: string;
156
+ icon?: ReactNode;
157
+ }[];
158
+ /** Avatar or logo beside the title (decorative). */
159
+ avatar?: ReactNode;
160
+ id?: string;
161
+ className?: string;
162
+ }
163
+ export declare function PageHeading({ title, description, eyebrow, actions, breadcrumbs, headingLevel, meta, avatar, id, className, }: PageHeadingProps): import("react").JSX.Element;
164
+ export interface MetricGroupProps {
165
+ label: string;
166
+ items: {
167
+ label: string;
168
+ value: string;
169
+ change?: string;
170
+ tone?: Tone;
171
+ /** Decorative icon before the label. */
172
+ icon?: ReactNode;
173
+ /** Direction of the change: adds an arrow and announces "Increased by" or "Decreased by". */
174
+ trend?: "up" | "down";
175
+ /** Prior value for comparison, shown as "from …" after the value. */
176
+ previous?: string;
177
+ }[];
178
+ /** `shared` joins metrics with shared borders; `cards` separates them into individual cards. */
179
+ variant?: "shared" | "cards";
180
+ className?: string;
181
+ }
182
+ export declare function MetricGroup({ label, items, variant, className, }: MetricGroupProps): import("react").JSX.Element;
183
+ export type SortDirection = "ascending" | "descending";
184
+ export interface TableSort {
185
+ key: string;
186
+ direction: SortDirection;
187
+ }
188
+ export interface DataTableColumn<T> {
189
+ key: string;
190
+ label: string;
191
+ render: (row: T) => ReactNode;
192
+ /** `end` aligns numbers and amounts to the trailing edge. */
193
+ align?: "start" | "end";
194
+ /** Enable header sorting. Supply sortValue unless row[key] is a string, number or Date. */
195
+ sortable?: boolean;
196
+ sortValue?: (row: T) => string | number | Date | null | undefined;
197
+ /** CSS width, e.g. "12rem" or "30%". */
198
+ width?: string;
199
+ /** Clip long values to one line with an ellipsis. */
200
+ truncate?: boolean;
201
+ }
202
+ export interface DataTableProps<T extends {
203
+ id: string;
204
+ }> {
205
+ caption: string;
206
+ columns: DataTableColumn<T>[];
207
+ rows: T[];
208
+ /** Selected row ids, which may include rows on other pages. */
209
+ selectedIds?: string[];
210
+ onSelectionChange?: (ids: string[]) => void;
211
+ rowLabel: (row: T) => string;
212
+ /** Link each row's first cell to a detail page; the whole row becomes the click target. */
213
+ rowHref?: (row: T) => string | undefined;
214
+ /** Initial sort when the table sorts locally. */
215
+ defaultSort?: TableSort;
216
+ /** Controlled sort. With onSortChange the consumer orders rows (e.g. server-side). */
217
+ sort?: TableSort | null;
218
+ onSortChange?: (sort: TableSort) => void;
219
+ /** Group rows under full-width header rows. Groups keep the order in which they first appear in rows; sorting applies within each group. */
220
+ groupBy?: (row: T) => string;
221
+ /** Scroll height; the header sticks while rows scroll. */
222
+ maxHeight?: string;
223
+ density?: "default" | "compact";
224
+ striped?: boolean;
225
+ toolbar?: ReactNode;
226
+ bulkActions?: ReactNode;
227
+ /** Rendered under the rows inside the table card, e.g. Pagination. */
228
+ footer?: ReactNode;
229
+ /** Shown when there are no rows. */
230
+ empty?: {
231
+ title: string;
232
+ description?: string;
233
+ action?: ReactNode;
234
+ };
235
+ /** Level of the empty state's heading. */
236
+ headingLevel?: HeadingLevel;
237
+ loading?: boolean;
238
+ error?: string;
239
+ id?: string;
240
+ className?: string;
241
+ }
242
+ /** Stable sort; empty values always sort last regardless of direction. */
243
+ export declare function sortRows<T>(rows: T[], column: Pick<DataTableColumn<T>, "key" | "sortValue">, direction: SortDirection): T[];
244
+ export declare function DataTable<T extends {
245
+ id: string;
246
+ }>({ caption, columns, rows, selectedIds, onSelectionChange, rowLabel, rowHref, defaultSort, sort: controlledSort, onSortChange, groupBy, maxHeight, density, striped, toolbar, bulkActions, footer, empty, headingLevel, loading, error, id, className, }: DataTableProps<T>): import("react").JSX.Element;
247
+ export interface TableProps {
248
+ caption: string;
249
+ captionVisible?: boolean;
250
+ /** `align: "end"` aligns numbers and amounts to the trailing edge. */
251
+ columns: {
252
+ key: string;
253
+ label: string;
254
+ align?: "start" | "end";
255
+ }[];
256
+ rows?: Record<string, ReactNode>[];
257
+ /** Labelled row groups, each its own tbody under a full-width header row. Ungrouped rows render first. */
258
+ groups?: {
259
+ label: string;
260
+ rows: Record<string, ReactNode>[];
261
+ }[];
262
+ /** Treat the first column as row headers. */
263
+ rowHeaders?: boolean;
264
+ striped?: boolean;
265
+ id?: string;
266
+ className?: string;
267
+ }
268
+ /** Plain semantic table for documents, settings summaries and comparisons without row actions. */
269
+ export declare function Table({ caption, captionVisible, columns, rows, groups, rowHeaders, striped, id, className, }: TableProps): import("react").JSX.Element;
270
+ export interface PaginationProps {
271
+ page: number;
272
+ pageCount: number;
273
+ onChange: (page: number) => void;
274
+ /** Show individual page buttons between Previous and Next. */
275
+ numbered?: boolean;
276
+ /** "Showing 51 to 60 of 114 results". */
277
+ summary?: {
278
+ from: number;
279
+ to: number;
280
+ total: number;
281
+ noun?: string;
282
+ };
283
+ /** Accessible name; give each pagination on a page a distinct one. */
284
+ label?: string;
285
+ className?: string;
286
+ }
287
+ export declare function Pagination({ page, pageCount, onChange, numbered, summary, label, className, }: PaginationProps): import("react").JSX.Element;
288
+ export interface SettingsSectionProps {
289
+ title: string;
290
+ description?: string;
291
+ children: ReactNode;
292
+ headingLevel?: HeadingLevel;
293
+ id?: string;
294
+ className?: string;
295
+ }
296
+ export declare function SettingsSection({ title, description, children, headingLevel, id, className, }: SettingsSectionProps): import("react").JSX.Element;
297
+ export interface ActivityFeedProps {
298
+ label: string;
299
+ items: {
300
+ id: string;
301
+ title: string;
302
+ description?: string;
303
+ time: string;
304
+ tone?: Tone;
305
+ /** Avatar of who acted; replaces the tone dot. */
306
+ actor?: ReactNode;
307
+ /** Comment text, shown as a quote under the title. */
308
+ body?: ReactNode;
309
+ }[];
310
+ className?: string;
311
+ }
312
+ export declare function ActivityFeed({ label, items, className }: ActivityFeedProps): import("react").JSX.Element;
313
+ export type { SignInProvider };
314
+ export interface SignInPanelProps {
315
+ title: string;
316
+ /** The page's h1 by default; use 2 when the panel sits inside a page that already has one. */
317
+ headingLevel?: HeadingLevel;
318
+ /** Image or quote beside the form on wide screens (split-screen layout). */
319
+ aside?: ReactNode;
320
+ description?: string;
321
+ logo?: ReactNode;
322
+ providers?: SignInProvider[];
323
+ email?: {
324
+ onSubmit: (email: string) => void;
325
+ label?: string;
326
+ placeholder?: string;
327
+ submitLabel?: string;
328
+ status?: "idle" | "submitting" | "success" | "error";
329
+ message?: string;
330
+ };
331
+ footer?: ReactNode;
332
+ dividerLabel?: string;
333
+ id?: string;
334
+ className?: string;
335
+ }
336
+ /**
337
+ * Brand-consistent sign-in surface. Mortar renders the form; the consumer's auth
338
+ * SDK supplies provider redirects and the email-link request through callbacks.
339
+ */
340
+ export declare function SignInPanel({ title, description, logo, providers, email, footer, dividerLabel, headingLevel, aside, id, className, }: SignInPanelProps): import("react").JSX.Element;
@@ -0,0 +1,240 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { KitLink } from "./navigation.js";
3
+ import { useId, useRef, useEffect, useState, } from "react";
4
+ import { Button, EmptyState, Input, Notice } from "./primitives.js";
5
+ import { BrandMark, Divider } from "./layout.js";
6
+ import { AuthHeader, ProviderButtons } from "./auth.js";
7
+ import { ArrowDown, ArrowUp, Check, SortArrows } from "./icons.js";
8
+ import { LinkContent, NavGroups, NavTreeLinks, MobileNavDialog, TermList, cx, componentRoot, classedIntroSlots, useDomId, } from "./internal.js";
9
+ function SidebarContent({ brand, logo, workspace, homeHref, navigation, sections = [], footer, label = "Primary", onNavigate, }) {
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
+ }
12
+ /** Vertical application navigation; AppShell composes the same content. */
13
+ export function Sidebar({ className, ...props }) {
14
+ return (_jsx("div", { ...componentRoot("Sidebar"), className: cx("m-sidebar-panel", className), children: _jsx(SidebarContent, { ...props }) }));
15
+ }
16
+ export function AppShell({ brand, logo, workspace, homeHref, footer, navigation, sections = [], header, actions, aside, asideLabel = "Details", children, className, }) {
17
+ const [open, setOpen] = useState(false);
18
+ const contentId = useDomId("m-content-");
19
+ const sidebar = {
20
+ brand,
21
+ logo,
22
+ workspace,
23
+ homeHref,
24
+ navigation,
25
+ sections,
26
+ footer,
27
+ };
28
+ const main = (_jsx("main", { id: contentId, tabIndex: -1, children: children }));
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
+ }
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, }) {
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] })] }));
35
+ }
36
+ /** Horizontal application navigation that collapses into a menu on narrow screens. */
37
+ export function Navbar({ brand, logo, homeHref = "/", items, actions, label = "Primary", className, }) {
38
+ const [open, setOpen] = useState(false);
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 })] }));
41
+ }
42
+ /** Trail to the current page; the item without an href is the current page. */
43
+ export function Breadcrumbs({ items, label = "Breadcrumb", separator = "slash", className, }) {
44
+ return (_jsx("nav", { ...componentRoot("Breadcrumbs"), className: cx("m-breadcrumbs", className), "aria-label": label, "data-separator": separator === "slash" ? undefined : separator, children: _jsx("ol", { children: items.map((b, i) => (_jsx("li", { children: b.href ? (_jsx(KitLink, { href: b.href, children: b.label })) : (_jsx("span", { "aria-current": "page", children: b.label })) }, i))) }) }));
45
+ }
46
+ /** Tabs that link between pages, e.g. settings sections. Uses links, not ARIA tabs. */
47
+ export function TabNav({ label, items, variant = "underline", fullWidth = false, className, }) {
48
+ return (_jsx("nav", { ...componentRoot("TabNav"), className: cx("m-tab-nav", className), "aria-label": label, "data-variant": variant === "underline" ? undefined : variant, "data-full-width": fullWidth || undefined, children: _jsx("ul", { children: items.map((item) => (_jsx("li", { children: _jsx(KitLink, { href: item.href, "aria-current": item.current ? "page" : undefined, children: _jsx(LinkContent, { prefix: "tab", icon: item.icon, label: item.label, count: item.count }) }) }, item.href))) }) }));
49
+ }
50
+ /** Where someone is in a multi-step flow. The current step is marked aria-current="step". */
51
+ export function ProgressSteps({ steps, label = "Progress", variant = "simple", navigation = true, className, }) {
52
+ const current = steps.findIndex((step) => step.status === "current");
53
+ const Root = navigation ? "nav" : "div";
54
+ return (_jsxs(Root, { ...componentRoot("ProgressSteps"), className: cx("m-progress-steps", className), role: navigation ? undefined : "group", "aria-label": label, "data-variant": variant === "simple" ? undefined : variant, children: [variant === "bullets" && current >= 0 && (_jsxs("p", { children: ["Step ", current + 1, " of ", steps.length] })), _jsx("ol", { children: steps.map((step, index) => {
55
+ const body = (_jsxs(_Fragment, { children: [(variant === "circles" || variant === "bullets") && (_jsx("span", { className: "m-progress-step-marker", "aria-hidden": "true", children: variant === "circles" && step.status === "complete" && (_jsx(Check, { variant: "step" })) })), _jsxs("span", { className: variant === "bullets" ? "m-sr-only" : "m-progress-step-text", children: [variant === "simple" && (_jsxs("span", { className: "m-progress-step-number", children: ["Step ", index + 1] })), _jsx("span", { className: "m-progress-step-name", children: step.name }), step.description && variant === "circles" && (_jsx("span", { className: "m-progress-step-description", children: step.description })), step.status === "complete" && (_jsx("span", { className: "m-sr-only", children: " (completed)" }))] })] }));
56
+ const ariaCurrent = step.status === "current" ? "step" : undefined;
57
+ return (_jsx("li", { "data-status": step.status, children: step.href ? (_jsx(KitLink, { href: step.href, "aria-current": ariaCurrent, children: body })) : (_jsx("span", { "aria-current": ariaCurrent, children: body })) }, step.id));
58
+ }) })] }));
59
+ }
60
+ /** Card with a title, explanation and one decisive action, e.g. deleting an account. */
61
+ export function ActionPanel({ title, description, action, children, tone = "default", headingLevel = 2, id, className, }) {
62
+ const headingId = useId();
63
+ const [, headingSlot, descriptionSlot] = classedIntroSlots({
64
+ title,
65
+ description,
66
+ id: headingId,
67
+ headingLevel,
68
+ descriptionClassName: null,
69
+ }, "m-action-panel-title");
70
+ return (_jsxs("section", { id: id, ...componentRoot("ActionPanel"), className: cx("m-action-panel", className), "aria-labelledby": headingId, "data-tone": tone === "default" ? undefined : tone, children: [_jsxs("div", { className: "m-action-panel-text", children: [headingSlot, descriptionSlot, children] }), action && _jsx("div", { className: "m-action-panel-action", children: action })] }));
71
+ }
72
+ export function PageHeading({ title, description, eyebrow, actions, breadcrumbs, headingLevel = 1, meta = [], avatar, id, className, }) {
73
+ const text = (_jsxs("div", { children: [...classedIntroSlots({
74
+ eyebrow,
75
+ title,
76
+ description,
77
+ headingLevel,
78
+ descriptionClassName: null,
79
+ }, "m-page-title"), meta.length > 0 && (_jsx("ul", { className: "m-heading-meta", children: meta.map((item, index) => (_jsxs("li", { children: [item.icon && _jsx("span", { "aria-hidden": "true", children: item.icon }), item.label] }, `${item.label}:${index}`))) }))] }));
80
+ return (_jsxs("header", { id: id, ...componentRoot("PageHeading"), className: cx("m-page-heading", className), children: [breadcrumbs && _jsx(Breadcrumbs, { items: breadcrumbs }), _jsxs("div", { className: "m-heading-row", children: [avatar ? (_jsxs("div", { className: "m-heading-main", children: [_jsx("span", { className: "m-heading-avatar", "aria-hidden": "true", children: avatar }), text] })) : (text), actions && _jsx("div", { className: "m-actions", children: actions })] })] }));
81
+ }
82
+ export function MetricGroup({ label, items, variant = "shared", className, }) {
83
+ return (_jsx(TermList, { ...componentRoot("MetricGroup"), className: cx("m-metrics", className), "aria-label": label, "data-variant": variant === "cards" ? "cards" : undefined, items: items.map((item) => ({
84
+ key: item.label,
85
+ term: (_jsxs(_Fragment, { children: [item.icon && (_jsx("span", { className: "m-metric-icon", "aria-hidden": "true", children: item.icon })), item.label] })),
86
+ 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
+ }
89
+ const collator = new Intl.Collator(undefined, {
90
+ numeric: true,
91
+ sensitivity: "base",
92
+ });
93
+ const isEmpty = (v) => v === null || v === undefined || v === "";
94
+ function compareValues(a, b) {
95
+ if (isEmpty(a) || isEmpty(b))
96
+ return isEmpty(a) === isEmpty(b) ? 0 : isEmpty(a) ? 1 : -1;
97
+ if (a instanceof Date && b instanceof Date)
98
+ return a.getTime() - b.getTime();
99
+ if (typeof a === "number" && typeof b === "number")
100
+ return a - b;
101
+ return collator.compare(String(a), String(b));
102
+ }
103
+ /** Stable sort; empty values always sort last regardless of direction. */
104
+ export function sortRows(rows, column, direction) {
105
+ const value = column.sortValue ||
106
+ ((row) => row[column.key]);
107
+ const sign = direction === "ascending" ? 1 : -1;
108
+ return rows
109
+ .map((row, index) => ({ row, index, v: value(row) }))
110
+ .sort((a, b) =>
111
+ // Empty values sort last in both directions, so only real values flip.
112
+ (isEmpty(a.v) || isEmpty(b.v) ? 1 : sign) * compareValues(a.v, b.v) ||
113
+ a.index - b.index)
114
+ .map((entry) => entry.row);
115
+ }
116
+ function SelectAll({ checked, partial, onChange, }) {
117
+ const ref = useRef(null);
118
+ useEffect(() => {
119
+ if (ref.current)
120
+ ref.current.indeterminate = partial;
121
+ }, [partial]);
122
+ return (_jsx("input", { ref: ref, type: "checkbox", "aria-label": "Select all rows", checked: checked, onChange: onChange }));
123
+ }
124
+ /** Buckets ordered rows by group; groups keep their first-appearance order in the unsorted rows. */
125
+ function groupRows(rows, ordered, groupBy) {
126
+ const groups = new Map(rows.map((row) => [groupBy(row), []]));
127
+ ordered.forEach((row) => groups.get(groupBy(row)).push(row));
128
+ return [...groups];
129
+ }
130
+ /** Full-width header row that labels the rows of its tbody. */
131
+ function TableGroupRow({ label, span }) {
132
+ return (_jsx("tr", { className: "m-table-group", children: _jsx("th", { colSpan: span, scope: "rowgroup", children: label }) }));
133
+ }
134
+ const columnAlign = (align) => align === "end" ? "end" : undefined;
135
+ export function DataTable({ caption, columns, rows, selectedIds = [], onSelectionChange, rowLabel, rowHref, defaultSort, sort: controlledSort, onSortChange, groupBy, maxHeight, density = "default", striped = false, toolbar, bulkActions, footer, empty = {
136
+ title: "No records",
137
+ description: "Records will appear here when they are available.",
138
+ }, headingLevel = 3, loading, error, id, className, }) {
139
+ const [localSort, setLocalSort] = useState(defaultSort || null);
140
+ const sort = controlledSort !== undefined ? controlledSort : localSort;
141
+ const sortColumn = sort && columns.find((c) => c.key === sort.key);
142
+ const visibleRows = sort && sortColumn && !onSortChange
143
+ ? sortRows(rows, sortColumn, sort.direction)
144
+ : rows;
145
+ const toggleSort = (key) => {
146
+ const next = {
147
+ key,
148
+ direction: sort?.key === key && sort.direction === "ascending"
149
+ ? "descending"
150
+ : "ascending",
151
+ };
152
+ if (controlledSort === undefined)
153
+ setLocalSort(next);
154
+ onSortChange?.(next);
155
+ };
156
+ const pageIds = rows.map((row) => row.id);
157
+ const selectedHere = pageIds.filter((rowId) => selectedIds.includes(rowId));
158
+ const allHere = rows.length > 0 && selectedHere.length === rows.length;
159
+ // Select all adds or removes this page's rows and keeps selections made on
160
+ // other pages.
161
+ const toggleAll = () => onSelectionChange?.(allHere
162
+ ? selectedIds.filter((rowId) => !pageIds.includes(rowId))
163
+ : [
164
+ ...selectedIds,
165
+ ...pageIds.filter((rowId) => !selectedIds.includes(rowId)),
166
+ ]);
167
+ const span = columns.length + (onSelectionChange ? 1 : 0);
168
+ const renderRow = (row) => {
169
+ const href = rowHref?.(row);
170
+ return (_jsxs("tr", { "data-selected": selectedIds.includes(row.id) || undefined, "data-linked": href ? true : undefined, children: [onSelectionChange && (_jsx("td", { children: _jsx("input", { type: "checkbox", "aria-label": `Select ${rowLabel(row)}`, checked: selectedIds.includes(row.id), onChange: () => onSelectionChange(selectedIds.includes(row.id)
171
+ ? selectedIds.filter((rowId) => rowId !== row.id)
172
+ : [...selectedIds, row.id]) }) })), columns.map((col, index) => {
173
+ 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));
175
+ })] }, row.id));
176
+ };
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) => {
178
+ 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));
180
+ })] }) }), 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
+ }
182
+ /** Plain semantic table for documents, settings summaries and comparisons without row actions. */
183
+ export function Table({ caption, captionVisible = false, columns, rows = [], groups, rowHeaders = true, striped = false, id, className, }) {
184
+ const renderRow = (row, i) => (_jsx("tr", { children: columns.map((col, index) => index === 0 && rowHeaders ? (_jsx("th", { scope: "row", "data-align": columnAlign(col.align), children: row[col.key] }, col.key)) : (_jsx("td", { "data-align": columnAlign(col.align), children: row[col.key] }, col.key))) }, i));
185
+ return (_jsx("div", { id: id, ...componentRoot("Table"), className: cx("m-table", className), role: "region", "aria-label": caption, tabIndex: 0, "data-striped": striped || undefined, children: _jsxs("table", { children: [_jsx("caption", { className: captionVisible ? undefined : "m-sr-only", children: caption }), _jsx("thead", { children: _jsx("tr", { children: columns.map((col) => (_jsx("th", { scope: "col", "data-align": columnAlign(col.align), children: col.label }, col.key))) }) }), (!groups || rows.length > 0) && _jsx("tbody", { children: rows.map(renderRow) }), groups?.map((group, g) => (_jsxs("tbody", { children: [_jsx(TableGroupRow, { label: group.label, span: columns.length }), group.rows.map(renderRow)] }, g)))] }) }));
186
+ }
187
+ /** Page numbers to show: first, last and the current page's neighbours, with gaps. */
188
+ function pageWindow(page, count) {
189
+ const pages = new Set([1, count, page - 1, page, page + 1]);
190
+ const shown = [...pages]
191
+ .filter((n) => n >= 1 && n <= count)
192
+ .sort((a, b) => a - b);
193
+ const out = [];
194
+ shown.forEach((n, i) => {
195
+ if (i && n - shown[i - 1] > 1)
196
+ out.push("gap");
197
+ out.push(n);
198
+ });
199
+ return out;
200
+ }
201
+ export function Pagination({ page, pageCount, onChange, numbered = false, summary, label = "Pagination", className, }) {
202
+ const count = Math.max(1, pageCount);
203
+ const atStart = page <= 1;
204
+ const atEnd = page >= count;
205
+ // Previous and Next use aria-disabled rather than disabled at the ends, so
206
+ // the button someone just pressed keeps focus instead of dropping it to the
207
+ // page body.
208
+ return (_jsxs("nav", { ...componentRoot("Pagination"), className: cx("m-pagination", className), "aria-label": label, children: [summary ? (_jsxs("p", { "aria-live": "polite", children: ["Showing ", _jsx("strong", { children: summary.from }), " to", " ", _jsx("strong", { children: summary.to }), " of ", _jsx("strong", { children: summary.total }), " ", summary.noun ?? "results"] })) : (_jsxs("span", { "aria-live": "polite", children: ["Page ", page, " of ", count] })), _jsxs("div", { className: "m-actions", children: [_jsx(Button, { variant: "secondary", "aria-disabled": atStart || undefined, onClick: () => !atStart && onChange(page - 1), children: "Previous" }), numbered && (_jsx("div", { className: "m-pagination-pages", children: pageWindow(page, count).map((n, i) => n === "gap" ? (_jsx("span", { className: "m-pagination-gap", "aria-hidden": "true", children: "\u2026" }, `gap-${i}`)) : (_jsx("button", { type: "button", "aria-current": n === page ? "page" : undefined, "aria-label": `Page ${n}`, onClick: () => onChange(n), children: n }, n))) })), _jsx(Button, { variant: "secondary", "aria-disabled": atEnd || undefined, onClick: () => !atEnd && onChange(page + 1), children: "Next" })] })] }));
209
+ }
210
+ export function SettingsSection({ title, description, children, headingLevel = 2, id, className, }) {
211
+ const headingId = useId();
212
+ return (_jsxs("section", { id: id, ...componentRoot("SettingsSection"), className: cx("m-settings-section", className), "aria-labelledby": headingId, children: [_jsxs("div", { children: [...classedIntroSlots({
213
+ title,
214
+ description,
215
+ id: headingId,
216
+ headingLevel,
217
+ descriptionClassName: null,
218
+ }, "m-settings-title")] }), _jsx("div", { className: "m-stack", children: children })] }));
219
+ }
220
+ export function ActivityFeed({ label, items, className }) {
221
+ return (_jsx("ol", { ...componentRoot("ActivityFeed"), className: cx("m-activity", className), "aria-label": label, children: items.map((item) => (_jsxs("li", { children: [item.actor ? (_jsx("span", { className: "m-activity-actor", children: item.actor })) : (_jsx("span", { "aria-hidden": "true", className: "m-activity-dot", "data-tone": item.tone && item.tone !== "neutral" ? item.tone : undefined })), _jsxs("div", { children: [_jsx("strong", { children: item.title }), item.description && _jsx("p", { children: item.description }), item.body && (_jsx("blockquote", { className: "m-activity-body", children: item.body }))] }), _jsx("span", { children: item.time })] }, item.id))) }));
222
+ }
223
+ /**
224
+ * Brand-consistent sign-in surface. Mortar renders the form; the consumer's auth
225
+ * SDK supplies provider redirects and the email-link request through callbacks.
226
+ */
227
+ export function SignInPanel({ title, description, logo, providers = [], email, footer, dividerLabel = "or", headingLevel = 1, aside, id, className, }) {
228
+ const [value, setValue] = useState("");
229
+ const headingId = useId();
230
+ const submit = (e) => {
231
+ e.preventDefault();
232
+ if (email?.status !== "submitting")
233
+ email?.onSubmit(value);
234
+ };
235
+ const success = email?.status === "success";
236
+ const panel = (_jsxs("section", { id: aside ? undefined : id, ...componentRoot("SignInPanel", !aside), className: cx("m-sign-in", !aside && className), "aria-labelledby": headingId, children: [_jsx(AuthHeader, { titleClassName: "m-sign-in-title", logo: logo, title: title, description: description, headingLevel: headingLevel, headingId: headingId }), providers.length > 0 && _jsx(ProviderButtons, { providers: providers }), providers.length > 0 && email && (_jsx(Divider, { label: dividerLabel, soft: true, spacing: "none" })), email && (_jsxs("form", { onSubmit: submit, "aria-busy": email.status === "submitting", children: [_jsx(Input, { label: email.label || "Email address", type: "email", autoComplete: "email", placeholder: email.placeholder, required: true, value: value, onChange: (e) => setValue(e.target.value), error: email.status === "error" ? email.message : undefined, disabled: email.status === "submitting" }), _jsx("p", { role: "status", className: success ? undefined : "m-sr-only", children: success
237
+ ? email.message || "Check your inbox for a sign-in link."
238
+ : "" }), _jsx(Button, { type: "submit", loading: email.status === "submitting", children: email.submitLabel || "Email me a sign-in link" })] })), footer && _jsx("footer", { children: footer })] }));
239
+ return aside ? (_jsxs("div", { id: id, ...componentRoot("SignInPanel"), className: cx("m-sign-in-split", className), children: [panel, _jsx("div", { className: "m-sign-in-aside", children: aside })] })) : (panel);
240
+ }