@polyxd/react 0.1.1 → 0.2.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 (37) hide show
  1. package/dist/components/chart.js +133 -2
  2. package/dist/components/chart.js.map +1 -1
  3. package/dist/components/content.d.ts +16 -0
  4. package/dist/components/content.js +494 -19
  5. package/dist/components/content.js.map +1 -1
  6. package/dist/components/flow.js +41 -1
  7. package/dist/components/flow.js.map +1 -1
  8. package/dist/components/index.js +5 -0
  9. package/dist/components/index.js.map +1 -1
  10. package/dist/components/inputs.d.ts +2 -0
  11. package/dist/components/inputs.js +237 -4
  12. package/dist/components/inputs.js.map +1 -1
  13. package/dist/components/marks.d.ts +40 -0
  14. package/dist/components/marks.js +221 -0
  15. package/dist/components/marks.js.map +1 -0
  16. package/dist/components/navigation.d.ts +2 -1
  17. package/dist/components/navigation.js +48 -12
  18. package/dist/components/navigation.js.map +1 -1
  19. package/dist/components/overlays.d.ts +18 -0
  20. package/dist/components/overlays.js +128 -0
  21. package/dist/components/overlays.js.map +1 -0
  22. package/dist/components/pickers.d.ts +10 -0
  23. package/dist/components/pickers.js +402 -0
  24. package/dist/components/pickers.js.map +1 -0
  25. package/dist/components/table.d.ts +5 -0
  26. package/dist/components/table.js +19 -10
  27. package/dist/components/table.js.map +1 -1
  28. package/dist/components/tree.d.ts +11 -0
  29. package/dist/components/tree.js +268 -0
  30. package/dist/components/tree.js.map +1 -0
  31. package/dist/format.d.ts +3 -1
  32. package/dist/format.js +19 -0
  33. package/dist/format.js.map +1 -1
  34. package/dist/surface.js +1 -1
  35. package/dist/surface.js.map +1 -1
  36. package/package.json +2 -2
  37. package/src/styles.css +1940 -0
@@ -1,14 +1,102 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useId } from "react";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useId, useMemo, useRef, useState } from "react";
3
3
  import { resolveFormat, useBindings, useSurface } from "../context.js";
4
4
  import { asList, absolute, childPointer, get } from "../data.js";
5
- import { formatValue } from "../format.js";
5
+ import { formatValue, safeColor } from "../format.js";
6
6
  import { Render, useA11y } from "../surface.js";
7
7
  import { Icon } from "./avatar.js";
8
8
  import { Heading } from "./structure.js";
9
+ import { Paging } from "./table.js";
10
+ /** A colour value shown as itself: a swatch beside the text. */
11
+ export function Swatch({ value }) {
12
+ const color = safeColor(value);
13
+ return _jsx("span", { className: `pxd-swatch${color ? "" : " pxd-swatch-unknown"}`, style: color ? { background: color } : undefined, "aria-hidden": "true" });
14
+ }
15
+ /** Only these link targets are live; anything else stays text. */
16
+ const LINK_OK = /^(https?:|mailto:|tel:|\/|#)/i;
17
+ /**
18
+ * The limited markup of a 'rich' Text: **bold**, *italic*, `code` and [text](href). Parsed into
19
+ * elements, never injected as HTML, so anything else (including tags) is shown exactly as typed.
20
+ */
21
+ export function richText(text) {
22
+ const out = [];
23
+ let buf = "";
24
+ let i = 0;
25
+ const flush = () => {
26
+ if (buf)
27
+ out.push(buf);
28
+ buf = "";
29
+ };
30
+ while (i < text.length) {
31
+ if (text[i] === "`") {
32
+ const end = text.indexOf("`", i + 1);
33
+ if (end > i + 1) {
34
+ flush();
35
+ out.push(_jsx("code", { children: text.slice(i + 1, end) }, out.length));
36
+ i = end + 1;
37
+ continue;
38
+ }
39
+ }
40
+ if (text.startsWith("**", i)) {
41
+ const end = text.indexOf("**", i + 2);
42
+ if (end > i + 2) {
43
+ flush();
44
+ out.push(_jsx("strong", { children: richText(text.slice(i + 2, end)) }, out.length));
45
+ i = end + 2;
46
+ continue;
47
+ }
48
+ }
49
+ if (text[i] === "*") {
50
+ const end = text.indexOf("*", i + 1);
51
+ if (end > i + 1) {
52
+ flush();
53
+ out.push(_jsx("em", { children: richText(text.slice(i + 1, end)) }, out.length));
54
+ i = end + 1;
55
+ continue;
56
+ }
57
+ }
58
+ if (text[i] === "[") {
59
+ const m = /^\[([^\]]+)\]\(([^)\s]+)\)/.exec(text.slice(i));
60
+ if (m) {
61
+ flush();
62
+ const href = LINK_OK.test(m[2]) ? m[2] : undefined;
63
+ out.push(href ? (_jsx("a", { href: href, rel: "noopener noreferrer", children: richText(m[1]) }, out.length)) : (_jsx("span", { children: richText(m[1]) }, out.length)));
64
+ i += m[0].length;
65
+ continue;
66
+ }
67
+ }
68
+ buf += text[i++];
69
+ }
70
+ flush();
71
+ return out;
72
+ }
9
73
  export function Text({ node }) {
10
74
  const b = useBindings();
11
- return (_jsx("p", { className: `pxd-text pxd-text-${node.variant ?? "body"}`, ...useA11y(node), children: b.text(node.text, node.format) }));
75
+ const s = useSurface();
76
+ const a11y = useA11y(node);
77
+ const variant = node.variant ?? "body";
78
+ const color = node.format?.type === "color";
79
+ const content = (v, format = node.format) => {
80
+ const text = b.text(v, format);
81
+ return format?.type === "color" ? (_jsxs(_Fragment, { children: [_jsx(Swatch, { value: text }), text] })) : (text);
82
+ };
83
+ if (variant === "rich") {
84
+ return (_jsx("p", { className: "pxd-text pxd-text-body pxd-text-rich", ...a11y, children: richText(b.text(node.text)) }));
85
+ }
86
+ if (variant === "quote") {
87
+ return (_jsxs("figure", { className: "pxd-text-quote", ...a11y, children: [_jsx("blockquote", { children: _jsx("p", { className: "pxd-text pxd-text-body", children: content(node.text) }) }), node.cite !== undefined && _jsx("figcaption", { className: "pxd-text-cite", children: b.text(node.cite) })] }));
88
+ }
89
+ if (variant === "list") {
90
+ const pointer = node.items ? absolute(node.items.path, b.scope) : undefined;
91
+ const entries = pointer ? asList(get(s.data, pointer)) : [];
92
+ const List = node.ordered ? "ol" : "ul";
93
+ const intro = b.text(node.text);
94
+ return (_jsxs("div", { className: "pxd-text-list", ...a11y, children: [intro && _jsx("p", { className: "pxd-text pxd-text-body", children: intro }), _jsx(List, { className: `pxd-text-list-items${node.ordered ? " pxd-text-list-ordered" : ""}`, children: entries.map((entry, i) => {
95
+ const raw = node.itemPath && pointer ? get(s.data, absolute(node.itemPath, { pointer: childPointer(pointer, i) })) : entry;
96
+ return _jsx("li", { children: content(raw) }, i);
97
+ }) })] }));
98
+ }
99
+ return (_jsx("p", { className: `pxd-text pxd-text-${variant}${color ? " pxd-text-color" : ""}`, ...a11y, children: content(node.text) }));
12
100
  }
