@tonycodes-ui/mortar 0.0.0-stage → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/AGENTS.md +10 -0
  2. package/CHANGELOG.md +268 -0
  3. package/CONTRIBUTING.md +60 -0
  4. package/DESIGN.md +51 -0
  5. package/LICENSE +21 -0
  6. package/README.md +155 -2
  7. package/TESTING.md +83 -0
  8. package/baseline-policy.json +16 -0
  9. package/bin/mortar.mjs +258 -0
  10. package/catalog.json +9900 -0
  11. package/cli/audit.mjs +943 -0
  12. package/cli/data.mjs +176 -0
  13. package/cli/mcp.mjs +216 -0
  14. package/cli/migrations.mjs +128 -0
  15. package/cli/packet.mjs +267 -0
  16. package/cli/search.mjs +246 -0
  17. package/cli/tools.mjs +229 -0
  18. package/design-principles.json +30 -0
  19. package/dist/agent-data.json +1 -0
  20. package/dist/application.d.ts +340 -0
  21. package/dist/application.js +240 -0
  22. package/dist/auth.d.ts +121 -0
  23. package/dist/auth.js +97 -0
  24. package/dist/brand.d.ts +69 -0
  25. package/dist/brand.js +266 -0
  26. package/dist/calendar.d.ts +75 -0
  27. package/dist/calendar.js +310 -0
  28. package/dist/choice.d.ts +36 -0
  29. package/dist/choice.js +36 -0
  30. package/dist/composer.d.ts +28 -0
  31. package/dist/composer.js +47 -0
  32. package/dist/display.d.ts +136 -0
  33. package/dist/display.js +75 -0
  34. package/dist/docs.d.ts +145 -0
  35. package/dist/docs.js +141 -0
  36. package/dist/ecommerce.d.ts +261 -0
  37. package/dist/ecommerce.js +177 -0
  38. package/dist/feedback.d.ts +80 -0
  39. package/dist/feedback.js +98 -0
  40. package/dist/foundation.d.ts +42 -0
  41. package/dist/foundation.js +121 -0
  42. package/dist/heading.d.ts +5 -0
  43. package/dist/heading.js +9 -0
  44. package/dist/icons.d.ts +50 -0
  45. package/dist/icons.js +70 -0
  46. package/dist/illustration.d.ts +47 -0
  47. package/dist/illustration.js +47 -0
  48. package/dist/index.d.ts +36 -0
  49. package/dist/index.js +18 -0
  50. package/dist/inspection.d.ts +24 -0
  51. package/dist/inspection.js +14 -0
  52. package/dist/internal.d.ts +514 -0
  53. package/dist/internal.js +332 -0
  54. package/dist/layout.d.ts +107 -0
  55. package/dist/layout.js +66 -0
  56. package/dist/marketing-sections.d.ts +161 -0
  57. package/dist/marketing-sections.js +58 -0
  58. package/dist/marketing.d.ts +277 -0
  59. package/dist/marketing.js +163 -0
  60. package/dist/navigation.d.ts +35 -0
  61. package/dist/navigation.js +44 -0
  62. package/dist/primitives.d.ts +258 -0
  63. package/dist/primitives.js +244 -0
  64. package/dist/showcase.d.ts +29 -0
  65. package/dist/showcase.js +10 -0
  66. package/dist/workspace.d.ts +80 -0
  67. package/dist/workspace.js +48 -0
  68. package/docs/agents.md +127 -0
  69. package/docs/auth-flows.md +71 -0
  70. package/docs/migrating-to-0.6.md +94 -0
  71. package/docs/theming.md +170 -0
  72. package/package.json +94 -4
  73. package/skills/mortar/SKILL.md +36 -0
  74. package/src/application.css +1487 -0
  75. package/src/application.tsx +1413 -0
  76. package/src/auth.tsx +452 -0
  77. package/src/brand.ts +335 -0
  78. package/src/calendar.css +468 -0
  79. package/src/calendar.tsx +868 -0
  80. package/src/choice.tsx +240 -0
  81. package/src/composer.tsx +158 -0
  82. package/src/data.css +690 -0
  83. package/src/display.tsx +452 -0
  84. package/src/docs.css +704 -0
  85. package/src/docs.tsx +737 -0
  86. package/src/ecommerce.css +810 -0
  87. package/src/ecommerce.tsx +1033 -0
  88. package/src/feedback.tsx +388 -0
  89. package/src/foundation.css +1867 -0
  90. package/src/foundation.tsx +227 -0
  91. package/src/heading.ts +13 -0
  92. package/src/icons.tsx +237 -0
  93. package/src/illustration.tsx +169 -0
  94. package/src/index.ts +233 -0
  95. package/src/inspection.tsx +91 -0
  96. package/src/internal.tsx +700 -0
  97. package/src/layout.tsx +342 -0
  98. package/src/marketing-sections.tsx +621 -0
  99. package/src/marketing.css +2579 -0
  100. package/src/marketing.tsx +1235 -0
  101. package/src/navigation.tsx +118 -0
  102. package/src/overlays.css +350 -0
  103. package/src/primitives.tsx +1135 -0
  104. package/src/showcase.tsx +120 -0
  105. package/src/tokens.css +449 -0
  106. package/src/workspace.tsx +333 -0
  107. package/styles.css +56 -0
