@tonycodes-ui/mortar 0.12.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. */
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ 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
10
  export { MarketingHeader, MarketingPage, Hero, FeatureGrid, FeatureSection, ProcessSteps, Testimonial, LogoCloud, Pricing, FAQ, CallToAction, Footer, HeaderSection, ContentSection, } from "./marketing.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
1
  import { type HTMLAttributes, type RefObject, type ReactNode } from "react";
2
- import { type ItemGroup } from "./navigation.js";
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
  /**
@@ -512,3 +512,17 @@ export declare function classedIntroSlots(props: IntroProps, titleClassName: str
512
512
  * or the one before it, searching up through the ancestors that remain.
513
513
  */
514
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
@@ -2,8 +2,9 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
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
4
  import { Fragment, createContext, createElement, useContext, useEffect, useId, useState, cloneElement, isValidElement, } from "react";
5
- import { Dialog, DialogPanel, DialogTitle } from "@headlessui/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
  /**
@@ -330,3 +331,13 @@ export function focusAfterRemoval(element) {
330
331
  }
331
332
  });
332
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,9 +1,10 @@
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[];
package/dist/marketing.js CHANGED
@@ -1,18 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { IconLinks, KitLink, } from "./navigation.js";
3
3
  import { useId, useState } from "react";
4
- import { CloseButton, DialogPanel, DialogTitle, Popover, PopoverButton, PopoverPanel, } from "@headlessui/react";
4
+ import { DialogPanel, DialogTitle } from "@headlessui/react";
5
5
  import { ActionLink, Button } from "./primitives.js";
6
6
  import { BrandMark, Prose } from "./layout.js";
7
7
  import { SegmentedControl } from "./inspection.js";
8
8
  import { ChevronDown } from "./icons.js";
9
9
  import { StatGroup } from "./marketing-sections.js";
10
10
  import { childLevel, headingTag } from "./heading.js";
11
- import { ActionLinks, DialogFrame, TermList, TitledIntro, cx, componentRoot, titledIntroSlots, sectionTone, useDomId, } from "./internal.js";
12
- function Flyout({ label, items, footer = [], layout = "list", onNavigate, }) {
13
- const dataLayout = layout === "list" ? undefined : layout;
14
- return (_jsxs(Popover, { className: "m-flyout", "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, 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}`))) }))] })] }));
15
- }
11
+ import { ActionLinks, DialogFrame, Flyout, TermList, TitledIntro, cx, componentRoot, titledIntroSlots, sectionTone, useDomId, } from "./internal.js";
16
12
  export function MarketingHeader({ brand, tagline, homeHref = "/", logo, links, action, controls, layout = "inline", mobileMenu = "inline", className, id, }) {
17
13
  const [open, setOpen] = useState(false);
18
14
  // Generated so two headers on one page keep distinct, valid references.
@@ -80,14 +80,16 @@ function DescribedChoice({ component, input, id, label, description, error, requ
80
80
  .join(" ") || undefined;
81
81
  return (_jsxs("div", { ...componentRoot(component), className: cx("m-choice", "m-choice-described", className), children: [input({ id: fieldId, "aria-describedby": ids }), _jsxs("div", { className: "m-choice-text", children: [_jsxs("label", { htmlFor: fieldId, children: [label, required && _jsx(RequiredMark, {})] }), description && _jsx("p", { id: descriptionId, children: description }), _jsx(FieldFeedback, { id: messageId, error: error })] })] }));
82
82
  }
83
- export function Checkbox({ label, description, error, className, ...props }) {
84
- const input = useRef(null);
85
- // Once an error has shown, keep the described layout: switching back to the
86
- // plain label would remount the input and drop focus while the person fixes
87
- // it. If the layout changes while the input has focus, focus follows it.
83
+ /**
84
+ * Keeps a choice in the described layout once it has had a description or
85
+ * error. Switching back to the plain label would remount the input and drop
86
+ * focus mid-interaction (for example a description that changes with the
87
+ * value). If the layout changes while the input has focus, focus follows it.
88
+ */
89
+ function useDescribedLayout(active, input) {
88
90
  const [described, setDescribed] = useState(false);
89
91
  const refocus = useRef(false);
90
- if (error && !described) {
92
+ if (active && !described) {
91
93
  refocus.current =
92
94
  typeof document !== "undefined" &&
93
95
  !!input.current &&
@@ -98,15 +100,22 @@ export function Checkbox({ label, description, error, className, ...props }) {
98
100
  if (refocus.current)
99
101
  input.current?.focus();
100
102
  refocus.current = false;
101
- }, [described]);
102
- if (description || error || described)
103
+ }, [described, input]);
104
+ return described;
105
+ }
106
+ export function Checkbox({ label, description, error, className, ...props }) {
107
+ const input = useRef(null);
108
+ const described = useDescribedLayout(!!(description || error), input);
109
+ if (described)
103
110
  return (_jsx(DescribedChoice, { component: "Checkbox", id: props.id, label: label, description: description, error: error, required: props.required, describedBy: props["aria-describedby"], className: className, input: (attrs) => (_jsx("input", { ...props, ...attrs, ref: input, "aria-invalid": error ? true : props["aria-invalid"], type: "checkbox" })) }));
104
111
  return (_jsxs("label", { ...componentRoot("Checkbox"), className: cx("m-choice", className), children: [_jsx("input", { ...props, ref: input, type: "checkbox" }), label, props.required && _jsx(RequiredMark, {})] }));
105
112
  }