13
101
  export function Metric({ node }) {
14
102
  const b = useBindings();
@@ -37,18 +125,28 @@ export function DetailList({ node }) {
37
125
  const raw = b.value(item.value);
38
126
  // In a receipt, a zero charge reads as "Free".
39
127
  if (receipt && item.format?.type === "currency" && raw === 0 && !item.total)
40
- return { text: "Free", free: true };
41
- return { text: b.text(item.value, item.format), free: false };
128
+ return { text: "Free", free: true, color: false };
129
+ return { text: b.text(item.value, item.format), free: false, color: item.format?.type === "color" };
42
130
  };
131
+ // The 'Change' link per row (GOV.UK summary list). The row's key travels with the context.
132
+ const change = (item, label) => node.rowAction ? (_jsxs("button", { type: "button", className: "pxd-link pxd-detail-change", onClick: () => s.dispatch({ event: { name: node.rowAction.event.name, context: { key: item.key ?? label, ...(node.rowAction.event.context ?? {}) } } }, b.scope, node.id), children: ["Change", _jsxs("span", { className: "pxd-sr-only", children: [" ", label] })] })) : null;
43
133
  const rows = node.items.filter((i) => !i.total);
44
134
  const totals = node.items.filter((i) => i.total);
45
- return (_jsxs("div", { className: `pxd-detail-list${receipt ? " pxd-receipt" : ""}${node.layout === "grid" ? " pxd-detail-grid" : ""}`, ...useA11y(node), children: [node.title !== undefined && _jsx(Heading, { className: "pxd-detail-title", children: b.text(node.title) }), _jsxs("dl", { children: [rows.map((item, i) => {
135
+ return (_jsxs("div", { className: `pxd-detail-list${receipt ? " pxd-receipt" : ""}${node.layout === "grid" ? " pxd-detail-grid" : ""}${node.rowAction ? " pxd-detail-actions" : ""}`, ...useA11y(node), children: [node.title !== undefined && _jsx(Heading, { className: "pxd-detail-title", children: b.text(node.title) }), _jsxs("dl", { children: [rows.map((item, i) => {
46
136
  const v = value(item);
47
- return (_jsxs("div", { className: "pxd-detail-row", children: [_jsx("dt", { children: b.text(item.label) }), _jsx("dd", { className: v.free ? "pxd-free" : undefined, children: v.text })] }, item.key ?? i));
48
- }), totals.map((item, i) => (_jsxs("div", { className: "pxd-detail-row pxd-total", children: [_jsx("dt", { children: b.text(item.label) }), _jsx("dd", { children: value(item).text })] }, item.key ?? `t${i}`)))] })] }));
137
+ const label = b.text(item.label);
138
+ return (_jsxs("div", { className: "pxd-detail-row", children: [_jsx("dt", { children: label }), _jsxs("dd", { className: v.free ? "pxd-free" : undefined, children: [v.color && _jsx(Swatch, { value: v.text }), v.text] }), node.rowAction && _jsx("dd", { className: "pxd-detail-row-action", children: change(item, label) })] }, item.key ?? i));
139
+ }), totals.map((item, i) => (_jsxs("div", { className: "pxd-detail-row pxd-total", children: [_jsx("dt", { children: b.text(item.label) }), _jsx("dd", { children: value(item).text }), node.rowAction && _jsx("dd", { className: "pxd-detail-row-action" })] }, item.key ?? `t${i}`)))] })] }));
49
140
  }