@@ -0,0 +1,177 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useId, useRef, useState, } from "react";
3
+ import { KitLink } from "./navigation.js";
4
+ import { ActionLink, Button, Checkbox, EmptyState, Modal, } from "./primitives.js";
5
+ import { Avatar, FilterChip } from "./display.js";
6
+ import { ProgressSteps } from "./application.js";
7
+ import { ChevronDown, Star } from "./icons.js";
8
+ import { childLevel, headingTag } from "./heading.js";
9
+ import { AsComponentRoot, Swatch, TermList, componentRoot, cx, } from "./internal.js";
10
+ /** Grid of product cards; each card's name link covers the card. */
11
+ export function ProductList({ label, products, columns = 4, itemHeadingLevel = 3, variant = "plain", id, className, }) {
12
+ const Heading = headingTag(itemHeadingLevel);
13
+ const overlay = variant === "overlay";
14
+ return (_jsx("ul", { id: id, ...componentRoot("ProductList"), className: cx("m-product-list", className), "aria-label": label, "data-columns": columns, "data-variant": variant === "plain" ? undefined : variant, children: products.map((product) => {
15
+ const text = (_jsxs(_Fragment, { children: [_jsxs("div", { className: "m-product-info", children: [_jsx(Heading, { className: "m-product-name", children: _jsx(KitLink, { href: product.href, children: product.name }) }), product.meta && _jsx("p", { className: "m-product-meta", children: product.meta }), product.description && (_jsx("p", { className: "m-product-summary", children: product.description }))] }), _jsx("p", { className: "m-product-price", children: product.price })] }));
16
+ return (_jsxs("li", { children: [_jsxs("div", { className: "m-product-image", children: [_jsx("img", { src: product.image.src, alt: product.image.alt, loading: "lazy" }), product.badge && (_jsx("span", { className: "m-product-badge", children: product.badge })), overlay && _jsx("div", { className: "m-product-overlay", children: text })] }), !overlay && text, product.swatches && product.swatches.length > 0 && (_jsx("ul", { className: "m-product-swatches", "aria-label": `${product.name} colours`, children: product.swatches.map((colour) => (_jsx("li", { children: _jsx(Swatch, { color: colour.swatch, label: colour.label }) }, colour.label))) })), product.action && (_jsx("div", { className: "m-product-card-action", children: product.action }))] }, product.id));
17
+ }) }));
18
+ }
19
+ /** Product page body: gallery, price, rating, option pickers, add to bag and details. */
20
+ export function ProductOverview({ name, price, description, images, rating, options = [], selected = {}, onSelect, onAdd, addLabel = "Add to bag", loading = false, details = [], headingLevel = 1, hideName = false, gallery = "thumbnails", id, className, }) {
21
+ const [current, setCurrent] = useState(0);
22
+ // A new image set (another product, or another colour) starts at its first image.
23
+ const imageKey = images.map((image) => image.src).join("\n");
24
+ const [shownKey, setShownKey] = useState(imageKey);
25
+ if (shownKey !== imageKey) {
26
+ setShownKey(imageKey);
27
+ setCurrent(0);
28
+ }
29
+ const group = useId();
30
+ const Heading = headingTag(headingLevel);
31
+ const shown = images[current] ?? images[0];
32
+ return (_jsxs("div", { id: id, ...componentRoot("ProductOverview"), className: cx("m-product-overview", className), children: [gallery === "grid" ? (_jsx("div", { className: "m-product-gallery", "data-gallery": "grid", children: images.map((image) => (_jsx("div", { className: "m-product-image", children: _jsx("img", { src: image.src, alt: image.alt }) }, image.src))) })) : (_jsxs("div", { className: "m-product-gallery", children: [shown && (_jsx("div", { className: "m-product-image", children: _jsx("img", { src: shown.src, alt: shown.alt }) })), images.length > 1 && (_jsx("div", { className: "m-product-thumbs", role: "group", "aria-label": "Product images", children: images.map((image, index) => (_jsx("button", { type: "button", "aria-pressed": index === current, "aria-label": `Show image ${index + 1} of ${images.length}: ${image.alt}`, onClick: () => setCurrent(index), children: _jsx("img", { src: image.src, alt: "" }) }, image.src))) }))] })), _jsxs("div", { className: "m-product-details", children: [!hideName && _jsx(Heading, { className: "m-product-name", children: name }), _jsx("p", { className: "m-product-price", children: price }), rating && (_jsxs("div", { className: "m-rating", children: [_jsx(Stars, { value: rating.value, label: `Rated ${rating.value} out of 5 from ${rating.count} reviews` }), rating.href ? (_jsxs(KitLink, { href: rating.href, children: [rating.count, " reviews"] })) : (_jsxs("span", { "aria-hidden": "true", children: [rating.count, " reviews"] }))] })), description && (_jsx("div", { className: "m-product-description", children: description })), _jsxs("form", { className: "m-product-form", onSubmit: (event) => {
33
+ event.preventDefault();
34
+ if (!loading)
35
+ onAdd?.();
36
+ }, children: [options.map((option) => (_jsxs("fieldset", { className: "m-product-option", "data-swatches": option.values.some((v) => v.swatch) || undefined, children: [_jsx("legend", { children: option.label }), _jsx("div", { children: option.values.map((value) => (_jsxs("label", { "data-disabled": value.disabled || undefined, children: [_jsx("input", { type: "radio", name: `${group}-${option.id}`, value: value.value, checked: selected[option.id] === value.value, disabled: value.disabled, onChange: () => onSelect?.(option.id, value.value) }), value.swatch ? (_jsx(Swatch, { color: value.swatch, label: value.label })) : (_jsx("span", { children: value.label }))] }, value.value))) })] }, option.id))), _jsx(Button, { type: "submit", loading: loading, children: addLabel })] }), details.length > 0 && (_jsx("ul", { className: "m-product-highlights", children: details.map((detail) => (_jsx("li", { children: detail }, detail))) }))] })] }));
37
+ }
38
+ /**
39
+ * Bag contents with per-line quantity and remove controls. After a removal,
40
+ * focus moves to the next line's Remove button (the previous one after the
41
+ * last line), or to the empty state once the bag is empty.
42
+ */
43
+ export function ShoppingCart({ summary, related, id, className, ...props }) {
44
+ if (!summary && !related)
45
+ return _jsx(CartLines, { ...props, id: id, className: className, root: true });
46
+ return (_jsxs("div", { id: id, ...componentRoot("ShoppingCart"), className: cx("m-cart-layout", className), "data-summary": summary ? true : undefined, children: [_jsx(CartLines, { ...props }), summary && _jsx("div", { className: "m-cart-summary", children: summary }), related && _jsx(CartRelated, { ...related })] }));
47
+ }
48
+ function CartRelated({ title, products, headingLevel = 2, }) {
49
+ const titleId = useId();
50
+ const Heading = headingTag(headingLevel);
51
+ return (_jsxs("section", { className: "m-cart-related", "aria-labelledby": titleId, children: [_jsx(Heading, { id: titleId, className: "m-cart-related-title", children: title }), _jsx(ProductList, { label: title, products: products, itemHeadingLevel: childLevel(headingLevel) })] }));
52
+ }
53
+ function CartLines({ items, onQuantityChange, onRemove, label = "Items in your bag", maxQuantity = 8, headingLevel = 3, empty = {
54
+ title: "Your bag is empty",
55
+ description: "Items you add will appear here.",
56
+ }, id, className, root: isRoot = false, }) {
57
+ const base = useId();
58
+ const Heading = headingTag(headingLevel);
59
+ const root = useRef(null);
60
+ // The line whose Remove button was pressed, until the consumer drops it.
61
+ const removing = useRef(null);
62
+ useEffect(() => {
63
+ const pending = removing.current;
64
+ if (!pending || items.some((item) => item.id === pending.id))
65
+ return;
66
+ removing.current = null;
67
+ const buttons = root.current?.querySelectorAll(".m-cart-remove");
68
+ if (buttons?.length)
69
+ buttons[Math.min(pending.index, buttons.length - 1)].focus();
70
+ else
71
+ root.current?.focus();
72
+ }, [items]);
73
+ if (!items.length)
74
+ return (_jsx("div", { id: id, ...componentRoot("ShoppingCart", isRoot), className: cx("m-cart-empty", className), ref: root, tabIndex: -1, children: _jsx(EmptyState, { title: empty.title, description: empty.description, action: empty.action, headingLevel: headingLevel }) }));
75
+ return (_jsx("ul", { id: id, ...componentRoot("ShoppingCart", isRoot), className: cx("m-cart", className), "aria-label": label, ref: root, children: items.map((item, index) => {
76
+ const quantityId = `${base}-${item.id}`;
77
+ const choices = Array.from({ length: Math.max(maxQuantity, item.quantity) }, (_, i) => i + 1);
78
+ return (_jsxs("li", { children: [_jsx("div", { className: "m-product-image", children: _jsx("img", { src: item.image.src, alt: item.image.alt, loading: "lazy" }) }), _jsxs("div", { className: "m-cart-info", children: [_jsx(Heading, { className: "m-cart-item-title", children: _jsx(KitLink, { href: item.href, children: item.name }) }), item.meta && _jsx("p", { className: "m-product-meta", children: item.meta }), _jsx("p", { className: "m-product-price", children: item.price }), item.stock && _jsx("p", { className: "m-cart-stock", children: item.stock })] }), _jsxs("div", { className: "m-cart-controls", children: [_jsxs("label", { className: "m-sr-only", htmlFor: quantityId, children: ["Quantity, ", item.name] }), _jsxs("div", { className: "m-select-control", children: [_jsx("select", { id: quantityId, value: item.quantity, onChange: (event) => onQuantityChange(item.id, Number(event.target.value)), children: choices.map((n) => (_jsx("option", { value: n, children: n }, n))) }), _jsx(ChevronDown, {})] }), _jsxs("button", { type: "button", className: "m-cart-remove", onClick: () => {
79
+ removing.current = { index, id: item.id };
80
+ onRemove(item.id);
81
+ }, children: ["Remove", _jsxs("span", { className: "m-sr-only", children: [" ", item.name] })] })] })] }, item.id));
82
+ }) }));
83
+ }
84
+ /** Cost breakdown with a distinguished total and a checkout action. */
85
+ export function OrderSummary({ title = "Order summary", lines, total, action, headingLevel = 2, children, id, className, }) {
86
+ const titleId = useId();
87
+ const Heading = headingTag(headingLevel);
88
+ return (_jsxs("section", { id: id, ...componentRoot("OrderSummary"), className: cx("m-order-summary", className), "aria-labelledby": titleId, children: [_jsx(Heading, { id: titleId, className: "m-order-summary-title", children: title }), _jsx(TermList, { items: lines.map((line) => ({
89
+ key: line.label,
90
+ term: line.hint ? (_jsxs(_Fragment, { children: [line.label, _jsx("span", { className: "m-order-hint", children: line.hint })] })) : (line.label),
91
+ details: line.value,
92
+ })), children: _jsxs("div", { className: "m-order-total", children: [_jsx("dt", { children: total.label }), _jsx("dd", { children: total.value })] }) }), children, action && _jsx("div", { className: "m-order-action", children: action })] }));
93
+ }
94
+ /** Product overview in a dialog, with a link to the full product page. */
95
+ export function ProductQuickView({ open, onClose, href, detailsLabel = "View full details", className, ...product }) {
96
+ return (_jsx(AsComponentRoot, { name: "ProductQuickView", of: "Modal", children: _jsx(Modal, { open: open, onClose: onClose, title: product.name, size: "lg", children: _jsxs("div", { className: cx("m-quick-view", className), children: [_jsx(ProductOverview, { ...product, hideName: true }), _jsx(ActionLink, { label: detailsLabel, href: href, variant: "ghost" })] }) }) }));
97
+ }
98
+ /**
99
+ * Collapsible checkbox facets with counts, plus removable chips for active
100
+ * filters. Every change, Clear all included, calls `onChange` once with the
101
+ * complete next selection.
102
+ */
103
+ export function ProductFilters({ facets, selected, onChange, label = "Filters", id, className, }) {
104
+ const root = useRef(null);
105
+ const active = facets.flatMap((facet) => (selected[facet.id] || []).map((value) => ({
106
+ facet,
107
+ value,
108
+ label: facet.options.find((option) => option.value === value)?.label ?? value,
109
+ })));
110
+ const toggle = (facet, value, on) => {
111
+ const current = selected[facet] || [];
112
+ onChange({
113
+ ...selected,
114
+ [facet]: on ? [...current, value] : current.filter((v) => v !== value),
115
+ });
116
+ };
117
+ const clearAll = () => {
118
+ onChange(Object.fromEntries([...Object.keys(selected), ...facets.map((facet) => facet.id)].map((facet) => [facet, []])));
119
+ // The chips and this button disappear: continue from the first facet.
120
+ root.current?.querySelector("summary")?.focus();
121
+ };
122
+ // A removed chip takes its button with it: focus the chip that takes its
123
+ // place (or the previous one), else the first facet.
124
+ const removedChip = useRef(null);
125
+ const activeKey = active
126
+ .map((item) => `${item.facet.id}:${item.value}`)
127
+ .join();
128
+ useEffect(() => {
129
+ const index = removedChip.current;
130
+ if (index === null)
131
+ return;
132
+ removedChip.current = null;
133
+ const buttons = root.current?.querySelectorAll(".m-filter-chips .m-chip button");
134
+ if (buttons?.length)
135
+ buttons[Math.min(index, buttons.length - 1)].focus();
136
+ else
137
+ root.current?.querySelector("summary")?.focus();
138
+ }, [activeKey]);
139
+ return (_jsxs("section", { id: id, ref: root, ...componentRoot("ProductFilters"), className: cx("m-filters", className), "aria-label": label, children: [active.length > 0 && (_jsxs("div", { className: "m-filter-chips", role: "group", "aria-label": "Active filters", children: [active.map((item, index) => (_jsx(FilterChip, { label: item.label, onRemove: () => {
140
+ removedChip.current = index;
141
+ toggle(item.facet.id, item.value, false);
142
+ } }, `${item.facet.id}:${item.value}`))), _jsx("button", { type: "button", className: "m-filter-clear", onClick: clearAll, children: "Clear all" })] })), facets.map((facet) => {
143
+ const count = (selected[facet.id] || []).length;
144
+ return (_jsxs("details", { open: true, children: [_jsxs("summary", { children: [facet.label, count > 0 && _jsx("span", { className: "m-filter-active", children: count })] }), _jsxs("fieldset", { children: [_jsx("legend", { className: "m-sr-only", children: facet.label }), facet.options.map((option) => (_jsx(Checkbox, { checked: (selected[facet.id] || []).includes(option.value), onChange: (event) => toggle(facet.id, option.value, event.target.checked), label: _jsxs(_Fragment, { children: [option.label, option.count !== undefined && (_jsxs("span", { className: "m-filter-count", children: ["(", option.count, ")"] }))] }) }, option.value)))] })] }, facet.id));
145
+ })] }));
146
+ }
147
+ function Stars({ value, label }) {
148
+ return (_jsx("div", { className: "m-stars", role: "img", "aria-label": label, children: [1, 2, 3, 4, 5].map((n) => (_jsx("span", { "data-filled": value >= n - 0.25 || undefined, children: _jsx(Star, {}) }, n))) }));
149
+ }
150
+ /** Rating summary with a star distribution, followed by individual reviews. */
151
+ export function ProductReviews({ title = "Customer reviews", average, count, distribution, reviews, action, avatars = false, headingLevel = 2, id, className, }) {
152
+ const base = useId();
153
+ const Heading = headingTag(headingLevel);
154
+ const ReviewHeading = headingTag(childLevel(headingLevel));
155
+ return (_jsxs("section", { id: id, ...componentRoot("ProductReviews"), className: cx("m-reviews", className), "aria-labelledby": base, "data-avatars": avatars || undefined, children: [_jsxs("div", { className: "m-reviews-summary", children: [_jsx(Heading, { id: base, className: "m-reviews-title", children: title }), _jsxs("div", { className: "m-rating", children: [_jsx(Stars, { value: average, label: `Rated ${average} out of 5 from ${count} reviews` }), _jsxs("span", { "aria-hidden": "true", children: ["Based on ", count, " reviews"] })] }), _jsx(TermList, { className: "m-reviews-distribution", items: distribution.map((row) => ({
156
+ key: String(row.stars),
157
+ term: (_jsxs(_Fragment, { children: [row.stars, " ", row.stars === 1 ? "star" : "stars"] })),
158
+ details: (_jsxs(_Fragment, { children: [_jsx("meter", { min: 0, max: count, value: row.count, "aria-label": `${row.stars} ${row.stars === 1 ? "star" : "stars"}: ${row.count} ${row.count === 1 ? "review" : "reviews"}` }), _jsx("span", { children: row.count })] })),
159
+ })) }), action] }), _jsx("div", { className: "m-review-list", children: reviews.map((review) => (_jsxs("article", { className: "m-review", "aria-labelledby": `${base}-${review.id}`, children: [_jsxs("header", { children: [avatars && (_jsx(Avatar, { className: "m-review-avatar", name: review.author, src: review.avatar, decorative: true })), _jsx(Stars, { value: review.rating, label: `Rated ${review.rating} out of 5` }), _jsx(ReviewHeading, { id: `${base}-${review.id}`, className: "m-review-title", children: review.title }), _jsxs("p", { className: "m-review-meta", children: [review.author, " \u00B7 ", review.date] })] }), _jsx("div", { className: "m-review-body", children: review.body })] }, review.id))) })] }));
160
+ }
161
+ /** Past orders, each with its summary, status and line items. */
162
+ export function OrderHistory({ orders, headingLevel = 2, empty = {
163
+ title: "No orders yet",
164
+ description: "Orders you place will appear here.",
165
+ }, id, className, }) {
166
+ const base = useId();
167
+ if (!orders.length)
168
+ return (_jsx("div", { id: id, ...componentRoot("OrderHistory"), className: cx("m-order-history", className), children: _jsx(EmptyState, { title: empty.title, description: empty.description, action: empty.action, headingLevel: headingLevel }) }));
169
+ const Heading = headingTag(headingLevel);
170
+ return (_jsx("div", { id: id, ...componentRoot("OrderHistory"), className: cx("m-order-history", className), children: orders.map((order) => {
171
+ const titleId = `${base}-${order.id}`;
172
+ return (_jsxs("section", { className: "m-order", "aria-labelledby": titleId, children: [_jsxs("header", { className: "m-order-header", children: [_jsxs(Heading, { id: titleId, className: "m-order-title", children: ["Order ", order.number] }), _jsx(TermList, { items: [
173
+ { term: "Date placed", details: order.date },
174
+ { term: "Total", details: order.total },
175
+ ] }), _jsxs(KitLink, { href: order.href, children: ["View order", _jsxs("span", { className: "m-sr-only", children: [" ", order.number] })] }), order.actions && (_jsx("div", { className: "m-order-actions", children: order.actions }))] }), _jsx("p", { className: "m-order-status", children: order.status }), order.progress && order.progress.length > 0 && (_jsx(ProgressSteps, { className: "m-order-progress", steps: order.progress, variant: "bar", navigation: false, label: `Order ${order.number} progress` })), _jsx("ul", { className: "m-order-items", children: order.items.map((item) => (_jsxs("li", { children: [_jsx("div", { className: "m-product-image", children: _jsx("img", { src: item.image.src, alt: item.image.alt, loading: "lazy" }) }), _jsxs("div", { children: [_jsx("p", { className: "m-order-item-name", children: item.href ? (_jsx(KitLink, { href: item.href, children: item.name })) : (item.name) }), _jsxs("p", { className: "m-product-meta", children: ["Qty ", item.quantity] })] }), _jsx("p", { className: "m-product-price", children: item.price }), item.actions && (_jsx("div", { className: "m-order-item-actions", children: item.actions }))] }, item.id))) })] }, order.id));
176
+ }) }));
177
+ }
@@ -0,0 +1,80 @@
1
+ import { type ReactNode } from "react";
2
+ import { type LinkAction, type Tone } from "./primitives.js";
3
+ export interface ToastAction {
4
+ label: string;
5
+ onSelect: () => void;
6
+ }
7
+ export interface ToastItem {
8
+ id: string;
9
+ title: string;
10
+ description?: string;
11
+ tone?: Tone;
12
+ action?: ToastAction;
13
+ /** A second action shown after the first, e.g. Decline next to Accept. */
14
+ secondaryAction?: ToastAction;
15
+ /** Avatar or icon shown instead of the tone icon, e.g. who replied. */
16
+ leading?: ReactNode;
17
+ }
18
+ export declare function Toast({ title, description, tone, action, onDismiss, leading, secondaryAction, className, }: Omit<ToastItem, "id"> & {
19
+ /** Shows a dismiss button. Focus then moves to the next control, never to the page body. */
20
+ onDismiss?: () => void;
21
+ className?: string;
22
+ }): import("react").JSX.Element;
23
+ /**
24
+ * Controlled toast region. The consumer owns the queue and timeouts. New
25
+ * toasts are announced once, through two persistent live regions: danger
26
+ * assertively, every other tone politely.
27
+ */
28
+ export declare function Toaster({ items, onDismiss, label, id, className, }: {
29
+ items: ToastItem[];
30
+ onDismiss: (id: string) => void;
31
+ label?: string;
32
+ id?: string;
33
+ className?: string;
34
+ }): import("react").JSX.Element;
35
+ export declare function ConfirmDialog({ open, onClose, onConfirm, title, description, confirmLabel, cancelLabel, confirmationText, loading, tone, className, }: {
36
+ open: boolean;
37
+ onClose: () => void;
38
+ onConfirm: () => void;
39
+ title: string;
40
+ description: ReactNode;
41
+ confirmLabel: string;
42
+ cancelLabel?: string;
43
+ /** When set, the user must type this value before confirming. */
44
+ confirmationText?: string;
45
+ /** Busy confirming: the confirm button keeps focus but ignores activation. */
46
+ loading?: boolean;
47
+ /** "danger" (default) for a destructive action; "default" for a primary one. */
48
+ tone?: "default" | "danger";
49
+ /** On the dialog panel. */
50
+ className?: string;
51
+ }): import("react").JSX.Element;
52
+ export declare function SaveBar({ open, message, saveLabel, discardLabel, onSave, onDiscard, loading, id, className, }: {
53
+ /** Shows the bar. Its message is announced each time it opens. */
54
+ open: boolean;
55
+ message?: string;
56
+ saveLabel?: string;
57
+ discardLabel?: string;
58
+ onSave: () => void;
59
+ onDiscard: () => void;
60
+ /** Busy saving: Save keeps focus but ignores activation; Discard is disabled. */
61
+ loading?: boolean;
62
+ id?: string;
63
+ className?: string;
64
+ }): import("react").JSX.Element;
65
+ /** Site-wide announcement bar with an optional link and dismiss button. */
66
+ export declare function Banner({ children, action, onDismiss, label, dismissLabel, tone, variant, placement, id, className, }: {
67
+ children: ReactNode;
68
+ action?: LinkAction;
69
+ onDismiss?: () => void;
70
+ label?: string;
71
+ dismissLabel?: string;
72
+ /** "inverse" (default) flips the colours; "default" uses the page surface; "accent" paints the brand's accent fill (its accent gradient when it defines one) with on-accent text. */
73
+ tone?: "default" | "inverse" | "accent";
74
+ /** bar: an edge-to-edge strip. floating: an inset, rounded panel with a shadow. */
75
+ variant?: "bar" | "floating";
76
+ /** inline: in the page flow. bottom: fixed to the bottom of the viewport, always with a dismiss button so it never covers content for good. */
77
+ placement?: "inline" | "bottom";
78
+ id?: string;
79
+ className?: string;
80
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,98 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useId, useRef, useState } from "react";
3
+ import { DialogPanel, DialogTitle, Description } from "@headlessui/react";
4
+ import { Button, Input } from "./primitives.js";
5
+ import { KitLink } from "./navigation.js";
6
+ import { Check, Close } from "./icons.js";
7
+ import { DialogFrame, componentRoot, cx, focusAway } from "./internal.js";
8
+ const glyph = {
9
+ neutral: "i",
10
+ info: "i",
11
+ success: _jsx(Check, {}),
12
+ warning: "!",
13
+ danger: "!",
14
+ };
15
+ export function Toast({ title, description, tone = "neutral", action, onDismiss, leading, secondaryAction, className, }) {
16
+ const root = useRef(null);
17
+ return (_jsxs("div", { ref: root, ...componentRoot("Toast"), className: cx("m-toast", className), "data-tone": tone === "neutral" ? undefined : tone, children: [leading ? (_jsx("span", { className: "m-toast-leading", children: leading })) : (_jsx("span", { className: "m-toast-icon", "aria-hidden": "true", children: glyph[tone] })), _jsxs("div", { children: [_jsx("strong", { children: title }), description && _jsx("p", { children: description })] }), _jsxs("div", { className: "m-actions", children: [action && (_jsx(Button, { variant: "ghost", onClick: action.onSelect, children: action.label })), secondaryAction && (_jsx(Button, { variant: "ghost", onClick: secondaryAction.onSelect, children: secondaryAction.label })), onDismiss && (_jsx(Button, { variant: "ghost", icon: _jsx(Close, {}), iconOnly: true, onClick: () => {
18
+ onDismiss();
19
+ focusAway(root.current);
20
+ }, children: `Dismiss ${title}` }))] })] }));
21
+ }
22
+ const spoken = (items) => items
23
+ .map((item) => [item.title, item.description].filter(Boolean).join(". "))
24
+ .join(". ");
25
+ /**
26
+ * Controlled toast region. The consumer owns the queue and timeouts. New
27
+ * toasts are announced once, through two persistent live regions: danger
28
+ * assertively, every other tone politely.
29
+ */
30
+ export function Toaster({ items, onDismiss, label = "Notifications", id, className, }) {
31
+ const region = useRef(null);
32
+ const seen = useRef(null);
33
+ const [announcement, setAnnouncement] = useState({
34
+ polite: "",
35
+ assertive: "",
36
+ count: 0,
37
+ });
38
+ useEffect(() => {
39
+ const known = seen.current;
40
+ seen.current = new Set(items.map((item) => item.id));
41
+ // Toasts present on mount are content, not news.
42
+ if (!known)
43
+ return;
44
+ const fresh = items.filter((item) => !known.has(item.id));
45
+ if (!fresh.length)
46
+ return;
47
+ const urgent = fresh.filter((item) => item.tone === "danger");
48
+ const calm = fresh.filter((item) => item.tone !== "danger");
49
+ setAnnouncement((previous) => ({
50
+ polite: calm.length ? spoken(calm) : "",
51
+ assertive: urgent.length ? spoken(urgent) : "",
52
+ count: previous.count + 1,
53
+ }));
54
+ }, [items]);
55
+ return (_jsxs("section", { ref: region, id: id, ...componentRoot("Toaster"), className: cx("m-toaster", className), "aria-label": label, tabIndex: -1, children: [_jsx("ol", { children: items.map(({ id, ...item }) => (_jsx("li", { children: _jsx(Toast, { ...item, onDismiss: () => {
56
+ const section = region.current;
57
+ const toast = section?.ownerDocument.activeElement?.closest("li");
58
+ if (section && toast && section.contains(toast))
59
+ focusAway(toast, { scope: section, fallback: section });
60
+ onDismiss(id);
61
+ } }) }, id))) }), _jsx("div", { className: "m-sr-only", role: "status", children: announcement.polite && (_jsx("span", { children: announcement.polite }, announcement.count)) }), _jsx("div", { className: "m-sr-only", role: "alert", children: announcement.assertive && (_jsx("span", { children: announcement.assertive }, announcement.count)) })] }));
62
+ }
63
+ export function ConfirmDialog({ open, onClose, onConfirm, title, description, confirmLabel, cancelLabel = "Cancel", confirmationText, loading = false, tone = "danger", className, }) {
64
+ const [typed, setTyped] = useState("");
65
+ useEffect(() => {
66
+ if (!open)
67
+ setTyped("");
68
+ }, [open]);
69
+ const ready = !confirmationText || typed === confirmationText;
70
+ const danger = tone === "danger";
71
+ return (_jsx(DialogFrame, { open: open, onClose: onClose, component: "ConfirmDialog", children: _jsx("div", { className: "m-dialog-position", children: _jsx(DialogPanel, { className: cx("m-dialog-panel", "m-confirm", className), children: _jsxs("form", { onSubmit: (e) => {
72
+ e.preventDefault();
73
+ if (ready && !loading)
74
+ onConfirm();
75
+ }, children: [_jsxs("div", { className: "m-confirm-body", children: [_jsx(DialogTitle, { children: title }), _jsx(Description, { as: "div", children: description }), confirmationText && (_jsx(Input, { label: `Type ${confirmationText} to confirm`, value: typed, onChange: (e) => setTyped(e.target.value), autoComplete: "off", spellCheck: false }))] }), _jsxs("div", { className: "m-confirm-actions", children: [_jsx(Button, { variant: "secondary", onClick: onClose, children: cancelLabel }), _jsx(Button, { type: "submit", variant: danger ? "danger" : "primary", "data-solid": danger || undefined, disabled: !ready, loading: loading, children: confirmLabel })] })] }) }) }) }));
76
+ }
77
+ export function SaveBar({ open, message = "You have unsaved changes.", saveLabel = "Save changes", discardLabel = "Discard", onSave, onDiscard, loading = false, id, className, }) {
78
+ const messageId = useId();
79
+ return (_jsxs(_Fragment, { children: [_jsx("span", { ...componentRoot("SaveBar"), id: messageId, role: "status", className: "m-sr-only", children: open ? message : "" }), open && (_jsxs("div", { id: id, className: cx("m-save-bar", className), role: "region", "aria-labelledby": messageId, children: [_jsx("span", { "aria-hidden": "true", children: message }), _jsxs("div", { className: "m-actions", children: [_jsx(Button, { variant: "secondary", onClick: onDiscard, disabled: loading, children: discardLabel }), _jsx(Button, { onClick: onSave, loading: loading, children: saveLabel })] })] }))] }));
80
+ }
81
+ /** Site-wide announcement bar with an optional link and dismiss button. */
82
+ export function Banner({ children, action, onDismiss, label = "Announcement", dismissLabel = "Dismiss announcement", tone = "inverse", variant = "bar", placement = "inline", id, className, }) {
83
+ const root = useRef(null);
84
+ const [dismissed, setDismissed] = useState(false);
85
+ const fixed = placement === "bottom";
86
+ // A fixed banner hides itself when dismissed, even if the page keeps it mounted.
87
+ if (dismissed)
88
+ return null;
89
+ const dismiss = onDismiss || fixed
90
+ ? () => {
91
+ onDismiss?.();
92
+ focusAway(root.current);
93
+ if (fixed)
94
+ setDismissed(true);
95
+ }
96
+ : undefined;
97
+ return (_jsxs("section", { ref: root, id: id, className: cx("m-banner", className), "aria-label": label, "data-tone": tone === "default" ? undefined : tone, "data-variant": variant === "bar" ? undefined : variant, "data-placement": fixed ? "bottom" : undefined, ...componentRoot("Banner"), children: [_jsxs("p", { children: [children, action && (_jsxs(_Fragment, { children: [" ", _jsxs(KitLink, { href: action.href, children: [action.label, _jsx("span", { "aria-hidden": "true", children: " \u2192" })] })] }))] }), dismiss && (_jsx("button", { type: "button", "aria-label": dismissLabel, onClick: dismiss, children: _jsx(Close, {}) }))] }));
98
+ }
@@ -0,0 +1,42 @@
1
+ import { type Navigate } from "./navigation.js";
2
+ import { type BrandDefinition } from "./brand.js";
3
+ import { type ReactNode } from "react";
4
+ export type Appearance = "light" | "dark" | "system";
5
+ /**
6
+ * A built-in brand, or the name of one declared in CSS
7
+ * (`[data-mortar-brand="name"]`) or through ThemeProvider's `brandTokens`.
8
+ */
9
+ export type Brand = "neutral" | "green" | (string & {});
10
+ export type Density = "compact" | "comfortable" | "spacious";
11
+ export interface ThemeProviderProps {
12
+ children: ReactNode;
13
+ initialAppearance?: Appearance;
14
+ appearance?: Appearance;
15
+ onAppearanceChange?: (value: Appearance) => void;
16
+ /** Connect the application router once for every kit link. A nested provider without it keeps the outer router. */
17
+ navigate?: Navigate;
18
+ brand?: Brand;
19
+ /**
20
+ * Defines `brand` at runtime from data instead of CSS, e.g. a tenant's
21
+ * colours. Rendered as a `<style>` beside the provider's content (and
22
+ * server-rendered); overlays opened inside the provider share it.
23
+ * Omitted values derive from `primary`.
24
+ */
25
+ brandTokens?: BrandDefinition;
26
+ density?: Density;
27
+ /**
28
+ * document writes the root element. container paints a wrapper and leaves
29
+ * the document unchanged; menus, listboxes and dialogs opened inside it keep
30
+ * its theme.
31
+ */
32
+ scope?: "document" | "container";
33
+ /** On the container wrapper (container scope only). */
34
+ className?: string;
35
+ }
36
+ /** One provider per application. Selection never requires a hosted account. */
37
+ export declare function ThemeProvider({ children, initialAppearance, appearance: controlledAppearance, onAppearanceChange, navigate, brand, brandTokens, density, scope, className, }: ThemeProviderProps): import("react").JSX.Element;
38
+ export declare function AppearanceControl({ value, onChange, className, }?: {
39
+ value?: Appearance;
40
+ onChange?: (value: Appearance) => void;
41
+ className?: string;
42
+ }): import("react").JSX.Element;
@@ -0,0 +1,121 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { NavigationContext } from "./navigation.js";
3
+ import { SegmentedControl } from "./inspection.js";
4
+ import { AsComponentRoot, ThemeScopeContext, componentRoot, } from "./internal.js";
5
+ import { Portal } from "@headlessui/react";
6
+ import { brandToCss } from "./brand.js";
7
+ import { createContext, useContext, useEffect, useLayoutEffect, useState, useSyncExternalStore, } from "react";
8
+ const ThemeContext = createContext(null);
9
+ const DARK = "(prefers-color-scheme: dark)";
10
+ function subscribeToScheme(update) {
11
+ const media = window.matchMedia(DARK);
12
+ media.addEventListener("change", update);
13
+ return () => media.removeEventListener("change", update);
14
+ }
15
+ /**
16
+ * The system colour scheme. The server snapshot is light, and React hydrates
17
+ * with it before switching to the real value, so markup always matches.
18
+ */
19
+ function useSystemDark() {
20
+ return useSyncExternalStore(subscribeToScheme, () => window.matchMedia(DARK).matches, () => false);
21
+ }
22
+ /**
23
+ * A body-level element that receives the container's overlays (menus,
24
+ * listboxes, popovers) so they keep its theme without being clipped by it.
25
+ */
26
+ function usePortalHost(enabled, attributes) {
27
+ // Created during the first client render so portals can target it at once.
28
+ const [target] = useState(() => ({
29
+ current: typeof document === "undefined" ? null : document.createElement("div"),
30
+ }));
31
+ const host = target.current;
32
+ useLayoutEffect(() => {
33
+ if (!host || !enabled)
34
+ return;
35
+ host.className = "m-theme-scope";
36
+ for (const [key, value] of Object.entries(attributes))
37
+ host.setAttribute(key, value);
38
+ });
39
+ // Headless UI attaches the host when an overlay opens; remove it with us.
40
+ useEffect(() => () => host?.remove(), [host]);
41
+ return target;
42
+ }
43
+ /** One provider per application. Selection never requires a hosted account. */
44
+ export function ThemeProvider({ children, initialAppearance = "system", appearance: controlledAppearance, onAppearanceChange, navigate, brand = "neutral", brandTokens, density = "comfortable", scope = "document", className, }) {
45
+ const [localAppearance, setLocalAppearance] = useState(initialAppearance);
46
+ const appearance = controlledAppearance ?? localAppearance;
47
+ const setAppearance = (value) => {
48
+ setLocalAppearance(value);
49
+ onAppearanceChange?.(value);
50
+ };
51
+ const systemDark = useSystemDark();
52
+ const outerNavigate = useContext(NavigationContext);
53
+ const resolvedAppearance = (appearance === "system" ? (systemDark ? "dark" : "light") : appearance);
54
+ const attributes = {
55
+ "data-mortar": "",
56
+ "data-mortar-brand": brand,
57
+ "data-mortar-density": density,
58
+ "data-mortar-appearance": resolvedAppearance,
59
+ };
60
+ const host = usePortalHost(scope === "container", attributes);
61
+ useEffect(() => {
62
+ if (scope !== "document")
63
+ return;
64
+ const element = document.documentElement;
65
+ const attributes = {
66
+ "data-mortar": "",
67
+ "data-mortar-brand": brand,
68
+ "data-mortar-density": density,
69
+ };
70
+ const previous = Object.keys(attributes).map((key) => [key, element.getAttribute(key)]);
71
+ for (const [key, value] of Object.entries(attributes))
72
+ element.setAttribute(key, value);
73
+ return () => {
74
+ for (const [key, value] of previous)
75
+ value === null
76
+ ? element.removeAttribute(key)
77
+ : element.setAttribute(key, value);
78
+ };
79
+ }, [brand, density, scope]);
80
+ useEffect(() => {
81
+ if (scope !== "document")
82
+ return;
83
+ const element = document.documentElement;
84
+ const previous = element.getAttribute("data-mortar-appearance");
85
+ element.setAttribute("data-mortar-appearance", resolvedAppearance);
86
+ return () => {
87
+ previous === null
88
+ ? element.removeAttribute("data-mortar-appearance")
89
+ : element.setAttribute("data-mortar-appearance", previous);
90
+ };
91
+ }, [resolvedAppearance, scope]);
92
+ const brandCss = brandTokens ? brandToCss(brand, brandTokens) : undefined;
93
+ const theme = (_jsxs(NavigationContext.Provider, { value: navigate ?? outerNavigate, children: [brandCss && (
94
+ // An ordinary element, not a hoisted resource: React updates its
95
+ // content when the tokens change and removes it on unmount, and it
96
+ // is server-rendered. The rule keys on the brand attribute, so
97
+ // portalled overlays match it wherever they render.
98
+ _jsx("style", { "data-mortar-brand-style": brand, children: brandCss })), _jsx(ThemeContext.Provider, { value: { appearance, setAppearance, brand, density }, children: children })] }));
99
+ if (scope === "container")
100
+ return (_jsx("div", { ...componentRoot("ThemeProvider"), ...attributes, className: className, children: _jsx(ThemeScopeContext.Provider, { value: attributes, children: _jsx(Portal.Group, { target: host, children: theme }) }) }));
101
+ return theme;
102
+ }
103
+ export function AppearanceControl({ value, onChange, className, } = {}) {
104
+ const controlled = value !== undefined || onChange !== undefined;
105
+ if ((value === undefined) !== (onChange === undefined))
106
+ throw new Error("AppearanceControl requires both value and onChange, or neither.");
107
+ const context = useContext(ThemeContext);
108
+ if (!controlled && !context)
109
+ throw new Error("AppearanceControl requires ThemeProvider");
110
+ const appearance = controlled ? value : context.appearance;
111
+ const select = (next) => {
112
+ if (controlled)
113
+ onChange?.(next);
114
+ else
115
+ context.setAppearance(next);
116
+ };
117
+ return (_jsx(AsComponentRoot, { name: "AppearanceControl", of: "SegmentedControl", children: _jsx(SegmentedControl, { label: "Appearance", value: appearance, className: className, options: ["light", "dark", "system"].map((option) => ({
118
+ value: option,
119
+ label: option[0].toUpperCase() + option.slice(1),
120
+ })), onChange: (next) => select(next) }) }));
121
+ }
@@ -0,0 +1,5 @@
1
+ export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
2
+ /** The heading element for a level, e.g. `headingTag(2)` is `"h2"`. */
3
+ export declare function headingTag<L extends HeadingLevel>(level: L): `h${L}`;
4
+ /** The level for headings nested under `level`, capped at `max` (default h6). */
5
+ export declare function childLevel(level: HeadingLevel, max?: HeadingLevel): HeadingLevel;
@@ -0,0 +1,9 @@
1
+ // Shared heading-level helpers. Internal: not exported from the package entry.
2
+ /** The heading element for a level, e.g. `headingTag(2)` is `"h2"`. */
3
+ export function headingTag(level) {
4
+ return `h${level}`;
5
+ }
6
+ /** The level for headings nested under `level`, capped at `max` (default h6). */
7
+ export function childLevel(level, max = 6) {
8
+ return Math.min(level + 1, max);
9
+ }
@@ -0,0 +1,50 @@
1
+ import type { ReactNode } from "react";
2
+ /**
3
+ * Decorative SVG on the 20×20 grid. Hidden from assistive technology unless
4
+ * `decorative` is false; meaning is carried by surrounding text.
5
+ */
6
+ export declare function Icon({ className, viewBox, fill, stroke, strokeWidth, decorative, focusable, children, }: {
7
+ className?: string;
8
+ viewBox?: string;
9
+ /** SVG fill; `null` omits the attribute (paths set their own). */
10
+ fill?: string | null;
11
+ stroke?: string;
12
+ strokeWidth?: string;
13
+ /** Adds aria-hidden="true". */
14
+ decorative?: boolean;
15
+ /** Keep the SVG out of the tab order (focusable="false"); `true` omits the attribute. */
16
+ focusable?: boolean;
17
+ children: ReactNode;
18
+ }): import("react").JSX.Element;
19
+ export declare function ChevronDown(): import("react").JSX.Element;
20
+ export declare function ChevronLeft(): import("react").JSX.Element;
21
+ export declare function ChevronRight(): import("react").JSX.Element;
22
+ /**
23
+ * Check mark. The three call sites predate this module and differ:
24
+ * `option` (listbox/combobox selected option), `list` (CheckList, 16×16 grid,
25
+ * fill on the path) and `step` (ProgressSteps circles: stroke on the svg,
26
+ * heavier weight, not aria-hidden because its marker span already is).
27
+ */
28
+ export declare function Check({ variant, className, }: {
29
+ variant?: "option" | "list" | "step";
30
+ className?: string;
31
+ }): import("react").JSX.Element;
32
+ export declare function Close(): import("react").JSX.Element;
33
+ export declare function ArrowUp(): import("react").JSX.Element;
34
+ export declare function ArrowDown(): import("react").JSX.Element;
35
+ export declare function Star(): import("react").JSX.Element;
36
+ export declare function Paperclip(): import("react").JSX.Element;
37
+ /** Generic person silhouette for avatars without a photo (24×24 grid, filled). */
38
+ export declare function Person({ className }: {
39
+ className?: string;
40
+ }): import("react").JSX.Element;
41
+ /** Table sort indicator; the active direction's chevron is marked data-active. */
42
+ export declare function SortArrows({ direction, }: {
43
+ direction?: "ascending" | "descending";
44
+ }): import("react").JSX.Element;
45
+ /** Open eye: shown while a password is hidden. */
46
+ export declare function Eye(): import("react").JSX.Element;
47
+ /** Struck-through eye: shown while a password is visible. */
48
+ export declare function EyeOff(): import("react").JSX.Element;
49
+ /** Hollow circle: a requirement that is not met yet. */
50
+ export declare function Circle(): import("react").JSX.Element;