@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.
@@ -0,0 +1,3423 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let react = require("react");
3
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
4
+ let react_jsx_runtime = require("react/jsx-runtime");
5
+ //#region src/core/formula.js
6
+ const FUNCTIONS = {
7
+ min: {
8
+ args: [1, Infinity],
9
+ run: (...values) => Math.min(...values.map(num))
10
+ },
11
+ max: {
12
+ args: [1, Infinity],
13
+ run: (...values) => Math.max(...values.map(num))
14
+ },
15
+ abs: {
16
+ args: [1, 1],
17
+ run: (value) => Math.abs(num(value))
18
+ },
19
+ floor: {
20
+ args: [1, 1],
21
+ run: (value) => Math.floor(num(value))
22
+ },
23
+ ceil: {
24
+ args: [1, 1],
25
+ run: (value) => Math.ceil(num(value))
26
+ },
27
+ sqrt: {
28
+ args: [1, 1],
29
+ run: (value) => Math.sqrt(num(value))
30
+ },
31
+ pow: {
32
+ args: [2, 2],
33
+ run: (base, power) => num(base) ** num(power)
34
+ },
35
+ round: {
36
+ args: [1, 2],
37
+ run: (value, digits = 0) => roundTo(num(value), digitsOf(digits))
38
+ },
39
+ clamp: {
40
+ args: [3, 3],
41
+ run: (value, low, high) => Math.min(Math.max(num(value), num(low)), num(high))
42
+ },
43
+ fixed: {
44
+ args: [1, 2],
45
+ run: (value, digits = 0) => roundTo(num(value), digitsOf(digits)).toFixed(digitsOf(digits))
46
+ },
47
+ format: {
48
+ args: [1, 2],
49
+ run: (value, digits = 0) => grouped$1(num(value), digitsOf(digits))
50
+ },
51
+ percent: {
52
+ args: [1, 2],
53
+ run: (value, digits = 0) => `${grouped$1(num(value) * 100, digitsOf(digits))}%`
54
+ }
55
+ };
56
+ const FORMULA_FUNCTIONS = Object.keys(FUNCTIONS);
57
+ const CONSTANTS = /* @__PURE__ */ new Map([["true", true], ["false", false]]);
58
+ const MAX_NESTING = 64;
59
+ var FormulaError = class extends Error {};
60
+ const fail = (message) => {
61
+ throw new FormulaError(message);
62
+ };
63
+ function num(value) {
64
+ if (typeof value === "number") return value;
65
+ if (typeof value === "boolean") return value ? 1 : 0;
66
+ const parsed = Number(value);
67
+ if (typeof value === "string" && value.trim() !== "" && !Number.isNaN(parsed)) return parsed;
68
+ return fail(`${JSON.stringify(value)} is not a number`);
69
+ }
70
+ const digitsOf = (digits) => {
71
+ const value = num(digits);
72
+ if (!Number.isInteger(value) || value < 0 || value > 10) fail("digits must be a whole number from 0 to 10");
73
+ return value;
74
+ };
75
+ const roundTo = (value, digits) => {
76
+ const scale = 10 ** digits;
77
+ return Math.round(value * scale) / scale;
78
+ };
79
+ function grouped$1(value, digits) {
80
+ if (!Number.isFinite(value)) return String(value);
81
+ const [whole, part] = Math.abs(roundTo(value, digits)).toFixed(digits).split(".");
82
+ return `${value < 0 && Number(whole + (part ?? "")) !== 0 ? "-" : ""}${whole.replace(/\B(?=(\d{3})+(?!\d))/g, ",")}${part ? `.${part}` : ""}`;
83
+ }
84
+ const IDENT = /^[A-Za-z_][A-Za-z0-9_]*$/;
85
+ const isName = (value) => typeof value === "string" && IDENT.test(value);
86
+ const OPERATORS = [
87
+ "==",
88
+ "!=",
89
+ "<=",
90
+ ">=",
91
+ "&&",
92
+ "||",
93
+ "+",
94
+ "-",
95
+ "*",
96
+ "/",
97
+ "%",
98
+ "<",
99
+ ">",
100
+ "!",
101
+ "?",
102
+ ":",
103
+ "(",
104
+ ")",
105
+ ","
106
+ ];
107
+ function tokenize(source) {
108
+ const tokens = [];
109
+ let at = 0;
110
+ while (at < source.length) {
111
+ const char = source[at];
112
+ if (/\s/.test(char)) {
113
+ at += 1;
114
+ continue;
115
+ }
116
+ const number = /^(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/.exec(source.slice(at));
117
+ if (number) {
118
+ tokens.push({
119
+ kind: "number",
120
+ value: Number(number[0]),
121
+ at
122
+ });
123
+ at += number[0].length;
124
+ continue;
125
+ }
126
+ const name = /^[A-Za-z_][A-Za-z0-9_]*/.exec(source.slice(at));
127
+ if (name) {
128
+ tokens.push({
129
+ kind: "name",
130
+ value: name[0],
131
+ at
132
+ });
133
+ at += name[0].length;
134
+ continue;
135
+ }
136
+ if (char === "\"" || char === "'") {
137
+ let end = at + 1;
138
+ let text = "";
139
+ while (end < source.length && source[end] !== char) {
140
+ if (source[end] === "\\" && end + 1 < source.length) {
141
+ text += source[end + 1];
142
+ end += 2;
143
+ continue;
144
+ }
145
+ text += source[end];
146
+ end += 1;
147
+ }
148
+ if (end >= source.length) fail(`unclosed string at ${at + 1}`);
149
+ tokens.push({
150
+ kind: "string",
151
+ value: text,
152
+ at
153
+ });
154
+ at = end + 1;
155
+ continue;
156
+ }
157
+ const operator = OPERATORS.find((op) => source.startsWith(op, at));
158
+ if (operator === void 0) fail(`unexpected ${JSON.stringify(char)} at ${at + 1}`);
159
+ tokens.push({
160
+ kind: "op",
161
+ value: operator,
162
+ at
163
+ });
164
+ at += operator.length;
165
+ }
166
+ return tokens;
167
+ }
168
+ const BINARY = [
169
+ ["||"],
170
+ ["&&"],
171
+ ["==", "!="],
172
+ [
173
+ "<",
174
+ "<=",
175
+ ">",
176
+ ">="
177
+ ],
178
+ ["+", "-"],
179
+ [
180
+ "*",
181
+ "/",
182
+ "%"
183
+ ]
184
+ ];
185
+ function parse(source) {
186
+ const tokens = tokenize(source);
187
+ let index = 0;
188
+ let depth = 0;
189
+ const peek = () => tokens[index];
190
+ const isOp = (value) => peek()?.kind === "op" && peek().value === value;
191
+ const expect = (value) => {
192
+ if (!isOp(value)) fail(peek() ? `expected "${value}" at ${peek().at + 1}` : `expected "${value}" at the end`);
193
+ index += 1;
194
+ };
195
+ const nest = (read) => {
196
+ depth += 1;
197
+ if (depth > MAX_NESTING) fail(`nested more than ${MAX_NESTING} levels`);
198
+ const node = read();
199
+ depth -= 1;
200
+ return node;
201
+ };
202
+ const ternary = () => nest(() => {
203
+ const test = binary(0);
204
+ if (!isOp("?")) return test;
205
+ index += 1;
206
+ const yes = ternary();
207
+ expect(":");
208
+ return {
209
+ type: "if",
210
+ test,
211
+ yes,
212
+ no: ternary()
213
+ };
214
+ });
215
+ const binary = (level) => {
216
+ if (level === BINARY.length) return unary();
217
+ let left = binary(level + 1);
218
+ while (peek()?.kind === "op" && BINARY[level].includes(peek().value)) {
219
+ const op = peek().value;
220
+ index += 1;
221
+ left = {
222
+ type: "binary",
223
+ op,
224
+ left,
225
+ right: binary(level + 1)
226
+ };
227
+ }
228
+ return left;
229
+ };
230
+ const unary = () => {
231
+ if (isOp("-") || isOp("!") || isOp("+")) {
232
+ const op = peek().value;
233
+ index += 1;
234
+ return nest(() => ({
235
+ type: "unary",
236
+ op,
237
+ value: unary()
238
+ }));
239
+ }
240
+ return primary();
241
+ };
242
+ const primary = () => {
243
+ const token = peek();
244
+ if (token === void 0) fail("the formula ends too early");
245
+ index += 1;
246
+ if (token.kind === "number" || token.kind === "string") return {
247
+ type: "value",
248
+ value: token.value
249
+ };
250
+ if (token.kind === "op" && token.value === "(") {
251
+ const inner = ternary();
252
+ expect(")");
253
+ return inner;
254
+ }
255
+ if (token.kind !== "name") fail(`unexpected "${token.value}" at ${token.at + 1}`);
256
+ if (!isOp("(")) return CONSTANTS.has(token.value) ? {
257
+ type: "value",
258
+ value: CONSTANTS.get(token.value)
259
+ } : {
260
+ type: "name",
261
+ name: token.value
262
+ };
263
+ const fn = Object.hasOwn(FUNCTIONS, token.value) ? FUNCTIONS[token.value] : void 0;
264
+ if (fn === void 0) fail(`unknown function "${token.value}"; allowed: ${FORMULA_FUNCTIONS.join(", ")}`);
265
+ index += 1;
266
+ const args = [];
267
+ if (!isOp(")")) {
268
+ args.push(ternary());
269
+ while (isOp(",")) {
270
+ index += 1;
271
+ args.push(ternary());
272
+ }
273
+ }
274
+ expect(")");
275
+ if (args.length < fn.args[0] || args.length > fn.args[1]) fail(`${token.value}() takes ${fn.args[0] === fn.args[1] ? fn.args[0] : fn.args[1] === Infinity ? `at least ${fn.args[0]}` : `${fn.args[0]} or ${fn.args[1]}`} argument${fn.args[1] === 1 ? "" : "s"}; got ${args.length}`);
276
+ return {
277
+ type: "call",
278
+ name: token.value,
279
+ args
280
+ };
281
+ };
282
+ if (tokens.length === 0) fail("the formula is empty");
283
+ const tree = ternary();
284
+ if (index < tokens.length) fail(`unexpected "${peek().value}" at ${peek().at + 1}`);
285
+ return tree;
286
+ }
287
+ function namesIn(node, found = /* @__PURE__ */ new Set()) {
288
+ if (node.type === "name") found.add(node.name);
289
+ else if (node.type === "unary") namesIn(node.value, found);
290
+ else if (node.type === "binary") {
291
+ namesIn(node.left, found);
292
+ namesIn(node.right, found);
293
+ } else if (node.type === "if") {
294
+ namesIn(node.test, found);
295
+ namesIn(node.yes, found);
296
+ namesIn(node.no, found);
297
+ } else if (node.type === "call") for (const arg of node.args) namesIn(arg, found);
298
+ return found;
299
+ }
300
+ function compileFormula(source) {
301
+ if (typeof source !== "string") return { error: "expected a formula string" };
302
+ try {
303
+ const tree = parse(source);
304
+ return {
305
+ tree,
306
+ names: [...namesIn(tree)]
307
+ };
308
+ } catch (error) {
309
+ if (error instanceof FormulaError) return { error: error.message };
310
+ throw error;
311
+ }
312
+ }
313
+ function run(node, lookup) {
314
+ switch (node.type) {
315
+ case "value": return node.value;
316
+ case "name": return lookup(node.name);
317
+ case "unary": {
318
+ const value = run(node.value, lookup);
319
+ if (node.op === "!") return !truthy(value);
320
+ return node.op === "-" ? -num(value) : num(value);
321
+ }
322
+ case "if": return truthy(run(node.test, lookup)) ? run(node.yes, lookup) : run(node.no, lookup);
323
+ case "call": return FUNCTIONS[node.name].run(...node.args.map((arg) => run(arg, lookup)));
324
+ default: return binary(node, lookup);
325
+ }
326
+ }
327
+ const truthy = (value) => typeof value === "string" ? value !== "" : Boolean(value);
328
+ function binary(node, lookup) {
329
+ if (node.op === "&&") return truthy(run(node.left, lookup)) ? run(node.right, lookup) : false;
330
+ if (node.op === "||") {
331
+ const left = run(node.left, lookup);
332
+ return truthy(left) ? left : run(node.right, lookup);
333
+ }
334
+ const left = run(node.left, lookup);
335
+ const right = run(node.right, lookup);
336
+ switch (node.op) {
337
+ case "+": return typeof left === "string" || typeof right === "string" ? `${show(left)}${show(right)}` : num(left) + num(right);
338
+ case "-": return num(left) - num(right);
339
+ case "*": return num(left) * num(right);
340
+ case "/": return num(left) / num(right);
341
+ case "%": return num(left) % num(right);
342
+ case "==": return left === right;
343
+ case "!=": return left !== right;
344
+ case "<": return num(left) < num(right);
345
+ case "<=": return num(left) <= num(right);
346
+ case ">": return num(left) > num(right);
347
+ default: return num(left) >= num(right);
348
+ }
349
+ }
350
+ function show(value) {
351
+ if (typeof value === "number") return Number.isFinite(value) ? String(Number(value.toPrecision(12))) : String(value);
352
+ return String(value);
353
+ }
354
+ function createScope(inputs, lets) {
355
+ const done = /* @__PURE__ */ new Map();
356
+ const busy = /* @__PURE__ */ new Set();
357
+ const lookup = (name) => {
358
+ if (inputs.has(name)) return inputs.get(name);
359
+ if (done.has(name)) return done.get(name);
360
+ const tree = lets.get(name);
361
+ if (tree === void 0) fail(`unknown name "${name}"`);
362
+ if (busy.has(name)) fail(`"${name}" refers to itself`);
363
+ busy.add(name);
364
+ try {
365
+ const value = run(tree, lookup);
366
+ done.set(name, value);
367
+ return value;
368
+ } finally {
369
+ busy.delete(name);
370
+ }
371
+ };
372
+ return lookup;
373
+ }
374
+ function evaluate(tree, lookup) {
375
+ try {
376
+ return { value: run(tree, lookup) };
377
+ } catch (error) {
378
+ if (error instanceof FormulaError) return { error: error.message };
379
+ if (error instanceof RangeError) return { error: "the formula is too deep to work out" };
380
+ throw error;
381
+ }
382
+ }
383
+ //#endregion
384
+ //#region src/core/views.js
385
+ const LAYOUT = ["stack", "row"];
386
+ const DISPLAY = [
387
+ "text",
388
+ "metric",
389
+ "table",
390
+ "chart",
391
+ "progress",
392
+ "callout",
393
+ "sources"
394
+ ];
395
+ const INPUT_TYPES = [
396
+ "slider",
397
+ "choice",
398
+ "toggle",
399
+ "number"
400
+ ];
401
+ const ANSWERS = [
402
+ "options",
403
+ "confirm",
404
+ "form",
405
+ "submit"
406
+ ];
407
+ const COMPONENTS = [
408
+ ...LAYOUT,
409
+ ...DISPLAY,
410
+ ...INPUT_TYPES,
411
+ ...ANSWERS
412
+ ];
413
+ const TONES$1 = [
414
+ "good",
415
+ "warn",
416
+ "bad"
417
+ ];
418
+ const TEXT_TONES = ["muted"];
419
+ const CALLOUT_TONES$1 = [
420
+ "info",
421
+ "good",
422
+ "warn",
423
+ "bad"
424
+ ];
425
+ const CHART_KINDS = [
426
+ "bar",
427
+ "hbar",
428
+ "line",
429
+ "area",
430
+ "donut"
431
+ ];
432
+ const INTERACTIVE = new Set(ANSWERS);
433
+ const INPUTS = new Set(INPUT_TYPES);
434
+ const PROPS = {
435
+ stack: ["children", "let"],
436
+ row: ["children", "let"],
437
+ text: ["text", "tone"],
438
+ metric: [
439
+ "label",
440
+ "value",
441
+ "tone",
442
+ "caption"
443
+ ],
444
+ table: ["columns", "rows"],
445
+ chart: [
446
+ "kind",
447
+ "categories",
448
+ "series",
449
+ "unit",
450
+ "caption"
451
+ ],
452
+ progress: [
453
+ "label",
454
+ "percent",
455
+ "value",
456
+ "max",
457
+ "valueLabel",
458
+ "tone"
459
+ ],
460
+ callout: ["text", "tone"],
461
+ sources: ["items"],
462
+ slider: [
463
+ "id",
464
+ "label",
465
+ "min",
466
+ "max",
467
+ "step",
468
+ "value",
469
+ "unit"
470
+ ],
471
+ choice: [
472
+ "id",
473
+ "label",
474
+ "options",
475
+ "value"
476
+ ],
477
+ toggle: [
478
+ "id",
479
+ "label",
480
+ "value"
481
+ ],
482
+ number: [
483
+ "id",
484
+ "label",
485
+ "min",
486
+ "max",
487
+ "step",
488
+ "value",
489
+ "unit"
490
+ ],
491
+ options: [
492
+ "id",
493
+ "options",
494
+ "multi"
495
+ ],
496
+ confirm: [
497
+ "id",
498
+ "confirmLabel",
499
+ "cancelLabel"
500
+ ],
501
+ form: [
502
+ "id",
503
+ "fields",
504
+ "submitLabel"
505
+ ],
506
+ submit: ["id", "label"]
507
+ };
508
+ const MAX_DEPTH = 8;
509
+ const MAX_NODES = 64;
510
+ const MAX_CHILDREN = 16;
511
+ const MAX_TEXT = 2e3;
512
+ const MAX_CELL = 200;
513
+ const MAX_COLUMNS = 8;
514
+ const MAX_ROWS = 50;
515
+ const MAX_OPTIONS = 8;
516
+ const MAX_FIELDS = 6;
517
+ const RESERVED = /* @__PURE__ */ new Set(["true", "false"]);
518
+ const VIEW_BYTE_LIMIT = 8192;
519
+ const isRecord$1 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
520
+ const quoted = (value) => JSON.stringify(value);
521
+ const allowed = (list) => list.join(", ");
522
+ const isNumber = (value) => typeof value === "number" && Number.isFinite(value);
523
+ const isScalar = (value) => typeof value === "string" || typeof value === "boolean" || isNumber(value);
524
+ const isFormula = (value) => isRecord$1(value) && Object.keys(value).length === 1 && Object.hasOwn(value, "=");
525
+ function viewBytes(title, view) {
526
+ return new TextEncoder().encode(`${JSON.stringify(view) ?? ""}${title ?? ""}`).length;
527
+ }
528
+ const NUMERIC = [
529
+ "value",
530
+ "percent",
531
+ "min",
532
+ "max",
533
+ "step",
534
+ "height"
535
+ ];
536
+ const numeric = (value) => typeof value === "string" && value.trim() !== "" && !Number.isNaN(Number(value)) ? Number(value) : value;
537
+ function normalizeView(value) {
538
+ if (Array.isArray(value)) return value.map(normalizeView);
539
+ if (!isRecord$1(value)) return value;
540
+ const keys = Object.keys(value);
541
+ if (keys.length === 1 && keys[0] === "item") {
542
+ const inner = normalizeView(value.item);
543
+ if (Array.isArray(inner)) return inner;
544
+ }
545
+ const out = {};
546
+ for (const key of keys) Object.defineProperty(out, key, {
547
+ value: normalizeView(value[key]),
548
+ writable: true,
549
+ enumerable: true,
550
+ configurable: true
551
+ });
552
+ for (const field of NUMERIC) if (Object.hasOwn(out, field)) out[field] = numeric(out[field]);
553
+ if (Array.isArray(out.values)) out.values = out.values.map(numeric);
554
+ return out;
555
+ }
556
+ function validateView(view) {
557
+ const ctx = {
558
+ ids: /* @__PURE__ */ new Set(),
559
+ nodes: 0,
560
+ interactive: false,
561
+ inputs: /* @__PURE__ */ new Map(),
562
+ lets: /* @__PURE__ */ new Map(),
563
+ formulas: []
564
+ };
565
+ const error = checkNode(view, "view", 1, ctx) ?? checkFormulas(ctx);
566
+ return error === void 0 ? {
567
+ ok: true,
568
+ interactive: ctx.interactive
569
+ } : {
570
+ ok: false,
571
+ error
572
+ };
573
+ }
574
+ function checkShownView(title, sent) {
575
+ let bytes;
576
+ try {
577
+ bytes = viewBytes(title, sent);
578
+ } catch {
579
+ return { error: "The view is not JSON data." };
580
+ }
581
+ if (bytes > 8192) return { error: `The view is ${bytes} bytes; the limit is ${VIEW_BYTE_LIMIT}.` };
582
+ if (title !== void 0 && title !== null && typeof title !== "string") return { error: "title: expected a string" };
583
+ const named = String(title ?? "").trim();
584
+ const view = normalizeView(sent);
585
+ const checked = validateView(view);
586
+ if (!checked.ok) return { error: checked.error };
587
+ return {
588
+ title: named,
589
+ view,
590
+ interactive: checked.interactive
591
+ };
592
+ }
593
+ function checkNode(node, path, depth, ctx) {
594
+ if (!isRecord$1(node)) return `${path}: expected a component object`;
595
+ if (depth > MAX_DEPTH) return `${path}: ${depth} levels exceeds the limit of ${MAX_DEPTH}`;
596
+ ctx.nodes += 1;
597
+ if (ctx.nodes > MAX_NODES) return `${path}: ${ctx.nodes} components exceeds the limit of ${MAX_NODES}`;
598
+ const type = node.type;
599
+ if (!COMPONENTS.includes(type)) return `${path}.type: unknown component ${quoted(type)}; allowed: ${allowed(COMPONENTS)}`;
600
+ const props = PROPS[type];
601
+ for (const key of Object.keys(node)) {
602
+ if (key === "type" || props.includes(key)) continue;
603
+ return `${path}.${key}: unknown property ${quoted(key)}; allowed: ${allowed(props)}`;
604
+ }
605
+ if (INTERACTIVE.has(type)) ctx.interactive = true;
606
+ switch (type) {
607
+ case "stack":
608
+ case "row": return checkLayout(node, path, depth, ctx);
609
+ case "text": return checkText(node, path, ctx);
610
+ case "metric": return checkMetric(node, path, ctx);
611
+ case "table": return checkTable(node, path, ctx);
612
+ case "chart": return checkChart(node, path, ctx);
613
+ case "progress": return checkProgress(node, path, ctx);
614
+ case "callout": return checkCallout(node, path, ctx);
615
+ case "sources": return checkSources(node, path);
616
+ case "slider":
617
+ case "number": return checkNumberInput(node, path, ctx);
618
+ case "choice": return checkChoice(node, path, ctx);
619
+ case "toggle": return checkToggle(node, path, ctx);
620
+ case "options": return checkOptions(node, path, ctx);
621
+ case "confirm": return checkConfirm(node, path, ctx);
622
+ case "form": return checkForm(node, path, ctx);
623
+ default: return checkSubmit(node, path, ctx);
624
+ }
625
+ }
626
+ function checkLayout(node, path, depth, ctx) {
627
+ if (node.let !== void 0) {
628
+ if (depth !== 1) return `${path}.let: only the outermost component can define names`;
629
+ if (!isRecord$1(node.let)) return `${path}.let: expected an object of names to formulas`;
630
+ for (const [name, source] of Object.entries(node.let)) {
631
+ if (!isName(name) || RESERVED.has(name)) return `${path}.let.${name}: a name is letters, digits and _, and starts with a letter or _`;
632
+ if (typeof source !== "string") return `${path}.let.${name}: expected a formula string`;
633
+ ctx.lets.set(name, {
634
+ source,
635
+ path: `${path}.let.${name}`
636
+ });
637
+ }
638
+ }
639
+ if (!Array.isArray(node.children)) return `${path}.children: expected an array of 1 to ${MAX_CHILDREN} components`;
640
+ if (node.children.length < 1 || node.children.length > MAX_CHILDREN) return `${path}.children: expected 1 to ${MAX_CHILDREN} children; got ${node.children.length}`;
641
+ for (let index = 0; index < node.children.length; index += 1) {
642
+ const error = checkNode(node.children[index], `${path}.children[${index}]`, depth + 1, ctx);
643
+ if (error !== void 0) return error;
644
+ }
645
+ }
646
+ function checkValue(node, key, path, ctx, kind, { required = false, max = MAX_TEXT } = {}) {
647
+ const value = node[key];
648
+ const at = `${path}.${key}`;
649
+ if (value === void 0) return required ? `${at}: ${expected(kind)}` : void 0;
650
+ if (isFormula(value)) {
651
+ ctx.formulas.push({
652
+ source: value["="],
653
+ path: at,
654
+ kind
655
+ });
656
+ return;
657
+ }
658
+ if (kind === "number") return isNumber(value) ? void 0 : `${at}: ${expected(kind)}`;
659
+ if (Array.isArray(kind)) return kind.includes(value) ? void 0 : `${at}: unknown tone ${quoted(value)}; allowed: ${allowed(kind)}`;
660
+ if (kind === "string") {
661
+ if (typeof value !== "string") return `${at}: ${expected(kind)}`;
662
+ return value.length > max ? `${at}: ${value.length} characters exceeds the limit of ${max}` : void 0;
663
+ }
664
+ if (!isScalar(value)) return `${at}: ${expected(kind)}`;
665
+ }
666
+ const expected = (kind) => kind === "number" ? "expected a number" : kind === "string" ? "expected a string" : Array.isArray(kind) ? `expected one of ${allowed(kind)}` : "expected a string or a number";
667
+ function checkText(node, path, ctx) {
668
+ return checkValue(node, "text", path, ctx, "string", { required: true }) ?? checkValue(node, "tone", path, ctx, TEXT_TONES);
669
+ }
670
+ function checkMetric(node, path, ctx) {
671
+ if (typeof node.label !== "string") return `${path}.label: expected a string`;
672
+ if (node.value === void 0) return `${path}.value: expected a string or a number`;
673
+ if (typeof node.value === "number" && !Number.isFinite(node.value)) return `${path}.value: expected a finite number`;
674
+ return checkValue(node, "value", path, ctx, "scalar") ?? checkValue(node, "tone", path, ctx, TONES$1) ?? checkValue(node, "caption", path, ctx, "string");
675
+ }
676
+ function checkTable(node, path, ctx) {
677
+ if (!Array.isArray(node.columns)) return `${path}.columns: expected an array of at most ${MAX_COLUMNS} columns`;
678
+ if (node.columns.length > MAX_COLUMNS) return `${path}.columns: ${node.columns.length} columns exceeds the limit of ${MAX_COLUMNS}`;
679
+ for (let index = 0; index < node.columns.length; index += 1) {
680
+ const column = node.columns[index];
681
+ const at = `${path}.columns[${index}]`;
682
+ if (!isRecord$1(column)) return `${at}: expected a column object`;
683
+ for (const key of Object.keys(column)) if (key !== "key" && key !== "label") return `${at}.${key}: unknown property ${quoted(key)}; allowed: key, label`;
684
+ if (typeof column.key !== "string" || column.key === "") return `${at}.key: expected a non-empty string`;
685
+ if (typeof column.label !== "string") return `${at}.label: expected a string`;
686
+ }
687
+ if (!Array.isArray(node.rows)) return `${path}.rows: expected an array of at most ${MAX_ROWS} rows`;
688
+ if (node.rows.length > MAX_ROWS) return `${path}.rows: ${node.rows.length} rows exceeds the limit of ${MAX_ROWS}`;
689
+ for (let row = 0; row < node.rows.length; row += 1) {
690
+ const cells = node.rows[row];
691
+ if (!Array.isArray(cells)) return `${path}.rows[${row}]: expected an array of cells`;
692
+ for (let column = 0; column < cells.length; column += 1) {
693
+ const at = `${path}.rows[${row}][${column}]`;
694
+ if (isFormula(cells[column])) {
695
+ ctx.formulas.push({
696
+ source: cells[column]["="],
697
+ path: at,
698
+ kind: "scalar"
699
+ });
700
+ continue;
701
+ }
702
+ const text = cellText$1(cells[column]);
703
+ if (text.length > MAX_CELL) return `${at}: ${text.length} characters exceeds the limit of ${MAX_CELL}`;
704
+ }
705
+ }
706
+ }
707
+ function checkChart(node, path, ctx) {
708
+ if (node.kind !== void 0 && !CHART_KINDS.includes(node.kind)) return `${path}.kind: unknown chart ${quoted(node.kind)}; allowed: ${allowed(CHART_KINDS)}`;
709
+ if (!Array.isArray(node.categories) || node.categories.length === 0) return `${path}.categories: expected a non-empty array of labels`;
710
+ for (let index = 0; index < node.categories.length; index += 1) {
711
+ const label = node.categories[index];
712
+ if (typeof label !== "string" && !isNumber(label)) return `${path}.categories[${index}]: expected a string`;
713
+ }
714
+ if (!Array.isArray(node.series) || node.series.length === 0) return `${path}.series: expected a non-empty array of { name?, values }`;
715
+ if (node.kind === "donut" && node.series.length > 1) return `${path}.series: a donut takes one series; got ${node.series.length}`;
716
+ for (let index = 0; index < node.series.length; index += 1) {
717
+ const series = node.series[index];
718
+ const at = `${path}.series[${index}]`;
719
+ if (!isRecord$1(series)) return `${at}: expected a series object`;
720
+ for (const key of Object.keys(series)) if (key !== "name" && key !== "values") return `${at}.${key}: unknown property ${quoted(key)}; allowed: name, values`;
721
+ if (series.name !== void 0 && typeof series.name !== "string") return `${at}.name: expected a string`;
722
+ if (!Array.isArray(series.values)) return `${at}.values: expected an array of numbers`;
723
+ if (series.values.length !== node.categories.length) return `${at}.values: ${series.values.length} values for ${node.categories.length} categories`;
724
+ for (let item = 0; item < series.values.length; item += 1) {
725
+ const value = series.values[item];
726
+ if (isFormula(value)) ctx.formulas.push({
727
+ source: value["="],
728
+ path: `${at}.values[${item}]`,
729
+ kind: "number"
730
+ });
731
+ else if (!isNumber(value)) return `${at}.values[${item}]: expected a number`;
732
+ }
733
+ }
734
+ if (node.unit !== void 0 && typeof node.unit !== "string") return `${path}.unit: expected a string`;
735
+ if (node.caption !== void 0 && typeof node.caption !== "string") return `${path}.caption: expected a string`;
736
+ }
737
+ function checkProgress(node, path, ctx) {
738
+ if (node.label !== void 0 && typeof node.label !== "string") return `${path}.label: expected a string`;
739
+ if (node.percent === void 0 === (node.value === void 0)) return `${path}: give percent (0 to 100), or value with max`;
740
+ if (node.percent !== void 0 && !isFormula(node.percent)) {
741
+ if (typeof node.percent !== "number" || !Number.isFinite(node.percent) || node.percent < 0 || node.percent > 100) return `${path}.percent: expected a number from 0 to 100`;
742
+ }
743
+ if (node.value !== void 0 && node.max === void 0) return `${path}.max: expected a number with value`;
744
+ if (node.value === void 0 && node.max !== void 0) return `${path}.value: expected a number with max`;
745
+ return checkValue(node, "percent", path, ctx, "number") ?? checkValue(node, "value", path, ctx, "number") ?? checkValue(node, "max", path, ctx, "number") ?? checkValue(node, "valueLabel", path, ctx, "scalar") ?? checkValue(node, "tone", path, ctx, TONES$1);
746
+ }
747
+ function checkCallout(node, path, ctx) {
748
+ return checkValue(node, "text", path, ctx, "string", { required: true }) ?? checkValue(node, "tone", path, ctx, CALLOUT_TONES$1);
749
+ }
750
+ function checkSources(node, path) {
751
+ if (!Array.isArray(node.items) || node.items.length === 0) return `${path}.items: expected a non-empty array of { label, url }`;
752
+ for (let index = 0; index < node.items.length; index += 1) {
753
+ const item = node.items[index];
754
+ const at = `${path}.items[${index}]`;
755
+ if (!isRecord$1(item)) return `${at}: expected a { label, url } object`;
756
+ for (const key of Object.keys(item)) if (key !== "label" && key !== "url") return `${at}.${key}: unknown property ${quoted(key)}; allowed: label, url`;
757
+ if (typeof item.label !== "string" || item.label.trim() === "") return `${at}.label: expected a non-empty string`;
758
+ if (typeof item.url !== "string" || !/^https?:\/\/[^\s]+$/i.test(item.url)) return `${at}.url: expected an http(s) address`;
759
+ }
760
+ }
761
+ function checkInputId(node, path, ctx) {
762
+ if (!isName(node.id) || RESERVED.has(node.id)) return `${path}.id: an input id is letters, digits and _, and starts with a letter or _, so formulas can use it`;
763
+ if (ctx.ids.has(node.id)) return `${path}.id: duplicate id ${quoted(node.id)}`;
764
+ ctx.ids.add(node.id);
765
+ if (typeof node.label !== "string") return `${path}.label: expected a string`;
766
+ }
767
+ function checkNumberInput(node, path, ctx) {
768
+ const idError = checkInputId(node, path, ctx);
769
+ if (idError !== void 0) return idError;
770
+ for (const key of [
771
+ "min",
772
+ "max",
773
+ "step"
774
+ ]) if (node[key] !== void 0 && !isNumber(node[key])) return `${path}.${key}: expected a number`;
775
+ if (node.type === "slider" && (node.min === void 0 || node.max === void 0)) return `${path}: a slider needs min and max`;
776
+ if (node.min !== void 0 && node.max !== void 0 && node.max <= node.min) return `${path}.max: expected more than min (${node.min})`;
777
+ if (node.step !== void 0 && node.step <= 0) return `${path}.step: expected more than 0`;
778
+ if (!isNumber(node.value)) return `${path}.value: expected a number`;
779
+ if (node.min !== void 0 && node.value < node.min || node.max !== void 0 && node.value > node.max) return `${path}.value: expected a number from ${node.min ?? "-∞"} to ${node.max ?? "∞"}`;
780
+ if (node.unit !== void 0 && typeof node.unit !== "string") return `${path}.unit: expected a string`;
781
+ ctx.inputs.set(node.id, node.value);
782
+ }
783
+ function checkChoice(node, path, ctx) {
784
+ const idError = checkInputId(node, path, ctx);
785
+ if (idError !== void 0) return idError;
786
+ if (!Array.isArray(node.options) || node.options.length === 0) return `${path}.options: expected a non-empty array of { label, value }`;
787
+ const labels = /* @__PURE__ */ new Set();
788
+ const values = /* @__PURE__ */ new Set();
789
+ for (let index = 0; index < node.options.length; index += 1) {
790
+ const option = node.options[index];
791
+ const at = `${path}.options[${index}]`;
792
+ if (!isRecord$1(option)) return `${at}: expected a { label, value } object`;
793
+ for (const key of Object.keys(option)) if (key !== "label" && key !== "value") return `${at}.${key}: unknown property ${quoted(key)}; allowed: label, value`;
794
+ if (typeof option.label !== "string" || option.label.trim() === "") return `${at}.label: expected a non-empty string`;
795
+ if (labels.has(option.label)) return `${at}.label: duplicate label ${quoted(option.label)}`;
796
+ labels.add(option.label);
797
+ const value = option.value ?? option.label;
798
+ if (!isScalar(value)) return `${at}.value: expected a string, a number or true/false`;
799
+ if (values.has(value)) return `${at}.value: duplicate value ${quoted(value)}`;
800
+ values.add(value);
801
+ }
802
+ const chosen = node.value ?? choiceValue(node.options[0]);
803
+ if (!values.has(chosen)) return `${path}.value: expected one of the options' values`;
804
+ ctx.inputs.set(node.id, chosen);
805
+ }
806
+ function checkToggle(node, path, ctx) {
807
+ const idError = checkInputId(node, path, ctx);
808
+ if (idError !== void 0) return idError;
809
+ if (node.value !== void 0 && typeof node.value !== "boolean") return `${path}.value: expected true or false`;
810
+ ctx.inputs.set(node.id, node.value === true);
811
+ }
812
+ function checkOptions(node, path, ctx) {
813
+ const idError = checkId(node, path, ctx);
814
+ if (idError !== void 0) return idError;
815
+ if (node.multi !== void 0 && typeof node.multi !== "boolean") return `${path}.multi: expected a boolean`;
816
+ if (!Array.isArray(node.options)) return `${path}.options: expected an array of 1 to ${MAX_OPTIONS} options`;
817
+ if (node.options.length < 1 || node.options.length > MAX_OPTIONS) return `${path}.options: expected 1 to ${MAX_OPTIONS} options; got ${node.options.length}`;
818
+ const labels = /* @__PURE__ */ new Set();
819
+ for (let index = 0; index < node.options.length; index += 1) {
820
+ const option = node.options[index];
821
+ const at = `${path}.options[${index}]`;
822
+ if (!isRecord$1(option)) return `${at}: expected an option object`;
823
+ for (const key of Object.keys(option)) if (key !== "label" && key !== "description") return `${at}.${key}: unknown property ${quoted(key)}; allowed: label, description`;
824
+ if (typeof option.label !== "string" || option.label.trim() === "") return `${at}.label: expected a non-empty string`;
825
+ if (labels.has(option.label)) return `${at}.label: duplicate label ${quoted(option.label)}`;
826
+ labels.add(option.label);
827
+ if (option.description !== void 0 && typeof option.description !== "string") return `${at}.description: expected a string`;
828
+ }
829
+ }
830
+ function checkConfirm(node, path, ctx) {
831
+ const idError = checkId(node, path, ctx);
832
+ if (idError !== void 0) return idError;
833
+ if (node.confirmLabel !== void 0 && typeof node.confirmLabel !== "string") return `${path}.confirmLabel: expected a string`;
834
+ if (node.cancelLabel !== void 0 && typeof node.cancelLabel !== "string") return `${path}.cancelLabel: expected a string`;
835
+ }
836
+ function checkForm(node, path, ctx) {
837
+ const idError = checkId(node, path, ctx);
838
+ if (idError !== void 0) return idError;
839
+ if (node.submitLabel !== void 0 && typeof node.submitLabel !== "string") return `${path}.submitLabel: expected a string`;
840
+ if (!Array.isArray(node.fields)) return `${path}.fields: expected an array of at most ${MAX_FIELDS} fields`;
841
+ if (node.fields.length > MAX_FIELDS) return `${path}.fields: ${node.fields.length} fields exceeds the limit of ${MAX_FIELDS}`;
842
+ const seen = /* @__PURE__ */ new Set();
843
+ for (let index = 0; index < node.fields.length; index += 1) {
844
+ const field = node.fields[index];
845
+ const at = `${path}.fields[${index}]`;
846
+ if (!isRecord$1(field)) return `${at}: expected a field object`;
847
+ for (const key of Object.keys(field)) if (key !== "id" && key !== "label" && key !== "placeholder") return `${at}.${key}: unknown property ${quoted(key)}; allowed: id, label, placeholder`;
848
+ if (typeof field.id !== "string" || field.id === "") return `${at}.id: expected a non-empty string`;
849
+ if (field.id === "__proto__") return `${at}.id: "__proto__" cannot be an id`;
850
+ if (seen.has(field.id)) return `${at}.id: duplicate field id ${quoted(field.id)}`;
851
+ seen.add(field.id);
852
+ if (typeof field.label !== "string") return `${at}.label: expected a string`;
853
+ if (field.placeholder !== void 0 && typeof field.placeholder !== "string") return `${at}.placeholder: expected a string`;
854
+ }
855
+ }
856
+ function checkSubmit(node, path, ctx) {
857
+ const idError = checkId(node, path, ctx);
858
+ if (idError !== void 0) return idError;
859
+ if (node.label !== void 0 && typeof node.label !== "string") return `${path}.label: expected a string`;
860
+ }
861
+ function checkId(node, path, ctx) {
862
+ if (typeof node.id !== "string" || node.id === "") return `${path}.id: expected a non-empty string`;
863
+ if (node.id === "__proto__") return `${path}.id: "__proto__" cannot be an id`;
864
+ if (ctx.ids.has(node.id)) return `${path}.id: duplicate id ${quoted(node.id)}`;
865
+ ctx.ids.add(node.id);
866
+ }
867
+ function checkFormulas(ctx) {
868
+ const known = /* @__PURE__ */ new Set([...ctx.inputs.keys(), ...ctx.lets.keys()]);
869
+ const lets = /* @__PURE__ */ new Map();
870
+ for (const [name, entry] of ctx.lets) {
871
+ if (ctx.inputs.has(name)) return `${entry.path}: ${quoted(name)} is already an input id`;
872
+ const compiled = compileFormula(entry.source);
873
+ if (compiled.error !== void 0) return `${entry.path}: ${compiled.error}`;
874
+ const unknown = compiled.names.find((item) => !known.has(item));
875
+ if (unknown !== void 0) return `${entry.path}: unknown name ${quoted(unknown)}; known: ${allowed([...known]) || "(none)"}`;
876
+ lets.set(name, compiled.tree);
877
+ }
878
+ const lookup = createScope(ctx.inputs, lets);
879
+ for (const [name, entry] of ctx.lets) {
880
+ const result = evaluate(lets.get(name), lookup);
881
+ if (result.error !== void 0) return `${entry.path}: ${result.error}`;
882
+ }
883
+ for (const formula of ctx.formulas) {
884
+ const compiled = compileFormula(formula.source);
885
+ if (compiled.error !== void 0) return `${formula.path}: ${compiled.error}`;
886
+ const unknown = compiled.names.find((item) => !known.has(item));
887
+ if (unknown !== void 0) return `${formula.path}: unknown name ${quoted(unknown)}; known: ${allowed([...known]) || "(none)"}`;
888
+ const result = evaluate(compiled.tree, lookup);
889
+ if (result.error !== void 0) return `${formula.path}: ${result.error}`;
890
+ const problem = kindProblem(result.value, formula.kind);
891
+ if (problem !== void 0) return `${formula.path}: the formula gives ${quoted(result.value)}; ${problem}`;
892
+ }
893
+ }
894
+ function kindProblem(value, kind) {
895
+ if (kind === "number") return isNumber(value) ? void 0 : "expected a number";
896
+ if (Array.isArray(kind)) return kind.includes(value) ? void 0 : `expected one of ${allowed(kind)}`;
897
+ if (kind === "string") return isScalar(value) ? void 0 : "expected text";
898
+ return isScalar(value) ? void 0 : "expected a string or a number";
899
+ }
900
+ function cellText$1(value) {
901
+ if (typeof value === "string") return value;
902
+ const text = JSON.stringify(value);
903
+ return typeof text === "string" ? text : "";
904
+ }
905
+ const compiled = /* @__PURE__ */ new WeakMap();
906
+ const treeOf = (formula) => {
907
+ let entry = compiled.get(formula);
908
+ if (entry === void 0) {
909
+ entry = compileFormula(formula["="]);
910
+ compiled.set(formula, entry);
911
+ }
912
+ return entry;
913
+ };
914
+ const choiceValue = (option) => option?.value ?? option?.label;
915
+ function walk(node, visit) {
916
+ if (!isRecord$1(node)) return;
917
+ visit(node);
918
+ if (LAYOUT.includes(node.type) && Array.isArray(node.children)) for (const child of node.children) walk(child, visit);
919
+ }
920
+ function viewInputs(view) {
921
+ const found = [];
922
+ walk(view, (node) => {
923
+ if (INPUTS.has(node.type)) found.push(node);
924
+ });
925
+ return found;
926
+ }
927
+ function inputDefault(node) {
928
+ if (node.type === "toggle") return node.value === true;
929
+ if (node.type === "choice") return node.value ?? choiceValue(Array.isArray(node.options) ? node.options[0] : void 0);
930
+ return node.value;
931
+ }
932
+ function viewScope(view, values = {}) {
933
+ const inputs = /* @__PURE__ */ new Map();
934
+ for (const node of viewInputs(view)) inputs.set(node.id, Object.hasOwn(values, node.id) ? values[node.id] : inputDefault(node));
935
+ const lets = /* @__PURE__ */ new Map();
936
+ for (const [name, source] of Object.entries(isRecord$1(view?.let) ? view.let : {})) {
937
+ const entry = compileFormula(source);
938
+ if (entry.tree !== void 0) lets.set(name, entry.tree);
939
+ }
940
+ return createScope(inputs, lets);
941
+ }
942
+ function resolve(value, lookup, fallback = void 0) {
943
+ if (!isFormula(value)) return value;
944
+ const entry = treeOf(value);
945
+ if (entry.tree === void 0) return fallback;
946
+ const result = evaluate(entry.tree, lookup);
947
+ return result.error === void 0 ? result.value : fallback;
948
+ }
949
+ const asText = (value) => value === void 0 || value === null ? "" : show(value);
950
+ const choiceLabel = (node, value) => (Array.isArray(node.options) ? node.options : []).find((option) => choiceValue(option) === value)?.label ?? asText(value);
951
+ function inputText(node, value) {
952
+ if (node.type === "toggle") return value === true ? "on" : "off";
953
+ if (node.type === "choice") return choiceLabel(node, value);
954
+ return `${asText(value)}${node.unit ? ` ${node.unit}` : ""}`;
955
+ }
956
+ function inputsSummary(view, inputs) {
957
+ if (!isRecord$1(inputs)) return "";
958
+ return viewInputs(view).filter((node) => Object.hasOwn(inputs, node.id)).map((node) => `${node.label || node.id}=${inputText(node, inputs[node.id])}`).join("; ");
959
+ }
960
+ function interactiveNodes(node, found = []) {
961
+ walk(node, (item) => {
962
+ if (INTERACTIVE.has(item.type)) found.push(item);
963
+ });
964
+ return found;
965
+ }
966
+ function validateViewAnswer(view, action, values) {
967
+ const nodes = interactiveNodes(view);
968
+ if (typeof action !== "string" || action === "") return { error: `action: expected an interactive component id; allowed: ${allowed(nodes.map((node) => node.id))}` };
969
+ const node = nodes.find((item) => item.id === action);
970
+ if (node === void 0) return { error: `action: no interactive component with id ${quoted(action)}; allowed: ${allowed(nodes.map((item) => item.id))}` };
971
+ if (!isRecord$1(values)) return { error: "values: expected an object" };
972
+ const inputs = validateInputs(view, values.inputs);
973
+ if (inputs.error !== void 0) return inputs;
974
+ const extra = inputs.inputs === void 0 ? {} : { inputs: inputs.inputs };
975
+ if (node.type === "submit") return { ...extra };
976
+ const own = node.type === "options" ? validateOptions(node, values) : node.type === "confirm" ? validateConfirm(values) : validateForm(node, values);
977
+ return own.error === void 0 ? {
978
+ ...own,
979
+ ...extra
980
+ } : own;
981
+ }
982
+ function validateInputs(view, sent) {
983
+ const nodes = viewInputs(view);
984
+ if (nodes.length === 0) return sent === void 0 ? {} : { error: "values.inputs: this view has no inputs" };
985
+ if (sent !== void 0 && !isRecord$1(sent)) return { error: "values.inputs: expected an object of input ids to values" };
986
+ const given = sent ?? {};
987
+ const ids = nodes.map((node) => node.id);
988
+ const unknown = Object.keys(given).find((key) => !ids.includes(key));
989
+ if (unknown !== void 0) return { error: `values.inputs.${unknown}: unknown input; allowed: ${allowed(ids)}` };
990
+ const inputs = {};
991
+ for (const node of nodes) {
992
+ const value = Object.hasOwn(given, node.id) ? given[node.id] : inputDefault(node);
993
+ const at = `values.inputs.${node.id}`;
994
+ if (node.type === "toggle" && typeof value !== "boolean") return { error: `${at}: expected true or false` };
995
+ if (node.type === "choice" && !(Array.isArray(node.options) ? node.options : []).some((option) => choiceValue(option) === value)) return { error: `${at}: expected one of the options' values` };
996
+ if (node.type === "slider" || node.type === "number") {
997
+ if (!isNumber(value)) return { error: `${at}: expected a number` };
998
+ if (node.min !== void 0 && value < node.min || node.max !== void 0 && value > node.max) return { error: `${at}: expected a number from ${node.min ?? "-∞"} to ${node.max ?? "∞"}` };
999
+ }
1000
+ inputs[node.id] = value;
1001
+ }
1002
+ return { inputs };
1003
+ }
1004
+ function validateOptions(node, values) {
1005
+ if (!Array.isArray(values.selected)) return { error: "values.selected: expected an array of option labels" };
1006
+ const labels = new Set((node.options ?? []).map((option) => option?.label));
1007
+ const picked = values.selected.filter((label) => typeof label === "string" && labels.has(label));
1008
+ const selected = node.multi === true ? [...new Set(picked)] : picked.slice(0, 1);
1009
+ if (selected.length === 0) return { error: "values.selected: select at least one option" };
1010
+ return { selected };
1011
+ }
1012
+ function validateConfirm(values) {
1013
+ if (typeof values.confirmed !== "boolean") return { error: "values.confirmed: expected a boolean" };
1014
+ return { confirmed: values.confirmed };
1015
+ }
1016
+ function validateForm(node, values) {
1017
+ if (!isRecord$1(values.fields)) return { error: "values.fields: expected an object of field ids to strings" };
1018
+ const declared = (node.fields ?? []).map((field) => field?.id).filter((id) => typeof id === "string");
1019
+ const allowedIds = new Set(declared);
1020
+ const fields = {};
1021
+ for (const [key, value] of Object.entries(values.fields)) {
1022
+ if (!allowedIds.has(key)) return { error: `values.fields.${key}: unknown field; allowed: ${allowed(declared)}` };
1023
+ if (typeof value !== "string") return { error: `values.fields.${key}: expected a string` };
1024
+ if (value.length > MAX_TEXT) return { error: `values.fields.${key}: ${value.length} characters exceeds the limit of ${MAX_TEXT}` };
1025
+ fields[key] = value;
1026
+ }
1027
+ return { fields };
1028
+ }
1029
+ //#endregion
1030
+ //#region ../dsh-plugin-kit/src/log-shared.ts
1031
+ const PLUGIN_LOG_LEVELS = [
1032
+ "debug",
1033
+ "info",
1034
+ "warn",
1035
+ "error",
1036
+ "fault"
1037
+ ];
1038
+ const DEFAULT_PLUGIN_LOG_LEVEL = "info";
1039
+ const PLUGIN_LOG_MAX_MESSAGE_CHARS = 16e3;
1040
+ const SOURCE_RE = /^[a-z][a-z0-9-]{0,63}$/;
1041
+ const EVENT_RE = /^[a-z][a-z0-9._-]{0,127}$/;
1042
+ const CONTEXT_KEY_RE = /^[a-zA-Z][a-zA-Z0-9._-]{0,63}$/;
1043
+ const SECRET_KEY_RE = /(?:api[_-]?key|access[_-]?token|refresh[_-]?token|bearer|authorization|cookie|set[_-]?cookie|nonce|credential|private[_-]?key|password|secret|app[_-]?secret|client[_-]?secret|z_c0|d_c0)$/i;
1044
+ const CHAT_PAYLOAD_RE = /"role"\s*:\s*"(?:system|user|assistant|tool)"|"(?:messages|content|prompt|input_text|input_image|image_url)"\s*:\s*[\[{"']/i;
1045
+ const CREDENTIAL_BODY_RE = /-----BEGIN (?:RSA |EC |OPENSSH |PRIVATE )?PRIVATE KEY-----|"type"\s*:\s*"service_account"|credentials\.json|\.dsh[/\\](?:credentials|auth)/i;
1046
+ const AUTH_HEADER_RE = /\b(Authorization|Proxy-Authorization|Cookie|Set-Cookie)\s*:\s*[^\r\n]*/gi;
1047
+ const BEARER_RE = /\bBearer\s+[A-Za-z0-9._\-+=/]{8,}/g;
1048
+ const SECRET_FIELD_RE = /(["']?(?:api[_-]?key|access[_-]?token|refresh[_-]?token|token|bearer|authorization|cookie|set-cookie|nonce|credential|private[_-]?key|password|secret|app[_-]?secret|client[_-]?secret|password|z_c0|d_c0)["']?\s*[:=]\s*)(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,&;}]+)/gi;
1049
+ const SECRET_QUERY_RE = /([?&](?:token|api[_-]?key|key|secret|password|bootstrap|code|sig|signature|credential|password)=)[^\s&#"']+/gi;
1050
+ const URL_CREDENTIAL_RE = /\b(https?:\/\/)[^\s/:@]+:[^\s/@]+@/gi;
1051
+ const BOOTSTRAP_RE = /\/bootstrap\/[^\s"'?&#]+/gi;
1052
+ const KNOWN_TOKEN_RE = /\b(?:sk-[A-Za-z0-9_-]{10,}|gh[pousr]_[A-Za-z0-9_]{16,}|github_pat_[A-Za-z0-9_]{20,}|tvly-[A-Za-z0-9_-]{10,}|AKIA[0-9A-Z]{16})\b/g;
1053
+ const PATH_RE = /(?:[A-Za-z]:\\|\\\\)[^\s"'`<>|]+|(?:\/(?:home|Users|tmp|var|root)\/|~\/)[^\s"'`<>|]+/g;
1054
+ function isPluginLogLevel(value) {
1055
+ return typeof value === "string" && PLUGIN_LOG_LEVELS.includes(value);
1056
+ }
1057
+ function pluginLogLevelEnabled(level, minimum) {
1058
+ return PLUGIN_LOG_LEVELS.indexOf(level) >= PLUGIN_LOG_LEVELS.indexOf(minimum);
1059
+ }
1060
+ function normalizePluginLogSource(raw) {
1061
+ const stripped = String(raw || "").trim().replace(/^@[^/]+\//, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
1062
+ const bounded = (/^[a-z]/.test(stripped) ? stripped : `p-${stripped}`).slice(0, 64).replace(/-+$/g, "");
1063
+ return SOURCE_RE.test(bounded) ? bounded : "dsh-plugin";
1064
+ }
1065
+ function normalizePluginLogEvent(raw) {
1066
+ const cleaned = String(raw || "").trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[^a-z]+/, "").slice(0, 128);
1067
+ return EVENT_RE.test(cleaned) ? cleaned : "plugin.event";
1068
+ }
1069
+ /**
1070
+ * Parse a level specification like `debug` or `dsh-zhihu=debug,dsh-dot:warn,info`.
1071
+ * Source-specific rules win over a bare fallback level.
1072
+ */
1073
+ function parsePluginLogLevel(spec, source, fallback = DEFAULT_PLUGIN_LOG_LEVEL) {
1074
+ if (!spec || typeof spec !== "string") return fallback;
1075
+ const normalizedSource = source ? normalizePluginLogSource(source) : void 0;
1076
+ let wildcard;
1077
+ for (const token of spec.split(",").map((part) => part.trim()).filter(Boolean)) {
1078
+ const lowered = token.toLowerCase();
1079
+ if (isPluginLogLevel(lowered)) {
1080
+ wildcard = lowered;
1081
+ continue;
1082
+ }
1083
+ const match = /^([^=:]+)[=:]([a-z]+)$/i.exec(token);
1084
+ if (!match) continue;
1085
+ const target = match[1].trim();
1086
+ const candidate = match[2].trim().toLowerCase();
1087
+ if (!isPluginLogLevel(candidate)) continue;
1088
+ if (target === "*") {
1089
+ wildcard = candidate;
1090
+ continue;
1091
+ }
1092
+ if (normalizedSource && normalizePluginLogSource(target) === normalizedSource) return candidate;
1093
+ }
1094
+ return wildcard ?? fallback;
1095
+ }
1096
+ function sanitizePluginLogText(text, maxChars = PLUGIN_LOG_MAX_MESSAGE_CHARS) {
1097
+ if (!text) return "";
1098
+ if (CREDENTIAL_BODY_RE.test(text)) return "[omitted credential material]";
1099
+ if (CHAT_PAYLOAD_RE.test(text)) return "[omitted chat payload]";
1100
+ return truncateLogText(text.replace(AUTH_HEADER_RE, "$1: [redacted]").replace(BEARER_RE, "Bearer [redacted]").replace(SECRET_FIELD_RE, (full, prefix) => {
1101
+ const value = full.slice(prefix.length);
1102
+ if (value.startsWith("\"")) return `${prefix}"[redacted]"`;
1103
+ if (value.startsWith("'")) return `${prefix}'[redacted]'`;
1104
+ return `${prefix}[redacted]`;
1105
+ }).replace(SECRET_QUERY_RE, "$1[redacted]").replace(URL_CREDENTIAL_RE, "$1[redacted]@").replace(BOOTSTRAP_RE, "/bootstrap/[redacted]").replace(KNOWN_TOKEN_RE, "[redacted]").replace(PATH_RE, "[path]"), maxChars);
1106
+ }
1107
+ function sanitizePluginLogContext(input) {
1108
+ if (!input) return void 0;
1109
+ const output = {};
1110
+ for (const [key, value] of Object.entries(input).slice(0, 32)) {
1111
+ if (value === void 0 || !CONTEXT_KEY_RE.test(key)) continue;
1112
+ if (typeof value === "number" && !Number.isFinite(value)) continue;
1113
+ if (SECRET_KEY_RE.test(key)) {
1114
+ output[key] = "[redacted]";
1115
+ continue;
1116
+ }
1117
+ output[key] = typeof value === "string" ? sanitizePluginLogText(value, 1e3) : value;
1118
+ }
1119
+ if (!Object.keys(output).length) return void 0;
1120
+ if (JSON.stringify(output).length <= 8e3) return output;
1121
+ return {
1122
+ omitted: true,
1123
+ reason: "Diagnostic context exceeded its size limit."
1124
+ };
1125
+ }
1126
+ function sanitizePluginLogError(error) {
1127
+ if (error === void 0) return void 0;
1128
+ const row = error && typeof error === "object" ? error : {};
1129
+ const rawMessage = typeof row.message === "string" ? row.message : String(error);
1130
+ const result = {
1131
+ name: typeof row.name === "string" && row.name.trim() ? truncateLogText(row.name.trim(), 100) : "Error",
1132
+ message: sanitizePluginLogText(rawMessage) || "Unknown error"
1133
+ };
1134
+ if (typeof row.stack === "string" && row.stack.trim()) result.stack = sanitizePluginLogText(row.stack, 24e3);
1135
+ if (typeof row.code === "string" && /^[A-Za-z0-9_./:-]{1,100}$/.test(row.code)) result.code = row.code;
1136
+ else if (typeof row.code === "number" && Number.isFinite(row.code)) result.code = String(row.code);
1137
+ return result;
1138
+ }
1139
+ function mergePluginLogContext(base, extra) {
1140
+ if (!base) return extra;
1141
+ if (!extra) return base;
1142
+ return {
1143
+ ...base,
1144
+ ...extra
1145
+ };
1146
+ }
1147
+ function buildPluginLogEntry(input, meta) {
1148
+ const entry = {
1149
+ version: 1,
1150
+ at: meta.at.toISOString(),
1151
+ level: isPluginLogLevel(input.level) ? input.level : "info",
1152
+ source: normalizePluginLogSource(meta.source),
1153
+ surface: meta.surface,
1154
+ ...meta.profile ? { profile: meta.profile } : {},
1155
+ event: normalizePluginLogEvent(input.event),
1156
+ message: sanitizePluginLogText(input.message || "No diagnostic message was provided.", PLUGIN_LOG_MAX_MESSAGE_CHARS),
1157
+ ...meta.pid !== void 0 ? { pid: meta.pid } : {}
1158
+ };
1159
+ const context = sanitizePluginLogContext(mergePluginLogContext(meta.correlation, input.context));
1160
+ if (context) entry.context = context;
1161
+ const error = sanitizePluginLogError(input.error);
1162
+ if (error) entry.error = error;
1163
+ return entry;
1164
+ }
1165
+ function createChildPluginLogger(parent, bound) {
1166
+ return {
1167
+ get source() {
1168
+ return parent.source;
1169
+ },
1170
+ get file() {
1171
+ return parent.file;
1172
+ },
1173
+ get lastWriteError() {
1174
+ return parent.lastWriteError;
1175
+ },
1176
+ debug: (event, message, context) => parent.debug(event, message, mergePluginLogContext(bound, context)),
1177
+ info: (event, message, context) => parent.info(event, message, mergePluginLogContext(bound, context)),
1178
+ warn: (event, message, error, context) => parent.warn(event, message, error, mergePluginLogContext(bound, context)),
1179
+ error: (event, message, error, context) => parent.error(event, message, error, mergePluginLogContext(bound, context)),
1180
+ fault: (event, message, error, context) => parent.fault(event, message, error, mergePluginLogContext(bound, context)),
1181
+ write: (input) => parent.write({
1182
+ ...input,
1183
+ context: mergePluginLogContext(bound, input.context)
1184
+ }),
1185
+ child: (next) => createChildPluginLogger(parent, mergePluginLogContext(bound, next) ?? {})
1186
+ };
1187
+ }
1188
+ function truncateLogText(text, maxChars) {
1189
+ if (text.length <= maxChars) return text;
1190
+ const marker = "[truncated]\n";
1191
+ if (maxChars <= 12) return text.slice(0, Math.max(0, maxChars));
1192
+ return `${marker}${text.slice(-(maxChars - 12))}`;
1193
+ }
1194
+ //#endregion
1195
+ //#region ../dsh-plugin-kit/src/client-logger.ts
1196
+ const CLIENT_PLUGIN_LOG_GLOBAL = "__KLARKXY_PLUGIN_LOGS__";
1197
+ function readStorageFlag(key) {
1198
+ try {
1199
+ const value = (globalThis.window?.localStorage)?.getItem?.(key);
1200
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
1201
+ } catch {
1202
+ return;
1203
+ }
1204
+ }
1205
+ function sharedClientBuffer(limit) {
1206
+ const carrier = globalThis;
1207
+ if (!Array.isArray(carrier["__KLARKXY_PLUGIN_LOGS__"])) try {
1208
+ carrier[CLIENT_PLUGIN_LOG_GLOBAL] = [];
1209
+ } catch {
1210
+ return [];
1211
+ }
1212
+ const list = carrier[CLIENT_PLUGIN_LOG_GLOBAL];
1213
+ const overflow = list.length - limit;
1214
+ if (overflow > 0) list.splice(0, overflow);
1215
+ return list;
1216
+ }
1217
+ function createClientPluginLogger(source, ctx, options = {}) {
1218
+ const normalized = normalizePluginLogSource(source);
1219
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
1220
+ const limit = Math.max(10, options.bufferLimit ?? 500);
1221
+ const browserCtx = ctx && typeof ctx === "object" ? ctx : void 0;
1222
+ const resolveMinimum = () => {
1223
+ if (options.minimumLevel) return options.minimumLevel;
1224
+ return parsePluginLogLevel(readStorageFlag("dsh-plugin-log-level"), normalized, DEFAULT_PLUGIN_LOG_LEVEL);
1225
+ };
1226
+ const logger = {
1227
+ source: normalized,
1228
+ file: null,
1229
+ lastWriteError: null,
1230
+ debug: (event, message, context) => logger.write({
1231
+ level: "debug",
1232
+ event,
1233
+ message,
1234
+ ...context ? { context } : {}
1235
+ }),
1236
+ info: (event, message, context) => logger.write({
1237
+ level: "info",
1238
+ event,
1239
+ message,
1240
+ ...context ? { context } : {}
1241
+ }),
1242
+ warn: (event, message, error, context) => logger.write({
1243
+ level: "warn",
1244
+ event,
1245
+ message,
1246
+ ...error !== void 0 ? { error } : {},
1247
+ ...context ? { context } : {}
1248
+ }),
1249
+ error: (event, message, error, context) => logger.write({
1250
+ level: "error",
1251
+ event,
1252
+ message,
1253
+ ...error !== void 0 ? { error } : {},
1254
+ ...context ? { context } : {}
1255
+ }),
1256
+ fault: (event, message, error, context) => logger.write({
1257
+ level: "fault",
1258
+ event,
1259
+ message,
1260
+ ...error !== void 0 ? { error } : {},
1261
+ ...context ? { context } : {}
1262
+ }),
1263
+ child: (context) => createChildPluginLogger(logger, context),
1264
+ write(input) {
1265
+ try {
1266
+ if (!pluginLogLevelEnabled(input.level, resolveMinimum())) return false;
1267
+ const entry = buildPluginLogEntry(input, {
1268
+ at: now(),
1269
+ source: normalized,
1270
+ surface: "client"
1271
+ });
1272
+ const buffer = sharedClientBuffer(limit);
1273
+ buffer.push(entry);
1274
+ const overflow = buffer.length - limit;
1275
+ if (overflow > 0) buffer.splice(0, overflow);
1276
+ if (options.forwardToHostLogger !== false && browserCtx?.logger) {
1277
+ const summary = sanitizePluginLogText(`${normalized} [${entry.event}]: ${entry.message}`, 500);
1278
+ if (entry.level === "warn" && typeof browserCtx.logger.warn === "function") browserCtx.logger.warn(summary);
1279
+ else if ((entry.level === "error" || entry.level === "fault") && typeof browserCtx.logger.error === "function") browserCtx.logger.error(summary);
1280
+ }
1281
+ if (readStorageFlag("dsh-plugin-log-console") === "1") {
1282
+ const con = globalThis.console;
1283
+ (entry.level === "debug" ? con?.debug : entry.level === "info" ? con?.info : entry.level === "warn" ? con?.warn : con?.error)?.call(con, `[${normalized}] ${entry.event}: ${entry.message}`, entry);
1284
+ }
1285
+ return true;
1286
+ } catch {
1287
+ return false;
1288
+ }
1289
+ }
1290
+ };
1291
+ return logger;
1292
+ }
1293
+ function installClientPluginLogger(ctx, source, options = {}) {
1294
+ const logger = createClientPluginLogger(source, ctx, options);
1295
+ logger.info("client.loaded", "Client plugin initialized.", options.loadedContext);
1296
+ const browserCtx = ctx && typeof ctx === "object" ? ctx : void 0;
1297
+ if (typeof browserCtx?.effect === "function") try {
1298
+ browserCtx.effect(() => () => {
1299
+ logger.debug("client.disposed", "Client plugin disposed.");
1300
+ }, `${logger.source}.client-logger`);
1301
+ } catch {}
1302
+ return logger;
1303
+ }
1304
+ //#endregion
1305
+ //#region src/contracts.ts
1306
+ const PLUGIN_NAME = "@klarkxy/dsh-deep-view";
1307
+ const RPC_CHANNEL = "/dsh-deep-view";
1308
+ const VIEW_OWNER = "dsh-deep-view";
1309
+ function isRecord(value) {
1310
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1311
+ }
1312
+ //#endregion
1313
+ //#region src/ui.tsx
1314
+ const english = (key, params) => key.replace(/\{(\w+)\}/g, (_, name) => String(params?.[name] ?? `{${name}}`));
1315
+ function LineIcon({ paths }) {
1316
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1317
+ width: "15",
1318
+ height: "15",
1319
+ viewBox: "0 0 20 20",
1320
+ fill: "none",
1321
+ stroke: "currentColor",
1322
+ strokeWidth: "1.5",
1323
+ strokeLinecap: "round",
1324
+ strokeLinejoin: "round",
1325
+ "aria-hidden": "true",
1326
+ children: paths.map((d, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d }, index))
1327
+ });
1328
+ }
1329
+ const DotsIcon = () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LineIcon, { paths: ["M5 10h.01M10 10h.01M15 10h.01"] });
1330
+ const CheckIcon = () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LineIcon, { paths: ["M4.5 10.5l3.5 3.5 7.5-8"] });
1331
+ const CopyIcon = () => /* @__PURE__ */ (0, react_jsx_runtime.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"] });
1332
+ const TableIcon = () => /* @__PURE__ */ (0, react_jsx_runtime.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"] });
1333
+ const InfoIcon = () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LineIcon, { paths: [
1334
+ "M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z",
1335
+ "M10 9.25v4",
1336
+ "M10 6.6h.01"
1337
+ ] });
1338
+ const LinkIcon = () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1339
+ width: "12",
1340
+ height: "12",
1341
+ viewBox: "0 0 20 20",
1342
+ fill: "none",
1343
+ stroke: "currentColor",
1344
+ strokeWidth: "1.8",
1345
+ strokeLinecap: "round",
1346
+ strokeLinejoin: "round",
1347
+ "aria-hidden": "true",
1348
+ children: [
1349
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8.5 11.5l3-3" }),
1350
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M7 9.5L5.5 11a2.5 2.5 0 0 0 3.5 3.5L10.5 13" }),
1351
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M13 10.5l1.5-1.5A2.5 2.5 0 0 0 11 5.5L9.5 7" })
1352
+ ]
1353
+ });
1354
+ const keyLetter = (index) => String.fromCharCode(65 + index);
1355
+ function Segmented({ label, value, className, children }) {
1356
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1357
+ className: className ? `dvui-seg ${className}` : "dvui-seg",
1358
+ role: "group",
1359
+ "aria-label": label,
1360
+ "data-active": value,
1361
+ children
1362
+ });
1363
+ }
1364
+ //#endregion
1365
+ //#region src/chart.tsx
1366
+ const PALETTE = [
1367
+ "var(--dsw-alias-state-business-primary)",
1368
+ "var(--dsw-alias-brand-primary)",
1369
+ "var(--dsw-alias-state-success-primary)",
1370
+ "var(--dsw-alias-state-warn-primary)",
1371
+ "var(--dsw-alias-state-error-primary)"
1372
+ ];
1373
+ const colorAt = (index) => PALETTE[index % PALETTE.length];
1374
+ const TICK = 10;
1375
+ const LABEL = 11;
1376
+ const textWidth = (text, size) => [...String(text)].reduce((sum, char) => sum + (char.charCodeAt(0) > 11904 ? size : size * .58), 0);
1377
+ const fit = (text, room, size) => {
1378
+ if (textWidth(text, size) <= room) return String(text);
1379
+ let out = "";
1380
+ for (const char of String(text)) {
1381
+ if (textWidth(`${out}${char}…`, size) > room) break;
1382
+ out += char;
1383
+ }
1384
+ return out === "" ? "" : `${out}…`;
1385
+ };
1386
+ function formatNumber(value) {
1387
+ if (!Number.isFinite(value)) return asText(value);
1388
+ const abs = Math.abs(value);
1389
+ const digits = abs >= 100 ? 0 : abs >= 10 ? 1 : 2;
1390
+ return Number(value.toFixed(digits)).toLocaleString("en-US", { maximumFractionDigits: digits });
1391
+ }
1392
+ const withUnit$1 = (value, unit) => unit ? unit === "%" ? `${formatNumber(value)}%` : `${formatNumber(value)} ${unit}` : formatNumber(value);
1393
+ function niceTicks(low, high) {
1394
+ let lo = low;
1395
+ let hi = high;
1396
+ if (lo === hi) {
1397
+ lo = Math.min(0, lo);
1398
+ hi = hi === 0 ? 1 : Math.max(0, hi);
1399
+ }
1400
+ if (lo === hi) hi = lo + 1;
1401
+ const raw = (hi - lo) / 4;
1402
+ const magnitude = 10 ** Math.floor(Math.log10(raw));
1403
+ const step = [
1404
+ 1,
1405
+ 2,
1406
+ 2.5,
1407
+ 5,
1408
+ 10
1409
+ ].map((scale) => scale * magnitude).find((candidate) => candidate >= raw);
1410
+ const start = Math.floor(lo / step + 1e-9) * step;
1411
+ const end = Math.ceil(hi / step - 1e-9) * step;
1412
+ const ticks = [];
1413
+ for (let value = start; value <= end + step / 2; value += step) ticks.push(Number(value.toPrecision(12)));
1414
+ return ticks;
1415
+ }
1416
+ function chartData(node, lookup) {
1417
+ const categories = (Array.isArray(node.categories) ? node.categories : []).map(asText);
1418
+ const series = (Array.isArray(node.series) ? node.series : []).map((item, index) => ({
1419
+ name: typeof item?.name === "string" ? item.name : "",
1420
+ color: colorAt(index),
1421
+ values: categories.map((_, at) => {
1422
+ const value = Number(resolve(Array.isArray(item?.values) ? item.values[at] : void 0, lookup, 0));
1423
+ return Number.isFinite(value) ? value : 0;
1424
+ })
1425
+ }));
1426
+ return {
1427
+ kind: node.kind ?? "bar",
1428
+ categories,
1429
+ series,
1430
+ unit: typeof node.unit === "string" ? node.unit : ""
1431
+ };
1432
+ }
1433
+ const csvCell = (value) => /[",\n]/.test(value) ? `"${value.replace(/"/g, "\"\"")}"` : value;
1434
+ function chartCsv({ categories, series, unit }, t = english) {
1435
+ 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");
1436
+ }
1437
+ function valueAxis(values, zero) {
1438
+ const finite = values.length > 0 ? values : [0];
1439
+ const low = Math.min(...finite);
1440
+ const high = Math.max(...finite);
1441
+ if (zero) return niceTicks(Math.min(0, low), Math.max(0, high));
1442
+ const pad = (high - low) * .1 || Math.abs(high) * .1 || 1;
1443
+ return niceTicks(low - pad, high + pad);
1444
+ }
1445
+ function columns({ categories, series, unit, kind }, width, active, shown) {
1446
+ const visible = series.filter((_, index) => shown[index]);
1447
+ const ticks = valueAxis(visible.flatMap((item) => item.values), kind !== "line");
1448
+ const lo = ticks[0];
1449
+ const hi = ticks.at(-1);
1450
+ const left = Math.ceil(Math.max(...ticks.map((tick) => textWidth(withUnit$1(tick, unit), TICK)))) + 10;
1451
+ const top = 8;
1452
+ const height = 220;
1453
+ const plot = {
1454
+ x: left,
1455
+ y: top,
1456
+ w: Math.max(40, width - left - 4),
1457
+ h: 188
1458
+ };
1459
+ const y = (value) => plot.y + plot.h - (value - lo) / (hi - lo) * plot.h;
1460
+ const band = plot.w / categories.length;
1461
+ const parts = [];
1462
+ for (const tick of ticks) {
1463
+ parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
1464
+ className: "dvui-chart-grid",
1465
+ x1: plot.x,
1466
+ x2: plot.x + plot.w,
1467
+ y1: y(tick),
1468
+ y2: y(tick)
1469
+ }, `g${tick}`));
1470
+ parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
1471
+ className: "dvui-chart-tick",
1472
+ x: plot.x - 8,
1473
+ y: y(tick) + 3,
1474
+ textAnchor: "end",
1475
+ children: withUnit$1(tick, unit)
1476
+ }, `t${tick}`));
1477
+ }
1478
+ const points = [];
1479
+ categories.forEach((label, at) => {
1480
+ const cx = plot.x + band * at + band / 2;
1481
+ parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
1482
+ className: "dvui-chart-label",
1483
+ x: cx,
1484
+ y: 213,
1485
+ textAnchor: "middle",
1486
+ children: fit(label, band - 4, LABEL)
1487
+ }, `c${at}`));
1488
+ points.push({
1489
+ x: cx,
1490
+ y: y(Math.max(...visible.map((item) => item.values[at]), lo))
1491
+ });
1492
+ });
1493
+ if (kind === "bar") {
1494
+ const group = band * .72;
1495
+ const gap = visible.length > 1 ? 2 : 0;
1496
+ const barW = Math.max(2, (group - gap * (visible.length - 1)) / visible.length);
1497
+ const zero = y(Math.max(lo, Math.min(0, hi)));
1498
+ categories.forEach((_, at) => visible.forEach((item, row) => {
1499
+ const value = item.values[at];
1500
+ const x = plot.x + band * at + (band - group) / 2 + row * (barW + gap);
1501
+ const top2 = Math.min(y(value), zero);
1502
+ const tall = Math.max(1, Math.abs(y(value) - zero));
1503
+ const r = Math.min(4, barW / 2, tall);
1504
+ 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`;
1505
+ parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1506
+ className: "dvui-chart-mark",
1507
+ "data-dim": active !== null && active !== at ? "" : void 0,
1508
+ d,
1509
+ style: { fill: item.color }
1510
+ }, `b${at}-${row}`));
1511
+ }));
1512
+ } else {
1513
+ const xs = categories.map((_, at) => plot.x + band * at + band / 2);
1514
+ visible.forEach((item, row) => {
1515
+ const line = item.values.map((value, at) => `${at === 0 ? "M" : "L"}${xs[at]},${y(value)}`).join("");
1516
+ if (kind === "area") {
1517
+ const base = y(Math.max(lo, Math.min(0, hi)));
1518
+ parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1519
+ className: "dvui-chart-area",
1520
+ d: `${line}L${xs.at(-1)},${base}L${xs[0]},${base}z`,
1521
+ style: { fill: item.color }
1522
+ }, `a${row}`));
1523
+ }
1524
+ parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1525
+ className: "dvui-chart-line",
1526
+ d: line,
1527
+ style: { stroke: item.color }
1528
+ }, `l${row}`));
1529
+ item.values.forEach((value, at) => parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
1530
+ className: "dvui-chart-dot",
1531
+ cx: xs[at],
1532
+ cy: y(value),
1533
+ r: active === at ? 4 : 2.5,
1534
+ style: { fill: item.color }
1535
+ }, `p${row}-${at}`)));
1536
+ });
1537
+ if (active !== null) parts.unshift(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
1538
+ className: "dvui-chart-guide",
1539
+ x1: xs[active],
1540
+ x2: xs[active],
1541
+ y1: plot.y,
1542
+ y2: plot.y + plot.h
1543
+ }, "guide"));
1544
+ }
1545
+ return {
1546
+ parts,
1547
+ height,
1548
+ points,
1549
+ hits: categories.map((_, at) => ({
1550
+ x: plot.x + band * at,
1551
+ y: plot.y,
1552
+ w: band,
1553
+ h: plot.h
1554
+ }))
1555
+ };
1556
+ }
1557
+ function rows({ categories, series, unit }, width, active, shown) {
1558
+ const visible = series.filter((_, index) => shown[index]);
1559
+ const ticks = valueAxis(visible.flatMap((item) => item.values), true);
1560
+ const lo = ticks[0];
1561
+ const hi = ticks.at(-1);
1562
+ const labelW = Math.min(width * .38, Math.ceil(Math.max(...categories.map((label) => textWidth(label, LABEL)))) + 10);
1563
+ const valueW = Math.ceil(Math.max(...visible.flatMap((item) => item.values).map((value) => textWidth(withUnit$1(value, unit), LABEL)))) + 10;
1564
+ const barH = 14;
1565
+ const rowH = 10 + visible.length * barH + (visible.length - 1) * 2;
1566
+ const top = 4;
1567
+ const height = top + rowH * categories.length + 22;
1568
+ const plot = {
1569
+ x: labelW,
1570
+ y: top,
1571
+ w: Math.max(40, width - labelW - valueW),
1572
+ h: rowH * categories.length
1573
+ };
1574
+ const x = (value) => plot.x + (value - lo) / (hi - lo) * plot.w;
1575
+ const parts = [];
1576
+ for (const tick of ticks) {
1577
+ parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
1578
+ className: "dvui-chart-grid",
1579
+ x1: x(tick),
1580
+ x2: x(tick),
1581
+ y1: plot.y,
1582
+ y2: plot.y + plot.h
1583
+ }, `g${tick}`));
1584
+ parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
1585
+ className: "dvui-chart-tick",
1586
+ x: x(tick),
1587
+ y: height - 6,
1588
+ textAnchor: "middle",
1589
+ children: withUnit$1(tick, unit)
1590
+ }, `t${tick}`));
1591
+ }
1592
+ const points = [];
1593
+ const zero = x(Math.max(lo, Math.min(0, hi)));
1594
+ categories.forEach((label, at) => {
1595
+ const rowY = plot.y + rowH * at + 5;
1596
+ parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
1597
+ className: "dvui-chart-label",
1598
+ x: labelW - 10,
1599
+ y: rowY + (rowH - 10) / 2 + 4,
1600
+ textAnchor: "end",
1601
+ children: fit(label, labelW - 12, LABEL)
1602
+ }, `c${at}`));
1603
+ visible.forEach((item, row) => {
1604
+ const value = item.values[at];
1605
+ const by = rowY + row * 16;
1606
+ const left = Math.min(x(value), zero);
1607
+ const long = Math.max(1, Math.abs(x(value) - zero));
1608
+ parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
1609
+ className: "dvui-chart-mark",
1610
+ "data-dim": active !== null && active !== at ? "" : void 0,
1611
+ x: left,
1612
+ y: by,
1613
+ width: long,
1614
+ height: barH,
1615
+ rx: Math.min(4, long / 2),
1616
+ style: { fill: item.color }
1617
+ }, `b${at}-${row}`));
1618
+ parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
1619
+ className: "dvui-chart-value",
1620
+ x: left + long + 6,
1621
+ y: by + barH - 3,
1622
+ children: withUnit$1(value, unit)
1623
+ }, `v${at}-${row}`));
1624
+ });
1625
+ points.push({
1626
+ x: Math.max(...visible.map((item) => x(item.values[at])), zero),
1627
+ y: rowY
1628
+ });
1629
+ });
1630
+ return {
1631
+ parts,
1632
+ height,
1633
+ points,
1634
+ hits: categories.map((_, at) => ({
1635
+ x: 0,
1636
+ y: plot.y + rowH * at,
1637
+ w: width,
1638
+ h: rowH
1639
+ }))
1640
+ };
1641
+ }
1642
+ function donut({ categories, series }, width, active, hover) {
1643
+ const values = (series[0]?.values ?? []).map((value) => Math.max(0, value));
1644
+ const total = values.reduce((sum, value) => sum + value, 0);
1645
+ const size = Math.min(180, Math.max(120, width * .42));
1646
+ const r = size / 2;
1647
+ const inner = r * .62;
1648
+ const cx = r;
1649
+ const cy = r;
1650
+ const parts = [];
1651
+ const points = [];
1652
+ let angle = -Math.PI / 2;
1653
+ values.forEach((value, at) => {
1654
+ const sweep = total > 0 ? value / total * Math.PI * 2 : 0;
1655
+ const end = angle + sweep;
1656
+ const mid = angle + sweep / 2;
1657
+ points.push({
1658
+ x: cx + Math.cos(mid) * (r - (r - inner) / 2),
1659
+ y: cy + Math.sin(mid) * (r - (r - inner) / 2)
1660
+ });
1661
+ if (sweep > 0) {
1662
+ const large = sweep > Math.PI ? 1 : 0;
1663
+ const full = sweep >= Math.PI * 2 - 1e-6;
1664
+ const p = (radius, theta) => `${cx + Math.cos(theta) * radius},${cy + Math.sin(theta) * radius}`;
1665
+ 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`;
1666
+ parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1667
+ className: "dvui-chart-mark",
1668
+ "data-dim": active !== null && active !== at ? "" : void 0,
1669
+ d,
1670
+ fillRule: "evenodd",
1671
+ style: { fill: colorAt(at) },
1672
+ onPointerEnter: () => hover(at)
1673
+ }, `s${at}`));
1674
+ }
1675
+ angle = end;
1676
+ });
1677
+ return {
1678
+ parts,
1679
+ height: size,
1680
+ size,
1681
+ points,
1682
+ total,
1683
+ values,
1684
+ hits: []
1685
+ };
1686
+ }
1687
+ function useWidth(box, initial) {
1688
+ const [width, setWidth] = (0, react.useState)(initial);
1689
+ (0, react.useLayoutEffect)(() => {
1690
+ const node = box.current;
1691
+ if (!node || typeof ResizeObserver !== "function") return void 0;
1692
+ const measure = () => {
1693
+ const next = Math.round(node.clientWidth);
1694
+ if (next > 0) setWidth(next);
1695
+ };
1696
+ measure();
1697
+ const observer = new ResizeObserver(measure);
1698
+ observer.observe(node);
1699
+ return () => observer.disconnect();
1700
+ }, [box]);
1701
+ return width;
1702
+ }
1703
+ function ChartTable({ data, t = english }) {
1704
+ const { categories, series, unit } = data;
1705
+ const heads = series.map((item, index) => item.name || (series.length === 1 ? unit || t("Value") : `${t("Series")} ${index + 1}`));
1706
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1707
+ className: "dvui-table",
1708
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { scope: "col" }), heads.map((head, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
1709
+ scope: "col",
1710
+ className: "dvui-num",
1711
+ children: head
1712
+ }, index))] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: categories.map((label, at) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
1713
+ scope: "row",
1714
+ children: label
1715
+ }), series.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
1716
+ className: "dvui-num",
1717
+ children: withUnit$1(item.values[at], series.length === 1 && !item.name ? "" : unit)
1718
+ }, index))] }, `${at}-${label}`)) })] })
1719
+ });
1720
+ }
1721
+ function WidgetChart({ node, lookup, registerChart, refreshCharts, t = english }) {
1722
+ const data = chartData(node, lookup);
1723
+ const { kind, categories, series, unit } = data;
1724
+ const box = (0, react.useRef)(null);
1725
+ const width = useWidth(box, 520);
1726
+ const [active, setActive] = (0, react.useState)(null);
1727
+ const [hidden, setHidden] = (0, react.useState)([]);
1728
+ const [asTable, setAsTable] = (0, react.useState)(false);
1729
+ const [copied, setCopied] = (0, react.useState)(false);
1730
+ const labelId = (0, react.useId)();
1731
+ const menuKey = (0, react.useId)();
1732
+ const table = (0, react.useRef)(asTable);
1733
+ table.current = asTable;
1734
+ const shown = series.map((_, index) => !hidden.includes(index));
1735
+ const label = typeof node.caption === "string" && node.caption !== "" ? node.caption : t("Chart");
1736
+ const isDonut = kind === "donut";
1737
+ const legendBeside = isDonut && width >= 360;
1738
+ const drawn = isDonut ? donut(data, legendBeside ? width - 200 : width, active, setActive) : kind === "hbar" ? rows(data, width, active, shown) : columns(data, width, active, shown);
1739
+ const svgWidth = isDonut ? drawn.size : width;
1740
+ const move = (event) => {
1741
+ const keys = {
1742
+ ArrowRight: 1,
1743
+ ArrowDown: 1,
1744
+ ArrowLeft: -1,
1745
+ ArrowUp: -1
1746
+ };
1747
+ if (event.key === "Escape") {
1748
+ setActive(null);
1749
+ return;
1750
+ }
1751
+ if (keys[event.key] === void 0) return;
1752
+ event.preventDefault();
1753
+ setActive((current) => current === null ? 0 : (current + keys[event.key] + categories.length) % categories.length);
1754
+ };
1755
+ const point = active === null ? null : drawn.points[active];
1756
+ 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}%`;
1757
+ const tipRows = active === null || isDonut ? [] : series.filter((_, index) => shown[index]).map((item) => ({
1758
+ color: item.color,
1759
+ name: item.name,
1760
+ value: withUnit$1(item.values[active], unit)
1761
+ }));
1762
+ const copy = () => {
1763
+ navigator.clipboard?.writeText(chartCsv(data, t)).then(() => {
1764
+ setCopied(true);
1765
+ setTimeout(() => setCopied(false), 1200);
1766
+ }).catch(() => {});
1767
+ };
1768
+ const toggleTable = () => {
1769
+ table.current = !table.current;
1770
+ setAsTable(table.current);
1771
+ };
1772
+ const action = (0, react.useRef)({
1773
+ label: null,
1774
+ asTable: false,
1775
+ toggleTable,
1776
+ copy
1777
+ });
1778
+ action.current = {
1779
+ label: typeof node.caption === "string" && node.caption !== "" ? node.caption : null,
1780
+ get asTable() {
1781
+ return table.current;
1782
+ },
1783
+ toggleTable,
1784
+ copy
1785
+ };
1786
+ (0, react.useLayoutEffect)(() => registerChart?.(menuKey, {
1787
+ get label() {
1788
+ return action.current.label;
1789
+ },
1790
+ get asTable() {
1791
+ return action.current.asTable;
1792
+ },
1793
+ toggleTable: () => action.current.toggleTable(),
1794
+ copy: () => action.current.copy()
1795
+ }), [registerChart, menuKey]);
1796
+ (0, react.useLayoutEffect)(() => {
1797
+ refreshCharts?.();
1798
+ }, [
1799
+ refreshCharts,
1800
+ asTable,
1801
+ label
1802
+ ]);
1803
+ const legend = isDonut ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1804
+ className: "dvui-chart-legend dvui-chart-legend-list",
1805
+ children: categories.map((name, at) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1806
+ className: "dvui-chart-key",
1807
+ "data-dim": active !== null && active !== at ? "" : void 0,
1808
+ onPointerEnter: () => setActive(at),
1809
+ onPointerLeave: () => setActive(null),
1810
+ children: [
1811
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { style: { background: colorAt(at) } }),
1812
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1813
+ className: "dvui-chart-key-name",
1814
+ children: name
1815
+ }),
1816
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1817
+ className: "dvui-chart-key-value",
1818
+ children: share(at)
1819
+ })
1820
+ ]
1821
+ }, `${at}-${name}`))
1822
+ }) : series.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1823
+ className: "dvui-chart-legend",
1824
+ children: series.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1825
+ type: "button",
1826
+ className: "dvui-chart-key",
1827
+ "aria-pressed": shown[index],
1828
+ onClick: () => setHidden((before) => before.includes(index) ? before.filter((item) => item !== index) : shown.filter(Boolean).length > 1 ? [...before, index] : before),
1829
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { style: { background: item.color } }), item.name || `${t("Series")} ${index + 1}`]
1830
+ }, `${index}-${item.name}`))
1831
+ }) : null;
1832
+ const plot = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1833
+ className: "dvui-chart-plot",
1834
+ style: isDonut ? { width: svgWidth } : void 0,
1835
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1836
+ className: "dvui-chart-svg",
1837
+ width: svgWidth,
1838
+ height: drawn.height,
1839
+ viewBox: `0 0 ${svgWidth} ${drawn.height}`,
1840
+ role: "img",
1841
+ "aria-labelledby": labelId,
1842
+ tabIndex: 0,
1843
+ onKeyDown: move,
1844
+ onBlur: () => setActive(null),
1845
+ onPointerLeave: () => setActive(null),
1846
+ children: [drawn.parts, isDonut ? active === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
1847
+ className: "dvui-chart-center",
1848
+ x: svgWidth / 2,
1849
+ y: drawn.height / 2 + 5,
1850
+ textAnchor: "middle",
1851
+ children: unit === "%" ? "" : withUnit$1(drawn.total, unit)
1852
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
1853
+ className: "dvui-chart-center-name",
1854
+ x: svgWidth / 2,
1855
+ y: drawn.height / 2 - 4,
1856
+ textAnchor: "middle",
1857
+ children: fit(categories[active], drawn.size * .56, LABEL)
1858
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
1859
+ className: "dvui-chart-center",
1860
+ x: svgWidth / 2,
1861
+ y: drawn.height / 2 + 14,
1862
+ textAnchor: "middle",
1863
+ children: share(active)
1864
+ })] }) : drawn.hits.map((hit, at) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
1865
+ className: "dvui-chart-hit",
1866
+ x: hit.x,
1867
+ y: hit.y,
1868
+ width: hit.w,
1869
+ height: hit.h,
1870
+ onPointerEnter: () => setActive(at)
1871
+ }, `h${at}`))]
1872
+ }), point && !asTable && !isDonut ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1873
+ className: "dvui-chart-tip",
1874
+ role: "status",
1875
+ style: {
1876
+ left: point.x,
1877
+ top: point.y
1878
+ },
1879
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: categories[active] }), tipRows.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
1880
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { style: { background: row.color } }),
1881
+ row.name ? `${row.name} ` : "",
1882
+ row.value
1883
+ ] }, index))]
1884
+ }) : null]
1885
+ });
1886
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("figure", {
1887
+ ref: box,
1888
+ className: "dvui-chart",
1889
+ "data-kind": kind,
1890
+ children: [
1891
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1892
+ id: labelId,
1893
+ hidden: true,
1894
+ children: label
1895
+ }),
1896
+ asTable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartTable, {
1897
+ data,
1898
+ t
1899
+ }) : isDonut ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1900
+ className: legendBeside ? "dvui-chart-donut dvui-chart-donut-beside" : "dvui-chart-donut",
1901
+ onPointerLeave: () => setActive(null),
1902
+ children: [plot, legend]
1903
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [legend, plot] }),
1904
+ copied ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1905
+ className: "dvui-chart-copied",
1906
+ role: "status",
1907
+ children: t("Copied")
1908
+ }) : null,
1909
+ typeof node.caption === "string" && node.caption !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("figcaption", {
1910
+ className: "dvui-caption",
1911
+ children: node.caption
1912
+ }) : null
1913
+ ]
1914
+ });
1915
+ }
1916
+ //#endregion
1917
+ //#region src/widget.tsx
1918
+ const TONES = [
1919
+ "good",
1920
+ "warn",
1921
+ "bad"
1922
+ ];
1923
+ const CALLOUT_TONES = [
1924
+ "info",
1925
+ "good",
1926
+ "warn",
1927
+ "bad"
1928
+ ];
1929
+ const optionsOf = (node) => Array.isArray(node.options) ? node.options : [];
1930
+ const fieldsOf = (node) => Array.isArray(node.fields) ? node.fields : [];
1931
+ const cellText = (value) => value === null || value === void 0 ? "" : typeof value === "object" ? JSON.stringify(value) : asText(value);
1932
+ const NUMERIC_CELL = /^[-+]?[\d.,]+\s?(%|[a-zA-Z]{1,4})?$/;
1933
+ function hasType(node, types) {
1934
+ return types.has(node.type) || Array.isArray(node.children) && node.children.some((child) => hasType(child, types));
1935
+ }
1936
+ const answerValues = (answer, node) => answer != null && answer.action === node.id ? answer.values : void 0;
1937
+ const withUnit = (value, unit) => `${asText(value)}${unit ? ` ${unit}` : ""}`;
1938
+ const grouped = (value) => typeof value === "number" && Number.isFinite(value) ? value.toLocaleString("en-US", { maximumFractionDigits: 6 }) : asText(value);
1939
+ function FallbackMarkdown({ text }) {
1940
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: text });
1941
+ }
1942
+ function OptionsInput({ node, ctx, t }) {
1943
+ const [picked, setPicked] = (0, react.useState)([]);
1944
+ const options = optionsOf(node);
1945
+ const multi = node.multi === true;
1946
+ const choose = (label) => {
1947
+ if (ctx.answerLocked) return;
1948
+ if (multi) setPicked((current) => current.includes(label) ? current.filter((item) => item !== label) : [...current, label]);
1949
+ else ctx.onSubmit?.(node.id, { selected: [label] });
1950
+ };
1951
+ const onKey = (event) => {
1952
+ const index = event.key.toUpperCase().charCodeAt(0) - 65;
1953
+ if (index >= 0 && index < options.length && !event.ctrlKey && !event.metaKey && !event.altKey) {
1954
+ event.preventDefault();
1955
+ choose(options[index].label);
1956
+ }
1957
+ };
1958
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1959
+ className: "dvui-options",
1960
+ role: "group",
1961
+ "aria-label": t("Options"),
1962
+ onKeyDown: onKey,
1963
+ children: options.map((option, index) => {
1964
+ const selected = multi && picked.includes(option.label);
1965
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1966
+ type: "button",
1967
+ className: "dvui-option",
1968
+ disabled: ctx.answerLocked,
1969
+ "aria-pressed": multi ? selected : void 0,
1970
+ "aria-keyshortcuts": keyLetter(index).toLowerCase(),
1971
+ onClick: () => choose(option.label),
1972
+ children: [
1973
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1974
+ className: "dvui-key",
1975
+ "aria-hidden": "true",
1976
+ children: keyLetter(index)
1977
+ }),
1978
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1979
+ className: "dvui-option-body",
1980
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1981
+ className: "dvui-option-label",
1982
+ children: option.label
1983
+ }), option.description ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1984
+ className: "dvui-option-desc",
1985
+ children: option.description
1986
+ }) : null]
1987
+ }),
1988
+ selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1989
+ className: "dvui-check",
1990
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, {})
1991
+ }) : null
1992
+ ]
1993
+ }, option.label);
1994
+ })
1995
+ }), multi ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1996
+ className: "dvui-actions",
1997
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1998
+ type: "button",
1999
+ className: "dvui-submit",
2000
+ disabled: ctx.answerLocked || picked.length === 0,
2001
+ onClick: () => ctx.onSubmit?.(node.id, { selected: picked }),
2002
+ children: t("Submit")
2003
+ })
2004
+ }) : null] });
2005
+ }
2006
+ function SettledOptions({ node, answer, t }) {
2007
+ const selected = answerValues(answer, node)?.selected;
2008
+ const chosen = Array.isArray(selected) ? selected : [];
2009
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2010
+ className: "dvui-options",
2011
+ role: "group",
2012
+ "aria-label": t("Your answer"),
2013
+ children: optionsOf(node).map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2014
+ className: "dvui-option",
2015
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2016
+ className: "dvui-option-body",
2017
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2018
+ className: "dvui-option-label",
2019
+ children: option.label
2020
+ }), option.description ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2021
+ className: "dvui-option-desc",
2022
+ children: option.description
2023
+ }) : null]
2024
+ }), chosen.includes(option.label) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2025
+ className: "dvui-check",
2026
+ role: "img",
2027
+ "aria-label": t("Selected"),
2028
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, {})
2029
+ }) : null]
2030
+ }, option.label))
2031
+ });
2032
+ }
2033
+ function ConfirmInput({ node, ctx, t }) {
2034
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2035
+ className: "dvui-actions",
2036
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2037
+ type: "button",
2038
+ className: "dvui-button",
2039
+ disabled: ctx.answerLocked,
2040
+ onClick: () => ctx.onSubmit?.(node.id, { confirmed: false }),
2041
+ children: node.cancelLabel ?? t("Cancel")
2042
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2043
+ type: "button",
2044
+ className: "dvui-submit",
2045
+ disabled: ctx.answerLocked,
2046
+ onClick: () => ctx.onSubmit?.(node.id, { confirmed: true }),
2047
+ children: node.confirmLabel ?? t("Confirm")
2048
+ })]
2049
+ });
2050
+ }
2051
+ function SettledConfirm({ node, answer, t }) {
2052
+ const confirmed = answerValues(answer, node)?.confirmed;
2053
+ if (typeof confirmed !== "boolean") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmInput, {
2054
+ node,
2055
+ ctx: { answerLocked: true },
2056
+ t
2057
+ });
2058
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2059
+ className: "dvui-badge",
2060
+ children: confirmed ? node.confirmLabel ?? t("Confirm") : node.cancelLabel ?? t("Cancel")
2061
+ });
2062
+ }
2063
+ function FormInput({ node, ctx, t }) {
2064
+ const fields = fieldsOf(node);
2065
+ const [draft, setDraft] = (0, react.useState)({});
2066
+ const submit = () => {
2067
+ if (!ctx.answerLocked) ctx.onSubmit?.(node.id, { fields: Object.fromEntries(fields.map((field) => [field.id, draft[field.id] ?? ""])) });
2068
+ };
2069
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
2070
+ className: "dvui-form",
2071
+ onSubmit: (event) => {
2072
+ event.preventDefault();
2073
+ submit();
2074
+ },
2075
+ children: [fields.map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2076
+ className: "dvui-field",
2077
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2078
+ className: "dvui-field-label",
2079
+ children: field.label
2080
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2081
+ className: "dvui-input",
2082
+ type: "text",
2083
+ value: draft[field.id] ?? "",
2084
+ autoComplete: "off",
2085
+ spellCheck: false,
2086
+ readOnly: ctx.answerLocked,
2087
+ "aria-label": field.label,
2088
+ placeholder: field.placeholder,
2089
+ onChange: (event) => setDraft((current) => ({
2090
+ ...current,
2091
+ [field.id]: event.target.value
2092
+ }))
2093
+ })]
2094
+ }, field.id)), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2095
+ className: "dvui-actions",
2096
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2097
+ type: "submit",
2098
+ className: "dvui-submit",
2099
+ disabled: ctx.answerLocked,
2100
+ children: node.submitLabel ?? t("Submit")
2101
+ })
2102
+ })]
2103
+ });
2104
+ }
2105
+ function SettledForm({ node, answer }) {
2106
+ const fields = answerValues(answer, node)?.fields;
2107
+ const values = fields !== null && typeof fields === "object" ? fields : {};
2108
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2109
+ className: "dvui-form",
2110
+ children: fieldsOf(node).map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2111
+ className: "dvui-field",
2112
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2113
+ className: "dvui-field-label",
2114
+ children: field.label
2115
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2116
+ className: "dvui-input",
2117
+ value: String(values[field.id] ?? ""),
2118
+ disabled: true,
2119
+ readOnly: true,
2120
+ "aria-label": field.label
2121
+ })]
2122
+ }, field.id))
2123
+ });
2124
+ }
2125
+ function WidgetTable({ node, lookup }) {
2126
+ const columns = Array.isArray(node.columns) ? node.columns : [];
2127
+ const rows = (Array.isArray(node.rows) ? node.rows : []).map((row) => (Array.isArray(row) ? row : []).map((cell) => resolve(cell, lookup, "")));
2128
+ const numeric = columns.map((_, index) => {
2129
+ const cells = rows.map((row) => row[index]).filter((cell) => cell !== void 0 && cell !== "");
2130
+ return cells.length > 0 && cells.every((cell) => typeof cell === "number" || typeof cell === "string" && NUMERIC_CELL.test(cell.trim()));
2131
+ });
2132
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2133
+ className: "dvui-table",
2134
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tr", { children: columns.map((column, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
2135
+ scope: "col",
2136
+ className: numeric[index] ? "dvui-num" : void 0,
2137
+ children: column.label
2138
+ }, column.key ?? index)) }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: rows.map((row, rowIndex) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tr", { children: columns.map((column, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
2139
+ className: numeric[index] ? "dvui-num" : void 0,
2140
+ children: cellText(row[index])
2141
+ }, column.key ?? index)) }, rowIndex)) })] })
2142
+ });
2143
+ }
2144
+ function WidgetProgress({ node, lookup, t }) {
2145
+ let percent;
2146
+ if (node.percent !== void 0) percent = Number(resolve(node.percent, lookup, 0));
2147
+ else {
2148
+ const max = Number(resolve(node.max, lookup, 0));
2149
+ percent = max > 0 ? Number(resolve(node.value, lookup, 0)) / max * 100 : 0;
2150
+ }
2151
+ percent = Math.max(0, Math.min(100, Number.isFinite(percent) ? percent : 0));
2152
+ const tone = resolve(node.tone, lookup);
2153
+ const right = node.valueLabel !== void 0 ? asText(resolve(node.valueLabel, lookup, "")) : `${Math.round(percent)}%`;
2154
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2155
+ className: "dvui-progress",
2156
+ "data-tone": TONES.includes(String(tone)) ? String(tone) : void 0,
2157
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2158
+ className: "dvui-progress-label",
2159
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: node.label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: right })]
2160
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2161
+ className: "dvui-progress-track",
2162
+ role: "progressbar",
2163
+ "aria-valuenow": Math.round(percent),
2164
+ "aria-valuemin": 0,
2165
+ "aria-valuemax": 100,
2166
+ "aria-label": node.label ?? t("Progress"),
2167
+ "aria-valuetext": right,
2168
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2169
+ className: "dvui-progress-fill",
2170
+ style: { width: `${percent}%` }
2171
+ })
2172
+ })]
2173
+ });
2174
+ }
2175
+ function WidgetMetric({ node, lookup }) {
2176
+ const tone = resolve(node.tone, lookup);
2177
+ const caption = node.caption === void 0 ? "" : asText(resolve(node.caption, lookup, ""));
2178
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2179
+ className: "dvui-metric",
2180
+ "data-tone": TONES.includes(String(tone)) ? String(tone) : void 0,
2181
+ children: [
2182
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2183
+ className: "dvui-metric-label",
2184
+ children: node.label
2185
+ }),
2186
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2187
+ className: "dvui-metric-value",
2188
+ children: asText(resolve(node.value, lookup, "—"))
2189
+ }),
2190
+ caption !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2191
+ className: "dvui-caption",
2192
+ children: caption
2193
+ }) : null
2194
+ ]
2195
+ });
2196
+ }
2197
+ function WidgetCallout({ node, lookup, Markdown, labels }) {
2198
+ const tone = resolve(node.tone, lookup);
2199
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2200
+ className: "dvui-callout",
2201
+ "data-tone": CALLOUT_TONES.includes(String(tone)) ? String(tone) : "info",
2202
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2203
+ className: "dvui-callout-icon",
2204
+ "aria-hidden": "true",
2205
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoIcon, {})
2206
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Markdown, {
2207
+ text: asText(resolve(node.text, lookup, "")),
2208
+ labels,
2209
+ variant: "compact"
2210
+ }) })]
2211
+ });
2212
+ }
2213
+ function WidgetSources({ node, t }) {
2214
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2215
+ className: "dvui-sources",
2216
+ role: "list",
2217
+ "aria-label": t("Sources"),
2218
+ children: (node.items ?? []).map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
2219
+ role: "listitem",
2220
+ className: "dvui-source",
2221
+ href: item.url,
2222
+ target: "_blank",
2223
+ rel: "noopener noreferrer",
2224
+ title: item.url,
2225
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LinkIcon, {}), item.label]
2226
+ }, `${item.url}-${index}`))
2227
+ });
2228
+ }
2229
+ function InputHead({ id, label, value }) {
2230
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2231
+ className: "dvui-input-head",
2232
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
2233
+ htmlFor: id,
2234
+ className: "dvui-input-label",
2235
+ children: label
2236
+ }), value !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("output", {
2237
+ htmlFor: id,
2238
+ className: "dvui-input-value",
2239
+ children: value
2240
+ }) : null]
2241
+ });
2242
+ }
2243
+ const numericBound = (bound) => typeof bound === "number" && Number.isFinite(bound) ? bound : typeof bound === "string" && bound.trim() !== "" && Number.isFinite(Number(bound)) ? Number(bound) : void 0;
2244
+ function SliderInput({ node, ctx }) {
2245
+ const id = (0, react.useId)();
2246
+ const value = Number(ctx.values[node.id]);
2247
+ const min = numericBound(resolve(node.min, ctx.lookup, void 0));
2248
+ const max = numericBound(resolve(node.max, ctx.lookup, void 0));
2249
+ const percent = min !== void 0 && max !== void 0 && max > min ? (value - min) / (max - min) * 100 : 0;
2250
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2251
+ className: "dvui-input-row",
2252
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(InputHead, {
2253
+ id,
2254
+ label: node.label,
2255
+ value: withUnit(grouped(value), node.unit)
2256
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2257
+ id,
2258
+ type: "range",
2259
+ className: "dvui-range",
2260
+ min,
2261
+ max,
2262
+ step: node.step ?? "any",
2263
+ value,
2264
+ disabled: ctx.locked,
2265
+ style: { "--_dvui-fill": `${percent}%` },
2266
+ onChange: (event) => ctx.setInput(node.id, Number(event.target.value))
2267
+ })]
2268
+ });
2269
+ }
2270
+ function NumberInput({ node, ctx }) {
2271
+ const id = (0, react.useId)();
2272
+ const [draft, setDraft] = (0, react.useState)(null);
2273
+ const value = ctx.values[node.id];
2274
+ const min = numericBound(resolve(node.min, ctx.lookup, void 0));
2275
+ const max = numericBound(resolve(node.max, ctx.lookup, void 0));
2276
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2277
+ className: "dvui-input-row",
2278
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(InputHead, {
2279
+ id,
2280
+ label: node.label
2281
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2282
+ className: "dvui-number",
2283
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2284
+ id,
2285
+ className: "dvui-input",
2286
+ type: "number",
2287
+ inputMode: "decimal",
2288
+ value: draft ?? asText(value),
2289
+ min,
2290
+ max,
2291
+ step: node.step ?? "any",
2292
+ disabled: ctx.locked,
2293
+ onChange: (event) => {
2294
+ const raw = event.target.value;
2295
+ setDraft(raw);
2296
+ const next = Number(raw);
2297
+ if (raw.trim() === "" || !Number.isFinite(next) || min !== void 0 && next < min || max !== void 0 && next > max) return;
2298
+ ctx.setInput(node.id, next);
2299
+ },
2300
+ onBlur: () => setDraft(null)
2301
+ }), node.unit ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2302
+ className: "dvui-unit",
2303
+ children: node.unit
2304
+ }) : null]
2305
+ })]
2306
+ });
2307
+ }
2308
+ function ChoiceInput({ node, ctx }) {
2309
+ const labelId = (0, react.useId)();
2310
+ const options = optionsOf(node);
2311
+ const current = ctx.values[node.id];
2312
+ const side = options.length <= 4 && options.every((option) => [...option.label].length <= 14);
2313
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2314
+ className: "dvui-input-row",
2315
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2316
+ className: "dvui-input-label",
2317
+ id: labelId,
2318
+ children: node.label
2319
+ }), side ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Segmented, {
2320
+ label: node.label ?? "",
2321
+ value: options.findIndex((option) => choiceValue(option) === current),
2322
+ className: "dvui-choice-seg",
2323
+ children: options.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2324
+ type: "button",
2325
+ "aria-pressed": choiceValue(option) === current,
2326
+ disabled: ctx.locked,
2327
+ onClick: () => ctx.setInput(node.id, choiceValue(option)),
2328
+ children: option.label
2329
+ }, option.label))
2330
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2331
+ className: "dvui-radios",
2332
+ role: "radiogroup",
2333
+ "aria-labelledby": labelId,
2334
+ children: options.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2335
+ type: "button",
2336
+ role: "radio",
2337
+ className: "dvui-radio",
2338
+ "aria-checked": choiceValue(option) === current,
2339
+ disabled: ctx.locked,
2340
+ onClick: () => ctx.setInput(node.id, choiceValue(option)),
2341
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2342
+ className: "dvui-radio-dot",
2343
+ "aria-hidden": "true"
2344
+ }), option.label]
2345
+ }, option.label))
2346
+ })]
2347
+ });
2348
+ }
2349
+ function ToggleInput({ node, ctx }) {
2350
+ const on = ctx.values[node.id] === true;
2351
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2352
+ className: "dvui-toggle",
2353
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2354
+ className: "dvui-input-label",
2355
+ children: node.label
2356
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2357
+ type: "button",
2358
+ role: "switch",
2359
+ className: "dvui-switch",
2360
+ "aria-checked": on,
2361
+ "aria-label": node.label,
2362
+ disabled: ctx.locked,
2363
+ onClick: () => ctx.setInput(node.id, !on),
2364
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2365
+ className: "dvui-switch-knob",
2366
+ "aria-hidden": "true"
2367
+ })
2368
+ })]
2369
+ });
2370
+ }
2371
+ function WidgetNode({ node, ctx, Markdown, labels, t }) {
2372
+ const { lookup } = ctx;
2373
+ switch (node.type) {
2374
+ case "stack":
2375
+ case "row": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2376
+ className: node.type === "row" ? "dvui-row" : "dvui-stack",
2377
+ children: (node.children ?? []).map((child, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetNode, {
2378
+ node: child,
2379
+ ctx,
2380
+ Markdown,
2381
+ labels,
2382
+ t
2383
+ }, child.id ?? `${child.type}-${index}`))
2384
+ });
2385
+ case "text": {
2386
+ const text = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Markdown, {
2387
+ text: asText(resolve(node.text, lookup, "")),
2388
+ labels,
2389
+ variant: "compact"
2390
+ });
2391
+ return node.tone === "muted" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2392
+ className: "dvui-muted",
2393
+ children: text
2394
+ }) : text;
2395
+ }
2396
+ case "metric": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetMetric, {
2397
+ node,
2398
+ lookup
2399
+ });
2400
+ case "table": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetTable, {
2401
+ node,
2402
+ lookup
2403
+ });
2404
+ case "chart": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetChart, {
2405
+ node,
2406
+ lookup,
2407
+ registerChart: ctx.registerChart,
2408
+ refreshCharts: ctx.refreshCharts,
2409
+ t
2410
+ });
2411
+ case "progress": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetProgress, {
2412
+ node,
2413
+ lookup,
2414
+ t
2415
+ });
2416
+ case "callout": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetCallout, {
2417
+ node,
2418
+ lookup,
2419
+ Markdown,
2420
+ labels
2421
+ });
2422
+ case "sources": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetSources, {
2423
+ node,
2424
+ t
2425
+ });
2426
+ case "slider": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SliderInput, {
2427
+ node,
2428
+ ctx
2429
+ });
2430
+ case "number": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberInput, {
2431
+ node,
2432
+ ctx
2433
+ });
2434
+ case "choice": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChoiceInput, {
2435
+ node,
2436
+ ctx
2437
+ });
2438
+ case "toggle": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleInput, {
2439
+ node,
2440
+ ctx
2441
+ });
2442
+ case "submit": return ctx.settled ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2443
+ className: "dvui-actions",
2444
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2445
+ type: "button",
2446
+ className: "dvui-submit",
2447
+ disabled: ctx.answerLocked,
2448
+ onClick: () => ctx.onSubmit?.(node.id, {}),
2449
+ children: node.label || t("Send")
2450
+ })
2451
+ });
2452
+ case "options": return ctx.settled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettledOptions, {
2453
+ node,
2454
+ answer: ctx.answer,
2455
+ t
2456
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OptionsInput, {
2457
+ node,
2458
+ ctx,
2459
+ t
2460
+ });
2461
+ case "confirm": return ctx.settled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettledConfirm, {
2462
+ node,
2463
+ answer: ctx.answer,
2464
+ t
2465
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmInput, {
2466
+ node,
2467
+ ctx,
2468
+ t
2469
+ });
2470
+ case "form": return ctx.settled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettledForm, {
2471
+ node,
2472
+ answer: ctx.answer
2473
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FormInput, {
2474
+ node,
2475
+ ctx,
2476
+ t
2477
+ });
2478
+ default: return null;
2479
+ }
2480
+ }
2481
+ function WidgetView({ view, onSubmit, settled = false, answer = null, busy = false, registerChart, refreshCharts, t = english, labels = {
2482
+ code: {
2483
+ copyLabel: "Copy",
2484
+ copiedLabel: "Copied"
2485
+ },
2486
+ footnotes: "Footnotes"
2487
+ }, Markdown = FallbackMarkdown }) {
2488
+ const inputs = viewInputs(view);
2489
+ const [values, setValues] = (0, react.useState)(() => Object.fromEntries(inputs.map((node) => [node.id, inputDefault(node)])));
2490
+ const sent = answer?.values?.inputs;
2491
+ const current = settled && sent !== null && typeof sent === "object" ? {
2492
+ ...values,
2493
+ ...sent
2494
+ } : values;
2495
+ const submit = (action, extra) => onSubmit?.(action, inputs.length > 0 ? {
2496
+ ...extra,
2497
+ inputs: current
2498
+ } : extra);
2499
+ const ctx = {
2500
+ onSubmit: submit,
2501
+ settled,
2502
+ answer,
2503
+ busy,
2504
+ lookup: viewScope(view, current),
2505
+ values: current,
2506
+ locked: settled || busy,
2507
+ answerLocked: settled || busy || onSubmit === void 0,
2508
+ setInput: (id, value) => setValues((before) => ({
2509
+ ...before,
2510
+ [id]: value
2511
+ })),
2512
+ registerChart,
2513
+ refreshCharts
2514
+ };
2515
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetNode, {
2516
+ node: view,
2517
+ ctx,
2518
+ Markdown,
2519
+ labels,
2520
+ t
2521
+ });
2522
+ }
2523
+ function useChartHandles() {
2524
+ const [, bump] = (0, react.useState)(0);
2525
+ const charts = (0, react.useRef)(/* @__PURE__ */ new Map());
2526
+ const refreshCharts = (0, react.useCallback)(() => bump((value) => value + 1), []);
2527
+ const registerChart = (0, react.useCallback)((key, handle) => {
2528
+ charts.current.set(key, handle);
2529
+ refreshCharts();
2530
+ return () => {
2531
+ charts.current.delete(key);
2532
+ refreshCharts();
2533
+ };
2534
+ }, [refreshCharts]);
2535
+ return {
2536
+ charts: [...charts.current.values()],
2537
+ registerChart,
2538
+ refreshCharts
2539
+ };
2540
+ }
2541
+ function ViewMenu({ charts, t }) {
2542
+ const [open, setOpen] = (0, react.useState)(false);
2543
+ const root = (0, react.useRef)(null);
2544
+ (0, react.useEffect)(() => {
2545
+ if (!open) return void 0;
2546
+ const away = (event) => {
2547
+ if (!root.current?.contains(event.target)) setOpen(false);
2548
+ };
2549
+ const key = (event) => {
2550
+ if (event.key === "Escape") setOpen(false);
2551
+ };
2552
+ document.addEventListener("pointerdown", away, true);
2553
+ document.addEventListener("keydown", key);
2554
+ return () => {
2555
+ document.removeEventListener("pointerdown", away, true);
2556
+ document.removeEventListener("keydown", key);
2557
+ };
2558
+ }, [open]);
2559
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2560
+ ref: root,
2561
+ className: "dvui-tools",
2562
+ "data-open": open ? "" : void 0,
2563
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2564
+ type: "button",
2565
+ className: "dvui-more",
2566
+ "aria-label": t("Chart options"),
2567
+ "aria-haspopup": "menu",
2568
+ "aria-expanded": open,
2569
+ onClick: () => setOpen((value) => !value),
2570
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DotsIcon, {})
2571
+ }), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2572
+ className: "dvui-menu",
2573
+ role: "menu",
2574
+ children: charts.map((chart, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [
2575
+ charts.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2576
+ className: "dvui-menu-label",
2577
+ children: chart.label ?? t("Chart {n}", { n: index + 1 })
2578
+ }) : null,
2579
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2580
+ type: "button",
2581
+ role: "menuitem",
2582
+ onClick: () => {
2583
+ setOpen(false);
2584
+ chart.toggleTable();
2585
+ },
2586
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TableIcon, {}), chart.asTable ? t("View as chart") : t("View as table")]
2587
+ }),
2588
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2589
+ type: "button",
2590
+ role: "menuitem",
2591
+ onClick: () => {
2592
+ setOpen(false);
2593
+ chart.copy();
2594
+ },
2595
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyIcon, {}), t("Copy data")]
2596
+ })
2597
+ ] }, `${chart.label}-${index}`))
2598
+ }) : null]
2599
+ });
2600
+ }
2601
+ function WidgetCard({ title = "", view, onSubmit, onDismiss, busy = false, settled = false, answer = null, t = english, labels, Markdown, localOnly = false }) {
2602
+ const titleId = (0, react.useId)();
2603
+ const { charts, registerChart, refreshCharts } = useChartHandles();
2604
+ const framed = hasType(view, /* @__PURE__ */ new Set([
2605
+ "slider",
2606
+ "number",
2607
+ "choice",
2608
+ "toggle",
2609
+ "options",
2610
+ "confirm",
2611
+ "form",
2612
+ "submit"
2613
+ ]));
2614
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2615
+ className: `dvui-view ${framed ? "dvui-framed" : "dvui-bare"}${settled ? " dvui-settled" : ""}`,
2616
+ role: "group",
2617
+ "aria-labelledby": title ? titleId : void 0,
2618
+ "aria-label": title ? void 0 : t("View"),
2619
+ "aria-busy": busy || void 0,
2620
+ children: [
2621
+ title || onDismiss || charts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2622
+ className: "dvui-head",
2623
+ children: [
2624
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2625
+ className: "dvui-title",
2626
+ id: titleId,
2627
+ children: title
2628
+ }),
2629
+ charts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ViewMenu, {
2630
+ charts,
2631
+ t
2632
+ }) : null,
2633
+ onDismiss ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2634
+ type: "button",
2635
+ className: "dvui-dismiss",
2636
+ "aria-label": t("Dismiss view"),
2637
+ onClick: onDismiss,
2638
+ disabled: busy,
2639
+ children: "×"
2640
+ }) : null
2641
+ ]
2642
+ }) : null,
2643
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2644
+ className: "dvui-body",
2645
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetView, {
2646
+ view,
2647
+ onSubmit,
2648
+ settled,
2649
+ answer,
2650
+ busy,
2651
+ registerChart,
2652
+ refreshCharts,
2653
+ t,
2654
+ labels,
2655
+ Markdown
2656
+ })
2657
+ }),
2658
+ localOnly && hasType(view, /* @__PURE__ */ new Set([
2659
+ "options",
2660
+ "confirm",
2661
+ "form",
2662
+ "submit"
2663
+ ])) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2664
+ className: "dvui-local-note",
2665
+ role: "note",
2666
+ children: t("Local preview only; this card does not send an answer.")
2667
+ }) : null
2668
+ ]
2669
+ });
2670
+ }
2671
+ //#endregion
2672
+ //#region src/styles.ts
2673
+ const viewStyles = `
2674
+ .dvui-root { min-width: 0; color: var(--dsw-alias-label-primary); font-family: inherit; font-size: 14px; line-height: 22px; }
2675
+ .dvui-root * { box-sizing: border-box; }
2676
+ .dvui-root .dvui-view { min-width: 0; display: flex; flex-direction: column; gap: 10px; margin: 10px 0; }
2677
+ .dvui-root .dvui-bare { gap: 8px; }
2678
+ .dvui-root .dvui-framed { width: min(100%, 560px); padding: 14px 16px; border-radius: var(--dsw-radius-lg); background: var(--dsw-alias-bg-layer-1); box-shadow: var(--dsw-elevation-stroke); }
2679
+ .dvui-root .dvui-settled { color: var(--dsw-alias-label-secondary); }
2680
+ .dvui-root .dvui-head { display: flex; align-items: flex-start; gap: 8px; min-width: 0; }
2681
+ .dvui-root .dvui-title { flex: 1 1 0; min-width: 0; margin: 0; font-size: 13px; line-height: 18px; font-weight: 500; overflow-wrap: anywhere; }
2682
+ .dvui-root .dvui-body { min-width: 0; }
2683
+ .dvui-root .dvui-stack { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
2684
+ .dvui-root .dvui-row { display: flex; flex-wrap: wrap; gap: 12px 24px; min-width: 0; }
2685
+ .dvui-root .dvui-row > * { flex: 1 1 160px; min-width: 0; }
2686
+ .dvui-root .dvui-metric { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
2687
+ .dvui-root .dvui-metric-label,.dvui-root .dvui-caption,.dvui-root .dvui-field-label,.dvui-root .dvui-input-label { font-size: 12px; line-height: 16px; color: var(--dsw-alias-label-secondary); overflow-wrap: anywhere; }
2688
+ .dvui-root .dvui-metric-value { font-size: 22px; line-height: 28px; font-weight: 600; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
2689
+ .dvui-root .dvui-metric[data-tone=good] .dvui-metric-value,.dvui-root .dvui-progress[data-tone=good] .dvui-progress-fill { color: var(--dsw-alias-state-success-primary); background: var(--dsw-alias-state-success-primary); }
2690
+ .dvui-root .dvui-metric[data-tone=warn] .dvui-metric-value,.dvui-root .dvui-progress[data-tone=warn] .dvui-progress-fill { color: var(--dsw-alias-state-warn-primary); background: var(--dsw-alias-state-warn-primary); }
2691
+ .dvui-root .dvui-metric[data-tone=bad] .dvui-metric-value,.dvui-root .dvui-progress[data-tone=bad] .dvui-progress-fill { color: var(--dsw-alias-state-error-primary); background: var(--dsw-alias-state-error-primary); }
2692
+ .dvui-root .dvui-muted { font-size: 12px; line-height: 16px; color: var(--dsw-alias-label-secondary); }
2693
+ .dvui-root .dvui-muted * { font-size: inherit !important; line-height: inherit !important; color: inherit !important; }
2694
+ .dvui-root .dvui-table { max-width: 100%; overflow-x: auto; border: .5px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-md); }
2695
+ .dvui-root .dvui-table table { border-collapse: collapse; min-width: 100%; font-size: 13px; line-height: 18px; }
2696
+ .dvui-root .dvui-table th { padding: 6px 10px; font-weight: 500; text-align: start; white-space: nowrap; color: var(--dsw-alias-label-secondary); border-bottom: .5px solid var(--dsw-alias-border-l2); }
2697
+ .dvui-root .dvui-table td { padding: 6px 10px; max-width: 280px; overflow-wrap: anywhere; }
2698
+ .dvui-root .dvui-table tbody tr + tr > * { border-top: .5px solid var(--dsw-alias-border-l1); }
2699
+ .dvui-root .dvui-table tbody th { font-weight: 400; border-bottom: 0; color: var(--dsw-alias-label-primary); }
2700
+ .dvui-root .dvui-num { text-align: end; font-variant-numeric: tabular-nums; }
2701
+ .dvui-root .dvui-progress { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
2702
+ .dvui-root .dvui-progress-label { display: flex; justify-content: space-between; gap: 8px; font-size: 12px; line-height: 16px; color: var(--dsw-alias-label-secondary); font-variant-numeric: tabular-nums; }
2703
+ .dvui-root .dvui-progress-label > span:first-child { color: var(--dsw-alias-label-primary); font-weight: 500; }
2704
+ .dvui-root .dvui-progress-track { height: 6px; border-radius: 999px; background: var(--dsw-alias-interactive-bg-hover); overflow: hidden; }
2705
+ .dvui-root .dvui-progress-fill { height: 100%; border-radius: inherit; background: var(--dsw-alias-state-business-primary); transition: width .12s ease; }
2706
+ .dvui-root .dvui-callout { display: flex; gap: 8px; padding: 8px 12px; border-radius: var(--dsw-radius-md); background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 8%, transparent); }
2707
+ .dvui-root .dvui-callout[data-tone=good] { background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 8%, transparent); }
2708
+ .dvui-root .dvui-callout[data-tone=warn] { background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 8%, transparent); }
2709
+ .dvui-root .dvui-callout[data-tone=bad] { background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 8%, transparent); }
2710
+ .dvui-root .dvui-callout-icon { flex: none; display: flex; padding-top: 3px; color: var(--dsw-alias-state-business-primary); }
2711
+ .dvui-root .dvui-callout[data-tone=good] .dvui-callout-icon { color: var(--dsw-alias-state-success-primary); }
2712
+ .dvui-root .dvui-callout[data-tone=warn] .dvui-callout-icon { color: var(--dsw-alias-state-warn-primary); }
2713
+ .dvui-root .dvui-callout[data-tone=bad] .dvui-callout-icon { color: var(--dsw-alias-state-error-primary); }
2714
+ .dvui-root .dvui-callout > div { flex: 1 1 auto; min-width: 0; }
2715
+ .dvui-root .dvui-sources { display: flex; flex-wrap: wrap; gap: 6px; }
2716
+ .dvui-root .dvui-source { display: inline-flex; align-items: center; gap: 4px; max-width: 100%; height: 22px; padding: 0 6px; border-radius: var(--dsw-radius-sm); background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-secondary); font-size: 11px; line-height: 16px; text-decoration: none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
2717
+ .dvui-root .dvui-source:hover { color: var(--dsw-alias-label-primary); }
2718
+ .dvui-root .dvui-input-row { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
2719
+ .dvui-root .dvui-input-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
2720
+ .dvui-root .dvui-input-value { font-size: 15px; line-height: 20px; font-weight: 600; font-variant-numeric: tabular-nums; }
2721
+ .dvui-root .dvui-range { appearance: none; width: 100%; height: 20px; margin: 0; background: transparent; cursor: pointer; }
2722
+ .dvui-root .dvui-range:disabled { cursor: default; opacity: .6; }
2723
+ .dvui-root .dvui-range::-webkit-slider-runnable-track { height: 6px; border-radius: 999px; background: linear-gradient(to right, var(--dsw-alias-state-business-primary) var(--_dvui-fill, 0%), var(--dsw-alias-interactive-bg-hover) var(--_dvui-fill, 0%)); }
2724
+ .dvui-root .dvui-range::-moz-range-track { height: 6px; border-radius: 999px; background: var(--dsw-alias-interactive-bg-hover); }
2725
+ .dvui-root .dvui-range::-moz-range-progress { height: 6px; border-radius: 999px; background: var(--dsw-alias-state-business-primary); }
2726
+ .dvui-root .dvui-range::-webkit-slider-thumb { appearance: none; width: 16px; height: 16px; margin-top: -5px; border-radius: 50%; background: var(--dsw-alias-label-primary-foreground); box-shadow: var(--dsw-elevation-soft); }
2727
+ .dvui-root .dvui-range::-moz-range-thumb { width: 16px; height: 16px; border: 0; border-radius: 50%; background: var(--dsw-alias-label-primary-foreground); box-shadow: var(--dsw-elevation-soft); }
2728
+ .dvui-root .dvui-number { display: flex; align-items: center; gap: 6px; max-width: 200px; }
2729
+ .dvui-root .dvui-unit { flex: none; font-size: 13px; color: var(--dsw-alias-label-secondary); }
2730
+ .dvui-root .dvui-seg { display: flex; flex-wrap: wrap; gap: 2px; padding: 2px; border-radius: var(--dsw-radius-md); background: var(--dsw-alias-interactive-bg-hover); }
2731
+ .dvui-root .dvui-choice-seg { align-self: flex-start; max-width: 100%; }
2732
+ .dvui-root .dvui-seg button { flex: 0 1 auto; min-height: 30px; padding: 4px 12px; border: 0; border-radius: var(--dsw-radius-sm); background: none; font: inherit; font-size: 13px; color: var(--dsw-alias-label-secondary); cursor: pointer; overflow-wrap: anywhere; }
2733
+ .dvui-root .dvui-seg button[aria-pressed=true] { background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); box-shadow: var(--dsw-elevation-soft); }
2734
+ .dvui-root .dvui-radios { display: flex; flex-direction: column; gap: 2px; }
2735
+ .dvui-root .dvui-radio { display: flex; align-items: center; gap: 8px; min-height: 30px; padding: 4px 8px; margin: 0 -8px; border: 0; border-radius: var(--dsw-radius-sm); background: none; font: inherit; text-align: start; color: inherit; cursor: pointer; }
2736
+ .dvui-root .dvui-radio:hover:not(:disabled),.dvui-root .dvui-option:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
2737
+ .dvui-root .dvui-radio-dot { flex: none; width: 16px; height: 16px; border-radius: 50%; box-shadow: inset 0 0 0 1.5px var(--dsw-alias-border-l3); }
2738
+ .dvui-root .dvui-radio[aria-checked=true] .dvui-radio-dot { box-shadow: inset 0 0 0 5px var(--dsw-alias-state-business-primary); }
2739
+ .dvui-root .dvui-toggle { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 28px; }
2740
+ .dvui-root .dvui-switch { position: relative; flex: none; width: 36px; height: 20px; padding: 0; border: 0; border-radius: 999px; background: var(--dsw-alias-border-l3); cursor: pointer; }
2741
+ .dvui-root .dvui-switch[aria-checked=true] { background: var(--dsw-alias-button-primary-fill); }
2742
+ .dvui-root .dvui-switch-knob { position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%; background: var(--dsw-alias-label-primary-foreground); box-shadow: var(--dsw-elevation-soft); transition: transform .15s ease; }
2743
+ .dvui-root .dvui-switch[aria-checked=true] .dvui-switch-knob { transform: translateX(16px); }
2744
+ .dvui-root .dvui-options { display: flex; flex-direction: column; gap: 1px; min-width: 0; margin: 0 -8px; }
2745
+ .dvui-root .dvui-option { position: relative; display: flex; align-items: flex-start; gap: 12px; width: 100%; min-width: 0; margin: 0; border: 0; border-radius: var(--dsw-radius-md); background: transparent; padding: 10px 8px; font: inherit; color: inherit; text-align: start; cursor: pointer; }
2746
+ .dvui-root .dvui-key { flex: none; width: 24px; height: 24px; border-radius: var(--dsw-radius-sm); background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 24px; text-align: center; }
2747
+ .dvui-root .dvui-option-body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 1px; padding-top: 2px; }
2748
+ .dvui-root .dvui-option-label { font-size: 14px; line-height: 20px; font-weight: 500; overflow-wrap: anywhere; }
2749
+ .dvui-root .dvui-option-desc { font-size: 12px; line-height: 17px; color: var(--dsw-alias-label-secondary); overflow-wrap: anywhere; }
2750
+ .dvui-root .dvui-check { flex: none; display: inline-flex; align-items: center; color: var(--dsw-alias-state-success-primary); }
2751
+ .dvui-root .dvui-form { display: flex; flex-direction: column; gap: 8px; margin: 0; }
2752
+ .dvui-root .dvui-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
2753
+ .dvui-root .dvui-input { width: 100%; min-width: 0; min-height: 32px; padding: 5px 11px; border: .5px solid var(--dsw-alias-border-l3); border-radius: var(--dsw-radius-sm); outline: none; background: transparent; color: var(--dsw-alias-label-primary); font: inherit; font-size: 14px; line-height: 20px; }
2754
+ .dvui-root .dvui-input::placeholder { color: var(--dsw-alias-label-dimmed); }
2755
+ .dvui-root .dvui-input:focus-visible { border-color: var(--dsw-alias-state-business-primary); }
2756
+ .dvui-root .dvui-actions { display: flex; justify-content: flex-end; gap: 8px; }
2757
+ .dvui-root .dvui-submit,.dvui-root .dvui-button { flex: none; min-height: 34px; padding: 0 14px; border: 0; border-radius: 999px; font: inherit; font-size: 14px; font-weight: 500; cursor: pointer; }
2758
+ .dvui-root .dvui-submit { background: var(--dsw-alias-button-primary-fill); color: var(--dsw-alias-label-primary-foreground); }
2759
+ .dvui-root .dvui-submit:hover:not(:disabled) { background: var(--dsw-alias-button-primary-hover); }
2760
+ .dvui-root .dvui-button { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
2761
+ .dvui-root .dvui-button:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-active); }
2762
+ .dvui-root button:disabled { opacity: .45; cursor: default; }
2763
+ .dvui-root .dvui-badge { height: 22px; padding: 0 7px; border-radius: 999px; border: 1px dotted var(--dsw-alias-border-l3); color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 20px; }
2764
+ .dvui-root .dvui-tools { position: relative; flex: none; align-self: flex-start; }
2765
+ .dvui-root .dvui-more,.dvui-root .dvui-dismiss { width: 28px; height: 28px; padding: 0; border: 0; border-radius: 50%; background: none; color: var(--dsw-alias-label-secondary); display: flex; align-items: center; justify-content: center; cursor: pointer; }
2766
+ .dvui-root .dvui-more:hover,.dvui-root .dvui-dismiss:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
2767
+ .dvui-root .dvui-menu { position: absolute; top: 32px; right: 0; z-index: 4; min-width: 180px; padding: 6px; border-radius: var(--dsw-radius-md); background: var(--dsw-menu-surface-fill); box-shadow: var(--dsw-elevation-panel); display: flex; flex-direction: column; gap: 2px; }
2768
+ .dvui-root .dvui-menu button { display: flex; align-items: center; gap: 8px; width: 100%; min-height: 30px; text-align: left; border: 0; background: none; border-radius: var(--dsw-radius-sm); padding: 6px 8px; font: inherit; font-size: 13px; line-height: 18px; color: var(--dsw-alias-label-primary); cursor: pointer; }
2769
+ .dvui-root .dvui-menu button:hover { background: var(--dsw-alias-interactive-bg-hover); }
2770
+ .dvui-root .dvui-menu-label { padding: 2px 8px; font-size: 11px; line-height: 14px; color: var(--dsw-alias-label-tertiary); }
2771
+ .dvui-root .dvui-local-note { margin: 0; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
2772
+ .dvui-root .dvui-chart { position: relative; margin: 0; min-width: 0; display: flex; flex-direction: column; gap: 8px; }
2773
+ .dvui-root .dvui-chart-plot { position: relative; min-width: 0; }
2774
+ .dvui-root .dvui-chart-svg { display: block; overflow: visible; outline: none; font-family: inherit; }
2775
+ .dvui-root .dvui-chart-svg text { pointer-events: none; }
2776
+ .dvui-root .dvui-chart-grid { stroke: var(--dsw-alias-border-l1); stroke-width: 1; stroke-dasharray: 3 3; }
2777
+ .dvui-root .dvui-chart-tick { font-size: 10px; fill: var(--dsw-alias-label-tertiary); font-variant-numeric: tabular-nums; }
2778
+ .dvui-root .dvui-chart-label,.dvui-root .dvui-chart-value { font-size: 11px; fill: var(--dsw-alias-label-secondary); }
2779
+ .dvui-root .dvui-chart-value { font-variant-numeric: tabular-nums; }
2780
+ .dvui-root .dvui-chart-center { font-size: 15px; font-weight: 600; fill: var(--dsw-alias-label-primary); font-variant-numeric: tabular-nums; }
2781
+ .dvui-root .dvui-chart-center-name { font-size: 11px; fill: var(--dsw-alias-label-secondary); }
2782
+ .dvui-root .dvui-chart-mark[data-dim] { opacity: .28; }
2783
+ .dvui-root .dvui-chart-line { fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
2784
+ .dvui-root .dvui-chart-area { opacity: .14; }
2785
+ .dvui-root .dvui-chart-guide { stroke: var(--dsw-alias-border-l3); stroke-width: 1; }
2786
+ .dvui-root .dvui-chart-hit { fill: transparent; }
2787
+ .dvui-root .dvui-chart-tip { position: absolute; z-index: 4; pointer-events: none; transform: translate(-50%, calc(-100% - 8px)); display: flex; flex-direction: column; gap: 2px; padding: 6px 10px; border-radius: var(--dsw-radius-md); background: var(--dsw-menu-surface-fill); box-shadow: var(--dsw-elevation-panel); font-size: 12px; line-height: 16px; white-space: nowrap; font-variant-numeric: tabular-nums; }
2788
+ .dvui-root .dvui-chart-tip span { display: flex; align-items: center; gap: 6px; color: var(--dsw-alias-label-secondary); }
2789
+ .dvui-root .dvui-chart-tip i,.dvui-root .dvui-chart-key i { flex: none; width: 8px; height: 8px; border-radius: 50%; }
2790
+ .dvui-root .dvui-chart-legend { display: flex; flex-wrap: wrap; gap: 4px 12px; padding-right: 32px; }
2791
+ .dvui-root .dvui-chart-key { display: inline-flex; align-items: center; gap: 6px; min-height: 20px; padding: 0; border: 0; background: none; font: inherit; font-size: 12px; line-height: 16px; color: var(--dsw-alias-label-secondary); cursor: pointer; }
2792
+ .dvui-root .dvui-chart-key[aria-pressed=false],.dvui-root .dvui-chart-key[data-dim] { opacity: .4; }
2793
+ .dvui-root .dvui-chart-legend-list { flex-direction: column; flex-wrap: nowrap; gap: 6px; padding: 0; min-width: 0; }
2794
+ .dvui-root .dvui-chart-legend-list .dvui-chart-key { cursor: default; width: 100%; }
2795
+ .dvui-root .dvui-chart-key-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-primary); }
2796
+ .dvui-root .dvui-chart-key-value { flex: none; font-variant-numeric: tabular-nums; }
2797
+ .dvui-root .dvui-chart-donut { display: flex; flex-direction: column; align-items: center; gap: 12px; }
2798
+ .dvui-root .dvui-chart-donut-beside { flex-direction: row; align-items: center; gap: 24px; }
2799
+ .dvui-root .dvui-chart-donut-beside .dvui-chart-legend-list { flex: 1 1 auto; max-width: 280px; }
2800
+ .dvui-root .dvui-chart-copied { position: absolute; top: 2px; right: 36px; font-size: 12px; line-height: 16px; color: var(--dsw-alias-label-secondary); }
2801
+ .dvui-root .dvui-reasoning { margin: 6px 0; color: var(--dsw-alias-label-secondary); }
2802
+ .dvui-root .dvui-reasoning > summary { cursor: pointer; font-size: 12px; line-height: 20px; }
2803
+ .dvui-root .dvui-reasoning-body { padding: 4px 0 4px 12px; border-left: 1px solid var(--dsw-alias-border-l2); }
2804
+ .dvui-root .dvui-unknown { white-space: pre-wrap; overflow-wrap: anywhere; color: var(--dsw-alias-label-secondary); font-size: 12px; }
2805
+ .dvui-root .dvui-tool-loading, .dvui-root.dvui-tool-loading { padding: 8px 0; color: var(--dsw-alias-label-secondary); font-size: 13px; }
2806
+ .dvui-root .dvui-tool-error, .dvui-root.dvui-tool-error { margin: 8px 0; color: var(--dsw-alias-label-error); font-size: 13px; }
2807
+ .dvui-root button:focus-visible,.dvui-root a:focus-visible,.dvui-root input:focus-visible,.dvui-root .dvui-chart-svg:focus-visible { outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); outline-offset: 2px; }
2808
+ @media (hover: hover) { .dvui-root .dvui-tools:not(:focus-within):not([data-open]) { opacity: 0; } .dvui-root .dvui-view:hover .dvui-tools { opacity: 1; } }
2809
+ @media (prefers-reduced-motion: reduce) { .dvui-root * { transition: none !important; } }
2810
+ `;
2811
+ //#endregion
2812
+ //#region src/answer-text.ts
2813
+ function controls(view) {
2814
+ return [
2815
+ "options",
2816
+ "confirm",
2817
+ "form",
2818
+ "submit"
2819
+ ].includes(view.type) ? [view] : (view.children ?? []).flatMap(controls);
2820
+ }
2821
+ function answerText(card, action, values) {
2822
+ const node = controls(card.view).find((node) => node.id === action);
2823
+ const lines = [card.title || "Deep View"];
2824
+ if (Array.isArray(values.selected)) lines.push(values.selected.map(String).join(", "));
2825
+ if (typeof values.confirmed === "boolean") lines.push(values.confirmed ? node?.confirmLabel ?? "Confirm" : node?.cancelLabel ?? "Cancel");
2826
+ if (isRecord(values.fields)) for (const field of node?.fields ?? []) lines.push(`${field.label}: ${String(values.fields[field.id] ?? "")}`);
2827
+ const inputs = inputsSummary(card.view, values.inputs);
2828
+ if (inputs) lines.push(inputs);
2829
+ if (node?.type === "submit") lines.push(node.label ?? "Submit");
2830
+ return lines.join("\n");
2831
+ }
2832
+ /** Legacy messages have no metadata. Restore only unambiguous, fully validated
2833
+ * answers that reproduce the exact text written by this plugin. Never guess
2834
+ * from ordinary prose, and never modify the stored message. */
2835
+ function legacyAnswer(card, text) {
2836
+ if (text.startsWith("{")) try {
2837
+ const data = JSON.parse(text);
2838
+ if (!isRecord(data) || data.kind !== "answer_to_pending_view" || data.tool !== "show_view" || !Array.isArray(data.answers)) return null;
2839
+ const answer = data.answers.find((value) => isRecord(value) && value.id === card.id);
2840
+ if (!isRecord(answer) || typeof answer.action !== "string") return null;
2841
+ const values = validateViewAnswer(card.view, answer.action, answer.values);
2842
+ return values.error === void 0 ? {
2843
+ id: card.id,
2844
+ action: answer.action,
2845
+ values
2846
+ } : null;
2847
+ } catch {
2848
+ return null;
2849
+ }
2850
+ const lines = text.split("\n");
2851
+ if (lines.shift() !== (card.title || "Deep View")) return null;
2852
+ const candidates = [];
2853
+ for (const node of controls(card.view)) {
2854
+ const rest = [...lines];
2855
+ const values = {};
2856
+ if (node.type === "options") {
2857
+ const line = rest.shift();
2858
+ if (line === void 0) continue;
2859
+ const labels = node.options?.map((option) => option.label) ?? [];
2860
+ if (node.multi && labels.some((label) => label.includes(", "))) continue;
2861
+ values.selected = node.multi ? line.split(", ") : [line];
2862
+ } else if (node.type === "confirm") {
2863
+ const line = rest.shift(), yes = node.confirmLabel ?? "Confirm", no = node.cancelLabel ?? "Cancel";
2864
+ if (yes === no || line !== yes && line !== no) continue;
2865
+ values.confirmed = line === yes;
2866
+ } else if (node.type === "form") {
2867
+ const fields = {};
2868
+ let valid = true;
2869
+ for (const field of node.fields ?? []) {
2870
+ const line = rest.shift(), prefix = `${field.label}: `;
2871
+ if (line === void 0 || !line.startsWith(prefix)) {
2872
+ valid = false;
2873
+ break;
2874
+ }
2875
+ fields[field.id] = line.slice(prefix.length);
2876
+ }
2877
+ if (!valid) continue;
2878
+ values.fields = fields;
2879
+ }
2880
+ const inputs = viewInputs(card.view);
2881
+ if (inputs.length) {
2882
+ const pairs = rest.shift()?.split("; ");
2883
+ if (!pairs || pairs.length !== inputs.length) continue;
2884
+ const sent = {};
2885
+ let valid = true;
2886
+ for (let i = 0; i < inputs.length; i++) {
2887
+ const input = inputs[i], prefix = `${input.label || input.id}=`;
2888
+ if (!pairs[i].startsWith(prefix)) {
2889
+ valid = false;
2890
+ break;
2891
+ }
2892
+ const value = pairs[i].slice(prefix.length);
2893
+ if (input.type === "toggle") sent[input.id] = value === "on" ? true : value === "off" ? false : void 0;
2894
+ else if (input.type === "choice") {
2895
+ const choices = input.options?.filter((option) => option.label === value) ?? [];
2896
+ sent[input.id] = choices.length === 1 ? choiceValue(choices[0]) : void 0;
2897
+ } else {
2898
+ const unit = input.unit ? ` ${input.unit}` : "";
2899
+ sent[input.id] = !unit || value.endsWith(unit) ? Number(unit ? value.slice(0, -unit.length) : value) : void 0;
2900
+ }
2901
+ }
2902
+ if (!valid) continue;
2903
+ values.inputs = sent;
2904
+ }
2905
+ if (node.type === "submit" && rest.shift() !== (node.label ?? "Submit")) continue;
2906
+ if (rest.length || !node.id) continue;
2907
+ const checked = validateViewAnswer(card.view, node.id, values);
2908
+ if (checked.error === void 0 && answerText(card, node.id, checked) === text) candidates.push({
2909
+ id: card.id,
2910
+ action: node.id,
2911
+ values: checked
2912
+ });
2913
+ }
2914
+ return candidates.length === 1 ? candidates[0] : null;
2915
+ }
2916
+ //#endregion
2917
+ //#region src/answers.ts
2918
+ /** Read visible, durable user/steering messages only, including inherited history. */
2919
+ function replyAnswers(snapshot) {
2920
+ const answers = /* @__PURE__ */ new Map();
2921
+ let cards = [];
2922
+ const collect = (block) => {
2923
+ const meta = block.meta;
2924
+ if (block.kind === "tool-result" && !block.isError && isRecord(meta) && meta.owner === "dsh-deep-view" && meta.awaitReply === true && typeof meta.viewId === "string") {
2925
+ const shown = checkShownView(meta.title, meta.view);
2926
+ if (!("error" in shown)) cards.push({
2927
+ id: meta.viewId,
2928
+ title: shown.title,
2929
+ view: shown.view
2930
+ });
2931
+ }
2932
+ for (const child of block.subCalls ?? []) collect(child);
2933
+ };
2934
+ for (const key of snapshot.order) {
2935
+ const node = snapshot.nodes.get(key);
2936
+ if (node?.kind === "tool-call" && node.data?.root) collect(node.data.root);
2937
+ if (node?.kind !== "user" && node?.kind !== "steering") continue;
2938
+ const source = node.data?.source;
2939
+ if (!isRecord(source) || source.kind !== "user") continue;
2940
+ if (source.deepView === void 0) {
2941
+ const text = node.data?.content?.filter(isRecord).filter((block) => block.type === "text").map((block) => block.text).join("") ?? "";
2942
+ const candidates = cards.map((card) => legacyAnswer(card, text)).filter((answer) => answer !== null);
2943
+ if (candidates.length === 1 && !answers.has(candidates[0].id)) answers.set(candidates[0].id, candidates[0]);
2944
+ }
2945
+ cards = [];
2946
+ if (!isRecord(source.deepView)) continue;
2947
+ const meta = source.deepView;
2948
+ if (meta.owner !== "dsh-deep-view" || meta.version !== 1 || !isRecord(meta.answer)) continue;
2949
+ const answer = meta.answer;
2950
+ if (typeof answer.id !== "string" || typeof answer.action !== "string" || !isRecord(answer.values)) continue;
2951
+ if (!answers.has(answer.id)) answers.set(answer.id, answer);
2952
+ }
2953
+ return answers;
2954
+ }
2955
+ function checkedAnswer(answer, id, view) {
2956
+ if (!answer || !id || answer.id !== id) return null;
2957
+ const values = validateViewAnswer(view, answer.action, answer.values);
2958
+ return values.error === void 0 ? {
2959
+ id,
2960
+ action: answer.action,
2961
+ values
2962
+ } : null;
2963
+ }
2964
+ function sameAnswers(left, right) {
2965
+ return left.size === right.size && [...left].every(([id, answer]) => {
2966
+ const next = right.get(id);
2967
+ return next === answer || next?.action === answer.action && JSON.stringify(next.values) === JSON.stringify(answer.values);
2968
+ });
2969
+ }
2970
+ //#endregion
2971
+ //#region src/copy.ts
2972
+ const uiZh = {
2973
+ copy: "复制",
2974
+ copied: "已复制",
2975
+ footnotes: "脚注",
2976
+ code: "代码",
2977
+ wrap: "自动换行",
2978
+ unwrap: "不换行",
2979
+ prepared: "卡片将在最终回复中显示",
2980
+ preparing: "正在准备视图…",
2981
+ error: "视图未能显示",
2982
+ invalid: "视图数据无效",
2983
+ local: "仅本地预览;此卡片不会发送回答。",
2984
+ answerFailed: "回答未能发送,请重试。",
2985
+ chart: "图表",
2986
+ chartOptions: "图表选项",
2987
+ viewAsChart: "查看图表",
2988
+ viewAsTable: "查看表格",
2989
+ copyData: "复制数据",
2990
+ chartNumber: "图表 {n}",
2991
+ value: "值",
2992
+ series: "系列",
2993
+ view: "视图",
2994
+ sources: "来源",
2995
+ progress: "进度",
2996
+ yourAnswer: "你的回答",
2997
+ options: "选项",
2998
+ submit: "提交",
2999
+ cancel: "取消",
3000
+ confirm: "确认",
3001
+ send: "发送",
3002
+ selected: "已选择"
3003
+ };
3004
+ const uiEn = {
3005
+ copy: "Copy",
3006
+ copied: "Copied",
3007
+ footnotes: "Footnotes",
3008
+ code: "Code",
3009
+ wrap: "Wrap lines",
3010
+ unwrap: "Do not wrap lines",
3011
+ prepared: "The card will appear in the final reply",
3012
+ preparing: "Preparing view…",
3013
+ error: "The view could not be shown",
3014
+ invalid: "Invalid view data",
3015
+ local: "Local preview only; this card does not send an answer.",
3016
+ answerFailed: "The answer could not be sent; try again.",
3017
+ chart: "Chart",
3018
+ chartOptions: "Chart options",
3019
+ viewAsChart: "View as chart",
3020
+ viewAsTable: "View as table",
3021
+ copyData: "Copy data",
3022
+ chartNumber: "Chart {n}",
3023
+ value: "Value",
3024
+ series: "Series",
3025
+ view: "View",
3026
+ sources: "Sources",
3027
+ progress: "Progress",
3028
+ yourAnswer: "Your answer",
3029
+ options: "Options",
3030
+ submit: "Submit",
3031
+ cancel: "Cancel",
3032
+ confirm: "Confirm",
3033
+ send: "Send",
3034
+ selected: "Selected"
3035
+ };
3036
+ //#endregion
3037
+ //#region src/client.tsx
3038
+ const name = "dsh-deep-view-client";
3039
+ const inject = [
3040
+ "slots",
3041
+ "connection",
3042
+ "locale"
3043
+ ];
3044
+ const LOCALE = "deep-view";
3045
+ const viewT = (t) => (key, params) => {
3046
+ const keys = {
3047
+ Copy: "copy",
3048
+ Copied: "copied",
3049
+ Footnotes: "footnotes",
3050
+ Chart: "chart",
3051
+ "Chart options": "chartOptions",
3052
+ "View as chart": "viewAsChart",
3053
+ "View as table": "viewAsTable",
3054
+ "Copy data": "copyData",
3055
+ "Chart {n}": "chartNumber",
3056
+ Value: "value",
3057
+ Series: "series",
3058
+ View: "view",
3059
+ Sources: "sources",
3060
+ Progress: "progress",
3061
+ "Your answer": "yourAnswer",
3062
+ "Local preview only; this card does not send an answer.": "local",
3063
+ Options: "options",
3064
+ Submit: "submit",
3065
+ Cancel: "cancel",
3066
+ Confirm: "confirm",
3067
+ Send: "send",
3068
+ Selected: "selected"
3069
+ };
3070
+ return keys[key] ? t(keys[key], params) : key.replace(/\{(\w+)\}/g, (_, name) => String(params?.[name] ?? `{${name}}`));
3071
+ };
3072
+ function ViewMarkdown(props) {
3073
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, { ...props });
3074
+ }
3075
+ function useMarkdownLabels(t) {
3076
+ return (0, react.useMemo)(() => ({
3077
+ code: {
3078
+ copyLabel: t("copy"),
3079
+ copiedLabel: t("copied"),
3080
+ toolbarLabels: {
3081
+ codeLabel: t("code"),
3082
+ wrapLabel: t("wrap"),
3083
+ unwrapLabel: t("unwrap")
3084
+ }
3085
+ },
3086
+ footnotes: t("footnotes")
3087
+ }), [t]);
3088
+ }
3089
+ function isArgsView(value) {
3090
+ return isRecord(value) && typeof value.text === "function" && typeof value.value === "function";
3091
+ }
3092
+ function argField(args, key) {
3093
+ if (!isArgsView(args)) return void 0;
3094
+ const value = args.value(key);
3095
+ return value !== void 0 ? value : args.text(key);
3096
+ }
3097
+ function readToolCall(props) {
3098
+ const block = props.block;
3099
+ const phase = props.phase ?? (block.kind === "tool-result" ? "result" : "start");
3100
+ const isError = phase === "result" && block.isError === true;
3101
+ const meta = phase === "result" ? block.meta : void 0;
3102
+ if (isRecord(meta) && meta.owner === "dsh-deep-view") {
3103
+ const shown = checkShownView(meta.title, meta.view);
3104
+ if ("error" in shown) return {
3105
+ title: meta.title,
3106
+ view: meta.view,
3107
+ viewReady: true,
3108
+ phase,
3109
+ isError: true
3110
+ };
3111
+ return {
3112
+ presentation: {
3113
+ owner: VIEW_OWNER,
3114
+ ...shown,
3115
+ ...typeof meta.viewId === "string" ? { viewId: meta.viewId } : {},
3116
+ awaitReply: meta.awaitReply === true
3117
+ },
3118
+ title: meta.title,
3119
+ view: meta.view,
3120
+ viewReady: true,
3121
+ phase,
3122
+ isError
3123
+ };
3124
+ }
3125
+ const args = block.args;
3126
+ const title = argField(args, "title");
3127
+ const viewReady = phase !== "preparing" || isArgsView(args) && args.complete("view");
3128
+ return {
3129
+ title,
3130
+ view: viewReady ? argField(args, "view") : void 0,
3131
+ viewReady,
3132
+ phase,
3133
+ isError
3134
+ };
3135
+ }
3136
+ function ToolCallView(props) {
3137
+ const t = props.t;
3138
+ const labels = useMarkdownLabels(t);
3139
+ const read = (0, react.useMemo)(() => readToolCall(props), [props.block, props.phase]);
3140
+ const shown = (0, react.useMemo)(() => {
3141
+ if (read.presentation) return read.presentation;
3142
+ if (!read.viewReady || read.phase === "preparing") return void 0;
3143
+ const checked = checkShownView(read.title, read.view);
3144
+ return "error" in checked ? void 0 : {
3145
+ ...checked,
3146
+ awaitReply: checked.interactive
3147
+ };
3148
+ }, [
3149
+ read.presentation,
3150
+ read.viewReady,
3151
+ read.phase,
3152
+ read.title,
3153
+ read.view
3154
+ ]);
3155
+ const [pending, setPending] = (0, react.useState)(null);
3156
+ const [busy, setBusy] = (0, react.useState)(false);
3157
+ const [failed, setFailed] = (0, react.useState)(false);
3158
+ const [localAnswer, setAnswer] = (0, react.useState)(null);
3159
+ const restored = (0, react.useMemo)(() => shown ? checkedAnswer(props.restoredAnswer, read.presentation?.viewId, shown.view) : null, [
3160
+ props.restoredAnswer,
3161
+ read.presentation?.viewId,
3162
+ shown
3163
+ ]);
3164
+ const answer = restored ?? localAnswer;
3165
+ const sequence = (0, react.useRef)(0);
3166
+ const callId = props.callId;
3167
+ (0, react.useEffect)(() => {
3168
+ const seq = ++sequence.current;
3169
+ setPending(null);
3170
+ setFailed(false);
3171
+ if (restored || !read.presentation?.awaitReply || !callId) return;
3172
+ props.call("pending", {
3173
+ sessionId: props.sessionId,
3174
+ callId
3175
+ }).then((result) => {
3176
+ if (sequence.current !== seq) return;
3177
+ const envelope = result;
3178
+ if (envelope?.ok) setPending(envelope.value);
3179
+ }).catch(() => {});
3180
+ return () => {
3181
+ sequence.current += 1;
3182
+ };
3183
+ }, [
3184
+ props.call,
3185
+ props.sessionId,
3186
+ callId,
3187
+ read.presentation?.awaitReply,
3188
+ read.presentation?.viewId,
3189
+ restored
3190
+ ]);
3191
+ const submit = (0, react.useCallback)((action, values) => {
3192
+ if (!pending || busy) return;
3193
+ setBusy(true);
3194
+ setFailed(false);
3195
+ props.call("reply", {
3196
+ sessionId: props.sessionId,
3197
+ viewId: pending.id,
3198
+ action,
3199
+ values
3200
+ }).then((result) => {
3201
+ const envelope = result;
3202
+ if (envelope?.ok) {
3203
+ setAnswer({
3204
+ id: pending.id,
3205
+ action,
3206
+ values
3207
+ });
3208
+ setPending(null);
3209
+ } else {
3210
+ createClientPluginLogger(PLUGIN_NAME).warn("reply.rejected", "Deep View reply was rejected by host.", void 0, {
3211
+ sessionId: props.sessionId,
3212
+ viewId: pending.id,
3213
+ code: envelope && !envelope.ok ? envelope.error.code : null
3214
+ });
3215
+ setFailed(true);
3216
+ }
3217
+ }).catch((error) => {
3218
+ createClientPluginLogger(PLUGIN_NAME).error("reply.transport-error", "Deep View reply RPC failed.", error, {
3219
+ sessionId: props.sessionId,
3220
+ viewId: pending.id
3221
+ });
3222
+ setFailed(true);
3223
+ }).finally(() => setBusy(false));
3224
+ }, [
3225
+ props.call,
3226
+ props.sessionId,
3227
+ pending,
3228
+ busy
3229
+ ]);
3230
+ if (read.phase === "preparing") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3231
+ className: "dvui-root dvui-tool-loading",
3232
+ role: "status",
3233
+ children: t("preparing")
3234
+ });
3235
+ if (read.isError || !shown) {
3236
+ if (read.phase !== "result" && !read.isError) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3237
+ className: "dvui-root dvui-tool-loading",
3238
+ role: "status",
3239
+ children: t("preparing")
3240
+ });
3241
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3242
+ className: "dvui-root dvui-tool-error",
3243
+ role: "status",
3244
+ children: t(read.isError ? "error" : "invalid")
3245
+ });
3246
+ }
3247
+ const canAnswer = shown.awaitReply && pending !== null && answer === null;
3248
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3249
+ className: "dvui-root",
3250
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetCard, {
3251
+ title: shown.title,
3252
+ view: shown.view,
3253
+ onSubmit: canAnswer ? submit : void 0,
3254
+ busy,
3255
+ settled: answer !== null || shown.awaitReply && !canAnswer,
3256
+ answer,
3257
+ t: viewT(t),
3258
+ labels,
3259
+ Markdown: ViewMarkdown,
3260
+ localOnly: answer === null && !canAnswer && shown.interactive
3261
+ }), failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3262
+ className: "dvui-tool-error",
3263
+ role: "alert",
3264
+ children: t("answerFailed")
3265
+ }) : null]
3266
+ });
3267
+ }
3268
+ /** Collect only this turn's successfully persisted cards, including PTC children.
3269
+ * The turn index also covers hidden process rows; no all-history scan or DOM
3270
+ * relocation is needed. Result metadata works even when arguments were evicted.
3271
+ */
3272
+ function replyCards(snapshot, turn) {
3273
+ const cards = [];
3274
+ const seen = /* @__PURE__ */ new Set();
3275
+ const visit = (block) => {
3276
+ if (block.kind === "tool-result" && !block.isError && !seen.has(block.callId) && isRecord(block.meta) && block.meta.owner === "dsh-deep-view") {
3277
+ const read = readToolCall({
3278
+ phase: "result",
3279
+ block
3280
+ });
3281
+ if (read.presentation && !read.isError) {
3282
+ seen.add(block.callId);
3283
+ cards.push(block);
3284
+ }
3285
+ }
3286
+ for (const child of block.subCalls ?? []) visit(child);
3287
+ };
3288
+ for (const key of snapshot.locations.getTurn(turn)) {
3289
+ const node = snapshot.nodes.get(key);
3290
+ if (node?.kind === "tool-call" && node.data?.root) visit(node.data.root);
3291
+ }
3292
+ return cards;
3293
+ }
3294
+ const sameCards = (left, right) => left.length === right.length && left.every((block, index) => block === right[index]);
3295
+ /** Native completed-turn surface: always outside the process disclosure. */
3296
+ function ReplyView(props) {
3297
+ const cards = props.useChat((snapshot) => replyCards(snapshot, props.turn.turn), sameCards);
3298
+ const answers = props.useChat(replyAnswers, sameAnswers);
3299
+ if (props.turn.status !== "closed" || cards.length === 0) return null;
3300
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3301
+ className: "dvui-root",
3302
+ "data-deep-view-reply": true,
3303
+ children: cards.map((block) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCallView, {
3304
+ sessionId: props.sessionId,
3305
+ callId: block.callId,
3306
+ phase: "result",
3307
+ block,
3308
+ t: props.t,
3309
+ call: props.call,
3310
+ restoredAnswer: isRecord(block.meta) && typeof block.meta.viewId === "string" ? answers.get(block.meta.viewId) : void 0
3311
+ }, `${props.sessionId}:${block.callId}`))
3312
+ });
3313
+ }
3314
+ /** Tool activity contains status only, never the user's interactive card. */
3315
+ function ToolCallStatus(props) {
3316
+ const read = readToolCall(props);
3317
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3318
+ className: "dvui-root",
3319
+ role: "status",
3320
+ children: props.t(read.isError ? "error" : read.phase === "result" && !read.presentation ? "invalid" : read.phase === "result" ? "prepared" : "preparing")
3321
+ });
3322
+ }
3323
+ /** Client service name other plugins read to draw show_view cards in their own surfaces. */
3324
+ const CARDS_SERVICE = "dshDeepView";
3325
+ function staticArgs(values) {
3326
+ return {
3327
+ has: (key) => values[key] !== void 0,
3328
+ complete: (key) => values[key] !== void 0,
3329
+ text: (key) => typeof values[key] === "string" ? values[key] : void 0,
3330
+ value: (key) => values[key]
3331
+ };
3332
+ }
3333
+ /** One show_view card for a plugin-drawn chat: same card, same answer channel as the host chat. */
3334
+ function createCards(call, locale) {
3335
+ const subscribe = (listener) => locale?.subscribe?.(listener) ?? (() => {});
3336
+ const active = () => locale?.getSnapshot?.().active ?? "en";
3337
+ function Card(props) {
3338
+ const language = (0, react.useSyncExternalStore)(subscribe, active);
3339
+ const t = (0, react.useMemo)(() => {
3340
+ const table = language.toLowerCase().startsWith("zh") ? uiZh : uiEn;
3341
+ return (key, params) => (table[key] ?? key).replace(/\{(\w+)\}/g, (_, name) => String(params?.[name] ?? `{${name}}`));
3342
+ }, [language]);
3343
+ const block = (0, react.useMemo)(() => props.presentation !== void 0 ? {
3344
+ callId: props.callId,
3345
+ kind: "tool-result",
3346
+ meta: props.presentation,
3347
+ isError: false
3348
+ } : props.failed ? {
3349
+ callId: props.callId,
3350
+ kind: "tool-result",
3351
+ isError: true
3352
+ } : {
3353
+ callId: props.callId,
3354
+ args: staticArgs({
3355
+ title: props.title,
3356
+ view: props.view
3357
+ })
3358
+ }, [
3359
+ props.callId,
3360
+ props.presentation,
3361
+ props.failed,
3362
+ props.title,
3363
+ props.view
3364
+ ]);
3365
+ const phase = props.presentation !== void 0 || props.failed ? "result" : "start";
3366
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCallView, {
3367
+ sessionId: props.sessionId,
3368
+ callId: props.callId,
3369
+ phase,
3370
+ block,
3371
+ t,
3372
+ call,
3373
+ ...props.answer ? { restoredAnswer: props.answer } : {}
3374
+ });
3375
+ }
3376
+ return { Card };
3377
+ }
3378
+ function apply(ctx) {
3379
+ const client = ctx;
3380
+ installClientPluginLogger(ctx, PLUGIN_NAME);
3381
+ client.provide(CARDS_SERVICE, createCards((endpoint, payload) => client.connection.rpc.call(RPC_CHANNEL, endpoint, payload), client.locale));
3382
+ ctx.effect(() => client.locale.register(LOCALE, {
3383
+ zh: { ...uiZh },
3384
+ en: { ...uiEn }
3385
+ }), "deep-view.locale");
3386
+ ctx.effect(() => {
3387
+ if (typeof document === "undefined") return () => {};
3388
+ const style = document.createElement("style");
3389
+ style.setAttribute("data-plugin", PLUGIN_NAME);
3390
+ style.textContent = viewStyles;
3391
+ document.head.appendChild(style);
3392
+ return () => style.remove();
3393
+ }, "deep-view.styles");
3394
+ ctx.effect(() => client.slots.inject("tool.call.toolview", () => client.slots.register({
3395
+ name: "tool.call.toolview",
3396
+ key: "show_view",
3397
+ locale: LOCALE
3398
+ }, ToolCallStatus)), "deep-view.tool");
3399
+ ctx.effect(() => client.slots.inject("conversation.chat.turnTail", () => client.slots.register({
3400
+ name: "conversation.chat.turnTail",
3401
+ id: "deep-view",
3402
+ order: 20,
3403
+ locale: LOCALE,
3404
+ inject: () => ({ call: (endpoint, payload) => client.connection.rpc.call(RPC_CHANNEL, endpoint, payload) })
3405
+ }, ReplyView)), "deep-view.reply");
3406
+ }
3407
+ //#endregion
3408
+ exports.CARDS_SERVICE = CARDS_SERVICE;
3409
+ exports.LOCALE = LOCALE;
3410
+ exports.ReplyView = ReplyView;
3411
+ exports.ToolCallStatus = ToolCallStatus;
3412
+ exports.ToolCallView = ToolCallView;
3413
+ exports.apply = apply;
3414
+ exports.createCards = createCards;
3415
+ exports.inject = inject;
3416
+ exports.name = name;
3417
+ exports.readToolCall = readToolCall;
3418
+ exports.replyCards = replyCards;
3419
+ exports.uiEn = uiEn;
3420
+ exports.uiZh = uiZh;
3421
+ exports.useMarkdownLabels = useMarkdownLabels;
3422
+
3423
+ //# sourceMappingURL=client.inner.cjs.map