106
113
  export function Switch({ label, description, className, ...props }) {
107
- if (description)
108
- return (_jsx(DescribedChoice, { component: "Switch", id: props.id, label: label, description: description, className: className, input: (attrs) => (_jsx("input", { ...props, ...attrs, type: "checkbox", role: "switch", className: "m-switch" })) }));
109
- return (_jsxs("label", { ...componentRoot("Switch"), className: cx("m-choice", className), children: [_jsx("input", { ...props, type: "checkbox", role: "switch", className: "m-switch" }), label] }));
114
+ const input = useRef(null);
115
+ const described = useDescribedLayout(!!description, input);
116
+ if (described)
117
+ return (_jsx(DescribedChoice, { component: "Switch", id: props.id, label: label, description: description, className: className, input: (attrs) => (_jsx("input", { ...props, ...attrs, ref: input, type: "checkbox", role: "switch", className: "m-switch" })) }));
118
+ return (_jsxs("label", { ...componentRoot("Switch"), className: cx("m-choice", className), children: [_jsx("input", { ...props, ref: input, type: "checkbox", role: "switch", className: "m-switch" }), label] }));
110
119
  }
111
120
  export function RadioGroup({ label, name, value, onChange, options, layout = "list", hint, error, required, id, className, }) {
112
121
  const { messageId, describedBy } = useField({ id, hint, error });
package/docs/agents.md CHANGED
@@ -94,27 +94,30 @@ Add `--json` for machine-readable output. `mortar audit` exits with status 1 whe
94
94
 
95
95
  `audit_app` reads the application's files and never changes them. It checks against the contracts of the Mortar version installed in the app (or, for releases older than this tooling, against the tool's own catalogue, and says so).
96
96
 
