@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,332 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Shared building blocks for the kit's components. Internal: nothing here is
3
+ // exported from the package entry, so these can change without a release note.
4
+ import { Fragment, createContext, createElement, useContext, useEffect, useId, useState, cloneElement, isValidElement, } from "react";
5
+ import { Dialog, DialogPanel, DialogTitle } from "@headlessui/react";
6
+ import { KitLink } from "./navigation.js";
7
+ import { ActionLink, Button } from "./primitives.js";
8
+ import { headingTag } from "./heading.js";
9
+ /**
10
+ * Names a catalogue component on its root: spread it on the outermost element
11
+ * the component renders, `<nav {...componentRoot("Pagination")}>`. A dialog
12
+ * marks its dialog root (portalled), and a component that renders sibling
13
+ * elements marks the first one it always renders. Pass `root = false` when
14
+ * this element is not the outermost one for the current props, e.g. a wrapper
15
+ * then takes the name. The attribute is a hook for tests and consumers, not
16
+ * for styling; the only selector on it is the internal Prose boundary.
17
+ */
18
+ export function componentRoot(name, root = true) {
19
+ return { "data-m-component": root ? name : undefined };
20
+ }
21
+ const ComponentRootContext = createContext(undefined);
22
+ /**
23
+ * Renders `children`, one `of` component, as the root of the component `name`:
24
+ * that root then carries `name` (SearchDialog renders a CommandPalette). Only
25
+ * an `of` component that reads `useComponentName` takes the name, so keep the
26
+ * same kind of component out of its slots.
27
+ */
28
+ export function AsComponentRoot({ name, of, children, }) {
29
+ return (_jsx(ComponentRootContext.Provider, { value: { name, of }, children: children }));
30
+ }
31
+ /**
32
+ * The root name of a component that another can render as its root: `name`,
33
+ * or the outer component's name inside its `AsComponentRoot`.
34
+ */
35
+ export function useComponentName(name) {
36
+ const outer = useContext(ComponentRootContext);
37
+ return outer?.of === name ? outer.name : name;
38
+ }
39
+ /** Joins the truthy class names with single spaces. */
40
+ export function cx(...classes) {
41
+ return classes.filter(Boolean).join(" ");
42
+ }
43
+ /**
44
+ * The eyebrow, heading and description that open a section, as three child
45
+ * slots. Spread them (`{...introSlots(props)}`) when consumer content follows
46
+ * in the same parent: React derives useId values from each child's position,
47
+ * so keeping the slots flat keeps the ids of that content unchanged.
48
+ */
49
+ export function introSlots({ eyebrow, title, emphasis, description, id, headingLevel = 2, descriptionClassName = "m-section-description", }) {
50
+ const Heading = headingTag(headingLevel);
51
+ return [
52
+ eyebrow && _jsx("p", { className: "m-eyebrow", children: eyebrow }),
53
+ title !== undefined && (_jsx(Heading, { id: id, children: emphasise(title, emphasis) })),
54
+ description && (_jsx("p", { className: descriptionClassName ?? undefined, children: description })),
55
+ ];
56
+ }
57
+ /** `title` with the first occurrence of `phrase` in `<em>`; unchanged when absent. */
58
+ function emphasise(title, phrase) {
59
+ const at = phrase ? title.indexOf(phrase) : -1;
60
+ if (!phrase || at < 0)
61
+ return title;
62
+ return (_jsxs(_Fragment, { children: [title.slice(0, at), _jsx("em", { className: "m-emphasis", children: phrase }), title.slice(at + phrase.length)] }));
63
+ }
64
+ /** The section intro as one element, for parents where nothing user-supplied follows it. */
65
+ export function SectionIntro(props) {
66
+ return _jsxs(_Fragment, { children: [...introSlots(props)] });
67
+ }
68
+ /** A row of link actions: the first primary, the rest secondary. */
69
+ export function ActionLinks({ actions }) {
70
+ return (_jsx("div", { className: "m-actions", children: actions.map((action, i) => (_jsx(ActionLink, { ...action, variant: i === 0 ? "primary" : "secondary" }, `${action.label}:${action.href}:${i}`))) }));
71
+ }
72
+ /** A description list whose terms each sit in a div with their details. */
73
+ export function TermList({ items, children, ...props }) {
74
+ // createElement keeps each element's child count the same as the hand-written
75
+ // lists this replaced (no empty `after` or `children` slot), so useId values
76
+ // inside the details do not move.
77
+ const rows = items.map((item) => createElement("div", { key: item.key ?? String(item.term), ...item.attributes }, _jsx("dt", { children: item.term }), _jsx("dd", { children: item.details }), ...("after" in item ? [item.after] : [])));
78
+ return createElement("dl", props, rows, ...(children === undefined ? [] : [children]));
79
+ }
80
+ /**
81
+ * Icon, label and count inside a navigation link or tab. `prefix` picks the
82
+ * class family (`m-nav-*` or `m-tab-*`). A count shows whenever it is defined.
83
+ */
84
+ export function LinkContent({ prefix, label, icon, count, marker = false, }) {
85
+ const glyph = (_jsx("span", { className: `m-${prefix}-icon`, "aria-hidden": "true", children: icon }));
86
+ // createElement keeps a fixed child count, so useId values inside a
87
+ // consumer's icon stay put.
88
+ return createElement(Fragment, null, marker ? (icon ? (glyph) : (_jsx("span", { className: "m-nav-marker", "aria-hidden": "true" }))) : (icon && glyph), label, count !== undefined && _jsx("span", { className: `m-${prefix}-count`, children: count }));
89
+ }
90
+ /** A generated id that is safe in CSS selectors and URL fragments. */
91
+ export function useDomId(prefix) {
92
+ return prefix + useId().replace(/[^a-zA-Z0-9-]/g, "");
93
+ }
94
+ /**
95
+ * Shared pause and resume for decorative motion. It plays only while the
96
+ * user has not paused it, reduced motion is not requested, the document is
97
+ * visible and `ref` is on screen. The user's pause is kept apart from the
98
+ * automatic reasons, so returning to the page never overrides it. `paused`
99
+ * and `onPausedChange` make the user's choice controlled, for example one
100
+ * state shared by several animations. Every listener is removed on unmount.
101
+ */
102
+ export function usePlayback(ref, { paused, onPausedChange, enabled = true, }) {
103
+ const [ownPaused, setOwnPaused] = useState(false);
104
+ const [reduced, setReduced] = useState(false);
105
+ const [hidden, setHidden] = useState(false);
106
+ const [offscreen, setOffscreen] = useState(false);
107
+ const userPaused = paused ?? ownPaused;
108
+ useEffect(() => {
109
+ if (!enabled)
110
+ return;
111
+ const media = window.matchMedia("(prefers-reduced-motion: reduce)");
112
+ const onMedia = () => setReduced(media.matches);
113
+ const onVisibility = () => setHidden(document.visibilityState === "hidden");
114
+ onMedia();
115
+ onVisibility();
116
+ media.addEventListener("change", onMedia);
117
+ document.addEventListener("visibilitychange", onVisibility);
118
+ const observer = ref.current && typeof IntersectionObserver !== "undefined"
119
+ ? new IntersectionObserver(([entry]) => setOffscreen(!entry.isIntersecting))
120
+ : null;
121
+ if (observer && ref.current)
122
+ observer.observe(ref.current);
123
+ return () => {
124
+ media.removeEventListener("change", onMedia);
125
+ document.removeEventListener("visibilitychange", onVisibility);
126
+ observer?.disconnect();
127
+ };
128
+ }, [enabled, ref]);
129
+ const reason = userPaused
130
+ ? "user"
131
+ : reduced
132
+ ? "motion"
133
+ : hidden
134
+ ? "hidden"
135
+ : offscreen
136
+ ? "offscreen"
137
+ : null;
138
+ const toggle = () => {
139
+ const next = !userPaused;
140
+ if (paused === undefined)
141
+ setOwnPaused(next);
142
+ onPausedChange?.(next);
143
+ };
144
+ return { reason, reduced, userPaused, toggle };
145
+ }
146
+ /**
147
+ * Next index for arrow, Home and End keys in a horizontal set (wrapping), or
148
+ * null for any other key.
149
+ */
150
+ export function rovingIndex(key, index, count) {
151
+ if (key === "ArrowRight")
152
+ return (index + 1) % count;
153
+ if (key === "ArrowLeft")
154
+ return (index - 1 + count) % count;
155
+ if (key === "Home")
156
+ return 0;
157
+ if (key === "End")
158
+ return count - 1;
159
+ return null;
160
+ }
161
+ /** Colour swatch whose label is announced in place of the colour. */
162
+ export function Swatch({ color, label }) {
163
+ return (_jsxs(_Fragment, { children: [_jsx("span", { className: "m-swatch", "aria-hidden": "true", style: { background: color } }), _jsx("span", { className: "m-sr-only", children: label })] }));
164
+ }
165
+ /** Ids and describedby wiring for a labelled field with a hint or error message. */
166
+ export function useField({ id, hint, error, describedBy, }) {
167
+ const generated = useId();
168
+ const fieldId = id || generated;
169
+ const messageId = fieldId + "-message";
170
+ return {
171
+ fieldId,
172
+ messageId,
173
+ describedBy: [describedBy, (error || hint) && messageId].filter(Boolean).join(" ") ||
174
+ undefined,
175
+ };
176
+ }
177
+ /** The hint or error under a field; the error wins and is styled as one. */
178
+ export function FieldMessage({ id, hint, error, }) {
179
+ return error || hint ? (_jsx("p", { id: id, className: error ? "m-error" : "", children: error || hint })) : null;
180
+ }
181
+ /** Headless dialog root with the shared backdrop. */
182
+ export function DialogFrame({ open, onClose, component, children, }) {
183
+ return (_jsx(Dialog, { open: open, onClose: onClose, className: "m-dialog", ...componentRoot(component ?? "", component !== undefined), children: _jsxs(ThemeScope, { children: [_jsx("div", { className: "m-backdrop", "aria-hidden": "true" }), children] }) }));
184
+ }
185
+ /** Narrow-screen navigation drawer used by AppShell and DocsShell. */
186
+ export function MobileNavDialog({ open, onClose, title, children, }) {
187
+ return (_jsx(DialogFrame, { open: open, onClose: onClose, children: _jsxs(DialogPanel, { className: "m-mobile-sidebar", children: [_jsxs("div", { className: "m-dialog-heading", children: [_jsx(DialogTitle, { children: title }), _jsx(Button, { variant: "ghost", onClick: onClose, children: "Close navigation" })] }), children] }) }));
188
+ }
189
+ /** `introSlots` with the shared `m-section-title` class on the heading. */
190
+ export function titledIntroSlots(props) {
191
+ return classedIntroSlots(props, "m-section-title");
192
+ }
193
+ /** `data-tone` for a section surface tone; the default tone sets no attribute. */
194
+ export function sectionTone(tone) {
195
+ return tone && tone !== "default" ? tone : undefined;
196
+ }
197
+ /** `SectionIntro` with the `m-section-title` heading class. */
198
+ export function TitledIntro(props) {
199
+ return _jsxs(_Fragment, { children: [...titledIntroSlots(props)] });
200
+ }
201
+ /**
202
+ * Navigation links with icon and count. An item with children becomes a
203
+ * disclosure, open when it or a child is current; its own page is the first
204
+ * link inside.
205
+ */
206
+ export function NavTreeLinks({ items, marker = false, onNavigate, }) {
207
+ const link = (item, key) => (_jsx(KitLink, { href: item.href, "aria-current": item.current ? "page" : undefined, onClick: onNavigate, children: _jsx(LinkContent, { prefix: "nav", marker: marker, icon: item.icon, label: item.label, count: item.count }) }, key));
208
+ return (_jsx(_Fragment, { children: items.map((item, index) => {
209
+ const key = `${item.label}:${item.href}:${index}`;
210
+ return item.children?.length ? (_jsxs("details", { className: "m-nav-group", open: item.current ||
211
+ item.children.some((child) => child.current) ||
212
+ undefined, children: [_jsx("summary", { children: _jsx(LinkContent, { prefix: "nav", marker: marker, icon: item.icon, label: item.label }) }), _jsxs("div", { className: "m-nav-children", children: [item.href && link({ ...item, icon: undefined }, "parent"), _jsx(NavTreeLinks, { items: item.children, marker: marker, onNavigate: onNavigate })] })] }, key)) : (link(item, key));
213
+ }) }));
214
+ }
215
+ /**
216
+ * Labelled groups of navigation links. Group labels are not headings, so the
217
+ * page's own h1 stays first in the heading outline.
218
+ */
219
+ export function NavGroups({ groups, marker, onNavigate, }) {
220
+ const groupId = useDomId("m-nav-group-");
221
+ return (_jsx(_Fragment, { children: groups.map((group, index) => (_jsxs("div", { role: "group", "aria-labelledby": `${groupId}-${index}`, children: [_jsx("p", { id: `${groupId}-${index}`, className: "m-nav-group-title", children: group.label }), _jsx(NavTreeLinks, { items: group.items, marker: marker, onNavigate: onNavigate })] }, group.label))) }));
222
+ }
223
+ /**
224
+ * The hint or error under a field. Unlike FieldMessage, the error is its own
225
+ * role="alert" element, so it is announced when it appears.
226
+ */
227
+ export function FieldFeedback({ id, hint, error, }) {
228
+ if (error)
229
+ return (_jsx("p", { id: id, className: "m-error", role: "alert", children: error }, "error"));
230
+ return hint ? (_jsx("p", { id: id, children: hint }, "hint")) : null;
231
+ }
232
+ /** Decorative required marker after a field label; the control itself is marked required. */
233
+ export function RequiredMark() {
234
+ return (_jsx("span", { className: "m-required", "aria-hidden": "true", children: "*" }));
235
+ }
236
+ const TABBABLE = 'a[href], button:not(:disabled), input:not(:disabled):not([type="hidden"]), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])';
237
+ /**
238
+ * Call before `element` is removed. If focus is inside it, move focus to the
239
+ * next tabbable element after it (within `scope`, default the document), else
240
+ * the previous one, else `fallback`, so focus never drops to the body.
241
+ */
242
+ export function focusAway(element, { scope, fallback, } = {}) {
243
+ if (!element)
244
+ return;
245
+ const document = element.ownerDocument;
246
+ if (!element.contains(document.activeElement))
247
+ return;
248
+ const candidates = [
249
+ ...(scope ?? document.body).querySelectorAll(TABBABLE),
250
+ ].filter((el) => !element.contains(el) &&
251
+ !el.closest("[hidden], [inert]") &&
252
+ el.getClientRects().length > 0);
253
+ const following = (el) => element.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING;
254
+ // Nearest first: following elements, then preceding ones from the closest.
255
+ // Some candidates have boxes but cannot take focus (content of a closed
256
+ // <details>), so keep trying until one does.
257
+ const ordered = [
258
+ ...candidates.filter(following),
259
+ ...candidates.filter((el) => !following(el)).reverse(),
260
+ ];
261
+ for (const el of fallback ? [...ordered, fallback] : ordered) {
262
+ el.focus();
263
+ if (document.activeElement === el)
264
+ return;
265
+ }
266
+ }
267
+ /**
268
+ * The data-mortar-* attributes of the nearest container-scoped ThemeProvider.
269
+ * Optional (undefined outside one), so it does not make dialogs depend on it.
270
+ */
271
+ export const ThemeScopeContext = createContext(undefined);
272
+ /**
273
+ * Re-applies a container-scoped theme to content rendered outside its wrapper
274
+ * (dialogs portal to the document body). The wrapper is display: contents, so
275
+ * it carries the tokens without adding a box.
276
+ */
277
+ export function ThemeScope({ children }) {
278
+ const attributes = useContext(ThemeScopeContext);
279
+ if (!attributes)
280
+ return _jsx(_Fragment, { children: children });
281
+ return (_jsx("div", { ...attributes, className: "m-theme-scope", children: children }));
282
+ }
283
+ /**
284
+ * introSlots with a class on the heading, so a title keeps its styles at any
285
+ * `headingLevel` instead of only as an h2.
286
+ */
287
+ export function classedIntroSlots(props, titleClassName) {
288
+ const [eyebrow, heading, description] = introSlots(props);
289
+ return [
290
+ eyebrow,
291
+ isValidElement(heading)
292
+ ? cloneElement(heading, { className: titleClassName })
293
+ : heading,
294
+ description,
295
+ ];
296
+ }
297
+ /**
298
+ * Call before a consumer callback that removes `element` (e.g. a chip's
299
+ * remove button). Once the removal has rendered, if focus fell to the body,
300
+ * it moves to the nearest focusable element that took the removed one's place,
301
+ * or the one before it, searching up through the ancestors that remain.
302
+ */
303
+ export function focusAfterRemoval(element) {
304
+ const trail = [];
305
+ for (let node = element; node?.parentElement;) {
306
+ const parent = node.parentElement;
307
+ trail.push([parent, [...parent.children].indexOf(node)]);
308
+ node = parent;
309
+ }
310
+ setTimeout(() => {
311
+ const active = element.ownerDocument.activeElement;
312
+ if (element.isConnected ||
313
+ (active && active !== element.ownerDocument.body))
314
+ return;
315
+ for (const [parent, index] of trail) {
316
+ if (!parent.isConnected)
317
+ continue;
318
+ const siblings = [...parent.children];
319
+ const candidates = [
320
+ ...siblings.slice(index),
321
+ ...siblings.slice(0, index).reverse(),
322
+ ];
323
+ for (const candidate of candidates) {
324
+ const target = candidate.matches(TABBABLE)
325
+ ? candidate
326
+ : candidate.querySelector(TABBABLE);
327
+ if (target)
328
+ return target.focus();
329
+ }
330
+ }
331
+ });
332
+ }
@@ -0,0 +1,107 @@
1
+ import { type ElementType, type ReactNode } from "react";
2
+ import { type HeadingLevel } from "./heading.js";
3
+ type Space = "none" | "xs" | "sm" | "md" | "lg" | "xl";
4
+ export interface StackProps {
5
+ as?: ElementType;
6
+ gap?: Space;
7
+ direction?: "column" | "row";
8
+ align?: "start" | "center" | "end" | "stretch";
9
+ justify?: "start" | "center" | "end" | "between";
10
+ wrap?: boolean;
11
+ children: ReactNode;
12
+ className?: string;
13
+ }
14
+ /** Vertical or horizontal rhythm without consumer flex/gap utilities. */
15
+ export declare function Stack({ as: Tag, gap, direction, align, justify, wrap, children, className, }: StackProps): import("react").JSX.Element;
16
+ export interface ContainerProps {
17
+ as?: ElementType;
18
+ size?: "sm" | "md" | "lg" | "xl";
19
+ padded?: boolean;
20
+ children: ReactNode;
21
+ className?: string;
22
+ }
23
+ /** Centred page width with the shared gutters used by marketing and application pages. */
24
+ export declare function Container({ as: Tag, size, padded, children, className, }: ContainerProps): import("react").JSX.Element;
25
+ export interface TextProps {
26
+ as?: ElementType;
27
+ /** Text colour; `data-tone` stays reserved for status and surface tones. */
28
+ color?: "default" | "secondary" | "muted";
29
+ size?: "sm" | "md" | "lg";
30
+ weight?: "regular" | "medium" | "semibold";
31
+ children: ReactNode;
32
+ className?: string;
33
+ }
34
+ /** Body copy with semantic colour and size, instead of text-muted/text-ink utilities. */
35
+ export declare function Text({ as: Tag, color, size, weight, children, className, }: TextProps): import("react").JSX.Element;
36
+ export type SectionTone = "default" | "muted" | "inverse";
37
+ export interface SectionProps {
38
+ id?: string;
39
+ eyebrow?: string;
40
+ title?: string;
41
+ description?: string;
42
+ tone?: SectionTone;
43
+ align?: "start" | "center";
44
+ headingLevel?: HeadingLevel;
45
+ children?: ReactNode;
46
+ className?: string;
47
+ }
48
+ /** Full-bleed marketing band for content that is not a grid or split feature. */
49
+ export declare function Section({ id, eyebrow, title, description, tone, align, headingLevel, children, className, }: SectionProps): import("react").JSX.Element;
50
+ /** Brand mark: a supplied logo node, or the neutral placeholder square. */
51
+ export declare function BrandMark({ logo }: {
52
+ logo?: ReactNode;
53
+ }): import("react").JSX.Element;
54
+ export interface DividerProps {
55
+ label?: string;
56
+ soft?: boolean;
57
+ spacing?: "none" | "sm" | "md" | "lg";
58
+ className?: string;
59
+ }
60
+ /** Horizontal rule between groups; a label names the break for assistive technology. */
61
+ export declare function Divider({ label, soft, spacing, className, }: DividerProps): import("react").JSX.Element;
62
+ export interface CardProps {
63
+ as?: "div" | "section" | "article";
64
+ title?: string;
65
+ description?: string;
66
+ headingLevel?: HeadingLevel;
67
+ actions?: ReactNode;
68
+ footer?: ReactNode;
69
+ padding?: "default" | "compact" | "none";
70
+ /** `muted` sets the card on the sunken surface. */
71
+ tone?: "default" | "muted";
72
+ children?: ReactNode;
73
+ id?: string;
74
+ className?: string;
75
+ }
76
+ /** Bordered surface with optional heading, header actions and footer. */
77
+ export declare function Card({ as: Tag, title, description, headingLevel, actions, footer, padding, tone, children, id, className, }: CardProps): import("react").JSX.Element;
78
+ export interface MediaObjectProps {
79
+ media: ReactNode;
80
+ title?: string;
81
+ align?: "start" | "center";
82
+ reverse?: boolean;
83
+ children?: ReactNode;
84
+ className?: string;
85
+ }
86
+ /** Image, avatar or icon beside text, e.g. comments and list rows. */
87
+ export declare function MediaObject({ media, title, align, reverse, children, className, }: MediaObjectProps): import("react").JSX.Element;
88
+ export interface SectionHeadingProps {
89
+ title: string;
90
+ description?: string;
91
+ actions?: ReactNode;
92
+ headingLevel?: HeadingLevel;
93
+ divider?: boolean;
94
+ /** Page tabs (e.g. TabNav) below the title row; their underline replaces the divider. */
95
+ tabs?: ReactNode;
96
+ id?: string;
97
+ className?: string;
98
+ }
99
+ /** Heading for a section within a page, with optional description and actions. */
100
+ export declare function SectionHeading({ title, description, actions, headingLevel, divider, tabs, id, className, }: SectionHeadingProps): import("react").JSX.Element;
101
+ export interface ProseProps {
102
+ children: ReactNode;
103
+ className?: string;
104
+ }
105
+ /** Long-form copy with the kit's typographic rhythm. */
106
+ export declare function Prose({ children, className }: ProseProps): import("react").JSX.Element;
107
+ export {};
package/dist/layout.js ADDED
@@ -0,0 +1,66 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId } from "react";
3
+ import { componentRoot, cx, classedIntroSlots, titledIntroSlots, } from "./internal.js";
4
+ /** Vertical or horizontal rhythm without consumer flex/gap utilities. */
5
+ export function Stack({ as: Tag = "div", gap = "md", direction = "column", align, justify, wrap = false, children, className, }) {
6
+ return (_jsx(Tag, { ...componentRoot("Stack"), className: cx("m-layout-stack", className), "data-gap": gap === "md" ? undefined : gap, "data-direction": direction === "column" ? undefined : direction, "data-align": align, "data-justify": justify, "data-wrap": wrap || undefined, children: children }));
7
+ }
8
+ /** Centred page width with the shared gutters used by marketing and application pages. */
9
+ export function Container({ as: Tag = "div", size = "lg", padded = true, children, className, }) {
10
+ return (_jsx(Tag, { ...componentRoot("Container"), className: cx("m-container", className), "data-size": size === "lg" ? undefined : size, "data-padded": padded || undefined, children: children }));
11
+ }
12
+ /** Body copy with semantic colour and size, instead of text-muted/text-ink utilities. */
13
+ export function Text({ as: Tag = "p", color = "default", size = "md", weight = "regular", children, className, }) {
14
+ return (_jsx(Tag, { ...componentRoot("Text"), className: cx("m-text", className), "data-color": color === "default" ? undefined : color, "data-size": size === "md" ? undefined : size, "data-weight": weight === "regular" ? undefined : weight, children: children }));
15
+ }
16
+ /** Full-bleed marketing band for content that is not a grid or split feature. */
17
+ export function Section({ id, eyebrow, title, description, tone = "default", align = "start", headingLevel = 2, children, className, }) {
18
+ return (_jsxs("section", { id: id, className: cx("m-marketing-section m-section", className), "data-tone": tone === "default" ? undefined : tone, "data-align": align === "start" ? undefined : align, ...componentRoot("Section"), children: [...titledIntroSlots({
19
+ eyebrow,
20
+ title: title || undefined,
21
+ description,
22
+ headingLevel,
23
+ }), children && _jsx("div", { className: "m-section-body", children: children })] }));
24
+ }
25
+ /** Brand mark: a supplied logo node, or the neutral placeholder square. */
26
+ export function BrandMark({ logo }) {
27
+ return logo ? (_jsx("span", { className: "m-logo", "aria-hidden": "true", children: logo })) : (_jsx("span", { className: "m-mark", "aria-hidden": "true" }));
28
+ }
29
+ /** Horizontal rule between groups; a label names the break for assistive technology. */
30
+ export function Divider({ label, soft = false, spacing = "md", className, }) {
31
+ const attributes = {
32
+ ...componentRoot("Divider"),
33
+ className: cx("m-divider", className),
34
+ "data-soft": soft || undefined,
35
+ "data-spacing": spacing === "md" ? undefined : spacing,
36
+ };
37
+ return label ? (_jsx("div", { ...attributes, role: "separator", "aria-label": label, children: _jsx("span", { "aria-hidden": "true", children: label }) })) : (_jsx("hr", { ...attributes }));
38
+ }
39
+ /** Bordered surface with optional heading, header actions and footer. */
40
+ export function Card({ as: Tag = "div", title, description, headingLevel = 2, actions, footer, padding = "default", tone = "default", children, id, className, }) {
41
+ const headingId = useId();
42
+ return (_jsxs(Tag, { id: id, ...componentRoot("Card"), className: cx("m-card", className), "data-padding": padding === "default" ? undefined : padding, "data-tone": tone === "default" ? undefined : tone, "aria-labelledby": title && Tag !== "div" ? headingId : undefined, children: [(title || actions) && (_jsxs("header", { className: "m-card-header", children: [title && (_jsxs("div", { className: "m-card-heading", children: [...classedIntroSlots({
43
+ title,
44
+ description,
45
+ id: headingId,
46
+ headingLevel,
47
+ descriptionClassName: null,
48
+ }, "m-card-title")] })), actions && _jsx("div", { className: "m-card-actions", children: actions })] })), children && _jsx("div", { className: "m-card-body", children: children }), footer && _jsx("footer", { className: "m-card-footer", children: footer })] }));
49
+ }
50
+ /** Image, avatar or icon beside text, e.g. comments and list rows. */
51
+ export function MediaObject({ media, title, align = "start", reverse = false, children, className, }) {
52
+ return (_jsxs("div", { ...componentRoot("MediaObject"), className: cx("m-media-object", className), "data-align": align === "start" ? undefined : align, "data-reverse": reverse || undefined, children: [_jsx("div", { className: "m-media-object-media", children: media }), _jsxs("div", { className: "m-media-object-body", children: [title && _jsx("p", { className: "m-media-object-title", children: title }), children] })] }));
53
+ }
54
+ /** Heading for a section within a page, with optional description and actions. */
55
+ export function SectionHeading({ title, description, actions, headingLevel = 2, divider = true, tabs, id, className, }) {
56
+ return (_jsxs("div", { id: id, ...componentRoot("SectionHeading"), className: cx("m-section-heading", className), "data-divider": divider || undefined, children: [_jsxs("div", { className: "m-section-heading-text", children: [...classedIntroSlots({
57
+ title,
58
+ description,
59
+ headingLevel,
60
+ descriptionClassName: null,
61
+ }, "m-section-heading-title")] }), actions && _jsx("div", { className: "m-actions", children: actions }), tabs && _jsx("div", { className: "m-section-heading-tabs", children: tabs })] }));
62
+ }
63
+ /** Long-form copy with the kit's typographic rhythm. */
64
+ export function Prose({ children, className }) {
65
+ return (_jsx("div", { ...componentRoot("Prose"), className: cx("m-prose", className), children: children }));
66
+ }
@@ -0,0 +1,161 @@
1
+ import { type IconLink } from "./navigation.js";
2
+ import { type ReactNode } from "react";
3
+ import { type LinkAction } from "./primitives.js";
4
+ import type { SectionTone } from "./layout.js";
5
+ import { type HeadingLevel } from "./heading.js";
6
+ export declare function BentoGrid({ id, className, eyebrow, title, description, items, tone, headingLevel, }: {
7
+ id?: string;
8
+ className?: string;
9
+ eyebrow?: string;
10
+ title: string;
11
+ description?: string;
12
+ items: {
13
+ title: string;
14
+ description: string;
15
+ visual?: ReactNode;
16
+ /** Columns the tile spans. */
17
+ span?: 1 | 2;
18
+ /** Rows the tile spans, e.g. tall tiles at both ends of a three-column grid. */
19
+ rowSpan?: 1 | 2;
20
+ /** The tile's surface; inverse remaps it to the inverse tokens. */
21
+ tone?: SectionTone;
22
+ }[];
23
+ /** The section's band; inverse gives a dark section whose tiles follow the remapped tokens. */
24
+ tone?: SectionTone;
25
+ /** The section title's level; tile titles sit one level below. */
26
+ headingLevel?: HeadingLevel;
27
+ }): import("react").JSX.Element;
28
+ export declare function StatGroup({ id, className, label, items, layout, background, }: {
29
+ id?: string;
30
+ className?: string;
31
+ label: string;
32
+ /** description: optional supporting sentence under the value. */
33
+ items: {
34
+ value: string;
35
+ label: string;
36
+ description?: string;
37
+ }[];
38
+ /** grid: ruled columns. timeline: a dotted line through the items, for dated milestones (value is the date). stepped: cards that rise in height from first to last. */
39
+ layout?: "grid" | "timeline" | "stepped";
40
+ /** Decorative full-cover image behind the stats. The group switches to the inverse tone with a scrim so text keeps its contrast. */
41
+ background?: ReactNode;
42
+ }): import("react").JSX.Element;
43
+ export declare function TestimonialGrid({ id, className, title, items, headingLevel, layout, }: {
44
+ id?: string;
45
+ className?: string;
46
+ title?: string;
47
+ items: {
48
+ quote: string;
49
+ name: string;
50
+ role?: string;
51
+ avatar?: string;
52
+ /** A larger quote that spans the grid's middle columns, or every column in masonry. */
53
+ featured?: boolean;
54
+ }[];
55
+ headingLevel?: HeadingLevel;
56
+ /** masonry: quotes flow down balanced columns at their own heights. grid: rows of equal height. */
57
+ layout?: "masonry" | "grid";
58
+ }): import("react").JSX.Element;
59
+ export declare function PlanComparison({ id, className, title, description, plans, featuredPlan, groups, headingLevel, }: {
60
+ id?: string;
61
+ className?: string;
62
+ title: string;
63
+ description?: string;
64
+ plans: string[];
65
+ featuredPlan?: string;
66
+ groups: {
67
+ label: string;
68
+ rows: {
69
+ feature: string;
70
+ values: (boolean | string)[];
71
+ }[];
72
+ }[];
73
+ headingLevel?: HeadingLevel;
74
+ }): import("react").JSX.Element;
75
+ export declare function PostList({ id, className, title, description, posts, action, headingLevel, itemHeadingLevel, }: {
76
+ id?: string;
77
+ className?: string;
78
+ title: string;
79
+ description?: string;
80
+ posts: {
81
+ href: string;
82
+ title: string;
83
+ excerpt: string;
84
+ date: string;
85
+ dateLabel: string;
86
+ category?: string;
87
+ image?: ReactNode;
88
+ /** Spans the full row with the image beside a larger title; usually the first post. */
89
+ featured?: boolean;
90
+ }[];
91
+ action?: LinkAction;
92
+ headingLevel?: HeadingLevel;
93
+ /** Level of each post title; defaults to one below `headingLevel`. */
94
+ itemHeadingLevel?: HeadingLevel;
95
+ }): import("react").JSX.Element;
96
+ export declare function TeamGrid({ id, className, title, description, members, headingLevel, layout, }: {
97
+ id?: string;
98
+ className?: string;
99
+ title: string;
100
+ description?: string;
101
+ members: {
102
+ name: string;
103
+ role: string;
104
+ image?: ReactNode;
105
+ bio?: string;
106
+ /** Profile links, e.g. icon-only social links named "Ana on LinkedIn". */
107
+ links?: IconLink[];
108
+ }[];
109
+ headingLevel?: HeadingLevel;
110
+ /** grid: small square portraits. large: fewer columns of tall portraits. list: each portrait beside the name, role and bio. */
111
+ layout?: "grid" | "large" | "list";
112
+ }): import("react").JSX.Element;
113
+ export declare function ContactSection({ id, className, title, description, details, headingLevel, media, offices, children, }: {
114
+ id?: string;
115
+ className?: string;
116
+ title: string;
117
+ description?: string;
118
+ details?: {
119
+ label: string;
120
+ value: ReactNode;
121
+ }[];
122
+ headingLevel?: HeadingLevel;
123
+ /** An image filling one half of a split layout, beside the intro and form; give it alt text or alt="" if decorative. */
124
+ media?: ReactNode;
125
+ /** Office locations in a grid below; each name is a heading one level below the title. */
126
+ offices?: {
127
+ name: string;
128
+ address: ReactNode;
129
+ }[];
130
+ /** The consumer's form. Compose with Input, Select, TextArea and Button. Optional when the section lists offices. */
131
+ children?: ReactNode;
132
+ }): import("react").JSX.Element;
133
+ export declare function Newsletter({ id, className, title, description, onSubmit, submitLabel, status, message, headingLevel, layout, variant, }: {
134
+ id?: string;
135
+ className?: string;
136
+ title: string;
137
+ description?: string;
138
+ onSubmit: (email: string) => void;
139
+ submitLabel?: string;
140
+ /** submitting: busy and inert. success: announces `message`. error: shows `message` on the field. */
141
+ status?: "idle" | "submitting" | "success" | "error";
142
+ message?: string;
143
+ headingLevel?: HeadingLevel;
144
+ /** split: the copy beside the form. center: the copy above a centred form. */
145
+ layout?: "split" | "center";
146
+ /** plain: a section divided by a top rule. panel: a rounded, sunken card. */
147
+ variant?: "plain" | "panel";
148
+ }): import("react").JSX.Element;
149
+ export declare function NotFound({ id, className, code, title, description, actions, illustration, background, headingLevel, }: {
150
+ id?: string;
151
+ className?: string;
152
+ code?: string;
153
+ title: string;
154
+ description: string;
155
+ actions: LinkAction[];
156
+ /** Decorative artwork above the code. */
157
+ illustration?: ReactNode;
158
+ /** Decorative full-cover image behind the message. The section switches to the inverse tone with a scrim so text keeps its contrast. */
159
+ background?: ReactNode;
160
+ headingLevel?: HeadingLevel;
161
+ }): import("react").JSX.Element;