50
141
  /** Stable value used for selecting an item: its id when present, else its index. */
51
142
  const itemValue = (item, index) => (item && typeof item === "object" && "id" in item ? item.id : index);
143
+ /** ISO date → UTC parts, or undefined when it isn't one. */
144
+ function dateParts(v) {
145
+ if (v === null || v === undefined || v === "")
146
+ return undefined;
147
+ const d = new Date(String(v));
148
+ return Number.isNaN(d.getTime()) ? undefined : { y: d.getUTCFullYear(), m: d.getUTCMonth(), d: d.getUTCDate() };
149
+ }
52
150
  export function Collection({ node }) {
53
151
  const b = useBindings();
54
152
  const s = useSurface();
@@ -66,30 +164,407 @@ export function Collection({ node }) {
66
164
  const cur = Array.isArray(selected) ? selected : [];
67
165
  b.write(node.selected, cur.includes(v) ? cur.filter((x) => x !== v) : [...cur, v]);
68
166
  };
167
+ const selectedCount = Array.isArray(selected) ? selected.length : 0;
69
168
  const label = b.text(node.label);
70
169
  // Grid for visual items (a Card with media), list otherwise, unless the document says which.
71
170
  const template = s.byId.get(node.items.componentId);
72
- const grid = node.layout === "grid" || (node.layout !== "list" && node.layout !== "timeline" && template?.component === "Card" && !!template.media);
73
- const timeline = node.layout === "timeline";
171
+ const layout = node.layout ?? "auto";
172
+ const grid = layout === "grid" || (layout === "auto" && template?.component === "Card" && !!template.media);
173
+ const timeline = layout === "timeline";
174
+ const carousel = layout === "carousel";
175
+ const calendar = layout === "calendar";
176
+ // Items in the order people put them ('order' holds item values); unknown values keep host order at the end.
177
+ const reorderable = node.reorderable === true;
178
+ const order = reorderable && node.order ? b.value(node.order) : undefined;
179
+ const values = items.map((item, i) => itemValue(item, i));
180
+ const indices = items.map((_, i) => i);
181
+ if (Array.isArray(order)) {
182
+ const rank = (i) => {
183
+ const r = order.indexOf(values[i]);
184
+ return r < 0 ? order.length + i : r;
185
+ };
186
+ indices.sort((a, c) => rank(a) - rank(c));
187
+ }
188
+ const move = (from, to) => {
189
+ if (!node.order || to < 0 || to >= indices.length || from === to)
190
+ return;
191
+ const next = [...indices];
192
+ next.splice(to, 0, ...next.splice(from, 1));
193
+ b.write(node.order, next.map((i) => values[i]));
194
+ };
195
+ const [dragging, setDragging] = useState(null);
196
+ // Carousel: which item is in view, for the readout and the buttons.
197
+ const track = useRef(null);
198
+ const [position, setPosition] = useState(0);
199
+ const slides = () => Array.from(track.current?.children ?? []);
200
+ const measure = () => {
201
+ const el = track.current;
202
+ if (!el)
203
+ return;
204
+ const left = el.scrollLeft;
205
+ let best = 0;
206
+ slides().forEach((c, i) => {
207
+ if (Math.abs(c.offsetLeft - el.offsetLeft - left) < Math.abs(slides()[best].offsetLeft - el.offsetLeft - left))
208
+ best = i;
209
+ });
210
+ setPosition(best);
211
+ };
212
+ const goTo = (i) => {
213
+ const el = track.current;
214
+ const target = slides()[Math.max(0, Math.min(items.length - 1, i))];
215
+ if (el && target)
216
+ el.scrollTo({ left: target.offsetLeft - el.offsetLeft, behavior: "smooth" });
217
+ };
218
+ // Calendar: the month shown starts at the first dated item (or today) and moves a month at a time.
219
+ const dated = calendar ? items.map((_, i) => dateParts(node.datePath ? get(s.data, absolute(node.datePath, { pointer: childPointer(pointer, i) })) : undefined)) : [];
220
+ const [month, setMonth] = useState(null);
221
+ const first = dated.find(Boolean);
222
+ const today = new Date();
223
+ const shown = month ?? (first ? { y: first.y, m: first.m } : { y: today.getUTCFullYear(), m: today.getUTCMonth() });
224
+ const shiftMonth = (by) => {
225
+ const d = new Date(Date.UTC(shown.y, shown.m + by, 1));
226
+ setMonth({ y: d.getUTCFullYear(), m: d.getUTCMonth() });
227
+ };
74
228
  if (!items.length && node.empty)
75
229
  return _jsx(Render, { id: node.empty });
76
- return (_jsxs("div", { className: "pxd-collection", ...useA11y(node), children: [_jsxs("div", { className: "pxd-collection-label", id: labelId, children: [label, " ", _jsxs("span", { className: "pxd-count", children: ["(", items.length, ")"] })] }), _jsx("ul", { className: `pxd-collection-list${grid ? " pxd-collection-grid" : ""}${timeline ? " pxd-timeline" : ""}`, "aria-labelledby": labelId, children: items.map((item, i) => {
77
- const v = itemValue(item, i);
78
- const scope = { pointer: childPointer(pointer, i) };
79
- return (_jsxs("li", { className: "pxd-collection-item", children: [selection !== "none" && (_jsx("input", { type: selection === "single" ? "radio" : "checkbox", className: "pxd-collection-select", name: selection === "single" ? `${node.id}-selection` : undefined, checked: isSelected(v), onChange: () => toggle(v), "aria-label": `Select item ${i + 1} of ${items.length}` })), _jsx("div", { className: "pxd-collection-content", children: _jsx(Render, { id: node.items.componentId, scope: scope }) })] }, String(v)));
80
- }) })] }));
230
+ const itemEl = (i, pos) => {
231
+ const item = items[i];
232
+ const v = values[i];
233
+ const scope = { pointer: childPointer(pointer, i) };
234
+ return (_jsxs("li", { className: `pxd-collection-item${dragging === pos ? " pxd-collection-dragging" : ""}`, draggable: reorderable || undefined, onDragStart: reorderable ? () => setDragging(pos) : undefined, onDragOver: reorderable ? (e) => e.preventDefault() : undefined, onDrop: reorderable
235
+ ? () => {
236
+ if (dragging !== null)
237
+ move(dragging, pos);
238
+ setDragging(null);
239
+ }
240
+ : undefined, onDragEnd: reorderable ? () => setDragging(null) : undefined, children: [reorderable && (_jsxs("div", { className: "pxd-collection-reorder", children: [_jsx("span", { className: "pxd-collection-handle", "aria-hidden": "true", children: _jsx(Icon, { name: "menu", size: 16 }) }), _jsx("button", { type: "button", className: "pxd-icon-button pxd-collection-move", "aria-label": `Move item ${pos + 1} up`, disabled: pos === 0, onClick: () => move(pos, pos - 1), children: _jsx(Icon, { name: "sortUp", size: 16 }) }), _jsx("button", { type: "button", className: "pxd-icon-button pxd-collection-move", "aria-label": `Move item ${pos + 1} down`, disabled: pos === items.length - 1, onClick: () => move(pos, pos + 1), children: _jsx(Icon, { name: "sortDown", size: 16 }) })] })), selection !== "none" && (_jsx("input", { type: selection === "single" ? "radio" : "checkbox", className: "pxd-collection-select", name: selection === "single" ? `${node.id}-selection` : undefined, checked: isSelected(v), onChange: () => toggle(v), "aria-label": `Select item ${pos + 1} of ${items.length}` })), _jsx("div", { className: "pxd-collection-content", children: _jsx(Render, { id: node.items.componentId, scope: scope }) })] }, String(v)));
241
+ };
242
+ const bulk = selection === "multiple" && selectedCount > 0 && (_jsxs("div", { className: "pxd-table-toolbar pxd-table-bulk", role: "toolbar", "aria-label": `Actions for ${selectedCount} selected`, children: [_jsxs("p", { className: "pxd-bulk-count", "aria-live": "polite", children: [selectedCount, " selected"] }), node.bulkActions && _jsx(Render, { id: node.bulkActions }), _jsx("button", { type: "button", className: "pxd-button pxd-button-tertiary", onClick: () => b.write(node.selected, []), children: "Cancel" })] }));
243
+ let body;
244
+ if (carousel) {
245
+ body = (_jsxs("div", { className: "pxd-carousel", role: "group", "aria-roledescription": "carousel", "aria-labelledby": labelId, children: [_jsx("ul", { className: "pxd-collection-list pxd-carousel-track", "aria-labelledby": labelId, ref: track, onScroll: measure, children: indices.map((i, pos) => itemEl(i, pos)) }), _jsxs("div", { className: "pxd-carousel-controls", children: [_jsx("button", { type: "button", className: "pxd-icon-button", "aria-label": "Previous item", disabled: position <= 0, onClick: () => goTo(position - 1), children: _jsx(Icon, { name: "chevronLeft", size: 18 }) }), _jsxs("p", { className: "pxd-carousel-position", "aria-live": "polite", children: [position + 1, " of ", items.length] }), _jsx("button", { type: "button", className: "pxd-icon-button", "aria-label": "Next item", disabled: position >= items.length - 1, onClick: () => goTo(position + 1), children: _jsx(Icon, { name: "chevronRight", size: 18 }) })] })] }));
246
+ }
247
+ else if (calendar) {
248
+ const monthName = new Intl.DateTimeFormat(s.locale, { month: "long", year: "numeric", timeZone: "UTC" }).format(new Date(Date.UTC(shown.y, shown.m, 1)));
249
+ // Weeks start on Monday; 2024-01-01 was one.
250
+ const weekdays = Array.from({ length: 7 }, (_, i) => new Intl.DateTimeFormat(s.locale, { weekday: "short", timeZone: "UTC" }).format(new Date(Date.UTC(2024, 0, 1 + i))));
251
+ const firstDay = (new Date(Date.UTC(shown.y, shown.m, 1)).getUTCDay() + 6) % 7;
252
+ const days = new Date(Date.UTC(shown.y, shown.m + 1, 0)).getUTCDate();
253
+ const cells = [...Array(firstDay).fill(null), ...Array.from({ length: days }, (_, i) => i + 1)];
254
+ while (cells.length % 7)
255
+ cells.push(null);
256
+ const weeks = Array.from({ length: cells.length / 7 }, (_, w) => cells.slice(w * 7, w * 7 + 7));
257
+ const onDay = (d) => indices.filter((i) => dated[i] && dated[i].y === shown.y && dated[i].m === shown.m && dated[i].d === d);
258
+ body = (_jsxs("div", { className: "pxd-calendar", children: [_jsxs("div", { className: "pxd-calendar-header", children: [_jsx("button", { type: "button", className: "pxd-icon-button", "aria-label": "Previous month", onClick: () => shiftMonth(-1), children: _jsx(Icon, { name: "chevronLeft", size: 18 }) }), _jsx("p", { className: "pxd-calendar-month", "aria-live": "polite", children: monthName }), _jsx("button", { type: "button", className: "pxd-icon-button", "aria-label": "Next month", onClick: () => shiftMonth(1), children: _jsx(Icon, { name: "chevronRight", size: 18 }) })] }), _jsxs("table", { className: "pxd-calendar-grid", children: [_jsxs("caption", { className: "pxd-sr-only", children: [label, ", ", monthName] }), _jsx("thead", { children: _jsx("tr", { children: weekdays.map((w) => (_jsx("th", { scope: "col", children: w }, w))) }) }), _jsx("tbody", { children: weeks.map((week, wi) => (_jsx("tr", { children: week.map((d, di) => d === null ? (_jsx("td", { className: "pxd-calendar-empty" }, di)) : (_jsxs("td", { className: `pxd-calendar-day${onDay(d).length ? " pxd-calendar-has-items" : ""}`, children: [_jsx("span", { className: "pxd-calendar-date", children: d }), onDay(d).length > 0 && (_jsx("ul", { className: "pxd-calendar-items", children: onDay(d).map((i) => (_jsxs("li", { className: "pxd-collection-item", children: [selection !== "none" && (_jsx("input", { type: selection === "single" ? "radio" : "checkbox", className: "pxd-collection-select", name: selection === "single" ? `${node.id}-selection` : undefined, checked: isSelected(values[i]), onChange: () => toggle(values[i]), "aria-label": `Select item on ${d}` })), _jsx("div", { className: "pxd-collection-content", children: _jsx(Render, { id: node.items.componentId, scope: { pointer: childPointer(pointer, i) } }) })] }, String(values[i])))) }))] }, di))) }, wi))) })] })] }));
259
+ }
260
+ else {
261
+ body = (_jsx("ul", { className: `pxd-collection-list${grid ? " pxd-collection-grid" : ""}${timeline ? " pxd-timeline" : ""}${reorderable ? " pxd-collection-reorderable" : ""}`, "aria-labelledby": labelId, children: indices.map((i, pos) => itemEl(i, pos)) }));
262
+ }
263
+ return (_jsxs("div", { className: "pxd-collection", ...useA11y(node), children: [_jsxs("div", { className: "pxd-collection-label", id: labelId, children: [label, " ", _jsxs("span", { className: "pxd-count", children: ["(", items.length, ")"] })] }), bulk, body, node.page && _jsx(Paging, { node: node, rows: items.length })] }));
81
264
  }