97
- | Rule | Severity | Finds |
98
- | ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
99
- | `not-declared` | error | Mortar imported but missing from package.json |
100
- | `not-installed` | error | Mortar imported but not in node_modules |
101
- | `version-mismatch` | error | The installed version does not satisfy the package.json range |
102
- | `unknown-export` | error | A named import the installed version does not export, with the replacement for names removed in 0.6 |
103
- | `deep-import` | error | Imports or aliases into the package's `src` or `dist` |
104
- | `unknown-prop` | error | A prop the component's contract does not declare, with the 0.6 rename where there is one |
105
- | `invalid-value` | error | A literal outside the prop's declared union, such as `layout="centered"` (now `center`) |
106
- | `css-override` | error | Consumer CSS selecting Mortar's `.m-*` classes or `[data-m-component]` |
107
- | `class-override` | error | A Tailwind arbitrary variant that targets `.m-*` classes, such as `[&_.m-button]:rounded-none` |
108
- | `local-copy` | error | A copy of a Mortar source file, or code rendering Mortar's component marker |
109
- | `missing-styles` | error | No import of `@tonycodes-ui/mortar/styles.css` |
110
- | `styles-order` | warning | Mortar's styles imported before Tailwind |
111
- | `missing-theme-provider` | warning | Mortar components rendered without a `ThemeProvider` |
112
- | `kit-class` | warning | Mortar-internal class names on the app's own markup |
113
- | `raw-colour` | warning | Hex, `rgb()`/`oklch()` colours or Tailwind palette classes in files that render Mortar components, except where they define a brand (`brandTokens`, `brandToCss`/`brandToStyle`/`registerBrand` arguments, `BrandDefinition` objects, `--m-brand-*` values) |
114
- | `limited-data` | info | The installed release has no agent data, so a newer catalogue was used |
97
+ | Rule | Severity | Finds |
98
+ | ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
99
+ | `not-declared` | error | Mortar imported but missing from package.json |
100
+ | `not-installed` | error | Mortar imported but not in node_modules |
101
+ | `version-mismatch` | error | The installed version does not satisfy the package.json range |
102
+ | `unknown-export` | error | A named import the installed version does not export, with the replacement for names removed in 0.6 |
103
+ | `deep-import` | error | Imports or aliases into the package's `src` or `dist` |
104
+ | `unknown-prop` | error | A prop the component's contract does not declare, with the 0.6 rename where there is one |
105
+ | `invalid-value` | error | A literal outside the prop's declared union, such as `layout="centered"` (now `center`) |
106
+ | `css-override` | error | Consumer CSS selecting Mortar's `.m-*` classes or `[data-m-component]` |
107
+ | `class-override` | error | A Tailwind arbitrary variant that targets `.m-*` classes, such as `[&_.m-button]:rounded-none` |
108
+ | `local-copy` | error | A copy of a Mortar source file, or code rendering Mortar's component marker |
109
+ | `raw-element` | error | A raw `<table>`, `<select>`, `<textarea>`, `<dialog>`, `<progress>` or `<input>` (by type; not `hidden`, `file`, `range`, `color` or `image`) anywhere in an app that uses Mortar, naming the component to use instead. A raw `<button>` is a warning. The mapping comes from each contract's `replaces` field, so new components extend it |
110
+ | `missing-styles` | error | No import of `@tonycodes-ui/mortar/styles.css` |
111
+ | `styles-order` | warning | Mortar's styles imported before Tailwind |
112
+ | `missing-theme-provider` | warning | Mortar components rendered without a `ThemeProvider` |
113
+ | `kit-class` | warning | Mortar-internal class names on the app's own markup |
114
+ | `raw-colour` | warning | Hex, `rgb()`/`oklch()` colours or Tailwind palette classes in files that render Mortar components, except where they define a brand (`brandTokens`, `brandToCss`/`brandToStyle`/`registerBrand` arguments, `BrandDefinition` objects, `--m-brand-*` values) |
115
+ | `limited-data` | info | The installed release has no agent data, so a newer catalogue was used |
115
116
 
116
117
  The audit is a fast scanner, not a type checker. It does not see props passed through a spread (`{...props}`), components re-exported or wrapped under another name, class names built at runtime, or markup in `.vue`, `.svelte`, `.astro` or `.html` files. Run the app's typecheck and build as well.
117
118
 
