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