@klarkxy/dsh-deep-view 0.0.0-stage → 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.
package/lib/widget.js ADDED
@@ -0,0 +1,1365 @@
1
+ import { a as choiceValue, d as resolve, g as viewScope, h as viewInputs, r as asText, s as inputDefault } from "./core2.js";
2
+ import { Fragment, useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ //#region src/ui.tsx
5
+ const english = (key, params) => key.replace(/\{(\w+)\}/g, (_, name) => String(params?.[name] ?? `{${name}}`));
6
+ function LineIcon({ paths }) {
7
+ return /* @__PURE__ */ jsx("svg", {
8
+ width: "15",
9
+ height: "15",
10
+ viewBox: "0 0 20 20",
11
+ fill: "none",
12
+ stroke: "currentColor",
13
+ strokeWidth: "1.5",
14
+ strokeLinecap: "round",
15
+ strokeLinejoin: "round",
16
+ "aria-hidden": "true",
17
+ children: paths.map((d, index) => /* @__PURE__ */ jsx("path", { d }, index))
18
+ });
19
+ }
20
+ const DotsIcon = () => /* @__PURE__ */ jsx(LineIcon, { paths: ["M5 10h.01M10 10h.01M15 10h.01"] });
21
+ const CheckIcon = () => /* @__PURE__ */ jsx(LineIcon, { paths: ["M4.5 10.5l3.5 3.5 7.5-8"] });
22
+ const CopyIcon = () => /* @__PURE__ */ jsx(LineIcon, { paths: ["M8.5 7h6A1.5 1.5 0 0 1 16 8.5v6a1.5 1.5 0 0 1-1.5 1.5h-6A1.5 1.5 0 0 1 7 14.5v-6A1.5 1.5 0 0 1 8.5 7z", "M4 12.5V5.5A1.5 1.5 0 0 1 5.5 4H13"] });
23
+ const TableIcon = () => /* @__PURE__ */ jsx(LineIcon, { paths: ["M4.5 4h11A1.5 1.5 0 0 1 17 5.5v9a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 14.5v-9A1.5 1.5 0 0 1 4.5 4z", "M3 8.5h14M8 8.5V16"] });
24
+ const InfoIcon = () => /* @__PURE__ */ jsx(LineIcon, { paths: [
25
+ "M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z",
26
+ "M10 9.25v4",
27
+ "M10 6.6h.01"
28
+ ] });
29
+ const LinkIcon = () => /* @__PURE__ */ jsxs("svg", {
30
+ width: "12",
31
+ height: "12",
32
+ viewBox: "0 0 20 20",
33
+ fill: "none",
34
+ stroke: "currentColor",
35
+ strokeWidth: "1.8",
36
+ strokeLinecap: "round",
37
+ strokeLinejoin: "round",
38
+ "aria-hidden": "true",
39
+ children: [
40
+ /* @__PURE__ */ jsx("path", { d: "M8.5 11.5l3-3" }),
41
+ /* @__PURE__ */ jsx("path", { d: "M7 9.5L5.5 11a2.5 2.5 0 0 0 3.5 3.5L10.5 13" }),
42
+ /* @__PURE__ */ jsx("path", { d: "M13 10.5l1.5-1.5A2.5 2.5 0 0 0 11 5.5L9.5 7" })
43
+ ]
44
+ });
45
+ const keyLetter = (index) => String.fromCharCode(65 + index);
46
+ function Segmented({ label, value, className, children }) {
47
+ return /* @__PURE__ */ jsx("div", {
48
+ className: className ? `dvui-seg ${className}` : "dvui-seg",
49
+ role: "group",
50
+ "aria-label": label,
51
+ "data-active": value,
52
+ children
53
+ });
54
+ }
55
+ //#endregion
56
+ //#region src/chart.tsx
57
+ const PALETTE = [
58
+ "var(--dsw-alias-state-business-primary)",
59
+ "var(--dsw-alias-brand-primary)",
60
+ "var(--dsw-alias-state-success-primary)",
61
+ "var(--dsw-alias-state-warn-primary)",
62
+ "var(--dsw-alias-state-error-primary)"
63
+ ];
64
+ const colorAt = (index) => PALETTE[index % PALETTE.length];
65
+ const TICK = 10;
66
+ const LABEL = 11;
67
+ const textWidth = (text, size) => [...String(text)].reduce((sum, char) => sum + (char.charCodeAt(0) > 11904 ? size : size * .58), 0);
68
+ const fit = (text, room, size) => {
69
+ if (textWidth(text, size) <= room) return String(text);
70
+ let out = "";
71
+ for (const char of String(text)) {
72
+ if (textWidth(`${out}${char}…`, size) > room) break;
73
+ out += char;
74
+ }
75
+ return out === "" ? "" : `${out}…`;
76
+ };
77
+ function formatNumber(value) {
78
+ if (!Number.isFinite(value)) return asText(value);
79
+ const abs = Math.abs(value);
80
+ const digits = abs >= 100 ? 0 : abs >= 10 ? 1 : 2;
81
+ return Number(value.toFixed(digits)).toLocaleString("en-US", { maximumFractionDigits: digits });
82
+ }
83
+ const withUnit$1 = (value, unit) => unit ? unit === "%" ? `${formatNumber(value)}%` : `${formatNumber(value)} ${unit}` : formatNumber(value);
84
+ function niceTicks(low, high) {
85
+ let lo = low;
86
+ let hi = high;
87
+ if (lo === hi) {
88
+ lo = Math.min(0, lo);
89
+ hi = hi === 0 ? 1 : Math.max(0, hi);
90
+ }
91
+ if (lo === hi) hi = lo + 1;
92
+ const raw = (hi - lo) / 4;
93
+ const magnitude = 10 ** Math.floor(Math.log10(raw));
94
+ const step = [
95
+ 1,
96
+ 2,
97
+ 2.5,
98
+ 5,
99
+ 10
100
+ ].map((scale) => scale * magnitude).find((candidate) => candidate >= raw);
101
+ const start = Math.floor(lo / step + 1e-9) * step;
102
+ const end = Math.ceil(hi / step - 1e-9) * step;
103
+ const ticks = [];
104
+ for (let value = start; value <= end + step / 2; value += step) ticks.push(Number(value.toPrecision(12)));
105
+ return ticks;
106
+ }
107
+ function chartData(node, lookup) {
108
+ const categories = (Array.isArray(node.categories) ? node.categories : []).map(asText);
109
+ const series = (Array.isArray(node.series) ? node.series : []).map((item, index) => ({
110
+ name: typeof item?.name === "string" ? item.name : "",
111
+ color: colorAt(index),
112
+ values: categories.map((_, at) => {
113
+ const value = Number(resolve(Array.isArray(item?.values) ? item.values[at] : void 0, lookup, 0));
114
+ return Number.isFinite(value) ? value : 0;
115
+ })
116
+ }));
117
+ return {
118
+ kind: node.kind ?? "bar",
119
+ categories,
120
+ series,
121
+ unit: typeof node.unit === "string" ? node.unit : ""
122
+ };
123
+ }
124
+ const csvCell = (value) => /[",\n]/.test(value) ? `"${value.replace(/"/g, "\"\"")}"` : value;
125
+ function chartCsv({ categories, series, unit }, t = english) {
126
+ return [["", ...series.map((item, index) => item.name || (series.length === 1 ? unit || t("Value") : `${t("Series")} ${index + 1}`))], ...categories.map((label, at) => [label, ...series.map((item) => String(item.values[at]))])].map((row) => row.map((cell) => csvCell(String(cell))).join(",")).join("\n");
127
+ }
128
+ function valueAxis(values, zero) {
129
+ const finite = values.length > 0 ? values : [0];
130
+ const low = Math.min(...finite);
131
+ const high = Math.max(...finite);
132
+ if (zero) return niceTicks(Math.min(0, low), Math.max(0, high));
133
+ const pad = (high - low) * .1 || Math.abs(high) * .1 || 1;
134
+ return niceTicks(low - pad, high + pad);
135
+ }
136
+ function columns({ categories, series, unit, kind }, width, active, shown) {
137
+ const visible = series.filter((_, index) => shown[index]);
138
+ const ticks = valueAxis(visible.flatMap((item) => item.values), kind !== "line");
139
+ const lo = ticks[0];
140
+ const hi = ticks.at(-1);
141
+ const left = Math.ceil(Math.max(...ticks.map((tick) => textWidth(withUnit$1(tick, unit), TICK)))) + 10;
142
+ const top = 8;
143
+ const height = 220;
144
+ const plot = {
145
+ x: left,
146
+ y: top,
147
+ w: Math.max(40, width - left - 4),
148
+ h: 188
149
+ };
150
+ const y = (value) => plot.y + plot.h - (value - lo) / (hi - lo) * plot.h;
151
+ const band = plot.w / categories.length;
152
+ const parts = [];
153
+ for (const tick of ticks) {
154
+ parts.push(/* @__PURE__ */ jsx("line", {
155
+ className: "dvui-chart-grid",
156
+ x1: plot.x,
157
+ x2: plot.x + plot.w,
158
+ y1: y(tick),
159
+ y2: y(tick)
160
+ }, `g${tick}`));
161
+ parts.push(/* @__PURE__ */ jsx("text", {
162
+ className: "dvui-chart-tick",
163
+ x: plot.x - 8,
164
+ y: y(tick) + 3,
165
+ textAnchor: "end",
166
+ children: withUnit$1(tick, unit)
167
+ }, `t${tick}`));
168
+ }
169
+ const points = [];
170
+ categories.forEach((label, at) => {
171
+ const cx = plot.x + band * at + band / 2;
172
+ parts.push(/* @__PURE__ */ jsx("text", {
173
+ className: "dvui-chart-label",
174
+ x: cx,
175
+ y: 213,
176
+ textAnchor: "middle",
177
+ children: fit(label, band - 4, LABEL)
178
+ }, `c${at}`));
179
+ points.push({
180
+ x: cx,
181
+ y: y(Math.max(...visible.map((item) => item.values[at]), lo))
182
+ });
183
+ });
184
+ if (kind === "bar") {
185
+ const group = band * .72;
186
+ const gap = visible.length > 1 ? 2 : 0;
187
+ const barW = Math.max(2, (group - gap * (visible.length - 1)) / visible.length);
188
+ const zero = y(Math.max(lo, Math.min(0, hi)));
189
+ categories.forEach((_, at) => visible.forEach((item, row) => {
190
+ const value = item.values[at];
191
+ const x = plot.x + band * at + (band - group) / 2 + row * (barW + gap);
192
+ const top2 = Math.min(y(value), zero);
193
+ const tall = Math.max(1, Math.abs(y(value) - zero));
194
+ const r = Math.min(4, barW / 2, tall);
195
+ const d = value >= 0 ? `M${x},${top2 + tall}V${top2 + r}q0,-${r} ${r},-${r}h${barW - 2 * r}q${r},0 ${r},${r}V${top2 + tall}z` : `M${x},${top2}V${top2 + tall - r}q0,${r} ${r},${r}h${barW - 2 * r}q${r},0 ${r},-${r}V${top2}z`;
196
+ parts.push(/* @__PURE__ */ jsx("path", {
197
+ className: "dvui-chart-mark",
198
+ "data-dim": active !== null && active !== at ? "" : void 0,
199
+ d,
200
+ style: { fill: item.color }
201
+ }, `b${at}-${row}`));
202
+ }));
203
+ } else {
204
+ const xs = categories.map((_, at) => plot.x + band * at + band / 2);
205
+ visible.forEach((item, row) => {
206
+ const line = item.values.map((value, at) => `${at === 0 ? "M" : "L"}${xs[at]},${y(value)}`).join("");
207
+ if (kind === "area") {
208
+ const base = y(Math.max(lo, Math.min(0, hi)));
209
+ parts.push(/* @__PURE__ */ jsx("path", {
210
+ className: "dvui-chart-area",
211
+ d: `${line}L${xs.at(-1)},${base}L${xs[0]},${base}z`,
212
+ style: { fill: item.color }
213
+ }, `a${row}`));
214
+ }
215
+ parts.push(/* @__PURE__ */ jsx("path", {
216
+ className: "dvui-chart-line",
217
+ d: line,
218
+ style: { stroke: item.color }
219
+ }, `l${row}`));
220
+ item.values.forEach((value, at) => parts.push(/* @__PURE__ */ jsx("circle", {
221
+ className: "dvui-chart-dot",
222
+ cx: xs[at],
223
+ cy: y(value),
224
+ r: active === at ? 4 : 2.5,
225
+ style: { fill: item.color }
226
+ }, `p${row}-${at}`)));
227
+ });
228
+ if (active !== null) parts.unshift(/* @__PURE__ */ jsx("line", {
229
+ className: "dvui-chart-guide",
230
+ x1: xs[active],
231
+ x2: xs[active],
232
+ y1: plot.y,
233
+ y2: plot.y + plot.h
234
+ }, "guide"));
235
+ }
236
+ return {
237
+ parts,
238
+ height,
239
+ points,
240
+ hits: categories.map((_, at) => ({
241
+ x: plot.x + band * at,
242
+ y: plot.y,
243
+ w: band,
244
+ h: plot.h
245
+ }))
246
+ };
247
+ }
248
+ function rows({ categories, series, unit }, width, active, shown) {
249
+ const visible = series.filter((_, index) => shown[index]);
250
+ const ticks = valueAxis(visible.flatMap((item) => item.values), true);
251
+ const lo = ticks[0];
252
+ const hi = ticks.at(-1);
253
+ const labelW = Math.min(width * .38, Math.ceil(Math.max(...categories.map((label) => textWidth(label, LABEL)))) + 10);
254
+ const valueW = Math.ceil(Math.max(...visible.flatMap((item) => item.values).map((value) => textWidth(withUnit$1(value, unit), LABEL)))) + 10;
255
+ const barH = 14;
256
+ const rowH = 10 + visible.length * barH + (visible.length - 1) * 2;
257
+ const top = 4;
258
+ const height = top + rowH * categories.length + 22;
259
+ const plot = {
260
+ x: labelW,
261
+ y: top,
262
+ w: Math.max(40, width - labelW - valueW),
263
+ h: rowH * categories.length
264
+ };
265
+ const x = (value) => plot.x + (value - lo) / (hi - lo) * plot.w;
266
+ const parts = [];
267
+ for (const tick of ticks) {
268
+ parts.push(/* @__PURE__ */ jsx("line", {
269
+ className: "dvui-chart-grid",
270
+ x1: x(tick),
271
+ x2: x(tick),
272
+ y1: plot.y,
273
+ y2: plot.y + plot.h
274
+ }, `g${tick}`));
275
+ parts.push(/* @__PURE__ */ jsx("text", {
276
+ className: "dvui-chart-tick",
277
+ x: x(tick),
278
+ y: height - 6,
279
+ textAnchor: "middle",
280
+ children: withUnit$1(tick, unit)
281
+ }, `t${tick}`));
282
+ }
283
+ const points = [];
284
+ const zero = x(Math.max(lo, Math.min(0, hi)));
285
+ categories.forEach((label, at) => {
286
+ const rowY = plot.y + rowH * at + 5;
287
+ parts.push(/* @__PURE__ */ jsx("text", {
288
+ className: "dvui-chart-label",
289
+ x: labelW - 10,
290
+ y: rowY + (rowH - 10) / 2 + 4,
291
+ textAnchor: "end",
292
+ children: fit(label, labelW - 12, LABEL)
293
+ }, `c${at}`));
294
+ visible.forEach((item, row) => {
295
+ const value = item.values[at];
296
+ const by = rowY + row * 16;
297
+ const left = Math.min(x(value), zero);
298
+ const long = Math.max(1, Math.abs(x(value) - zero));
299
+ parts.push(/* @__PURE__ */ jsx("rect", {
300
+ className: "dvui-chart-mark",
301
+ "data-dim": active !== null && active !== at ? "" : void 0,
302
+ x: left,
303
+ y: by,
304
+ width: long,
305
+ height: barH,
306
+ rx: Math.min(4, long / 2),
307
+ style: { fill: item.color }
308
+ }, `b${at}-${row}`));
309
+ parts.push(/* @__PURE__ */ jsx("text", {
310
+ className: "dvui-chart-value",
311
+ x: left + long + 6,
312
+ y: by + barH - 3,
313
+ children: withUnit$1(value, unit)
314
+ }, `v${at}-${row}`));
315
+ });
316
+ points.push({
317
+ x: Math.max(...visible.map((item) => x(item.values[at])), zero),
318
+ y: rowY
319
+ });
320
+ });
321
+ return {
322
+ parts,
323
+ height,
324
+ points,
325
+ hits: categories.map((_, at) => ({
326
+ x: 0,
327
+ y: plot.y + rowH * at,
328
+ w: width,
329
+ h: rowH
330
+ }))
331
+ };
332
+ }
333
+ function donut({ categories, series }, width, active, hover) {
334
+ const values = (series[0]?.values ?? []).map((value) => Math.max(0, value));
335
+ const total = values.reduce((sum, value) => sum + value, 0);
336
+ const size = Math.min(180, Math.max(120, width * .42));
337
+ const r = size / 2;
338
+ const inner = r * .62;
339
+ const cx = r;
340
+ const cy = r;
341
+ const parts = [];
342
+ const points = [];
343
+ let angle = -Math.PI / 2;
344
+ values.forEach((value, at) => {
345
+ const sweep = total > 0 ? value / total * Math.PI * 2 : 0;
346
+ const end = angle + sweep;
347
+ const mid = angle + sweep / 2;
348
+ points.push({
349
+ x: cx + Math.cos(mid) * (r - (r - inner) / 2),
350
+ y: cy + Math.sin(mid) * (r - (r - inner) / 2)
351
+ });
352
+ if (sweep > 0) {
353
+ const large = sweep > Math.PI ? 1 : 0;
354
+ const full = sweep >= Math.PI * 2 - 1e-6;
355
+ const p = (radius, theta) => `${cx + Math.cos(theta) * radius},${cy + Math.sin(theta) * radius}`;
356
+ const d = full ? `M${p(r, angle)}A${r},${r} 0 1 1 ${p(r, angle + Math.PI)}A${r},${r} 0 1 1 ${p(r, angle)}M${p(inner, angle)}A${inner},${inner} 0 1 0 ${p(inner, angle + Math.PI)}A${inner},${inner} 0 1 0 ${p(inner, angle)}z` : `M${p(r, angle)}A${r},${r} 0 ${large} 1 ${p(r, end)}L${p(inner, end)}A${inner},${inner} 0 ${large} 0 ${p(inner, angle)}z`;
357
+ parts.push(/* @__PURE__ */ jsx("path", {
358
+ className: "dvui-chart-mark",
359
+ "data-dim": active !== null && active !== at ? "" : void 0,
360
+ d,
361
+ fillRule: "evenodd",
362
+ style: { fill: colorAt(at) },
363
+ onPointerEnter: () => hover(at)
364
+ }, `s${at}`));
365
+ }
366
+ angle = end;
367
+ });
368
+ return {
369
+ parts,
370
+ height: size,
371
+ size,
372
+ points,
373
+ total,
374
+ values,
375
+ hits: []
376
+ };
377
+ }
378
+ function useWidth(box, initial) {
379
+ const [width, setWidth] = useState(initial);
380
+ useLayoutEffect(() => {
381
+ const node = box.current;
382
+ if (!node || typeof ResizeObserver !== "function") return void 0;
383
+ const measure = () => {
384
+ const next = Math.round(node.clientWidth);
385
+ if (next > 0) setWidth(next);
386
+ };
387
+ measure();
388
+ const observer = new ResizeObserver(measure);
389
+ observer.observe(node);
390
+ return () => observer.disconnect();
391
+ }, [box]);
392
+ return width;
393
+ }
394
+ function ChartTable({ data, t = english }) {
395
+ const { categories, series, unit } = data;
396
+ const heads = series.map((item, index) => item.name || (series.length === 1 ? unit || t("Value") : `${t("Series")} ${index + 1}`));
397
+ return /* @__PURE__ */ jsx("div", {
398
+ className: "dvui-table",
399
+ children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [/* @__PURE__ */ jsx("th", { scope: "col" }), heads.map((head, index) => /* @__PURE__ */ jsx("th", {
400
+ scope: "col",
401
+ className: "dvui-num",
402
+ children: head
403
+ }, index))] }) }), /* @__PURE__ */ jsx("tbody", { children: categories.map((label, at) => /* @__PURE__ */ jsxs("tr", { children: [/* @__PURE__ */ jsx("th", {
404
+ scope: "row",
405
+ children: label
406
+ }), series.map((item, index) => /* @__PURE__ */ jsx("td", {
407
+ className: "dvui-num",
408
+ children: withUnit$1(item.values[at], series.length === 1 && !item.name ? "" : unit)
409
+ }, index))] }, `${at}-${label}`)) })] })
410
+ });
411
+ }
412
+ function WidgetChart({ node, lookup, registerChart, refreshCharts, t = english }) {
413
+ const data = chartData(node, lookup);
414
+ const { kind, categories, series, unit } = data;
415
+ const box = useRef(null);
416
+ const width = useWidth(box, 520);
417
+ const [active, setActive] = useState(null);
418
+ const [hidden, setHidden] = useState([]);
419
+ const [asTable, setAsTable] = useState(false);
420
+ const [copied, setCopied] = useState(false);
421
+ const labelId = useId();
422
+ const menuKey = useId();
423
+ const table = useRef(asTable);
424
+ table.current = asTable;
425
+ const shown = series.map((_, index) => !hidden.includes(index));
426
+ const label = typeof node.caption === "string" && node.caption !== "" ? node.caption : t("Chart");
427
+ const isDonut = kind === "donut";
428
+ const legendBeside = isDonut && width >= 360;
429
+ const drawn = isDonut ? donut(data, legendBeside ? width - 200 : width, active, setActive) : kind === "hbar" ? rows(data, width, active, shown) : columns(data, width, active, shown);
430
+ const svgWidth = isDonut ? drawn.size : width;
431
+ const move = (event) => {
432
+ const keys = {
433
+ ArrowRight: 1,
434
+ ArrowDown: 1,
435
+ ArrowLeft: -1,
436
+ ArrowUp: -1
437
+ };
438
+ if (event.key === "Escape") {
439
+ setActive(null);
440
+ return;
441
+ }
442
+ if (keys[event.key] === void 0) return;
443
+ event.preventDefault();
444
+ setActive((current) => current === null ? 0 : (current + keys[event.key] + categories.length) % categories.length);
445
+ };
446
+ const point = active === null ? null : drawn.points[active];
447
+ const share = (at) => unit === "%" ? withUnit$1(drawn.values[at], unit) : `${withUnit$1(drawn.values[at], unit)} · ${drawn.total > 0 ? Math.round(drawn.values[at] / drawn.total * 100) : 0}%`;
448
+ const tipRows = active === null || isDonut ? [] : series.filter((_, index) => shown[index]).map((item) => ({
449
+ color: item.color,
450
+ name: item.name,
451
+ value: withUnit$1(item.values[active], unit)
452
+ }));
453
+ const copy = () => {
454
+ navigator.clipboard?.writeText(chartCsv(data, t)).then(() => {
455
+ setCopied(true);
456
+ setTimeout(() => setCopied(false), 1200);
457
+ }).catch(() => {});
458
+ };
459
+ const toggleTable = () => {
460
+ table.current = !table.current;
461
+ setAsTable(table.current);
462
+ };
463
+ const action = useRef({
464
+ label: null,
465
+ asTable: false,
466
+ toggleTable,
467
+ copy
468
+ });
469
+ action.current = {
470
+ label: typeof node.caption === "string" && node.caption !== "" ? node.caption : null,
471
+ get asTable() {
472
+ return table.current;
473
+ },
474
+ toggleTable,
475
+ copy
476
+ };
477
+ useLayoutEffect(() => registerChart?.(menuKey, {
478
+ get label() {
479
+ return action.current.label;
480
+ },
481
+ get asTable() {
482
+ return action.current.asTable;
483
+ },
484
+ toggleTable: () => action.current.toggleTable(),
485
+ copy: () => action.current.copy()
486
+ }), [registerChart, menuKey]);
487
+ useLayoutEffect(() => {
488
+ refreshCharts?.();
489
+ }, [
490
+ refreshCharts,
491
+ asTable,
492
+ label
493
+ ]);
494
+ const legend = isDonut ? /* @__PURE__ */ jsx("div", {
495
+ className: "dvui-chart-legend dvui-chart-legend-list",
496
+ children: categories.map((name, at) => /* @__PURE__ */ jsxs("div", {
497
+ className: "dvui-chart-key",
498
+ "data-dim": active !== null && active !== at ? "" : void 0,
499
+ onPointerEnter: () => setActive(at),
500
+ onPointerLeave: () => setActive(null),
501
+ children: [
502
+ /* @__PURE__ */ jsx("i", { style: { background: colorAt(at) } }),
503
+ /* @__PURE__ */ jsx("span", {
504
+ className: "dvui-chart-key-name",
505
+ children: name
506
+ }),
507
+ /* @__PURE__ */ jsx("span", {
508
+ className: "dvui-chart-key-value",
509
+ children: share(at)
510
+ })
511
+ ]
512
+ }, `${at}-${name}`))
513
+ }) : series.length > 1 ? /* @__PURE__ */ jsx("div", {
514
+ className: "dvui-chart-legend",
515
+ children: series.map((item, index) => /* @__PURE__ */ jsxs("button", {
516
+ type: "button",
517
+ className: "dvui-chart-key",
518
+ "aria-pressed": shown[index],
519
+ onClick: () => setHidden((before) => before.includes(index) ? before.filter((item) => item !== index) : shown.filter(Boolean).length > 1 ? [...before, index] : before),
520
+ children: [/* @__PURE__ */ jsx("i", { style: { background: item.color } }), item.name || `${t("Series")} ${index + 1}`]
521
+ }, `${index}-${item.name}`))
522
+ }) : null;
523
+ const plot = /* @__PURE__ */ jsxs("div", {
524
+ className: "dvui-chart-plot",
525
+ style: isDonut ? { width: svgWidth } : void 0,
526
+ children: [/* @__PURE__ */ jsxs("svg", {
527
+ className: "dvui-chart-svg",
528
+ width: svgWidth,
529
+ height: drawn.height,
530
+ viewBox: `0 0 ${svgWidth} ${drawn.height}`,
531
+ role: "img",
532
+ "aria-labelledby": labelId,
533
+ tabIndex: 0,
534
+ onKeyDown: move,
535
+ onBlur: () => setActive(null),
536
+ onPointerLeave: () => setActive(null),
537
+ children: [drawn.parts, isDonut ? active === null ? /* @__PURE__ */ jsx("text", {
538
+ className: "dvui-chart-center",
539
+ x: svgWidth / 2,
540
+ y: drawn.height / 2 + 5,
541
+ textAnchor: "middle",
542
+ children: unit === "%" ? "" : withUnit$1(drawn.total, unit)
543
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("text", {
544
+ className: "dvui-chart-center-name",
545
+ x: svgWidth / 2,
546
+ y: drawn.height / 2 - 4,
547
+ textAnchor: "middle",
548
+ children: fit(categories[active], drawn.size * .56, LABEL)
549
+ }), /* @__PURE__ */ jsx("text", {
550
+ className: "dvui-chart-center",
551
+ x: svgWidth / 2,
552
+ y: drawn.height / 2 + 14,
553
+ textAnchor: "middle",
554
+ children: share(active)
555
+ })] }) : drawn.hits.map((hit, at) => /* @__PURE__ */ jsx("rect", {
556
+ className: "dvui-chart-hit",
557
+ x: hit.x,
558
+ y: hit.y,
559
+ width: hit.w,
560
+ height: hit.h,
561
+ onPointerEnter: () => setActive(at)
562
+ }, `h${at}`))]
563
+ }), point && !asTable && !isDonut ? /* @__PURE__ */ jsxs("div", {
564
+ className: "dvui-chart-tip",
565
+ role: "status",
566
+ style: {
567
+ left: point.x,
568
+ top: point.y
569
+ },
570
+ children: [/* @__PURE__ */ jsx("b", { children: categories[active] }), tipRows.map((row, index) => /* @__PURE__ */ jsxs("span", { children: [
571
+ /* @__PURE__ */ jsx("i", { style: { background: row.color } }),
572
+ row.name ? `${row.name} ` : "",
573
+ row.value
574
+ ] }, index))]
575
+ }) : null]
576
+ });
577
+ return /* @__PURE__ */ jsxs("figure", {
578
+ ref: box,
579
+ className: "dvui-chart",
580
+ "data-kind": kind,
581
+ children: [
582
+ /* @__PURE__ */ jsx("span", {
583
+ id: labelId,
584
+ hidden: true,
585
+ children: label
586
+ }),
587
+ asTable ? /* @__PURE__ */ jsx(ChartTable, {
588
+ data,
589
+ t
590
+ }) : isDonut ? /* @__PURE__ */ jsxs("div", {
591
+ className: legendBeside ? "dvui-chart-donut dvui-chart-donut-beside" : "dvui-chart-donut",
592
+ onPointerLeave: () => setActive(null),
593
+ children: [plot, legend]
594
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [legend, plot] }),
595
+ copied ? /* @__PURE__ */ jsx("span", {
596
+ className: "dvui-chart-copied",
597
+ role: "status",
598
+ children: t("Copied")
599
+ }) : null,
600
+ typeof node.caption === "string" && node.caption !== "" ? /* @__PURE__ */ jsx("figcaption", {
601
+ className: "dvui-caption",
602
+ children: node.caption
603
+ }) : null
604
+ ]
605
+ });
606
+ }
607
+ //#endregion
608
+ //#region src/widget.tsx
609
+ const TONES = [
610
+ "good",
611
+ "warn",
612
+ "bad"
613
+ ];
614
+ const CALLOUT_TONES = [
615
+ "info",
616
+ "good",
617
+ "warn",
618
+ "bad"
619
+ ];
620
+ const optionsOf = (node) => Array.isArray(node.options) ? node.options : [];
621
+ const fieldsOf = (node) => Array.isArray(node.fields) ? node.fields : [];
622
+ const cellText = (value) => value === null || value === void 0 ? "" : typeof value === "object" ? JSON.stringify(value) : asText(value);
623
+ const NUMERIC_CELL = /^[-+]?[\d.,]+\s?(%|[a-zA-Z]{1,4})?$/;
624
+ function hasType(node, types) {
625
+ return types.has(node.type) || Array.isArray(node.children) && node.children.some((child) => hasType(child, types));
626
+ }
627
+ const answerValues = (answer, node) => answer != null && answer.action === node.id ? answer.values : void 0;
628
+ const withUnit = (value, unit) => `${asText(value)}${unit ? ` ${unit}` : ""}`;
629
+ const grouped = (value) => typeof value === "number" && Number.isFinite(value) ? value.toLocaleString("en-US", { maximumFractionDigits: 6 }) : asText(value);
630
+ function FallbackMarkdown({ text }) {
631
+ return /* @__PURE__ */ jsx("span", { children: text });
632
+ }
633
+ function OptionsInput({ node, ctx, t }) {
634
+ const [picked, setPicked] = useState([]);
635
+ const options = optionsOf(node);
636
+ const multi = node.multi === true;
637
+ const choose = (label) => {
638
+ if (ctx.answerLocked) return;
639
+ if (multi) setPicked((current) => current.includes(label) ? current.filter((item) => item !== label) : [...current, label]);
640
+ else ctx.onSubmit?.(node.id, { selected: [label] });
641
+ };
642
+ const onKey = (event) => {
643
+ const index = event.key.toUpperCase().charCodeAt(0) - 65;
644
+ if (index >= 0 && index < options.length && !event.ctrlKey && !event.metaKey && !event.altKey) {
645
+ event.preventDefault();
646
+ choose(options[index].label);
647
+ }
648
+ };
649
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
650
+ className: "dvui-options",
651
+ role: "group",
652
+ "aria-label": t("Options"),
653
+ onKeyDown: onKey,
654
+ children: options.map((option, index) => {
655
+ const selected = multi && picked.includes(option.label);
656
+ return /* @__PURE__ */ jsxs("button", {
657
+ type: "button",
658
+ className: "dvui-option",
659
+ disabled: ctx.answerLocked,
660
+ "aria-pressed": multi ? selected : void 0,
661
+ "aria-keyshortcuts": keyLetter(index).toLowerCase(),
662
+ onClick: () => choose(option.label),
663
+ children: [
664
+ /* @__PURE__ */ jsx("span", {
665
+ className: "dvui-key",
666
+ "aria-hidden": "true",
667
+ children: keyLetter(index)
668
+ }),
669
+ /* @__PURE__ */ jsxs("span", {
670
+ className: "dvui-option-body",
671
+ children: [/* @__PURE__ */ jsx("span", {
672
+ className: "dvui-option-label",
673
+ children: option.label
674
+ }), option.description ? /* @__PURE__ */ jsx("span", {
675
+ className: "dvui-option-desc",
676
+ children: option.description
677
+ }) : null]
678
+ }),
679
+ selected ? /* @__PURE__ */ jsx("span", {
680
+ className: "dvui-check",
681
+ children: /* @__PURE__ */ jsx(CheckIcon, {})
682
+ }) : null
683
+ ]
684
+ }, option.label);
685
+ })
686
+ }), multi ? /* @__PURE__ */ jsx("div", {
687
+ className: "dvui-actions",
688
+ children: /* @__PURE__ */ jsx("button", {
689
+ type: "button",
690
+ className: "dvui-submit",
691
+ disabled: ctx.answerLocked || picked.length === 0,
692
+ onClick: () => ctx.onSubmit?.(node.id, { selected: picked }),
693
+ children: t("Submit")
694
+ })
695
+ }) : null] });
696
+ }
697
+ function SettledOptions({ node, answer, t }) {
698
+ const selected = answerValues(answer, node)?.selected;
699
+ const chosen = Array.isArray(selected) ? selected : [];
700
+ return /* @__PURE__ */ jsx("div", {
701
+ className: "dvui-options",
702
+ role: "group",
703
+ "aria-label": t("Your answer"),
704
+ children: optionsOf(node).map((option) => /* @__PURE__ */ jsxs("div", {
705
+ className: "dvui-option",
706
+ children: [/* @__PURE__ */ jsxs("span", {
707
+ className: "dvui-option-body",
708
+ children: [/* @__PURE__ */ jsx("span", {
709
+ className: "dvui-option-label",
710
+ children: option.label
711
+ }), option.description ? /* @__PURE__ */ jsx("span", {
712
+ className: "dvui-option-desc",
713
+ children: option.description
714
+ }) : null]
715
+ }), chosen.includes(option.label) ? /* @__PURE__ */ jsx("span", {
716
+ className: "dvui-check",
717
+ role: "img",
718
+ "aria-label": t("Selected"),
719
+ children: /* @__PURE__ */ jsx(CheckIcon, {})
720
+ }) : null]
721
+ }, option.label))
722
+ });
723
+ }
724
+ function ConfirmInput({ node, ctx, t }) {
725
+ return /* @__PURE__ */ jsxs("div", {
726
+ className: "dvui-actions",
727
+ children: [/* @__PURE__ */ jsx("button", {
728
+ type: "button",
729
+ className: "dvui-button",
730
+ disabled: ctx.answerLocked,
731
+ onClick: () => ctx.onSubmit?.(node.id, { confirmed: false }),
732
+ children: node.cancelLabel ?? t("Cancel")
733
+ }), /* @__PURE__ */ jsx("button", {
734
+ type: "button",
735
+ className: "dvui-submit",
736
+ disabled: ctx.answerLocked,
737
+ onClick: () => ctx.onSubmit?.(node.id, { confirmed: true }),
738
+ children: node.confirmLabel ?? t("Confirm")
739
+ })]
740
+ });
741
+ }
742
+ function SettledConfirm({ node, answer, t }) {
743
+ const confirmed = answerValues(answer, node)?.confirmed;
744
+ if (typeof confirmed !== "boolean") return /* @__PURE__ */ jsx(ConfirmInput, {
745
+ node,
746
+ ctx: { answerLocked: true },
747
+ t
748
+ });
749
+ return /* @__PURE__ */ jsx("span", {
750
+ className: "dvui-badge",
751
+ children: confirmed ? node.confirmLabel ?? t("Confirm") : node.cancelLabel ?? t("Cancel")
752
+ });
753
+ }
754
+ function FormInput({ node, ctx, t }) {
755
+ const fields = fieldsOf(node);
756
+ const [draft, setDraft] = useState({});
757
+ const submit = () => {
758
+ if (!ctx.answerLocked) ctx.onSubmit?.(node.id, { fields: Object.fromEntries(fields.map((field) => [field.id, draft[field.id] ?? ""])) });
759
+ };
760
+ return /* @__PURE__ */ jsxs("form", {
761
+ className: "dvui-form",
762
+ onSubmit: (event) => {
763
+ event.preventDefault();
764
+ submit();
765
+ },
766
+ children: [fields.map((field) => /* @__PURE__ */ jsxs("label", {
767
+ className: "dvui-field",
768
+ children: [/* @__PURE__ */ jsx("span", {
769
+ className: "dvui-field-label",
770
+ children: field.label
771
+ }), /* @__PURE__ */ jsx("input", {
772
+ className: "dvui-input",
773
+ type: "text",
774
+ value: draft[field.id] ?? "",
775
+ autoComplete: "off",
776
+ spellCheck: false,
777
+ readOnly: ctx.answerLocked,
778
+ "aria-label": field.label,
779
+ placeholder: field.placeholder,
780
+ onChange: (event) => setDraft((current) => ({
781
+ ...current,
782
+ [field.id]: event.target.value
783
+ }))
784
+ })]
785
+ }, field.id)), /* @__PURE__ */ jsx("div", {
786
+ className: "dvui-actions",
787
+ children: /* @__PURE__ */ jsx("button", {
788
+ type: "submit",
789
+ className: "dvui-submit",
790
+ disabled: ctx.answerLocked,
791
+ children: node.submitLabel ?? t("Submit")
792
+ })
793
+ })]
794
+ });
795
+ }
796
+ function SettledForm({ node, answer }) {
797
+ const fields = answerValues(answer, node)?.fields;
798
+ const values = fields !== null && typeof fields === "object" ? fields : {};
799
+ return /* @__PURE__ */ jsx("div", {
800
+ className: "dvui-form",
801
+ children: fieldsOf(node).map((field) => /* @__PURE__ */ jsxs("label", {
802
+ className: "dvui-field",
803
+ children: [/* @__PURE__ */ jsx("span", {
804
+ className: "dvui-field-label",
805
+ children: field.label
806
+ }), /* @__PURE__ */ jsx("input", {
807
+ className: "dvui-input",
808
+ value: String(values[field.id] ?? ""),
809
+ disabled: true,
810
+ readOnly: true,
811
+ "aria-label": field.label
812
+ })]
813
+ }, field.id))
814
+ });
815
+ }
816
+ function WidgetTable({ node, lookup }) {
817
+ const columns = Array.isArray(node.columns) ? node.columns : [];
818
+ const rows = (Array.isArray(node.rows) ? node.rows : []).map((row) => (Array.isArray(row) ? row : []).map((cell) => resolve(cell, lookup, "")));
819
+ const numeric = columns.map((_, index) => {
820
+ const cells = rows.map((row) => row[index]).filter((cell) => cell !== void 0 && cell !== "");
821
+ return cells.length > 0 && cells.every((cell) => typeof cell === "number" || typeof cell === "string" && NUMERIC_CELL.test(cell.trim()));
822
+ });
823
+ return /* @__PURE__ */ jsx("div", {
824
+ className: "dvui-table",
825
+ children: /* @__PURE__ */ jsxs("table", { children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", { children: columns.map((column, index) => /* @__PURE__ */ jsx("th", {
826
+ scope: "col",
827
+ className: numeric[index] ? "dvui-num" : void 0,
828
+ children: column.label
829
+ }, column.key ?? index)) }) }), /* @__PURE__ */ jsx("tbody", { children: rows.map((row, rowIndex) => /* @__PURE__ */ jsx("tr", { children: columns.map((column, index) => /* @__PURE__ */ jsx("td", {
830
+ className: numeric[index] ? "dvui-num" : void 0,
831
+ children: cellText(row[index])
832
+ }, column.key ?? index)) }, rowIndex)) })] })
833
+ });
834
+ }
835
+ function WidgetProgress({ node, lookup, t }) {
836
+ let percent;
837
+ if (node.percent !== void 0) percent = Number(resolve(node.percent, lookup, 0));
838
+ else {
839
+ const max = Number(resolve(node.max, lookup, 0));
840
+ percent = max > 0 ? Number(resolve(node.value, lookup, 0)) / max * 100 : 0;
841
+ }
842
+ percent = Math.max(0, Math.min(100, Number.isFinite(percent) ? percent : 0));
843
+ const tone = resolve(node.tone, lookup);
844
+ const right = node.valueLabel !== void 0 ? asText(resolve(node.valueLabel, lookup, "")) : `${Math.round(percent)}%`;
845
+ return /* @__PURE__ */ jsxs("div", {
846
+ className: "dvui-progress",
847
+ "data-tone": TONES.includes(String(tone)) ? String(tone) : void 0,
848
+ children: [/* @__PURE__ */ jsxs("span", {
849
+ className: "dvui-progress-label",
850
+ children: [/* @__PURE__ */ jsx("span", { children: node.label }), /* @__PURE__ */ jsx("span", { children: right })]
851
+ }), /* @__PURE__ */ jsx("div", {
852
+ className: "dvui-progress-track",
853
+ role: "progressbar",
854
+ "aria-valuenow": Math.round(percent),
855
+ "aria-valuemin": 0,
856
+ "aria-valuemax": 100,
857
+ "aria-label": node.label ?? t("Progress"),
858
+ "aria-valuetext": right,
859
+ children: /* @__PURE__ */ jsx("div", {
860
+ className: "dvui-progress-fill",
861
+ style: { width: `${percent}%` }
862
+ })
863
+ })]
864
+ });
865
+ }
866
+ function WidgetMetric({ node, lookup }) {
867
+ const tone = resolve(node.tone, lookup);
868
+ const caption = node.caption === void 0 ? "" : asText(resolve(node.caption, lookup, ""));
869
+ return /* @__PURE__ */ jsxs("div", {
870
+ className: "dvui-metric",
871
+ "data-tone": TONES.includes(String(tone)) ? String(tone) : void 0,
872
+ children: [
873
+ /* @__PURE__ */ jsx("span", {
874
+ className: "dvui-metric-label",
875
+ children: node.label
876
+ }),
877
+ /* @__PURE__ */ jsx("span", {
878
+ className: "dvui-metric-value",
879
+ children: asText(resolve(node.value, lookup, "—"))
880
+ }),
881
+ caption !== "" ? /* @__PURE__ */ jsx("span", {
882
+ className: "dvui-caption",
883
+ children: caption
884
+ }) : null
885
+ ]
886
+ });
887
+ }
888
+ function WidgetCallout({ node, lookup, Markdown, labels }) {
889
+ const tone = resolve(node.tone, lookup);
890
+ return /* @__PURE__ */ jsxs("div", {
891
+ className: "dvui-callout",
892
+ "data-tone": CALLOUT_TONES.includes(String(tone)) ? String(tone) : "info",
893
+ children: [/* @__PURE__ */ jsx("span", {
894
+ className: "dvui-callout-icon",
895
+ "aria-hidden": "true",
896
+ children: /* @__PURE__ */ jsx(InfoIcon, {})
897
+ }), /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Markdown, {
898
+ text: asText(resolve(node.text, lookup, "")),
899
+ labels,
900
+ variant: "compact"
901
+ }) })]
902
+ });
903
+ }
904
+ function WidgetSources({ node, t }) {
905
+ return /* @__PURE__ */ jsx("div", {
906
+ className: "dvui-sources",
907
+ role: "list",
908
+ "aria-label": t("Sources"),
909
+ children: (node.items ?? []).map((item, index) => /* @__PURE__ */ jsxs("a", {
910
+ role: "listitem",
911
+ className: "dvui-source",
912
+ href: item.url,
913
+ target: "_blank",
914
+ rel: "noopener noreferrer",
915
+ title: item.url,
916
+ children: [/* @__PURE__ */ jsx(LinkIcon, {}), item.label]
917
+ }, `${item.url}-${index}`))
918
+ });
919
+ }
920
+ function InputHead({ id, label, value }) {
921
+ return /* @__PURE__ */ jsxs("div", {
922
+ className: "dvui-input-head",
923
+ children: [/* @__PURE__ */ jsx("label", {
924
+ htmlFor: id,
925
+ className: "dvui-input-label",
926
+ children: label
927
+ }), value !== void 0 ? /* @__PURE__ */ jsx("output", {
928
+ htmlFor: id,
929
+ className: "dvui-input-value",
930
+ children: value
931
+ }) : null]
932
+ });
933
+ }
934
+ const numericBound = (bound) => typeof bound === "number" && Number.isFinite(bound) ? bound : typeof bound === "string" && bound.trim() !== "" && Number.isFinite(Number(bound)) ? Number(bound) : void 0;
935
+ function SliderInput({ node, ctx }) {
936
+ const id = useId();
937
+ const value = Number(ctx.values[node.id]);
938
+ const min = numericBound(resolve(node.min, ctx.lookup, void 0));
939
+ const max = numericBound(resolve(node.max, ctx.lookup, void 0));
940
+ const percent = min !== void 0 && max !== void 0 && max > min ? (value - min) / (max - min) * 100 : 0;
941
+ return /* @__PURE__ */ jsxs("div", {
942
+ className: "dvui-input-row",
943
+ children: [/* @__PURE__ */ jsx(InputHead, {
944
+ id,
945
+ label: node.label,
946
+ value: withUnit(grouped(value), node.unit)
947
+ }), /* @__PURE__ */ jsx("input", {
948
+ id,
949
+ type: "range",
950
+ className: "dvui-range",
951
+ min,
952
+ max,
953
+ step: node.step ?? "any",
954
+ value,
955
+ disabled: ctx.locked,
956
+ style: { "--_dvui-fill": `${percent}%` },
957
+ onChange: (event) => ctx.setInput(node.id, Number(event.target.value))
958
+ })]
959
+ });
960
+ }
961
+ function NumberInput({ node, ctx }) {
962
+ const id = useId();
963
+ const [draft, setDraft] = useState(null);
964
+ const value = ctx.values[node.id];
965
+ const min = numericBound(resolve(node.min, ctx.lookup, void 0));
966
+ const max = numericBound(resolve(node.max, ctx.lookup, void 0));
967
+ return /* @__PURE__ */ jsxs("div", {
968
+ className: "dvui-input-row",
969
+ children: [/* @__PURE__ */ jsx(InputHead, {
970
+ id,
971
+ label: node.label
972
+ }), /* @__PURE__ */ jsxs("span", {
973
+ className: "dvui-number",
974
+ children: [/* @__PURE__ */ jsx("input", {
975
+ id,
976
+ className: "dvui-input",
977
+ type: "number",
978
+ inputMode: "decimal",
979
+ value: draft ?? asText(value),
980
+ min,
981
+ max,
982
+ step: node.step ?? "any",
983
+ disabled: ctx.locked,
984
+ onChange: (event) => {
985
+ const raw = event.target.value;
986
+ setDraft(raw);
987
+ const next = Number(raw);
988
+ if (raw.trim() === "" || !Number.isFinite(next) || min !== void 0 && next < min || max !== void 0 && next > max) return;
989
+ ctx.setInput(node.id, next);
990
+ },
991
+ onBlur: () => setDraft(null)
992
+ }), node.unit ? /* @__PURE__ */ jsx("span", {
993
+ className: "dvui-unit",
994
+ children: node.unit
995
+ }) : null]
996
+ })]
997
+ });
998
+ }
999
+ function ChoiceInput({ node, ctx }) {
1000
+ const labelId = useId();
1001
+ const options = optionsOf(node);
1002
+ const current = ctx.values[node.id];
1003
+ const side = options.length <= 4 && options.every((option) => [...option.label].length <= 14);
1004
+ return /* @__PURE__ */ jsxs("div", {
1005
+ className: "dvui-input-row",
1006
+ children: [/* @__PURE__ */ jsx("span", {
1007
+ className: "dvui-input-label",
1008
+ id: labelId,
1009
+ children: node.label
1010
+ }), side ? /* @__PURE__ */ jsx(Segmented, {
1011
+ label: node.label ?? "",
1012
+ value: options.findIndex((option) => choiceValue(option) === current),
1013
+ className: "dvui-choice-seg",
1014
+ children: options.map((option) => /* @__PURE__ */ jsx("button", {
1015
+ type: "button",
1016
+ "aria-pressed": choiceValue(option) === current,
1017
+ disabled: ctx.locked,
1018
+ onClick: () => ctx.setInput(node.id, choiceValue(option)),
1019
+ children: option.label
1020
+ }, option.label))
1021
+ }) : /* @__PURE__ */ jsx("div", {
1022
+ className: "dvui-radios",
1023
+ role: "radiogroup",
1024
+ "aria-labelledby": labelId,
1025
+ children: options.map((option) => /* @__PURE__ */ jsxs("button", {
1026
+ type: "button",
1027
+ role: "radio",
1028
+ className: "dvui-radio",
1029
+ "aria-checked": choiceValue(option) === current,
1030
+ disabled: ctx.locked,
1031
+ onClick: () => ctx.setInput(node.id, choiceValue(option)),
1032
+ children: [/* @__PURE__ */ jsx("span", {
1033
+ className: "dvui-radio-dot",
1034
+ "aria-hidden": "true"
1035
+ }), option.label]
1036
+ }, option.label))
1037
+ })]
1038
+ });
1039
+ }
1040
+ function ToggleInput({ node, ctx }) {
1041
+ const on = ctx.values[node.id] === true;
1042
+ return /* @__PURE__ */ jsxs("div", {
1043
+ className: "dvui-toggle",
1044
+ children: [/* @__PURE__ */ jsx("span", {
1045
+ className: "dvui-input-label",
1046
+ children: node.label
1047
+ }), /* @__PURE__ */ jsx("button", {
1048
+ type: "button",
1049
+ role: "switch",
1050
+ className: "dvui-switch",
1051
+ "aria-checked": on,
1052
+ "aria-label": node.label,
1053
+ disabled: ctx.locked,
1054
+ onClick: () => ctx.setInput(node.id, !on),
1055
+ children: /* @__PURE__ */ jsx("span", {
1056
+ className: "dvui-switch-knob",
1057
+ "aria-hidden": "true"
1058
+ })
1059
+ })]
1060
+ });
1061
+ }
1062
+ function WidgetNode({ node, ctx, Markdown, labels, t }) {
1063
+ const { lookup } = ctx;
1064
+ switch (node.type) {
1065
+ case "stack":
1066
+ case "row": return /* @__PURE__ */ jsx("div", {
1067
+ className: node.type === "row" ? "dvui-row" : "dvui-stack",
1068
+ children: (node.children ?? []).map((child, index) => /* @__PURE__ */ jsx(WidgetNode, {
1069
+ node: child,
1070
+ ctx,
1071
+ Markdown,
1072
+ labels,
1073
+ t
1074
+ }, child.id ?? `${child.type}-${index}`))
1075
+ });
1076
+ case "text": {
1077
+ const text = /* @__PURE__ */ jsx(Markdown, {
1078
+ text: asText(resolve(node.text, lookup, "")),
1079
+ labels,
1080
+ variant: "compact"
1081
+ });
1082
+ return node.tone === "muted" ? /* @__PURE__ */ jsx("div", {
1083
+ className: "dvui-muted",
1084
+ children: text
1085
+ }) : text;
1086
+ }
1087
+ case "metric": return /* @__PURE__ */ jsx(WidgetMetric, {
1088
+ node,
1089
+ lookup
1090
+ });
1091
+ case "table": return /* @__PURE__ */ jsx(WidgetTable, {
1092
+ node,
1093
+ lookup
1094
+ });
1095
+ case "chart": return /* @__PURE__ */ jsx(WidgetChart, {
1096
+ node,
1097
+ lookup,
1098
+ registerChart: ctx.registerChart,
1099
+ refreshCharts: ctx.refreshCharts,
1100
+ t
1101
+ });
1102
+ case "progress": return /* @__PURE__ */ jsx(WidgetProgress, {
1103
+ node,
1104
+ lookup,
1105
+ t
1106
+ });
1107
+ case "callout": return /* @__PURE__ */ jsx(WidgetCallout, {
1108
+ node,
1109
+ lookup,
1110
+ Markdown,
1111
+ labels
1112
+ });
1113
+ case "sources": return /* @__PURE__ */ jsx(WidgetSources, {
1114
+ node,
1115
+ t
1116
+ });
1117
+ case "slider": return /* @__PURE__ */ jsx(SliderInput, {
1118
+ node,
1119
+ ctx
1120
+ });
1121
+ case "number": return /* @__PURE__ */ jsx(NumberInput, {
1122
+ node,
1123
+ ctx
1124
+ });
1125
+ case "choice": return /* @__PURE__ */ jsx(ChoiceInput, {
1126
+ node,
1127
+ ctx
1128
+ });
1129
+ case "toggle": return /* @__PURE__ */ jsx(ToggleInput, {
1130
+ node,
1131
+ ctx
1132
+ });
1133
+ case "submit": return ctx.settled ? null : /* @__PURE__ */ jsx("div", {
1134
+ className: "dvui-actions",
1135
+ children: /* @__PURE__ */ jsx("button", {
1136
+ type: "button",
1137
+ className: "dvui-submit",
1138
+ disabled: ctx.answerLocked,
1139
+ onClick: () => ctx.onSubmit?.(node.id, {}),
1140
+ children: node.label || t("Send")
1141
+ })
1142
+ });
1143
+ case "options": return ctx.settled ? /* @__PURE__ */ jsx(SettledOptions, {
1144
+ node,
1145
+ answer: ctx.answer,
1146
+ t
1147
+ }) : /* @__PURE__ */ jsx(OptionsInput, {
1148
+ node,
1149
+ ctx,
1150
+ t
1151
+ });
1152
+ case "confirm": return ctx.settled ? /* @__PURE__ */ jsx(SettledConfirm, {
1153
+ node,
1154
+ answer: ctx.answer,
1155
+ t
1156
+ }) : /* @__PURE__ */ jsx(ConfirmInput, {
1157
+ node,
1158
+ ctx,
1159
+ t
1160
+ });
1161
+ case "form": return ctx.settled ? /* @__PURE__ */ jsx(SettledForm, {
1162
+ node,
1163
+ answer: ctx.answer
1164
+ }) : /* @__PURE__ */ jsx(FormInput, {
1165
+ node,
1166
+ ctx,
1167
+ t
1168
+ });
1169
+ default: return null;
1170
+ }
1171
+ }
1172
+ function WidgetView({ view, onSubmit, settled = false, answer = null, busy = false, registerChart, refreshCharts, t = english, labels = {
1173
+ code: {
1174
+ copyLabel: "Copy",
1175
+ copiedLabel: "Copied"
1176
+ },
1177
+ footnotes: "Footnotes"
1178
+ }, Markdown = FallbackMarkdown }) {
1179
+ const inputs = viewInputs(view);
1180
+ const [values, setValues] = useState(() => Object.fromEntries(inputs.map((node) => [node.id, inputDefault(node)])));
1181
+ const sent = answer?.values?.inputs;
1182
+ const current = settled && sent !== null && typeof sent === "object" ? {
1183
+ ...values,
1184
+ ...sent
1185
+ } : values;
1186
+ const submit = (action, extra) => onSubmit?.(action, inputs.length > 0 ? {
1187
+ ...extra,
1188
+ inputs: current
1189
+ } : extra);
1190
+ const ctx = {
1191
+ onSubmit: submit,
1192
+ settled,
1193
+ answer,
1194
+ busy,
1195
+ lookup: viewScope(view, current),
1196
+ values: current,
1197
+ locked: settled || busy,
1198
+ answerLocked: settled || busy || onSubmit === void 0,
1199
+ setInput: (id, value) => setValues((before) => ({
1200
+ ...before,
1201
+ [id]: value
1202
+ })),
1203
+ registerChart,
1204
+ refreshCharts
1205
+ };
1206
+ return /* @__PURE__ */ jsx(WidgetNode, {
1207
+ node: view,
1208
+ ctx,
1209
+ Markdown,
1210
+ labels,
1211
+ t
1212
+ });
1213
+ }
1214
+ function useChartHandles() {
1215
+ const [, bump] = useState(0);
1216
+ const charts = useRef(/* @__PURE__ */ new Map());
1217
+ const refreshCharts = useCallback(() => bump((value) => value + 1), []);
1218
+ const registerChart = useCallback((key, handle) => {
1219
+ charts.current.set(key, handle);
1220
+ refreshCharts();
1221
+ return () => {
1222
+ charts.current.delete(key);
1223
+ refreshCharts();
1224
+ };
1225
+ }, [refreshCharts]);
1226
+ return {
1227
+ charts: [...charts.current.values()],
1228
+ registerChart,
1229
+ refreshCharts
1230
+ };
1231
+ }
1232
+ function ViewMenu({ charts, t }) {
1233
+ const [open, setOpen] = useState(false);
1234
+ const root = useRef(null);
1235
+ useEffect(() => {
1236
+ if (!open) return void 0;
1237
+ const away = (event) => {
1238
+ if (!root.current?.contains(event.target)) setOpen(false);
1239
+ };
1240
+ const key = (event) => {
1241
+ if (event.key === "Escape") setOpen(false);
1242
+ };
1243
+ document.addEventListener("pointerdown", away, true);
1244
+ document.addEventListener("keydown", key);
1245
+ return () => {
1246
+ document.removeEventListener("pointerdown", away, true);
1247
+ document.removeEventListener("keydown", key);
1248
+ };
1249
+ }, [open]);
1250
+ return /* @__PURE__ */ jsxs("div", {
1251
+ ref: root,
1252
+ className: "dvui-tools",
1253
+ "data-open": open ? "" : void 0,
1254
+ children: [/* @__PURE__ */ jsx("button", {
1255
+ type: "button",
1256
+ className: "dvui-more",
1257
+ "aria-label": t("Chart options"),
1258
+ "aria-haspopup": "menu",
1259
+ "aria-expanded": open,
1260
+ onClick: () => setOpen((value) => !value),
1261
+ children: /* @__PURE__ */ jsx(DotsIcon, {})
1262
+ }), open ? /* @__PURE__ */ jsx("div", {
1263
+ className: "dvui-menu",
1264
+ role: "menu",
1265
+ children: charts.map((chart, index) => /* @__PURE__ */ jsxs(Fragment, { children: [
1266
+ charts.length > 1 ? /* @__PURE__ */ jsx("div", {
1267
+ className: "dvui-menu-label",
1268
+ children: chart.label ?? t("Chart {n}", { n: index + 1 })
1269
+ }) : null,
1270
+ /* @__PURE__ */ jsxs("button", {
1271
+ type: "button",
1272
+ role: "menuitem",
1273
+ onClick: () => {
1274
+ setOpen(false);
1275
+ chart.toggleTable();
1276
+ },
1277
+ children: [/* @__PURE__ */ jsx(TableIcon, {}), chart.asTable ? t("View as chart") : t("View as table")]
1278
+ }),
1279
+ /* @__PURE__ */ jsxs("button", {
1280
+ type: "button",
1281
+ role: "menuitem",
1282
+ onClick: () => {
1283
+ setOpen(false);
1284
+ chart.copy();
1285
+ },
1286
+ children: [/* @__PURE__ */ jsx(CopyIcon, {}), t("Copy data")]
1287
+ })
1288
+ ] }, `${chart.label}-${index}`))
1289
+ }) : null]
1290
+ });
1291
+ }
1292
+ function WidgetCard({ title = "", view, onSubmit, onDismiss, busy = false, settled = false, answer = null, t = english, labels, Markdown, localOnly = false }) {
1293
+ const titleId = useId();
1294
+ const { charts, registerChart, refreshCharts } = useChartHandles();
1295
+ const framed = hasType(view, /* @__PURE__ */ new Set([
1296
+ "slider",
1297
+ "number",
1298
+ "choice",
1299
+ "toggle",
1300
+ "options",
1301
+ "confirm",
1302
+ "form",
1303
+ "submit"
1304
+ ]));
1305
+ return /* @__PURE__ */ jsxs("div", {
1306
+ className: `dvui-view ${framed ? "dvui-framed" : "dvui-bare"}${settled ? " dvui-settled" : ""}`,
1307
+ role: "group",
1308
+ "aria-labelledby": title ? titleId : void 0,
1309
+ "aria-label": title ? void 0 : t("View"),
1310
+ "aria-busy": busy || void 0,
1311
+ children: [
1312
+ title || onDismiss || charts.length > 0 ? /* @__PURE__ */ jsxs("div", {
1313
+ className: "dvui-head",
1314
+ children: [
1315
+ /* @__PURE__ */ jsx("p", {
1316
+ className: "dvui-title",
1317
+ id: titleId,
1318
+ children: title
1319
+ }),
1320
+ charts.length > 0 ? /* @__PURE__ */ jsx(ViewMenu, {
1321
+ charts,
1322
+ t
1323
+ }) : null,
1324
+ onDismiss ? /* @__PURE__ */ jsx("button", {
1325
+ type: "button",
1326
+ className: "dvui-dismiss",
1327
+ "aria-label": t("Dismiss view"),
1328
+ onClick: onDismiss,
1329
+ disabled: busy,
1330
+ children: "×"
1331
+ }) : null
1332
+ ]
1333
+ }) : null,
1334
+ /* @__PURE__ */ jsx("div", {
1335
+ className: "dvui-body",
1336
+ children: /* @__PURE__ */ jsx(WidgetView, {
1337
+ view,
1338
+ onSubmit,
1339
+ settled,
1340
+ answer,
1341
+ busy,
1342
+ registerChart,
1343
+ refreshCharts,
1344
+ t,
1345
+ labels,
1346
+ Markdown
1347
+ })
1348
+ }),
1349
+ localOnly && hasType(view, /* @__PURE__ */ new Set([
1350
+ "options",
1351
+ "confirm",
1352
+ "form",
1353
+ "submit"
1354
+ ])) ? /* @__PURE__ */ jsx("p", {
1355
+ className: "dvui-local-note",
1356
+ role: "note",
1357
+ children: t("Local preview only; this card does not send an answer.")
1358
+ }) : null
1359
+ ]
1360
+ });
1361
+ }
1362
+ //#endregion
1363
+ export { WidgetCard, WidgetView };
1364
+
1365
+ //# sourceMappingURL=widget.js.map