119
+ A clean audit means the audit found none of the problems above. It is not proof that a page is built only from Mortar components: hand-written layout, typography or animation in the app's own markup is invisible to it unless it touches Mortar's classes or colours. Do not report a passing audit as that proof. To show a page is composed from the kit, point to the components it renders (each root carries `data-m-component`) and to [what you can extend](theming.md#what-you-can-extend).
120
+
118
121
  ## Agent skill
119
122
 
120
123
  [`skills/mortar/SKILL.md`](../skills/mortar/SKILL.md) is a ready-made skill with these workflow defaults. For Claude Code, install it once:
package/docs/theming.md CHANGED
@@ -107,7 +107,7 @@ tenant.theme = {
107
107
  };
108
108
  ```
109
109
 
110
- ThemeProvider renders the brand as a `<style>` element beside its content. It is included in server-rendered HTML, so there is no unstyled flash; it updates when the tokens change and is removed with the provider. Overlays opened inside the provider share it, because the rule matches the brand attribute they carry. Brand names may contain only letters, digits and hyphens. Every value is checked, and anything else throws an error that names the key (such as `dark.accent`), so tenant data cannot inject CSS and a value of the wrong kind cannot fail silently:
110
+ ThemeProvider renders the brand as an inline `<style>` element beside its content, so it needs a Content Security Policy that allows inline styles (see [Under a strict Content Security Policy](#under-a-strict-content-security-policy)). It is included in server-rendered HTML, so there is no unstyled flash; it updates when the tokens change and is removed with the provider. Overlays opened inside the provider share it, because the rule matches the brand attribute they carry. Brand names may contain only letters, digits and hyphens. Every value is checked, and anything else throws an error that names the key (such as `dark.accent`), so tenant data cannot inject CSS and a value of the wrong kind cannot fail silently:
111
111
 
112
112
  - **Colours** are hex, a colour name (including `transparent` and `currentcolor`), or `rgb()`, `rgba()`, `hsl()`, `hsla()`, `hwb()`, `lab()`, `lch()`, `oklab()`, `oklch()`, `color()`, `color-mix()`, `light-dark()` or `var(--name)`. Arguments may nest those functions and `calc()`, `min()`, `max()` or `clamp()`. A gradient, `url()`, top-level `calc()`, `attr()` or `image-set()` is rejected.
113
113
  - **`accentGradient`** is a gradient whose stops follow the colour rules (see [Accent gradients](#accent-gradients)).
@@ -132,10 +132,81 @@ brandToStyle(theme); // inline custom properties for one element
132
132
 
133
133
  Inline custom properties apply only to that element's subtree. Menus and dialogs portal outside it, so prefer `brandTokens` or `registerBrand` for anything with overlays.
134
134
 
135
+ ### Under a strict Content Security Policy
136
+
137
+ `brandTokens` and `registerBrand` add an inline `<style>` element. Under a policy of `style-src 'self'` without `'unsafe-inline'`, the browser blocks it. The page still renders and `data-mortar-brand` is still set, but no `--m-brand-*` property applies, so the brand silently falls back to Mortar's defaults. The console shows a `style-src` violation. A server-rendered `style` attribute, for example from `brandToStyle` on the server, is blocked in the same way (`style-src-attr`).
138
+
139
+ **For brands you know at build time, ship the brand as a stylesheet.** `brandToCss` returns the same rule `brandTokens` injects. Write it into your CSS at build time, import it after Mortar's styles, and give ThemeProvider the brand name only:
140
+
141
+ ```ts
142
+ // scripts/brand-css.mjs, run before your build (for example in "prebuild")
143
+ import { writeFileSync } from "node:fs";
144
+ import { brandToCss } from "@tonycodes-ui/mortar/brand";
145
+ import { acme } from "../src/brand.js"; // a BrandDefinition
146
+
147
+ writeFileSync("src/brand.generated.css", `${brandToCss("acme", acme)}\n`);
148
+ ```
149
+
150
+ ```css
151
+ @import "@tonycodes-ui/mortar/styles.css";
152
+ @import "./brand.generated.css";
153
+ ```
154
+
155
+ ```tsx
156
+ <ThemeProvider brand="acme">…</ThemeProvider> {/* no brandTokens */}
157
+ ```
158
+
159
+ A same-origin stylesheet satisfies `style-src 'self'`, and the rule is identical to the injected one, so overlays, inverse bands and container-scoped providers behave the same. `brandToCss` validates the definition exactly as `brandTokens` does, so a bad value fails the build instead of the page. To keep a checked-in stylesheet in step with the definition, compare them in a test: `expect(readFileSync("src/brand.generated.css", "utf8").trim()).toBe(brandToCss("acme", acme))`.
160
+
161
+ **For tenant brands loaded at runtime** (from a database or CMS), `brandTokens` needs the policy to allow the inline style:
162
+
163
+ - **`'unsafe-inline'` in `style-src`.** This is simplest, but it allows any inline style.
164
+ - **A hash of the exact rule** (`'sha256-…'`). This is brittle: any change to the palette changes the hash.
165
+ - **Serve each tenant's rule as a stylesheet instead**, for example from a route that returns `brandToCss(tenant.slug, tenant.theme)` as `text/css`, linked with `<link rel="stylesheet">`. Tenant data is validated by `brandToCss`, so it cannot inject other CSS.
166
+
167
+ Mortar's own styles and the no-JavaScript appearance fallback are ordinary stylesheet rules and need nothing beyond `style-src 'self'`.
168
+
135
169
  ### With the brand builder
136
170
 
137
171
  The playground's [brand builder](https://mortar.tony.codes/playground/?page=brand) (`/playground/?page=brand`) designs a brand against this contract. Start from a preset, set the light and dark primary pairs, and open Advanced for the optional tokens, surfaces and accent gradient; empty fields show the value they derive. Set heading and body fonts below. Light and dark previews, each with an inverse band, an accent band, an accent button, a heading sample and overlays, update as you type, and a table checks contrast: 4.5:1 for text on primary, hover, active and accent, for links, text and muted text on page, surface and sunken, and for on-accent text on the accent fill (every gradient stop) at rest, hover and active; 3:1 for strong lines on page, surface and sunken, and for the focus ring. Copy the result as the CSS rule (from `brandToCss`) or as the `brandTokens` object. The brand is kept in the URL, so a link shares it.
138
172
 
173
+ ## Marketing surfaces and illustrations
174
+
175
+ A branded landing page needs no CSS of its own. MarketingPage, MarketingHeader, Hero, Footer and NetworkIllustration read the same semantic tokens as every other component, so one brand, in CSS or from `brandTokens` at runtime, restyles all of them in both appearances. The [split landing template](../playground/template-pages.tsx) (`?recipe=split-landing`) is composed this way.
176
+
177
+ | Role | Token | Brand contract | Used by |
178
+ | ------------------- | ---------------------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
179
+ | Canvas and surfaces | `--m-paper`, `--m-surface`, `--m-sunken` | `paper`, `surface`, `sunken` | The page; illustration nodes and the pause control on `surface` |
180
+ | Text | `--m-ink`, `--m-secondary`, `--m-muted` | (fixed by Mortar) | Titles; the hero description and illustration labels; the tagline, hero note and inline footer |
181
+ | Borders | `--m-line`, `--m-line-strong` | (fixed by Mortar) | Header and inline-footer rules and the tagline divider; illustration connections and node outlines on `line-strong`, which keeps 3:1 |
182
+ | Emphasis | `--m-link` | `link` | Hero `emphasis`. On an accent or background hero it takes the band's text colour instead |
183
+ | Accent | `--m-accent`, `--m-accent-fill` | `accent`, `accentGradient` | Accent nodes and travelling signals; `tone="accent"` on Hero, CallToAction and Banner; `variant="accent"` buttons |
184
+ | Focus | `--m-focus` | `focus` | Every link and button, including the illustration's pause control |
185
+
186
+ Emphasis uses the brand's `link` colour because link is the brand's text-safe colour. Accent is a fill and may be too light or too dark to read as text. In the neutral brand, link is ink, so an emphasised phrase looks like the rest of the title and keeps only its `<em>` meaning. Set `link` to give it a colour.
187
+
188
+ No new tokens were needed for these components: each role above already had one. If your brand needs a role that is missing, raise it as an issue rather than styling around it.
189
+
190
+ ### Your own SVG
191
+
192
+ Draw your own illustration with the same tokens so it follows the brand and appearance: `fill="var(--m-accent)"`, `stroke="var(--m-line-strong)"`, or Tailwind's `fill-accent` and `stroke-line` on your own elements. Give decorative SVG `aria-hidden="true"`, and give informative SVG `role="img"` and a `<title>`. Pause any motion under `prefers-reduced-motion`, and let people pause it.
193
+
194
+ ### What you can extend
195
+
196
+ Supported extension points:
197
+
198
+ - **The brand contract.** CSS `--m-brand-*` values, or `brandTokens` and `ThemeProvider` (including `scope="container"` for one branded region).
199
+ - **Props and slots.** Logos stay your own assets, passed through `logo`. Also `controls`, `action`, `tagline`, Hero `children`, `images`, `background` and `note`, MarketingPage `header`, `footer` and children, and NetworkIllustration's `nodes` and `connections`.
200
+ - **`className` for layout only.** Margins, grid placement and a maximum width on a component's root.
201
+ - **The scales and Tailwind utilities** in your own markup (above and below).
202
+
203
+ Not supported, and flagged by `mortar audit` where it can see them:
204
+
205
+ - Selectors on `.m-*` classes or `[data-m-component]` (`css-override`)
206
+ - Tailwind arbitrary variants into the kit (`class-override`)
207
+ - Raw colours on Mortar components (`raw-colour`)
208
+ - Styling by state attributes such as `data-paused`, which are for tests and scripts
209
+
139
210
  ## Appearance before JavaScript runs
140
211
 
141
212
  Until ThemeProvider sets `data-mortar-appearance` (during server rendering, or with JavaScript off), Mortar follows the operating system's colour scheme, so dark-mode users don't see a light flash. To always start light, render `data-mortar-appearance="light"` on `<html>`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tonycodes-ui/mortar",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Mortar: a shared design foundation, application and marketing UI for React and Tailwind.",
5
5
  "type": "module",
6
6
  "files": [
@@ -31,6 +31,6 @@ If a shared pattern is missing or a component is wrong, fix it centrally instead
31
31
 
32
32
  ## Completion
33
33
 
34
- Required evidence: the app's build, typecheck, a clean `mortar audit`, and browser checks of responsive layout, light and dark, and keyboard and accessibility. Automated checks cannot prove visual quality or complete accessibility; report what you verified and any remaining blockers. Do not declare completion with failing required checks.
34
+ Required evidence: the app's build, typecheck, a clean `mortar audit`, and browser checks of responsive layout, light and dark, and keyboard and accessibility. Automated checks cannot prove visual quality or complete accessibility; report what you verified and any remaining blockers. A clean audit is not proof that a page uses Mortar components exclusively; point to the rendered components instead. Do not declare completion with failing required checks.
35
35
 
36
36
  Provide a local preview by default. Deploy only with the user's authorisation; existing explicit authorisation counts. A passing audit is not deployment permission.