@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.
- package/CHANGELOG.md +16 -39
- package/DESIGN.md +1 -1
- package/catalog.json +413 -377
- package/cli/audit.mjs +152 -0
- package/dist/agent-data.json +1 -1
- package/dist/application.d.ts +30 -5
- package/dist/application.js +20 -8
- package/dist/index.d.ts +1 -1
- package/dist/inspection.js +17 -1
- package/dist/internal.d.ts +15 -1
- package/dist/internal.js +12 -1
- package/dist/marketing.d.ts +3 -2
- package/dist/marketing.js +2 -6
- package/dist/primitives.js +20 -11
- package/docs/agents.md +21 -18
- package/docs/theming.md +72 -1
- package/package.json +1 -1
- package/skills/mortar/SKILL.md +1 -1
- package/src/application.css +95 -1
- package/src/application.tsx +88 -19
- package/src/data.css +58 -2
- package/src/foundation.css +10 -0
- package/src/index.ts +3 -0
- package/src/inspection.tsx +15 -0
- package/src/internal.tsx +93 -2
- package/src/marketing.tsx +4 -74
- package/src/primitives.tsx +38 -19
package/dist/application.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
73
|
-
items:
|
|
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;
|
package/dist/application.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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";
|
package/dist/inspection.js
CHANGED
|
@@ -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
|
-
|
|
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, }) {
|
package/dist/internal.d.ts
CHANGED
|
@@ -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
|
+
}
|
package/dist/marketing.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { type IconLink, type ItemGroup
|
|
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
|
-
|
|
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 {
|
|
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.
|
package/dist/primitives.js
CHANGED
|
@@ -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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
| `
|
|
110
|
-
| `styles
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
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
|
|
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
package/skills/mortar/SKILL.md
CHANGED
|
@@ -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.
|