82
- const NUMERIC = new Set(["number", "currency", "percent", "duration"]);
265
+ /**
266
+ * Media: an image by default; a video or audio player with native controls and a transcript; a
267
+ * gallery of the host's images; or a QR code of a value, drawn here so no URL leaves the surface.
268
+ */
83
269
  export function Media({ node }) {
84
270
  const b = useBindings();
85
271
  const s = useSurface();
86
- const ref = b.value(node.src);
272
+ const a11y = useA11y(node);
273
+ const kind = node.kind ?? "image";
274
+ const ref = node.src ? b.value(node.src) : undefined;
87
275
  const url = ref ? s.resolveMedia?.(ref) : undefined;
88
276
  const alt = node.decorative ? "" : b.text(node.alt);
89
277
  const cls = `pxd-media pxd-media-${node.aspect ?? "auto"}`;
278
+ if (kind === "qr")
279
+ return _jsx(QRCode, { text: b.text(node.value), alt: alt, a11y: a11y });
280
+ if (kind === "gallery") {
281
+ const pointer = node.items ? absolute(node.items.path, b.scope) : undefined;
282
+ const entries = pointer ? asList(get(s.data, pointer)) : [];
283
+ return (_jsx("ul", { className: "pxd-gallery", "aria-label": alt || undefined, ...a11y, children: entries.map((_, i) => {
284
+ const scope = { pointer: childPointer(pointer, i) };
285
+ const imageRef = node.imagePath ? get(s.data, absolute(node.imagePath, scope)) : undefined;
286
+ const src = imageRef ? s.resolveMedia?.(String(imageRef)) : undefined;
287
+ const itemAlt = node.altPath ? String(get(s.data, absolute(node.altPath, scope)) ?? "") : "";
288
+ return (_jsx("li", { children: src ? _jsx("img", { className: `${cls} pxd-gallery-image`, src: src, alt: itemAlt }) : _jsx("div", { className: `${cls} pxd-media-placeholder pxd-gallery-image`, role: "img", "aria-label": itemAlt || undefined }) }, i));
289
+ }) }));
290
+ }
291
+ if (kind === "video" || kind === "audio") {
292
+ const transcript = node.transcript !== undefined ? b.text(node.transcript) : undefined;
293
+ const posterRef = node.poster !== undefined ? b.text(node.poster) : undefined;
294
+ const poster = posterRef ? s.resolveMedia?.(posterRef) : undefined;
295
+ return (_jsxs("figure", { className: `pxd-media-player pxd-media-${kind}`, ...a11y, children: [kind === "video" ? (url ? (_jsx("video", { className: cls, controls: true, src: url, poster: poster, "aria-label": alt || undefined })) : (_jsx("div", { className: `${cls} pxd-media-placeholder`, role: "img", "aria-label": alt || undefined }))) : url ? (_jsx("audio", { className: "pxd-media-audio-player", controls: true, src: url, "aria-label": alt || undefined })) : (_jsx("div", { className: "pxd-media-placeholder pxd-media-audio-placeholder", role: "img", "aria-label": alt || undefined })), alt && _jsx("figcaption", { className: "pxd-media-caption", children: alt }), transcript && (_jsxs("details", { className: "pxd-media-transcript", children: [_jsx("summary", { children: "Transcript" }), _jsx("p", { className: "pxd-text pxd-text-body", children: transcript })] }))] }));
296
+ }
90
297
  if (!url)
91
298
  return _jsx("div", { className: `${cls} pxd-media-placeholder`, role: node.decorative ? "presentation" : "img", "aria-label": alt || undefined });
92
- return _jsx("img", { className: cls, src: url, alt: alt, ...useA11y(node) });
299
+ return _jsx("img", { className: cls, src: url, alt: alt, ...a11y });
300
+ }
301
+ /* ---- QR codes: byte mode, error correction level M, versions 1–10 (up to 213 bytes). ---- */
302
+ /** Per version at level M: error-correction codewords per block, then [blocks, data codewords] groups. */
303
+ const QR_BLOCKS = [
304
+ [10, [[1, 16]]],
305
+ [16, [[1, 28]]],
306
+ [26, [[1, 44]]],
307
+ [18, [[2, 32]]],
308
+ [24, [[2, 43]]],
309
+ [16, [[4, 27]]],
310
+ [18, [[4, 31]]],
311
+ [22, [[2, 38], [2, 39]]],
312
+ [22, [[3, 36], [2, 37]]],
313
+ [26, [[4, 43], [1, 44]]],
314
+ ];
315
+ const QR_ALIGN = [[], [6, 18], [6, 22], [6, 26], [6, 30], [6, 34], [6, 22, 38], [6, 24, 42], [6, 26, 46], [6, 28, 50]];
316
+ function gfMul(x, y) {
317
+ let z = 0;
318
+ for (let i = 7; i >= 0; i--) {
319
+ z = (z << 1) ^ ((z >>> 7) * 0x11d);
320
+ z ^= ((y >>> i) & 1) * x;
321
+ }
322
+ return z & 0xff;
323
+ }
324
+ function rsRemainder(data, degree) {
325
+ const divisor = new Array(degree).fill(0);
326
+ divisor[degree - 1] = 1;
327
+ let root = 1;
328
+ for (let i = 0; i < degree; i++) {
329
+ for (let j = 0; j < degree; j++) {
330
+ divisor[j] = gfMul(divisor[j], root);
331
+ if (j + 1 < degree)
332
+ divisor[j] ^= divisor[j + 1];
333
+ }
334
+ root = gfMul(root, 2);
335
+ }
336
+ const result = new Array(degree).fill(0);
337
+ for (const byte of data) {
338
+ const factor = byte ^ result.shift();
339
+ result.push(0);
340
+ divisor.forEach((coef, i) => (result[i] ^= gfMul(coef, factor)));
341
+ }
342
+ return result;
343
+ }
344
+ const QR_MASKS = [
345
+ (x, y) => (x + y) % 2 === 0,
346
+ (_x, y) => y % 2 === 0,
347
+ (x) => x % 3 === 0,
348
+ (x, y) => (x + y) % 3 === 0,
349
+ (x, y) => (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0,
350
+ (x, y) => ((x * y) % 2) + ((x * y) % 3) === 0,
351
+ (x, y) => (((x * y) % 2) + ((x * y) % 3)) % 2 === 0,
352
+ (x, y) => (((x + y) % 2) + ((x * y) % 3)) % 2 === 0,
353
+ ];
354
+ /** The module grid for a text, or undefined when it is longer than version 10 holds. */
355
+ export function qrEncode(text) {
356
+ const bytes = Array.from(new TextEncoder().encode(text));
357
+ let version = 0;
358
+ let dataCodewords = 0;
359
+ for (let v = 1; v <= 10 && !version; v++) {
360
+ const total = QR_BLOCKS[v - 1][1].reduce((a, [n, len]) => a + n * len, 0);
361
+ if (4 + (v < 10 ? 8 : 16) + bytes.length * 8 <= total * 8)
362
+ (version = v), (dataCodewords = total);
363
+ }
364
+ if (!version)
365
+ return undefined;
366
+ const [ecPerBlock, groups] = QR_BLOCKS[version - 1];
367
+ // Bit stream: mode, count, data, terminator, byte alignment, pad codewords.
368
+ const bits = [];
369
+ const push = (val, n) => {
370
+ for (let i = n - 1; i >= 0; i--)
371
+ bits.push((val >>> i) & 1);
372
+ };
373
+ push(4, 4);
374
+ push(bytes.length, version < 10 ? 8 : 16);
375
+ for (const b of bytes)
376
+ push(b, 8);
377
+ const capacity = dataCodewords * 8;
378
+ push(0, Math.min(4, capacity - bits.length));
379
+ while (bits.length % 8)
380
+ bits.push(0);
381
+ for (let pad = 0xec; bits.length < capacity; pad ^= 0xec ^ 0x11)
382
+ push(pad, 8);
383
+ const data = [];
384
+ for (let i = 0; i < bits.length; i += 8)
385
+ data.push(bits.slice(i, i + 8).reduce((a, bit) => (a << 1) | bit, 0));
386
+ // Blocks with their error correction, interleaved.
387
+ const blocks = [];
388
+ let k = 0;
389
+ for (const [count, len] of groups) {
390
+ for (let i = 0; i < count; i++) {
391
+ const d = data.slice(k, k + len);
392
+ blocks.push({ d, e: rsRemainder(d, ecPerBlock) });
393
+ k += len;
394
+ }
395
+ }
396
+ const codewords = [];
397
+ const longest = Math.max(...blocks.map((bl) => bl.d.length));
398
+ for (let i = 0; i < longest; i++)
399
+ for (const bl of blocks)
400
+ if (i < bl.d.length)
401
+ codewords.push(bl.d[i]);
402
+ for (let i = 0; i < ecPerBlock; i++)
403
+ for (const bl of blocks)
404
+ codewords.push(bl.e[i]);
405
+ // The grid, with function patterns marked so data and masks leave them alone.
406
+ const size = version * 4 + 17;
407
+ const grid = Array.from({ length: size }, () => new Array(size).fill(false));
408
+ const isFn = Array.from({ length: size }, () => new Array(size).fill(false));
409
+ const set = (x, y, dark) => {
410
+ if (x >= 0 && y >= 0 && x < size && y < size)
411
+ (grid[y][x] = dark), (isFn[y][x] = true);
412
+ };
413
+ for (let i = 0; i < size; i++)
414
+ (set(6, i, i % 2 === 0), set(i, 6, i % 2 === 0));
415
+ const finder = (cx, cy) => {
416
+ for (let dy = -4; dy <= 4; dy++)
417
+ for (let dx = -4; dx <= 4; dx++) {
418
+ const dist = Math.max(Math.abs(dx), Math.abs(dy));
419
+ set(cx + dx, cy + dy, dist !== 2 && dist !== 4);
420
+ }
421
+ };
422
+ finder(3, 3);
423
+ finder(size - 4, 3);
424
+ finder(3, size - 4);
425
+ const positions = QR_ALIGN[version - 1];
426
+ const last = positions.length - 1;
427
+ positions.forEach((px, i) => positions.forEach((py, j) => {
428
+ if ((i === 0 && j === 0) || (i === 0 && j === last) || (i === last && j === 0))
429
+ return;
430
+ for (let dy = -2; dy <= 2; dy++)
431
+ for (let dx = -2; dx <= 2; dx++)
432
+ set(px + dx, py + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
433
+ }));
434
+ const formatBits = (mask) => {
435
+ const d = mask; // level M is 00
436
+ let rem = d;
437
+ for (let i = 0; i < 10; i++)
438
+ rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
439
+ const f = ((d << 10) | rem) ^ 0x5412;
440
+ const bit = (i) => ((f >>> i) & 1) === 1;
441
+ for (let i = 0; i <= 5; i++)
442
+ set(8, i, bit(i));
443
+ set(8, 7, bit(6));
444
+ set(8, 8, bit(7));
445
+ set(7, 8, bit(8));
446
+ for (let i = 9; i < 15; i++)
447
+ set(14 - i, 8, bit(i));
448
+ for (let i = 0; i < 8; i++)
449
+ set(size - 1 - i, 8, bit(i));
450
+ for (let i = 8; i < 15; i++)
451
+ set(8, size - 15 + i, bit(i));
452
+ set(8, size - 8, true);
453
+ };
454
+ formatBits(0);
455
+ if (version >= 7) {
456
+ let rem = version;
457
+ for (let i = 0; i < 12; i++)
458
+ rem = (rem << 1) ^ ((rem >>> 11) * 0x1f25);
459
+ const v = (version << 12) | rem;
460
+ for (let i = 0; i < 18; i++) {
461
+ const bit = ((v >>> i) & 1) === 1;
462
+ const a = size - 11 + (i % 3);
463
+ const c = Math.floor(i / 3);
464
+ set(a, c, bit);
465
+ set(c, a, bit);
466
+ }
467
+ }
468
+ // Codewords zigzag upward and downward in two-module columns from the right, skipping the timing column.
469
+ let bi = 0;
470
+ for (let right = size - 1; right >= 1; right -= 2) {
471
+ if (right === 6)
472
+ right = 5;
473
+ for (let vert = 0; vert < size; vert++) {
474
+ for (let j = 0; j < 2; j++) {
475
+ const x = right - j;
476
+ const y = ((right + 1) & 2) === 0 ? size - 1 - vert : vert;
477
+ if (!isFn[y][x] && bi < codewords.length * 8) {
478
+ grid[y][x] = ((codewords[bi >>> 3] >>> (7 - (bi & 7))) & 1) === 1;
479
+ bi++;
480
+ }
481
+ }
482
+ }
483
+ }
484
+ // The mask with the lowest penalty.
485
+ const apply = (mask) => {
486
+ for (let y = 0; y < size; y++)
487
+ for (let x = 0; x < size; x++)
488
+ if (!isFn[y][x] && QR_MASKS[mask](x, y))
489
+ grid[y][x] = !grid[y][x];
490
+ };
491
+ const penalty = () => {
492
+ let p = 0;
493
+ const line = (cells) => {
494
+ let run = 0;
495
+ let prev = null;
496
+ cells.forEach((c, i) => {
497
+ if (c === prev) {
498
+ run++;
499
+ if (run === 5)
500
+ p += 3;
501
+ else if (run > 5)
502
+ p += 1;
503
+ }
504
+ else
505
+ (prev = c), (run = 1);
506
+ if (i + 7 <= cells.length) {
507
+ const seg = cells.slice(i, i + 7);
508
+ const finderLike = seg[0] && !seg[1] && seg[2] && seg[3] && seg[4] && !seg[5] && seg[6];
509
+ const lightBefore = i >= 4 && cells.slice(i - 4, i).every((c) => !c);
510
+ const lightAfter = i + 11 <= cells.length && cells.slice(i + 7, i + 11).every((c) => !c);
511
+ if (finderLike && (lightBefore || lightAfter))
512
+ p += 40;
513
+ }
514
+ });
515
+ };
516
+ for (let i = 0; i < size; i++)
517
+ (line(grid[i]), line(grid.map((row) => row[i])));
518
+ for (let y = 0; y < size - 1; y++)
519
+ for (let x = 0; x < size - 1; x++)
520
+ if (grid[y][x] === grid[y][x + 1] && grid[y][x] === grid[y + 1][x] && grid[y][x] === grid[y + 1][x + 1])
521
+ p += 3;
522
+ const dark = grid.flat().filter(Boolean).length;
523
+ const total = size * size;
524
+ p += (Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1) * 10;
525
+ return p;
526
+ };
527
+ let best = 0;
528
+ let bestScore = Infinity;
529
+ for (let m = 0; m < 8; m++) {
530
+ formatBits(m);
531
+ apply(m);
532
+ const score = penalty();
533
+ apply(m);
534
+ if (score < bestScore)
535
+ (bestScore = score), (best = m);
536
+ }
537
+ formatBits(best);
538
+ apply(best);
539
+ return grid;
540
+ }
541
+ /** A QR code on a canvas, in the surface's text and surface colours; too long a value falls back to text. */
542
+ function QRCode({ text, alt, a11y }) {
543
+ const ref = useRef(null);
544
+ const grid = useMemo(() => (text ? qrEncode(text) : undefined), [text]);
545
+ useEffect(() => {
546
+ const canvas = ref.current;
547
+ if (!canvas || !grid)
548
+ return;
549
+ const scale = 4;
550
+ const quiet = 4;
551
+ const size = grid.length;
552
+ canvas.width = canvas.height = (size + quiet * 2) * scale;
553
+ const ctx = canvas.getContext("2d");
554
+ if (!ctx)
555
+ return;
556
+ const style = getComputedStyle(canvas);
557
+ const dark = style.getPropertyValue("--pxd-color-text-default").trim() || "#000";
558
+ const light = style.getPropertyValue("--pxd-color-surface-default").trim() || "#fff";
559
+ ctx.fillStyle = light;
560
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
561
+ ctx.fillStyle = dark;
562
+ grid.forEach((row, y) => row.forEach((on, x) => on && ctx.fillRect((x + quiet) * scale, (y + quiet) * scale, scale, scale)));
563
+ }, [grid]);
564
+ if (!grid) {
565
+ return (_jsxs("div", { className: "pxd-qr pxd-qr-placeholder", role: "img", "aria-label": alt || `QR code: ${text}`, ...a11y, children: [_jsx("span", { className: "pxd-qr-mark", "aria-hidden": "true", children: "QR" }), _jsx("span", { className: "pxd-qr-text", children: text })] }));
566
+ }
567
+ return _jsx("canvas", { ref: ref, className: "pxd-qr", role: "img", "aria-label": alt || `QR code: ${text}`, ...a11y });
93
568
  }
94
569
  const STATUS_ICON = { info: "info", success: "check", warning: "alert", error: "alert", empty: "inbox", undo: "check" };
95
570
  /**