@klarkxy/dsh-deep-view 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/NOTICE +23 -0
- package/README.md +25 -2
- package/cordis.patch.yml +5 -0
- package/docs/README.zh-CN.md +26 -0
- package/icon.svg +6 -0
- package/lib/client.inner.cjs +3073 -0
- package/lib/client.inner.cjs.map +1 -0
- package/lib/client.js +3082 -0
- package/lib/contracts.d.ts +98 -0
- package/lib/contracts.js +11 -0
- package/lib/contracts.js.map +1 -0
- package/lib/index.d.ts +73 -0
- package/lib/index.js +1436 -0
- package/lib/index.js.map +1 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +127 -4
|
@@ -0,0 +1,3073 @@
|
|
|
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 src/contracts.ts
|
|
1031
|
+
const PLUGIN_NAME = "@klarkxy/dsh-deep-view";
|
|
1032
|
+
const RPC_CHANNEL = "/dsh-deep-view";
|
|
1033
|
+
const VIEW_OWNER = "dsh-deep-view";
|
|
1034
|
+
function isRecord(value) {
|
|
1035
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1036
|
+
}
|
|
1037
|
+
//#endregion
|
|
1038
|
+
//#region src/ui.tsx
|
|
1039
|
+
const english = (key, params) => key.replace(/\{(\w+)\}/g, (_, name) => String(params?.[name] ?? `{${name}}`));
|
|
1040
|
+
function LineIcon({ paths }) {
|
|
1041
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1042
|
+
width: "15",
|
|
1043
|
+
height: "15",
|
|
1044
|
+
viewBox: "0 0 20 20",
|
|
1045
|
+
fill: "none",
|
|
1046
|
+
stroke: "currentColor",
|
|
1047
|
+
strokeWidth: "1.5",
|
|
1048
|
+
strokeLinecap: "round",
|
|
1049
|
+
strokeLinejoin: "round",
|
|
1050
|
+
"aria-hidden": "true",
|
|
1051
|
+
children: paths.map((d, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d }, index))
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
const DotsIcon = () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LineIcon, { paths: ["M5 10h.01M10 10h.01M15 10h.01"] });
|
|
1055
|
+
const CheckIcon = () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LineIcon, { paths: ["M4.5 10.5l3.5 3.5 7.5-8"] });
|
|
1056
|
+
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"] });
|
|
1057
|
+
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"] });
|
|
1058
|
+
const InfoIcon = () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LineIcon, { paths: [
|
|
1059
|
+
"M10 3.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13z",
|
|
1060
|
+
"M10 9.25v4",
|
|
1061
|
+
"M10 6.6h.01"
|
|
1062
|
+
] });
|
|
1063
|
+
const LinkIcon = () => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
1064
|
+
width: "12",
|
|
1065
|
+
height: "12",
|
|
1066
|
+
viewBox: "0 0 20 20",
|
|
1067
|
+
fill: "none",
|
|
1068
|
+
stroke: "currentColor",
|
|
1069
|
+
strokeWidth: "1.8",
|
|
1070
|
+
strokeLinecap: "round",
|
|
1071
|
+
strokeLinejoin: "round",
|
|
1072
|
+
"aria-hidden": "true",
|
|
1073
|
+
children: [
|
|
1074
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8.5 11.5l3-3" }),
|
|
1075
|
+
/* @__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" }),
|
|
1076
|
+
/* @__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" })
|
|
1077
|
+
]
|
|
1078
|
+
});
|
|
1079
|
+
const keyLetter = (index) => String.fromCharCode(65 + index);
|
|
1080
|
+
function Segmented({ label, value, className, children }) {
|
|
1081
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1082
|
+
className: className ? `dvui-seg ${className}` : "dvui-seg",
|
|
1083
|
+
role: "group",
|
|
1084
|
+
"aria-label": label,
|
|
1085
|
+
"data-active": value,
|
|
1086
|
+
children
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
//#endregion
|
|
1090
|
+
//#region src/chart.tsx
|
|
1091
|
+
const PALETTE = [
|
|
1092
|
+
"var(--dsw-alias-state-business-primary)",
|
|
1093
|
+
"var(--dsw-alias-brand-primary)",
|
|
1094
|
+
"var(--dsw-alias-state-success-primary)",
|
|
1095
|
+
"var(--dsw-alias-state-warn-primary)",
|
|
1096
|
+
"var(--dsw-alias-state-error-primary)"
|
|
1097
|
+
];
|
|
1098
|
+
const colorAt = (index) => PALETTE[index % PALETTE.length];
|
|
1099
|
+
const TICK = 10;
|
|
1100
|
+
const LABEL = 11;
|
|
1101
|
+
const textWidth = (text, size) => [...String(text)].reduce((sum, char) => sum + (char.charCodeAt(0) > 11904 ? size : size * .58), 0);
|
|
1102
|
+
const fit = (text, room, size) => {
|
|
1103
|
+
if (textWidth(text, size) <= room) return String(text);
|
|
1104
|
+
let out = "";
|
|
1105
|
+
for (const char of String(text)) {
|
|
1106
|
+
if (textWidth(`${out}${char}…`, size) > room) break;
|
|
1107
|
+
out += char;
|
|
1108
|
+
}
|
|
1109
|
+
return out === "" ? "" : `${out}…`;
|
|
1110
|
+
};
|
|
1111
|
+
function formatNumber(value) {
|
|
1112
|
+
if (!Number.isFinite(value)) return asText(value);
|
|
1113
|
+
const abs = Math.abs(value);
|
|
1114
|
+
const digits = abs >= 100 ? 0 : abs >= 10 ? 1 : 2;
|
|
1115
|
+
return Number(value.toFixed(digits)).toLocaleString("en-US", { maximumFractionDigits: digits });
|
|
1116
|
+
}
|
|
1117
|
+
const withUnit$1 = (value, unit) => unit ? unit === "%" ? `${formatNumber(value)}%` : `${formatNumber(value)} ${unit}` : formatNumber(value);
|
|
1118
|
+
function niceTicks(low, high) {
|
|
1119
|
+
let lo = low;
|
|
1120
|
+
let hi = high;
|
|
1121
|
+
if (lo === hi) {
|
|
1122
|
+
lo = Math.min(0, lo);
|
|
1123
|
+
hi = hi === 0 ? 1 : Math.max(0, hi);
|
|
1124
|
+
}
|
|
1125
|
+
if (lo === hi) hi = lo + 1;
|
|
1126
|
+
const raw = (hi - lo) / 4;
|
|
1127
|
+
const magnitude = 10 ** Math.floor(Math.log10(raw));
|
|
1128
|
+
const step = [
|
|
1129
|
+
1,
|
|
1130
|
+
2,
|
|
1131
|
+
2.5,
|
|
1132
|
+
5,
|
|
1133
|
+
10
|
|
1134
|
+
].map((scale) => scale * magnitude).find((candidate) => candidate >= raw);
|
|
1135
|
+
const start = Math.floor(lo / step + 1e-9) * step;
|
|
1136
|
+
const end = Math.ceil(hi / step - 1e-9) * step;
|
|
1137
|
+
const ticks = [];
|
|
1138
|
+
for (let value = start; value <= end + step / 2; value += step) ticks.push(Number(value.toPrecision(12)));
|
|
1139
|
+
return ticks;
|
|
1140
|
+
}
|
|
1141
|
+
function chartData(node, lookup) {
|
|
1142
|
+
const categories = (Array.isArray(node.categories) ? node.categories : []).map(asText);
|
|
1143
|
+
const series = (Array.isArray(node.series) ? node.series : []).map((item, index) => ({
|
|
1144
|
+
name: typeof item?.name === "string" ? item.name : "",
|
|
1145
|
+
color: colorAt(index),
|
|
1146
|
+
values: categories.map((_, at) => {
|
|
1147
|
+
const value = Number(resolve(Array.isArray(item?.values) ? item.values[at] : void 0, lookup, 0));
|
|
1148
|
+
return Number.isFinite(value) ? value : 0;
|
|
1149
|
+
})
|
|
1150
|
+
}));
|
|
1151
|
+
return {
|
|
1152
|
+
kind: node.kind ?? "bar",
|
|
1153
|
+
categories,
|
|
1154
|
+
series,
|
|
1155
|
+
unit: typeof node.unit === "string" ? node.unit : ""
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
const csvCell = (value) => /[",\n]/.test(value) ? `"${value.replace(/"/g, "\"\"")}"` : value;
|
|
1159
|
+
function chartCsv({ categories, series, unit }, t = english) {
|
|
1160
|
+
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");
|
|
1161
|
+
}
|
|
1162
|
+
function valueAxis(values, zero) {
|
|
1163
|
+
const finite = values.length > 0 ? values : [0];
|
|
1164
|
+
const low = Math.min(...finite);
|
|
1165
|
+
const high = Math.max(...finite);
|
|
1166
|
+
if (zero) return niceTicks(Math.min(0, low), Math.max(0, high));
|
|
1167
|
+
const pad = (high - low) * .1 || Math.abs(high) * .1 || 1;
|
|
1168
|
+
return niceTicks(low - pad, high + pad);
|
|
1169
|
+
}
|
|
1170
|
+
function columns({ categories, series, unit, kind }, width, active, shown) {
|
|
1171
|
+
const visible = series.filter((_, index) => shown[index]);
|
|
1172
|
+
const ticks = valueAxis(visible.flatMap((item) => item.values), kind !== "line");
|
|
1173
|
+
const lo = ticks[0];
|
|
1174
|
+
const hi = ticks.at(-1);
|
|
1175
|
+
const left = Math.ceil(Math.max(...ticks.map((tick) => textWidth(withUnit$1(tick, unit), TICK)))) + 10;
|
|
1176
|
+
const top = 8;
|
|
1177
|
+
const height = 220;
|
|
1178
|
+
const plot = {
|
|
1179
|
+
x: left,
|
|
1180
|
+
y: top,
|
|
1181
|
+
w: Math.max(40, width - left - 4),
|
|
1182
|
+
h: 188
|
|
1183
|
+
};
|
|
1184
|
+
const y = (value) => plot.y + plot.h - (value - lo) / (hi - lo) * plot.h;
|
|
1185
|
+
const band = plot.w / categories.length;
|
|
1186
|
+
const parts = [];
|
|
1187
|
+
for (const tick of ticks) {
|
|
1188
|
+
parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
|
|
1189
|
+
className: "dvui-chart-grid",
|
|
1190
|
+
x1: plot.x,
|
|
1191
|
+
x2: plot.x + plot.w,
|
|
1192
|
+
y1: y(tick),
|
|
1193
|
+
y2: y(tick)
|
|
1194
|
+
}, `g${tick}`));
|
|
1195
|
+
parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
1196
|
+
className: "dvui-chart-tick",
|
|
1197
|
+
x: plot.x - 8,
|
|
1198
|
+
y: y(tick) + 3,
|
|
1199
|
+
textAnchor: "end",
|
|
1200
|
+
children: withUnit$1(tick, unit)
|
|
1201
|
+
}, `t${tick}`));
|
|
1202
|
+
}
|
|
1203
|
+
const points = [];
|
|
1204
|
+
categories.forEach((label, at) => {
|
|
1205
|
+
const cx = plot.x + band * at + band / 2;
|
|
1206
|
+
parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
1207
|
+
className: "dvui-chart-label",
|
|
1208
|
+
x: cx,
|
|
1209
|
+
y: 213,
|
|
1210
|
+
textAnchor: "middle",
|
|
1211
|
+
children: fit(label, band - 4, LABEL)
|
|
1212
|
+
}, `c${at}`));
|
|
1213
|
+
points.push({
|
|
1214
|
+
x: cx,
|
|
1215
|
+
y: y(Math.max(...visible.map((item) => item.values[at]), lo))
|
|
1216
|
+
});
|
|
1217
|
+
});
|
|
1218
|
+
if (kind === "bar") {
|
|
1219
|
+
const group = band * .72;
|
|
1220
|
+
const gap = visible.length > 1 ? 2 : 0;
|
|
1221
|
+
const barW = Math.max(2, (group - gap * (visible.length - 1)) / visible.length);
|
|
1222
|
+
const zero = y(Math.max(lo, Math.min(0, hi)));
|
|
1223
|
+
categories.forEach((_, at) => visible.forEach((item, row) => {
|
|
1224
|
+
const value = item.values[at];
|
|
1225
|
+
const x = plot.x + band * at + (band - group) / 2 + row * (barW + gap);
|
|
1226
|
+
const top2 = Math.min(y(value), zero);
|
|
1227
|
+
const tall = Math.max(1, Math.abs(y(value) - zero));
|
|
1228
|
+
const r = Math.min(4, barW / 2, tall);
|
|
1229
|
+
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`;
|
|
1230
|
+
parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1231
|
+
className: "dvui-chart-mark",
|
|
1232
|
+
"data-dim": active !== null && active !== at ? "" : void 0,
|
|
1233
|
+
d,
|
|
1234
|
+
style: { fill: item.color }
|
|
1235
|
+
}, `b${at}-${row}`));
|
|
1236
|
+
}));
|
|
1237
|
+
} else {
|
|
1238
|
+
const xs = categories.map((_, at) => plot.x + band * at + band / 2);
|
|
1239
|
+
visible.forEach((item, row) => {
|
|
1240
|
+
const line = item.values.map((value, at) => `${at === 0 ? "M" : "L"}${xs[at]},${y(value)}`).join("");
|
|
1241
|
+
if (kind === "area") {
|
|
1242
|
+
const base = y(Math.max(lo, Math.min(0, hi)));
|
|
1243
|
+
parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1244
|
+
className: "dvui-chart-area",
|
|
1245
|
+
d: `${line}L${xs.at(-1)},${base}L${xs[0]},${base}z`,
|
|
1246
|
+
style: { fill: item.color }
|
|
1247
|
+
}, `a${row}`));
|
|
1248
|
+
}
|
|
1249
|
+
parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1250
|
+
className: "dvui-chart-line",
|
|
1251
|
+
d: line,
|
|
1252
|
+
style: { stroke: item.color }
|
|
1253
|
+
}, `l${row}`));
|
|
1254
|
+
item.values.forEach((value, at) => parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
1255
|
+
className: "dvui-chart-dot",
|
|
1256
|
+
cx: xs[at],
|
|
1257
|
+
cy: y(value),
|
|
1258
|
+
r: active === at ? 4 : 2.5,
|
|
1259
|
+
style: { fill: item.color }
|
|
1260
|
+
}, `p${row}-${at}`)));
|
|
1261
|
+
});
|
|
1262
|
+
if (active !== null) parts.unshift(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
|
|
1263
|
+
className: "dvui-chart-guide",
|
|
1264
|
+
x1: xs[active],
|
|
1265
|
+
x2: xs[active],
|
|
1266
|
+
y1: plot.y,
|
|
1267
|
+
y2: plot.y + plot.h
|
|
1268
|
+
}, "guide"));
|
|
1269
|
+
}
|
|
1270
|
+
return {
|
|
1271
|
+
parts,
|
|
1272
|
+
height,
|
|
1273
|
+
points,
|
|
1274
|
+
hits: categories.map((_, at) => ({
|
|
1275
|
+
x: plot.x + band * at,
|
|
1276
|
+
y: plot.y,
|
|
1277
|
+
w: band,
|
|
1278
|
+
h: plot.h
|
|
1279
|
+
}))
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
function rows({ categories, series, unit }, width, active, shown) {
|
|
1283
|
+
const visible = series.filter((_, index) => shown[index]);
|
|
1284
|
+
const ticks = valueAxis(visible.flatMap((item) => item.values), true);
|
|
1285
|
+
const lo = ticks[0];
|
|
1286
|
+
const hi = ticks.at(-1);
|
|
1287
|
+
const labelW = Math.min(width * .38, Math.ceil(Math.max(...categories.map((label) => textWidth(label, LABEL)))) + 10);
|
|
1288
|
+
const valueW = Math.ceil(Math.max(...visible.flatMap((item) => item.values).map((value) => textWidth(withUnit$1(value, unit), LABEL)))) + 10;
|
|
1289
|
+
const barH = 14;
|
|
1290
|
+
const rowH = 10 + visible.length * barH + (visible.length - 1) * 2;
|
|
1291
|
+
const top = 4;
|
|
1292
|
+
const height = top + rowH * categories.length + 22;
|
|
1293
|
+
const plot = {
|
|
1294
|
+
x: labelW,
|
|
1295
|
+
y: top,
|
|
1296
|
+
w: Math.max(40, width - labelW - valueW),
|
|
1297
|
+
h: rowH * categories.length
|
|
1298
|
+
};
|
|
1299
|
+
const x = (value) => plot.x + (value - lo) / (hi - lo) * plot.w;
|
|
1300
|
+
const parts = [];
|
|
1301
|
+
for (const tick of ticks) {
|
|
1302
|
+
parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
|
|
1303
|
+
className: "dvui-chart-grid",
|
|
1304
|
+
x1: x(tick),
|
|
1305
|
+
x2: x(tick),
|
|
1306
|
+
y1: plot.y,
|
|
1307
|
+
y2: plot.y + plot.h
|
|
1308
|
+
}, `g${tick}`));
|
|
1309
|
+
parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
1310
|
+
className: "dvui-chart-tick",
|
|
1311
|
+
x: x(tick),
|
|
1312
|
+
y: height - 6,
|
|
1313
|
+
textAnchor: "middle",
|
|
1314
|
+
children: withUnit$1(tick, unit)
|
|
1315
|
+
}, `t${tick}`));
|
|
1316
|
+
}
|
|
1317
|
+
const points = [];
|
|
1318
|
+
const zero = x(Math.max(lo, Math.min(0, hi)));
|
|
1319
|
+
categories.forEach((label, at) => {
|
|
1320
|
+
const rowY = plot.y + rowH * at + 5;
|
|
1321
|
+
parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
1322
|
+
className: "dvui-chart-label",
|
|
1323
|
+
x: labelW - 10,
|
|
1324
|
+
y: rowY + (rowH - 10) / 2 + 4,
|
|
1325
|
+
textAnchor: "end",
|
|
1326
|
+
children: fit(label, labelW - 12, LABEL)
|
|
1327
|
+
}, `c${at}`));
|
|
1328
|
+
visible.forEach((item, row) => {
|
|
1329
|
+
const value = item.values[at];
|
|
1330
|
+
const by = rowY + row * 16;
|
|
1331
|
+
const left = Math.min(x(value), zero);
|
|
1332
|
+
const long = Math.max(1, Math.abs(x(value) - zero));
|
|
1333
|
+
parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
1334
|
+
className: "dvui-chart-mark",
|
|
1335
|
+
"data-dim": active !== null && active !== at ? "" : void 0,
|
|
1336
|
+
x: left,
|
|
1337
|
+
y: by,
|
|
1338
|
+
width: long,
|
|
1339
|
+
height: barH,
|
|
1340
|
+
rx: Math.min(4, long / 2),
|
|
1341
|
+
style: { fill: item.color }
|
|
1342
|
+
}, `b${at}-${row}`));
|
|
1343
|
+
parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
1344
|
+
className: "dvui-chart-value",
|
|
1345
|
+
x: left + long + 6,
|
|
1346
|
+
y: by + barH - 3,
|
|
1347
|
+
children: withUnit$1(value, unit)
|
|
1348
|
+
}, `v${at}-${row}`));
|
|
1349
|
+
});
|
|
1350
|
+
points.push({
|
|
1351
|
+
x: Math.max(...visible.map((item) => x(item.values[at])), zero),
|
|
1352
|
+
y: rowY
|
|
1353
|
+
});
|
|
1354
|
+
});
|
|
1355
|
+
return {
|
|
1356
|
+
parts,
|
|
1357
|
+
height,
|
|
1358
|
+
points,
|
|
1359
|
+
hits: categories.map((_, at) => ({
|
|
1360
|
+
x: 0,
|
|
1361
|
+
y: plot.y + rowH * at,
|
|
1362
|
+
w: width,
|
|
1363
|
+
h: rowH
|
|
1364
|
+
}))
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
function donut({ categories, series }, width, active, hover) {
|
|
1368
|
+
const values = (series[0]?.values ?? []).map((value) => Math.max(0, value));
|
|
1369
|
+
const total = values.reduce((sum, value) => sum + value, 0);
|
|
1370
|
+
const size = Math.min(180, Math.max(120, width * .42));
|
|
1371
|
+
const r = size / 2;
|
|
1372
|
+
const inner = r * .62;
|
|
1373
|
+
const cx = r;
|
|
1374
|
+
const cy = r;
|
|
1375
|
+
const parts = [];
|
|
1376
|
+
const points = [];
|
|
1377
|
+
let angle = -Math.PI / 2;
|
|
1378
|
+
values.forEach((value, at) => {
|
|
1379
|
+
const sweep = total > 0 ? value / total * Math.PI * 2 : 0;
|
|
1380
|
+
const end = angle + sweep;
|
|
1381
|
+
const mid = angle + sweep / 2;
|
|
1382
|
+
points.push({
|
|
1383
|
+
x: cx + Math.cos(mid) * (r - (r - inner) / 2),
|
|
1384
|
+
y: cy + Math.sin(mid) * (r - (r - inner) / 2)
|
|
1385
|
+
});
|
|
1386
|
+
if (sweep > 0) {
|
|
1387
|
+
const large = sweep > Math.PI ? 1 : 0;
|
|
1388
|
+
const full = sweep >= Math.PI * 2 - 1e-6;
|
|
1389
|
+
const p = (radius, theta) => `${cx + Math.cos(theta) * radius},${cy + Math.sin(theta) * radius}`;
|
|
1390
|
+
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`;
|
|
1391
|
+
parts.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1392
|
+
className: "dvui-chart-mark",
|
|
1393
|
+
"data-dim": active !== null && active !== at ? "" : void 0,
|
|
1394
|
+
d,
|
|
1395
|
+
fillRule: "evenodd",
|
|
1396
|
+
style: { fill: colorAt(at) },
|
|
1397
|
+
onPointerEnter: () => hover(at)
|
|
1398
|
+
}, `s${at}`));
|
|
1399
|
+
}
|
|
1400
|
+
angle = end;
|
|
1401
|
+
});
|
|
1402
|
+
return {
|
|
1403
|
+
parts,
|
|
1404
|
+
height: size,
|
|
1405
|
+
size,
|
|
1406
|
+
points,
|
|
1407
|
+
total,
|
|
1408
|
+
values,
|
|
1409
|
+
hits: []
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
function useWidth(box, initial) {
|
|
1413
|
+
const [width, setWidth] = (0, react.useState)(initial);
|
|
1414
|
+
(0, react.useLayoutEffect)(() => {
|
|
1415
|
+
const node = box.current;
|
|
1416
|
+
if (!node || typeof ResizeObserver !== "function") return void 0;
|
|
1417
|
+
const measure = () => {
|
|
1418
|
+
const next = Math.round(node.clientWidth);
|
|
1419
|
+
if (next > 0) setWidth(next);
|
|
1420
|
+
};
|
|
1421
|
+
measure();
|
|
1422
|
+
const observer = new ResizeObserver(measure);
|
|
1423
|
+
observer.observe(node);
|
|
1424
|
+
return () => observer.disconnect();
|
|
1425
|
+
}, [box]);
|
|
1426
|
+
return width;
|
|
1427
|
+
}
|
|
1428
|
+
function ChartTable({ data, t = english }) {
|
|
1429
|
+
const { categories, series, unit } = data;
|
|
1430
|
+
const heads = series.map((item, index) => item.name || (series.length === 1 ? unit || t("Value") : `${t("Series")} ${index + 1}`));
|
|
1431
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1432
|
+
className: "dvui-table",
|
|
1433
|
+
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", {
|
|
1434
|
+
scope: "col",
|
|
1435
|
+
className: "dvui-num",
|
|
1436
|
+
children: head
|
|
1437
|
+
}, 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", {
|
|
1438
|
+
scope: "row",
|
|
1439
|
+
children: label
|
|
1440
|
+
}), series.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
1441
|
+
className: "dvui-num",
|
|
1442
|
+
children: withUnit$1(item.values[at], series.length === 1 && !item.name ? "" : unit)
|
|
1443
|
+
}, index))] }, `${at}-${label}`)) })] })
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
function WidgetChart({ node, lookup, registerChart, refreshCharts, t = english }) {
|
|
1447
|
+
const data = chartData(node, lookup);
|
|
1448
|
+
const { kind, categories, series, unit } = data;
|
|
1449
|
+
const box = (0, react.useRef)(null);
|
|
1450
|
+
const width = useWidth(box, 520);
|
|
1451
|
+
const [active, setActive] = (0, react.useState)(null);
|
|
1452
|
+
const [hidden, setHidden] = (0, react.useState)([]);
|
|
1453
|
+
const [asTable, setAsTable] = (0, react.useState)(false);
|
|
1454
|
+
const [copied, setCopied] = (0, react.useState)(false);
|
|
1455
|
+
const labelId = (0, react.useId)();
|
|
1456
|
+
const menuKey = (0, react.useId)();
|
|
1457
|
+
const table = (0, react.useRef)(asTable);
|
|
1458
|
+
table.current = asTable;
|
|
1459
|
+
const shown = series.map((_, index) => !hidden.includes(index));
|
|
1460
|
+
const label = typeof node.caption === "string" && node.caption !== "" ? node.caption : t("Chart");
|
|
1461
|
+
const isDonut = kind === "donut";
|
|
1462
|
+
const legendBeside = isDonut && width >= 360;
|
|
1463
|
+
const drawn = isDonut ? donut(data, legendBeside ? width - 200 : width, active, setActive) : kind === "hbar" ? rows(data, width, active, shown) : columns(data, width, active, shown);
|
|
1464
|
+
const svgWidth = isDonut ? drawn.size : width;
|
|
1465
|
+
const move = (event) => {
|
|
1466
|
+
const keys = {
|
|
1467
|
+
ArrowRight: 1,
|
|
1468
|
+
ArrowDown: 1,
|
|
1469
|
+
ArrowLeft: -1,
|
|
1470
|
+
ArrowUp: -1
|
|
1471
|
+
};
|
|
1472
|
+
if (event.key === "Escape") {
|
|
1473
|
+
setActive(null);
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
if (keys[event.key] === void 0) return;
|
|
1477
|
+
event.preventDefault();
|
|
1478
|
+
setActive((current) => current === null ? 0 : (current + keys[event.key] + categories.length) % categories.length);
|
|
1479
|
+
};
|
|
1480
|
+
const point = active === null ? null : drawn.points[active];
|
|
1481
|
+
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}%`;
|
|
1482
|
+
const tipRows = active === null || isDonut ? [] : series.filter((_, index) => shown[index]).map((item) => ({
|
|
1483
|
+
color: item.color,
|
|
1484
|
+
name: item.name,
|
|
1485
|
+
value: withUnit$1(item.values[active], unit)
|
|
1486
|
+
}));
|
|
1487
|
+
const copy = () => {
|
|
1488
|
+
navigator.clipboard?.writeText(chartCsv(data, t)).then(() => {
|
|
1489
|
+
setCopied(true);
|
|
1490
|
+
setTimeout(() => setCopied(false), 1200);
|
|
1491
|
+
}).catch(() => {});
|
|
1492
|
+
};
|
|
1493
|
+
const toggleTable = () => {
|
|
1494
|
+
table.current = !table.current;
|
|
1495
|
+
setAsTable(table.current);
|
|
1496
|
+
};
|
|
1497
|
+
const action = (0, react.useRef)({
|
|
1498
|
+
label: null,
|
|
1499
|
+
asTable: false,
|
|
1500
|
+
toggleTable,
|
|
1501
|
+
copy
|
|
1502
|
+
});
|
|
1503
|
+
action.current = {
|
|
1504
|
+
label: typeof node.caption === "string" && node.caption !== "" ? node.caption : null,
|
|
1505
|
+
get asTable() {
|
|
1506
|
+
return table.current;
|
|
1507
|
+
},
|
|
1508
|
+
toggleTable,
|
|
1509
|
+
copy
|
|
1510
|
+
};
|
|
1511
|
+
(0, react.useLayoutEffect)(() => registerChart?.(menuKey, {
|
|
1512
|
+
get label() {
|
|
1513
|
+
return action.current.label;
|
|
1514
|
+
},
|
|
1515
|
+
get asTable() {
|
|
1516
|
+
return action.current.asTable;
|
|
1517
|
+
},
|
|
1518
|
+
toggleTable: () => action.current.toggleTable(),
|
|
1519
|
+
copy: () => action.current.copy()
|
|
1520
|
+
}), [registerChart, menuKey]);
|
|
1521
|
+
(0, react.useLayoutEffect)(() => {
|
|
1522
|
+
refreshCharts?.();
|
|
1523
|
+
}, [
|
|
1524
|
+
refreshCharts,
|
|
1525
|
+
asTable,
|
|
1526
|
+
label
|
|
1527
|
+
]);
|
|
1528
|
+
const legend = isDonut ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1529
|
+
className: "dvui-chart-legend dvui-chart-legend-list",
|
|
1530
|
+
children: categories.map((name, at) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1531
|
+
className: "dvui-chart-key",
|
|
1532
|
+
"data-dim": active !== null && active !== at ? "" : void 0,
|
|
1533
|
+
onPointerEnter: () => setActive(at),
|
|
1534
|
+
onPointerLeave: () => setActive(null),
|
|
1535
|
+
children: [
|
|
1536
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { style: { background: colorAt(at) } }),
|
|
1537
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1538
|
+
className: "dvui-chart-key-name",
|
|
1539
|
+
children: name
|
|
1540
|
+
}),
|
|
1541
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1542
|
+
className: "dvui-chart-key-value",
|
|
1543
|
+
children: share(at)
|
|
1544
|
+
})
|
|
1545
|
+
]
|
|
1546
|
+
}, `${at}-${name}`))
|
|
1547
|
+
}) : series.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1548
|
+
className: "dvui-chart-legend",
|
|
1549
|
+
children: series.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1550
|
+
type: "button",
|
|
1551
|
+
className: "dvui-chart-key",
|
|
1552
|
+
"aria-pressed": shown[index],
|
|
1553
|
+
onClick: () => setHidden((before) => before.includes(index) ? before.filter((item) => item !== index) : shown.filter(Boolean).length > 1 ? [...before, index] : before),
|
|
1554
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { style: { background: item.color } }), item.name || `${t("Series")} ${index + 1}`]
|
|
1555
|
+
}, `${index}-${item.name}`))
|
|
1556
|
+
}) : null;
|
|
1557
|
+
const plot = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1558
|
+
className: "dvui-chart-plot",
|
|
1559
|
+
style: isDonut ? { width: svgWidth } : void 0,
|
|
1560
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
1561
|
+
className: "dvui-chart-svg",
|
|
1562
|
+
width: svgWidth,
|
|
1563
|
+
height: drawn.height,
|
|
1564
|
+
viewBox: `0 0 ${svgWidth} ${drawn.height}`,
|
|
1565
|
+
role: "img",
|
|
1566
|
+
"aria-labelledby": labelId,
|
|
1567
|
+
tabIndex: 0,
|
|
1568
|
+
onKeyDown: move,
|
|
1569
|
+
onBlur: () => setActive(null),
|
|
1570
|
+
onPointerLeave: () => setActive(null),
|
|
1571
|
+
children: [drawn.parts, isDonut ? active === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
1572
|
+
className: "dvui-chart-center",
|
|
1573
|
+
x: svgWidth / 2,
|
|
1574
|
+
y: drawn.height / 2 + 5,
|
|
1575
|
+
textAnchor: "middle",
|
|
1576
|
+
children: unit === "%" ? "" : withUnit$1(drawn.total, unit)
|
|
1577
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
1578
|
+
className: "dvui-chart-center-name",
|
|
1579
|
+
x: svgWidth / 2,
|
|
1580
|
+
y: drawn.height / 2 - 4,
|
|
1581
|
+
textAnchor: "middle",
|
|
1582
|
+
children: fit(categories[active], drawn.size * .56, LABEL)
|
|
1583
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
1584
|
+
className: "dvui-chart-center",
|
|
1585
|
+
x: svgWidth / 2,
|
|
1586
|
+
y: drawn.height / 2 + 14,
|
|
1587
|
+
textAnchor: "middle",
|
|
1588
|
+
children: share(active)
|
|
1589
|
+
})] }) : drawn.hits.map((hit, at) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
1590
|
+
className: "dvui-chart-hit",
|
|
1591
|
+
x: hit.x,
|
|
1592
|
+
y: hit.y,
|
|
1593
|
+
width: hit.w,
|
|
1594
|
+
height: hit.h,
|
|
1595
|
+
onPointerEnter: () => setActive(at)
|
|
1596
|
+
}, `h${at}`))]
|
|
1597
|
+
}), point && !asTable && !isDonut ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1598
|
+
className: "dvui-chart-tip",
|
|
1599
|
+
role: "status",
|
|
1600
|
+
style: {
|
|
1601
|
+
left: point.x,
|
|
1602
|
+
top: point.y
|
|
1603
|
+
},
|
|
1604
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("b", { children: categories[active] }), tipRows.map((row, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
1605
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { style: { background: row.color } }),
|
|
1606
|
+
row.name ? `${row.name} ` : "",
|
|
1607
|
+
row.value
|
|
1608
|
+
] }, index))]
|
|
1609
|
+
}) : null]
|
|
1610
|
+
});
|
|
1611
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("figure", {
|
|
1612
|
+
ref: box,
|
|
1613
|
+
className: "dvui-chart",
|
|
1614
|
+
"data-kind": kind,
|
|
1615
|
+
children: [
|
|
1616
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1617
|
+
id: labelId,
|
|
1618
|
+
hidden: true,
|
|
1619
|
+
children: label
|
|
1620
|
+
}),
|
|
1621
|
+
asTable ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartTable, {
|
|
1622
|
+
data,
|
|
1623
|
+
t
|
|
1624
|
+
}) : isDonut ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1625
|
+
className: legendBeside ? "dvui-chart-donut dvui-chart-donut-beside" : "dvui-chart-donut",
|
|
1626
|
+
onPointerLeave: () => setActive(null),
|
|
1627
|
+
children: [plot, legend]
|
|
1628
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [legend, plot] }),
|
|
1629
|
+
copied ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1630
|
+
className: "dvui-chart-copied",
|
|
1631
|
+
role: "status",
|
|
1632
|
+
children: t("Copied")
|
|
1633
|
+
}) : null,
|
|
1634
|
+
typeof node.caption === "string" && node.caption !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("figcaption", {
|
|
1635
|
+
className: "dvui-caption",
|
|
1636
|
+
children: node.caption
|
|
1637
|
+
}) : null
|
|
1638
|
+
]
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
//#endregion
|
|
1642
|
+
//#region src/widget.tsx
|
|
1643
|
+
const TONES = [
|
|
1644
|
+
"good",
|
|
1645
|
+
"warn",
|
|
1646
|
+
"bad"
|
|
1647
|
+
];
|
|
1648
|
+
const CALLOUT_TONES = [
|
|
1649
|
+
"info",
|
|
1650
|
+
"good",
|
|
1651
|
+
"warn",
|
|
1652
|
+
"bad"
|
|
1653
|
+
];
|
|
1654
|
+
const optionsOf = (node) => Array.isArray(node.options) ? node.options : [];
|
|
1655
|
+
const fieldsOf = (node) => Array.isArray(node.fields) ? node.fields : [];
|
|
1656
|
+
const cellText = (value) => value === null || value === void 0 ? "" : typeof value === "object" ? JSON.stringify(value) : asText(value);
|
|
1657
|
+
const NUMERIC_CELL = /^[-+]?[\d.,]+\s?(%|[a-zA-Z]{1,4})?$/;
|
|
1658
|
+
function hasType(node, types) {
|
|
1659
|
+
return types.has(node.type) || Array.isArray(node.children) && node.children.some((child) => hasType(child, types));
|
|
1660
|
+
}
|
|
1661
|
+
const answerValues = (answer, node) => answer != null && answer.action === node.id ? answer.values : void 0;
|
|
1662
|
+
const withUnit = (value, unit) => `${asText(value)}${unit ? ` ${unit}` : ""}`;
|
|
1663
|
+
const grouped = (value) => typeof value === "number" && Number.isFinite(value) ? value.toLocaleString("en-US", { maximumFractionDigits: 6 }) : asText(value);
|
|
1664
|
+
function FallbackMarkdown({ text }) {
|
|
1665
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: text });
|
|
1666
|
+
}
|
|
1667
|
+
function OptionsInput({ node, ctx, t }) {
|
|
1668
|
+
const [picked, setPicked] = (0, react.useState)([]);
|
|
1669
|
+
const options = optionsOf(node);
|
|
1670
|
+
const multi = node.multi === true;
|
|
1671
|
+
const choose = (label) => {
|
|
1672
|
+
if (ctx.answerLocked) return;
|
|
1673
|
+
if (multi) setPicked((current) => current.includes(label) ? current.filter((item) => item !== label) : [...current, label]);
|
|
1674
|
+
else ctx.onSubmit?.(node.id, { selected: [label] });
|
|
1675
|
+
};
|
|
1676
|
+
const onKey = (event) => {
|
|
1677
|
+
const index = event.key.toUpperCase().charCodeAt(0) - 65;
|
|
1678
|
+
if (index >= 0 && index < options.length && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
1679
|
+
event.preventDefault();
|
|
1680
|
+
choose(options[index].label);
|
|
1681
|
+
}
|
|
1682
|
+
};
|
|
1683
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1684
|
+
className: "dvui-options",
|
|
1685
|
+
role: "group",
|
|
1686
|
+
"aria-label": t("Options"),
|
|
1687
|
+
onKeyDown: onKey,
|
|
1688
|
+
children: options.map((option, index) => {
|
|
1689
|
+
const selected = multi && picked.includes(option.label);
|
|
1690
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1691
|
+
type: "button",
|
|
1692
|
+
className: "dvui-option",
|
|
1693
|
+
disabled: ctx.answerLocked,
|
|
1694
|
+
"aria-pressed": multi ? selected : void 0,
|
|
1695
|
+
"aria-keyshortcuts": keyLetter(index).toLowerCase(),
|
|
1696
|
+
onClick: () => choose(option.label),
|
|
1697
|
+
children: [
|
|
1698
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1699
|
+
className: "dvui-key",
|
|
1700
|
+
"aria-hidden": "true",
|
|
1701
|
+
children: keyLetter(index)
|
|
1702
|
+
}),
|
|
1703
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1704
|
+
className: "dvui-option-body",
|
|
1705
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1706
|
+
className: "dvui-option-label",
|
|
1707
|
+
children: option.label
|
|
1708
|
+
}), option.description ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1709
|
+
className: "dvui-option-desc",
|
|
1710
|
+
children: option.description
|
|
1711
|
+
}) : null]
|
|
1712
|
+
}),
|
|
1713
|
+
selected ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1714
|
+
className: "dvui-check",
|
|
1715
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, {})
|
|
1716
|
+
}) : null
|
|
1717
|
+
]
|
|
1718
|
+
}, option.label);
|
|
1719
|
+
})
|
|
1720
|
+
}), multi ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1721
|
+
className: "dvui-actions",
|
|
1722
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1723
|
+
type: "button",
|
|
1724
|
+
className: "dvui-submit",
|
|
1725
|
+
disabled: ctx.answerLocked || picked.length === 0,
|
|
1726
|
+
onClick: () => ctx.onSubmit?.(node.id, { selected: picked }),
|
|
1727
|
+
children: t("Submit")
|
|
1728
|
+
})
|
|
1729
|
+
}) : null] });
|
|
1730
|
+
}
|
|
1731
|
+
function SettledOptions({ node, answer, t }) {
|
|
1732
|
+
const selected = answerValues(answer, node)?.selected;
|
|
1733
|
+
const chosen = Array.isArray(selected) ? selected : [];
|
|
1734
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1735
|
+
className: "dvui-options",
|
|
1736
|
+
role: "group",
|
|
1737
|
+
"aria-label": t("Your answer"),
|
|
1738
|
+
children: optionsOf(node).map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1739
|
+
className: "dvui-option",
|
|
1740
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1741
|
+
className: "dvui-option-body",
|
|
1742
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1743
|
+
className: "dvui-option-label",
|
|
1744
|
+
children: option.label
|
|
1745
|
+
}), option.description ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1746
|
+
className: "dvui-option-desc",
|
|
1747
|
+
children: option.description
|
|
1748
|
+
}) : null]
|
|
1749
|
+
}), chosen.includes(option.label) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1750
|
+
className: "dvui-check",
|
|
1751
|
+
role: "img",
|
|
1752
|
+
"aria-label": t("Selected"),
|
|
1753
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckIcon, {})
|
|
1754
|
+
}) : null]
|
|
1755
|
+
}, option.label))
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
function ConfirmInput({ node, ctx, t }) {
|
|
1759
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1760
|
+
className: "dvui-actions",
|
|
1761
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1762
|
+
type: "button",
|
|
1763
|
+
className: "dvui-button",
|
|
1764
|
+
disabled: ctx.answerLocked,
|
|
1765
|
+
onClick: () => ctx.onSubmit?.(node.id, { confirmed: false }),
|
|
1766
|
+
children: node.cancelLabel ?? t("Cancel")
|
|
1767
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1768
|
+
type: "button",
|
|
1769
|
+
className: "dvui-submit",
|
|
1770
|
+
disabled: ctx.answerLocked,
|
|
1771
|
+
onClick: () => ctx.onSubmit?.(node.id, { confirmed: true }),
|
|
1772
|
+
children: node.confirmLabel ?? t("Confirm")
|
|
1773
|
+
})]
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
function SettledConfirm({ node, answer, t }) {
|
|
1777
|
+
const confirmed = answerValues(answer, node)?.confirmed;
|
|
1778
|
+
if (typeof confirmed !== "boolean") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmInput, {
|
|
1779
|
+
node,
|
|
1780
|
+
ctx: { answerLocked: true },
|
|
1781
|
+
t
|
|
1782
|
+
});
|
|
1783
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1784
|
+
className: "dvui-badge",
|
|
1785
|
+
children: confirmed ? node.confirmLabel ?? t("Confirm") : node.cancelLabel ?? t("Cancel")
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
function FormInput({ node, ctx, t }) {
|
|
1789
|
+
const fields = fieldsOf(node);
|
|
1790
|
+
const [draft, setDraft] = (0, react.useState)({});
|
|
1791
|
+
const submit = () => {
|
|
1792
|
+
if (!ctx.answerLocked) ctx.onSubmit?.(node.id, { fields: Object.fromEntries(fields.map((field) => [field.id, draft[field.id] ?? ""])) });
|
|
1793
|
+
};
|
|
1794
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
|
|
1795
|
+
className: "dvui-form",
|
|
1796
|
+
onSubmit: (event) => {
|
|
1797
|
+
event.preventDefault();
|
|
1798
|
+
submit();
|
|
1799
|
+
},
|
|
1800
|
+
children: [fields.map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1801
|
+
className: "dvui-field",
|
|
1802
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1803
|
+
className: "dvui-field-label",
|
|
1804
|
+
children: field.label
|
|
1805
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1806
|
+
className: "dvui-input",
|
|
1807
|
+
type: "text",
|
|
1808
|
+
value: draft[field.id] ?? "",
|
|
1809
|
+
autoComplete: "off",
|
|
1810
|
+
spellCheck: false,
|
|
1811
|
+
readOnly: ctx.answerLocked,
|
|
1812
|
+
"aria-label": field.label,
|
|
1813
|
+
placeholder: field.placeholder,
|
|
1814
|
+
onChange: (event) => setDraft((current) => ({
|
|
1815
|
+
...current,
|
|
1816
|
+
[field.id]: event.target.value
|
|
1817
|
+
}))
|
|
1818
|
+
})]
|
|
1819
|
+
}, field.id)), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1820
|
+
className: "dvui-actions",
|
|
1821
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1822
|
+
type: "submit",
|
|
1823
|
+
className: "dvui-submit",
|
|
1824
|
+
disabled: ctx.answerLocked,
|
|
1825
|
+
children: node.submitLabel ?? t("Submit")
|
|
1826
|
+
})
|
|
1827
|
+
})]
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
function SettledForm({ node, answer }) {
|
|
1831
|
+
const fields = answerValues(answer, node)?.fields;
|
|
1832
|
+
const values = fields !== null && typeof fields === "object" ? fields : {};
|
|
1833
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1834
|
+
className: "dvui-form",
|
|
1835
|
+
children: fieldsOf(node).map((field) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1836
|
+
className: "dvui-field",
|
|
1837
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1838
|
+
className: "dvui-field-label",
|
|
1839
|
+
children: field.label
|
|
1840
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1841
|
+
className: "dvui-input",
|
|
1842
|
+
value: String(values[field.id] ?? ""),
|
|
1843
|
+
disabled: true,
|
|
1844
|
+
readOnly: true,
|
|
1845
|
+
"aria-label": field.label
|
|
1846
|
+
})]
|
|
1847
|
+
}, field.id))
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
function WidgetTable({ node, lookup }) {
|
|
1851
|
+
const columns = Array.isArray(node.columns) ? node.columns : [];
|
|
1852
|
+
const rows = (Array.isArray(node.rows) ? node.rows : []).map((row) => (Array.isArray(row) ? row : []).map((cell) => resolve(cell, lookup, "")));
|
|
1853
|
+
const numeric = columns.map((_, index) => {
|
|
1854
|
+
const cells = rows.map((row) => row[index]).filter((cell) => cell !== void 0 && cell !== "");
|
|
1855
|
+
return cells.length > 0 && cells.every((cell) => typeof cell === "number" || typeof cell === "string" && NUMERIC_CELL.test(cell.trim()));
|
|
1856
|
+
});
|
|
1857
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1858
|
+
className: "dvui-table",
|
|
1859
|
+
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", {
|
|
1860
|
+
scope: "col",
|
|
1861
|
+
className: numeric[index] ? "dvui-num" : void 0,
|
|
1862
|
+
children: column.label
|
|
1863
|
+
}, 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", {
|
|
1864
|
+
className: numeric[index] ? "dvui-num" : void 0,
|
|
1865
|
+
children: cellText(row[index])
|
|
1866
|
+
}, column.key ?? index)) }, rowIndex)) })] })
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
function WidgetProgress({ node, lookup, t }) {
|
|
1870
|
+
let percent;
|
|
1871
|
+
if (node.percent !== void 0) percent = Number(resolve(node.percent, lookup, 0));
|
|
1872
|
+
else {
|
|
1873
|
+
const max = Number(resolve(node.max, lookup, 0));
|
|
1874
|
+
percent = max > 0 ? Number(resolve(node.value, lookup, 0)) / max * 100 : 0;
|
|
1875
|
+
}
|
|
1876
|
+
percent = Math.max(0, Math.min(100, Number.isFinite(percent) ? percent : 0));
|
|
1877
|
+
const tone = resolve(node.tone, lookup);
|
|
1878
|
+
const right = node.valueLabel !== void 0 ? asText(resolve(node.valueLabel, lookup, "")) : `${Math.round(percent)}%`;
|
|
1879
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1880
|
+
className: "dvui-progress",
|
|
1881
|
+
"data-tone": TONES.includes(String(tone)) ? String(tone) : void 0,
|
|
1882
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1883
|
+
className: "dvui-progress-label",
|
|
1884
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: node.label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: right })]
|
|
1885
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1886
|
+
className: "dvui-progress-track",
|
|
1887
|
+
role: "progressbar",
|
|
1888
|
+
"aria-valuenow": Math.round(percent),
|
|
1889
|
+
"aria-valuemin": 0,
|
|
1890
|
+
"aria-valuemax": 100,
|
|
1891
|
+
"aria-label": node.label ?? t("Progress"),
|
|
1892
|
+
"aria-valuetext": right,
|
|
1893
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1894
|
+
className: "dvui-progress-fill",
|
|
1895
|
+
style: { width: `${percent}%` }
|
|
1896
|
+
})
|
|
1897
|
+
})]
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
function WidgetMetric({ node, lookup }) {
|
|
1901
|
+
const tone = resolve(node.tone, lookup);
|
|
1902
|
+
const caption = node.caption === void 0 ? "" : asText(resolve(node.caption, lookup, ""));
|
|
1903
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1904
|
+
className: "dvui-metric",
|
|
1905
|
+
"data-tone": TONES.includes(String(tone)) ? String(tone) : void 0,
|
|
1906
|
+
children: [
|
|
1907
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1908
|
+
className: "dvui-metric-label",
|
|
1909
|
+
children: node.label
|
|
1910
|
+
}),
|
|
1911
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1912
|
+
className: "dvui-metric-value",
|
|
1913
|
+
children: asText(resolve(node.value, lookup, "—"))
|
|
1914
|
+
}),
|
|
1915
|
+
caption !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1916
|
+
className: "dvui-caption",
|
|
1917
|
+
children: caption
|
|
1918
|
+
}) : null
|
|
1919
|
+
]
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
function WidgetCallout({ node, lookup, Markdown, labels }) {
|
|
1923
|
+
const tone = resolve(node.tone, lookup);
|
|
1924
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1925
|
+
className: "dvui-callout",
|
|
1926
|
+
"data-tone": CALLOUT_TONES.includes(String(tone)) ? String(tone) : "info",
|
|
1927
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1928
|
+
className: "dvui-callout-icon",
|
|
1929
|
+
"aria-hidden": "true",
|
|
1930
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfoIcon, {})
|
|
1931
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Markdown, {
|
|
1932
|
+
text: asText(resolve(node.text, lookup, "")),
|
|
1933
|
+
labels,
|
|
1934
|
+
variant: "compact"
|
|
1935
|
+
}) })]
|
|
1936
|
+
});
|
|
1937
|
+
}
|
|
1938
|
+
function WidgetSources({ node, t }) {
|
|
1939
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1940
|
+
className: "dvui-sources",
|
|
1941
|
+
role: "list",
|
|
1942
|
+
"aria-label": t("Sources"),
|
|
1943
|
+
children: (node.items ?? []).map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
|
|
1944
|
+
role: "listitem",
|
|
1945
|
+
className: "dvui-source",
|
|
1946
|
+
href: item.url,
|
|
1947
|
+
target: "_blank",
|
|
1948
|
+
rel: "noopener noreferrer",
|
|
1949
|
+
title: item.url,
|
|
1950
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LinkIcon, {}), item.label]
|
|
1951
|
+
}, `${item.url}-${index}`))
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
function InputHead({ id, label, value }) {
|
|
1955
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1956
|
+
className: "dvui-input-head",
|
|
1957
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
1958
|
+
htmlFor: id,
|
|
1959
|
+
className: "dvui-input-label",
|
|
1960
|
+
children: label
|
|
1961
|
+
}), value !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("output", {
|
|
1962
|
+
htmlFor: id,
|
|
1963
|
+
className: "dvui-input-value",
|
|
1964
|
+
children: value
|
|
1965
|
+
}) : null]
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
const numericBound = (bound) => typeof bound === "number" && Number.isFinite(bound) ? bound : typeof bound === "string" && bound.trim() !== "" && Number.isFinite(Number(bound)) ? Number(bound) : void 0;
|
|
1969
|
+
function SliderInput({ node, ctx }) {
|
|
1970
|
+
const id = (0, react.useId)();
|
|
1971
|
+
const value = Number(ctx.values[node.id]);
|
|
1972
|
+
const min = numericBound(resolve(node.min, ctx.lookup, void 0));
|
|
1973
|
+
const max = numericBound(resolve(node.max, ctx.lookup, void 0));
|
|
1974
|
+
const percent = min !== void 0 && max !== void 0 && max > min ? (value - min) / (max - min) * 100 : 0;
|
|
1975
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1976
|
+
className: "dvui-input-row",
|
|
1977
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(InputHead, {
|
|
1978
|
+
id,
|
|
1979
|
+
label: node.label,
|
|
1980
|
+
value: withUnit(grouped(value), node.unit)
|
|
1981
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1982
|
+
id,
|
|
1983
|
+
type: "range",
|
|
1984
|
+
className: "dvui-range",
|
|
1985
|
+
min,
|
|
1986
|
+
max,
|
|
1987
|
+
step: node.step ?? "any",
|
|
1988
|
+
value,
|
|
1989
|
+
disabled: ctx.locked,
|
|
1990
|
+
style: { "--_dvui-fill": `${percent}%` },
|
|
1991
|
+
onChange: (event) => ctx.setInput(node.id, Number(event.target.value))
|
|
1992
|
+
})]
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
function NumberInput({ node, ctx }) {
|
|
1996
|
+
const id = (0, react.useId)();
|
|
1997
|
+
const [draft, setDraft] = (0, react.useState)(null);
|
|
1998
|
+
const value = ctx.values[node.id];
|
|
1999
|
+
const min = numericBound(resolve(node.min, ctx.lookup, void 0));
|
|
2000
|
+
const max = numericBound(resolve(node.max, ctx.lookup, void 0));
|
|
2001
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2002
|
+
className: "dvui-input-row",
|
|
2003
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(InputHead, {
|
|
2004
|
+
id,
|
|
2005
|
+
label: node.label
|
|
2006
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2007
|
+
className: "dvui-number",
|
|
2008
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2009
|
+
id,
|
|
2010
|
+
className: "dvui-input",
|
|
2011
|
+
type: "number",
|
|
2012
|
+
inputMode: "decimal",
|
|
2013
|
+
value: draft ?? asText(value),
|
|
2014
|
+
min,
|
|
2015
|
+
max,
|
|
2016
|
+
step: node.step ?? "any",
|
|
2017
|
+
disabled: ctx.locked,
|
|
2018
|
+
onChange: (event) => {
|
|
2019
|
+
const raw = event.target.value;
|
|
2020
|
+
setDraft(raw);
|
|
2021
|
+
const next = Number(raw);
|
|
2022
|
+
if (raw.trim() === "" || !Number.isFinite(next) || min !== void 0 && next < min || max !== void 0 && next > max) return;
|
|
2023
|
+
ctx.setInput(node.id, next);
|
|
2024
|
+
},
|
|
2025
|
+
onBlur: () => setDraft(null)
|
|
2026
|
+
}), node.unit ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2027
|
+
className: "dvui-unit",
|
|
2028
|
+
children: node.unit
|
|
2029
|
+
}) : null]
|
|
2030
|
+
})]
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
2033
|
+
function ChoiceInput({ node, ctx }) {
|
|
2034
|
+
const labelId = (0, react.useId)();
|
|
2035
|
+
const options = optionsOf(node);
|
|
2036
|
+
const current = ctx.values[node.id];
|
|
2037
|
+
const side = options.length <= 4 && options.every((option) => [...option.label].length <= 14);
|
|
2038
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2039
|
+
className: "dvui-input-row",
|
|
2040
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2041
|
+
className: "dvui-input-label",
|
|
2042
|
+
id: labelId,
|
|
2043
|
+
children: node.label
|
|
2044
|
+
}), side ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Segmented, {
|
|
2045
|
+
label: node.label ?? "",
|
|
2046
|
+
value: options.findIndex((option) => choiceValue(option) === current),
|
|
2047
|
+
className: "dvui-choice-seg",
|
|
2048
|
+
children: options.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2049
|
+
type: "button",
|
|
2050
|
+
"aria-pressed": choiceValue(option) === current,
|
|
2051
|
+
disabled: ctx.locked,
|
|
2052
|
+
onClick: () => ctx.setInput(node.id, choiceValue(option)),
|
|
2053
|
+
children: option.label
|
|
2054
|
+
}, option.label))
|
|
2055
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2056
|
+
className: "dvui-radios",
|
|
2057
|
+
role: "radiogroup",
|
|
2058
|
+
"aria-labelledby": labelId,
|
|
2059
|
+
children: options.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2060
|
+
type: "button",
|
|
2061
|
+
role: "radio",
|
|
2062
|
+
className: "dvui-radio",
|
|
2063
|
+
"aria-checked": choiceValue(option) === current,
|
|
2064
|
+
disabled: ctx.locked,
|
|
2065
|
+
onClick: () => ctx.setInput(node.id, choiceValue(option)),
|
|
2066
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2067
|
+
className: "dvui-radio-dot",
|
|
2068
|
+
"aria-hidden": "true"
|
|
2069
|
+
}), option.label]
|
|
2070
|
+
}, option.label))
|
|
2071
|
+
})]
|
|
2072
|
+
});
|
|
2073
|
+
}
|
|
2074
|
+
function ToggleInput({ node, ctx }) {
|
|
2075
|
+
const on = ctx.values[node.id] === true;
|
|
2076
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2077
|
+
className: "dvui-toggle",
|
|
2078
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2079
|
+
className: "dvui-input-label",
|
|
2080
|
+
children: node.label
|
|
2081
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2082
|
+
type: "button",
|
|
2083
|
+
role: "switch",
|
|
2084
|
+
className: "dvui-switch",
|
|
2085
|
+
"aria-checked": on,
|
|
2086
|
+
"aria-label": node.label,
|
|
2087
|
+
disabled: ctx.locked,
|
|
2088
|
+
onClick: () => ctx.setInput(node.id, !on),
|
|
2089
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2090
|
+
className: "dvui-switch-knob",
|
|
2091
|
+
"aria-hidden": "true"
|
|
2092
|
+
})
|
|
2093
|
+
})]
|
|
2094
|
+
});
|
|
2095
|
+
}
|
|
2096
|
+
function WidgetNode({ node, ctx, Markdown, labels, t }) {
|
|
2097
|
+
const { lookup } = ctx;
|
|
2098
|
+
switch (node.type) {
|
|
2099
|
+
case "stack":
|
|
2100
|
+
case "row": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2101
|
+
className: node.type === "row" ? "dvui-row" : "dvui-stack",
|
|
2102
|
+
children: (node.children ?? []).map((child, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetNode, {
|
|
2103
|
+
node: child,
|
|
2104
|
+
ctx,
|
|
2105
|
+
Markdown,
|
|
2106
|
+
labels,
|
|
2107
|
+
t
|
|
2108
|
+
}, child.id ?? `${child.type}-${index}`))
|
|
2109
|
+
});
|
|
2110
|
+
case "text": {
|
|
2111
|
+
const text = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Markdown, {
|
|
2112
|
+
text: asText(resolve(node.text, lookup, "")),
|
|
2113
|
+
labels,
|
|
2114
|
+
variant: "compact"
|
|
2115
|
+
});
|
|
2116
|
+
return node.tone === "muted" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2117
|
+
className: "dvui-muted",
|
|
2118
|
+
children: text
|
|
2119
|
+
}) : text;
|
|
2120
|
+
}
|
|
2121
|
+
case "metric": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetMetric, {
|
|
2122
|
+
node,
|
|
2123
|
+
lookup
|
|
2124
|
+
});
|
|
2125
|
+
case "table": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetTable, {
|
|
2126
|
+
node,
|
|
2127
|
+
lookup
|
|
2128
|
+
});
|
|
2129
|
+
case "chart": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetChart, {
|
|
2130
|
+
node,
|
|
2131
|
+
lookup,
|
|
2132
|
+
registerChart: ctx.registerChart,
|
|
2133
|
+
refreshCharts: ctx.refreshCharts,
|
|
2134
|
+
t
|
|
2135
|
+
});
|
|
2136
|
+
case "progress": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetProgress, {
|
|
2137
|
+
node,
|
|
2138
|
+
lookup,
|
|
2139
|
+
t
|
|
2140
|
+
});
|
|
2141
|
+
case "callout": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetCallout, {
|
|
2142
|
+
node,
|
|
2143
|
+
lookup,
|
|
2144
|
+
Markdown,
|
|
2145
|
+
labels
|
|
2146
|
+
});
|
|
2147
|
+
case "sources": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetSources, {
|
|
2148
|
+
node,
|
|
2149
|
+
t
|
|
2150
|
+
});
|
|
2151
|
+
case "slider": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SliderInput, {
|
|
2152
|
+
node,
|
|
2153
|
+
ctx
|
|
2154
|
+
});
|
|
2155
|
+
case "number": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberInput, {
|
|
2156
|
+
node,
|
|
2157
|
+
ctx
|
|
2158
|
+
});
|
|
2159
|
+
case "choice": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChoiceInput, {
|
|
2160
|
+
node,
|
|
2161
|
+
ctx
|
|
2162
|
+
});
|
|
2163
|
+
case "toggle": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleInput, {
|
|
2164
|
+
node,
|
|
2165
|
+
ctx
|
|
2166
|
+
});
|
|
2167
|
+
case "submit": return ctx.settled ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2168
|
+
className: "dvui-actions",
|
|
2169
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2170
|
+
type: "button",
|
|
2171
|
+
className: "dvui-submit",
|
|
2172
|
+
disabled: ctx.answerLocked,
|
|
2173
|
+
onClick: () => ctx.onSubmit?.(node.id, {}),
|
|
2174
|
+
children: node.label || t("Send")
|
|
2175
|
+
})
|
|
2176
|
+
});
|
|
2177
|
+
case "options": return ctx.settled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettledOptions, {
|
|
2178
|
+
node,
|
|
2179
|
+
answer: ctx.answer,
|
|
2180
|
+
t
|
|
2181
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OptionsInput, {
|
|
2182
|
+
node,
|
|
2183
|
+
ctx,
|
|
2184
|
+
t
|
|
2185
|
+
});
|
|
2186
|
+
case "confirm": return ctx.settled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettledConfirm, {
|
|
2187
|
+
node,
|
|
2188
|
+
answer: ctx.answer,
|
|
2189
|
+
t
|
|
2190
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmInput, {
|
|
2191
|
+
node,
|
|
2192
|
+
ctx,
|
|
2193
|
+
t
|
|
2194
|
+
});
|
|
2195
|
+
case "form": return ctx.settled ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettledForm, {
|
|
2196
|
+
node,
|
|
2197
|
+
answer: ctx.answer
|
|
2198
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FormInput, {
|
|
2199
|
+
node,
|
|
2200
|
+
ctx,
|
|
2201
|
+
t
|
|
2202
|
+
});
|
|
2203
|
+
default: return null;
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
function WidgetView({ view, onSubmit, settled = false, answer = null, busy = false, registerChart, refreshCharts, t = english, labels = {
|
|
2207
|
+
code: {
|
|
2208
|
+
copyLabel: "Copy",
|
|
2209
|
+
copiedLabel: "Copied"
|
|
2210
|
+
},
|
|
2211
|
+
footnotes: "Footnotes"
|
|
2212
|
+
}, Markdown = FallbackMarkdown }) {
|
|
2213
|
+
const inputs = viewInputs(view);
|
|
2214
|
+
const [values, setValues] = (0, react.useState)(() => Object.fromEntries(inputs.map((node) => [node.id, inputDefault(node)])));
|
|
2215
|
+
const sent = answer?.values?.inputs;
|
|
2216
|
+
const current = settled && sent !== null && typeof sent === "object" ? {
|
|
2217
|
+
...values,
|
|
2218
|
+
...sent
|
|
2219
|
+
} : values;
|
|
2220
|
+
const submit = (action, extra) => onSubmit?.(action, inputs.length > 0 ? {
|
|
2221
|
+
...extra,
|
|
2222
|
+
inputs: current
|
|
2223
|
+
} : extra);
|
|
2224
|
+
const ctx = {
|
|
2225
|
+
onSubmit: submit,
|
|
2226
|
+
settled,
|
|
2227
|
+
answer,
|
|
2228
|
+
busy,
|
|
2229
|
+
lookup: viewScope(view, current),
|
|
2230
|
+
values: current,
|
|
2231
|
+
locked: settled || busy,
|
|
2232
|
+
answerLocked: settled || busy || onSubmit === void 0,
|
|
2233
|
+
setInput: (id, value) => setValues((before) => ({
|
|
2234
|
+
...before,
|
|
2235
|
+
[id]: value
|
|
2236
|
+
})),
|
|
2237
|
+
registerChart,
|
|
2238
|
+
refreshCharts
|
|
2239
|
+
};
|
|
2240
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetNode, {
|
|
2241
|
+
node: view,
|
|
2242
|
+
ctx,
|
|
2243
|
+
Markdown,
|
|
2244
|
+
labels,
|
|
2245
|
+
t
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
function useChartHandles() {
|
|
2249
|
+
const [, bump] = (0, react.useState)(0);
|
|
2250
|
+
const charts = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
2251
|
+
const refreshCharts = (0, react.useCallback)(() => bump((value) => value + 1), []);
|
|
2252
|
+
const registerChart = (0, react.useCallback)((key, handle) => {
|
|
2253
|
+
charts.current.set(key, handle);
|
|
2254
|
+
refreshCharts();
|
|
2255
|
+
return () => {
|
|
2256
|
+
charts.current.delete(key);
|
|
2257
|
+
refreshCharts();
|
|
2258
|
+
};
|
|
2259
|
+
}, [refreshCharts]);
|
|
2260
|
+
return {
|
|
2261
|
+
charts: [...charts.current.values()],
|
|
2262
|
+
registerChart,
|
|
2263
|
+
refreshCharts
|
|
2264
|
+
};
|
|
2265
|
+
}
|
|
2266
|
+
function ViewMenu({ charts, t }) {
|
|
2267
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
2268
|
+
const root = (0, react.useRef)(null);
|
|
2269
|
+
(0, react.useEffect)(() => {
|
|
2270
|
+
if (!open) return void 0;
|
|
2271
|
+
const away = (event) => {
|
|
2272
|
+
if (!root.current?.contains(event.target)) setOpen(false);
|
|
2273
|
+
};
|
|
2274
|
+
const key = (event) => {
|
|
2275
|
+
if (event.key === "Escape") setOpen(false);
|
|
2276
|
+
};
|
|
2277
|
+
document.addEventListener("pointerdown", away, true);
|
|
2278
|
+
document.addEventListener("keydown", key);
|
|
2279
|
+
return () => {
|
|
2280
|
+
document.removeEventListener("pointerdown", away, true);
|
|
2281
|
+
document.removeEventListener("keydown", key);
|
|
2282
|
+
};
|
|
2283
|
+
}, [open]);
|
|
2284
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2285
|
+
ref: root,
|
|
2286
|
+
className: "dvui-tools",
|
|
2287
|
+
"data-open": open ? "" : void 0,
|
|
2288
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2289
|
+
type: "button",
|
|
2290
|
+
className: "dvui-more",
|
|
2291
|
+
"aria-label": t("Chart options"),
|
|
2292
|
+
"aria-haspopup": "menu",
|
|
2293
|
+
"aria-expanded": open,
|
|
2294
|
+
onClick: () => setOpen((value) => !value),
|
|
2295
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DotsIcon, {})
|
|
2296
|
+
}), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2297
|
+
className: "dvui-menu",
|
|
2298
|
+
role: "menu",
|
|
2299
|
+
children: charts.map((chart, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [
|
|
2300
|
+
charts.length > 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2301
|
+
className: "dvui-menu-label",
|
|
2302
|
+
children: chart.label ?? t("Chart {n}", { n: index + 1 })
|
|
2303
|
+
}) : null,
|
|
2304
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2305
|
+
type: "button",
|
|
2306
|
+
role: "menuitem",
|
|
2307
|
+
onClick: () => {
|
|
2308
|
+
setOpen(false);
|
|
2309
|
+
chart.toggleTable();
|
|
2310
|
+
},
|
|
2311
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TableIcon, {}), chart.asTable ? t("View as chart") : t("View as table")]
|
|
2312
|
+
}),
|
|
2313
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2314
|
+
type: "button",
|
|
2315
|
+
role: "menuitem",
|
|
2316
|
+
onClick: () => {
|
|
2317
|
+
setOpen(false);
|
|
2318
|
+
chart.copy();
|
|
2319
|
+
},
|
|
2320
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyIcon, {}), t("Copy data")]
|
|
2321
|
+
})
|
|
2322
|
+
] }, `${chart.label}-${index}`))
|
|
2323
|
+
}) : null]
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
function WidgetCard({ title = "", view, onSubmit, onDismiss, busy = false, settled = false, answer = null, t = english, labels, Markdown, localOnly = false }) {
|
|
2327
|
+
const titleId = (0, react.useId)();
|
|
2328
|
+
const { charts, registerChart, refreshCharts } = useChartHandles();
|
|
2329
|
+
const framed = hasType(view, /* @__PURE__ */ new Set([
|
|
2330
|
+
"slider",
|
|
2331
|
+
"number",
|
|
2332
|
+
"choice",
|
|
2333
|
+
"toggle",
|
|
2334
|
+
"options",
|
|
2335
|
+
"confirm",
|
|
2336
|
+
"form",
|
|
2337
|
+
"submit"
|
|
2338
|
+
]));
|
|
2339
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2340
|
+
className: `dvui-view ${framed ? "dvui-framed" : "dvui-bare"}${settled ? " dvui-settled" : ""}`,
|
|
2341
|
+
role: "group",
|
|
2342
|
+
"aria-labelledby": title ? titleId : void 0,
|
|
2343
|
+
"aria-label": title ? void 0 : t("View"),
|
|
2344
|
+
"aria-busy": busy || void 0,
|
|
2345
|
+
children: [
|
|
2346
|
+
title || onDismiss || charts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2347
|
+
className: "dvui-head",
|
|
2348
|
+
children: [
|
|
2349
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2350
|
+
className: "dvui-title",
|
|
2351
|
+
id: titleId,
|
|
2352
|
+
children: title
|
|
2353
|
+
}),
|
|
2354
|
+
charts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ViewMenu, {
|
|
2355
|
+
charts,
|
|
2356
|
+
t
|
|
2357
|
+
}) : null,
|
|
2358
|
+
onDismiss ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2359
|
+
type: "button",
|
|
2360
|
+
className: "dvui-dismiss",
|
|
2361
|
+
"aria-label": t("Dismiss view"),
|
|
2362
|
+
onClick: onDismiss,
|
|
2363
|
+
disabled: busy,
|
|
2364
|
+
children: "×"
|
|
2365
|
+
}) : null
|
|
2366
|
+
]
|
|
2367
|
+
}) : null,
|
|
2368
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2369
|
+
className: "dvui-body",
|
|
2370
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetView, {
|
|
2371
|
+
view,
|
|
2372
|
+
onSubmit,
|
|
2373
|
+
settled,
|
|
2374
|
+
answer,
|
|
2375
|
+
busy,
|
|
2376
|
+
registerChart,
|
|
2377
|
+
refreshCharts,
|
|
2378
|
+
t,
|
|
2379
|
+
labels,
|
|
2380
|
+
Markdown
|
|
2381
|
+
})
|
|
2382
|
+
}),
|
|
2383
|
+
localOnly && hasType(view, /* @__PURE__ */ new Set([
|
|
2384
|
+
"options",
|
|
2385
|
+
"confirm",
|
|
2386
|
+
"form",
|
|
2387
|
+
"submit"
|
|
2388
|
+
])) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2389
|
+
className: "dvui-local-note",
|
|
2390
|
+
role: "note",
|
|
2391
|
+
children: t("Local preview only; this card does not send an answer.")
|
|
2392
|
+
}) : null
|
|
2393
|
+
]
|
|
2394
|
+
});
|
|
2395
|
+
}
|
|
2396
|
+
//#endregion
|
|
2397
|
+
//#region src/styles.ts
|
|
2398
|
+
const viewStyles = `
|
|
2399
|
+
.dvui-root { min-width: 0; color: var(--dsw-alias-label-primary); font-family: inherit; font-size: 14px; line-height: 22px; }
|
|
2400
|
+
.dvui-root * { box-sizing: border-box; }
|
|
2401
|
+
.dvui-root .dvui-view { min-width: 0; display: flex; flex-direction: column; gap: 10px; margin: 10px 0; }
|
|
2402
|
+
.dvui-root .dvui-bare { gap: 8px; }
|
|
2403
|
+
.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); }
|
|
2404
|
+
.dvui-root .dvui-settled { color: var(--dsw-alias-label-secondary); }
|
|
2405
|
+
.dvui-root .dvui-head { display: flex; align-items: flex-start; gap: 8px; min-width: 0; }
|
|
2406
|
+
.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; }
|
|
2407
|
+
.dvui-root .dvui-body { min-width: 0; }
|
|
2408
|
+
.dvui-root .dvui-stack { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
|
|
2409
|
+
.dvui-root .dvui-row { display: flex; flex-wrap: wrap; gap: 12px 24px; min-width: 0; }
|
|
2410
|
+
.dvui-root .dvui-row > * { flex: 1 1 160px; min-width: 0; }
|
|
2411
|
+
.dvui-root .dvui-metric { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
|
|
2412
|
+
.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; }
|
|
2413
|
+
.dvui-root .dvui-metric-value { font-size: 22px; line-height: 28px; font-weight: 600; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
|
|
2414
|
+
.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); }
|
|
2415
|
+
.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); }
|
|
2416
|
+
.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); }
|
|
2417
|
+
.dvui-root .dvui-muted { font-size: 12px; line-height: 16px; color: var(--dsw-alias-label-secondary); }
|
|
2418
|
+
.dvui-root .dvui-muted * { font-size: inherit !important; line-height: inherit !important; color: inherit !important; }
|
|
2419
|
+
.dvui-root .dvui-table { max-width: 100%; overflow-x: auto; border: .5px solid var(--dsw-alias-border-l2); border-radius: var(--dsw-radius-md); }
|
|
2420
|
+
.dvui-root .dvui-table table { border-collapse: collapse; min-width: 100%; font-size: 13px; line-height: 18px; }
|
|
2421
|
+
.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); }
|
|
2422
|
+
.dvui-root .dvui-table td { padding: 6px 10px; max-width: 280px; overflow-wrap: anywhere; }
|
|
2423
|
+
.dvui-root .dvui-table tbody tr + tr > * { border-top: .5px solid var(--dsw-alias-border-l1); }
|
|
2424
|
+
.dvui-root .dvui-table tbody th { font-weight: 400; border-bottom: 0; color: var(--dsw-alias-label-primary); }
|
|
2425
|
+
.dvui-root .dvui-num { text-align: end; font-variant-numeric: tabular-nums; }
|
|
2426
|
+
.dvui-root .dvui-progress { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
|
|
2427
|
+
.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; }
|
|
2428
|
+
.dvui-root .dvui-progress-label > span:first-child { color: var(--dsw-alias-label-primary); font-weight: 500; }
|
|
2429
|
+
.dvui-root .dvui-progress-track { height: 6px; border-radius: 999px; background: var(--dsw-alias-interactive-bg-hover); overflow: hidden; }
|
|
2430
|
+
.dvui-root .dvui-progress-fill { height: 100%; border-radius: inherit; background: var(--dsw-alias-state-business-primary); transition: width .12s ease; }
|
|
2431
|
+
.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); }
|
|
2432
|
+
.dvui-root .dvui-callout[data-tone=good] { background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 8%, transparent); }
|
|
2433
|
+
.dvui-root .dvui-callout[data-tone=warn] { background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 8%, transparent); }
|
|
2434
|
+
.dvui-root .dvui-callout[data-tone=bad] { background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 8%, transparent); }
|
|
2435
|
+
.dvui-root .dvui-callout-icon { flex: none; display: flex; padding-top: 3px; color: var(--dsw-alias-state-business-primary); }
|
|
2436
|
+
.dvui-root .dvui-callout[data-tone=good] .dvui-callout-icon { color: var(--dsw-alias-state-success-primary); }
|
|
2437
|
+
.dvui-root .dvui-callout[data-tone=warn] .dvui-callout-icon { color: var(--dsw-alias-state-warn-primary); }
|
|
2438
|
+
.dvui-root .dvui-callout[data-tone=bad] .dvui-callout-icon { color: var(--dsw-alias-state-error-primary); }
|
|
2439
|
+
.dvui-root .dvui-callout > div { flex: 1 1 auto; min-width: 0; }
|
|
2440
|
+
.dvui-root .dvui-sources { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
2441
|
+
.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; }
|
|
2442
|
+
.dvui-root .dvui-source:hover { color: var(--dsw-alias-label-primary); }
|
|
2443
|
+
.dvui-root .dvui-input-row { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
|
|
2444
|
+
.dvui-root .dvui-input-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
|
|
2445
|
+
.dvui-root .dvui-input-value { font-size: 15px; line-height: 20px; font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
2446
|
+
.dvui-root .dvui-range { appearance: none; width: 100%; height: 20px; margin: 0; background: transparent; cursor: pointer; }
|
|
2447
|
+
.dvui-root .dvui-range:disabled { cursor: default; opacity: .6; }
|
|
2448
|
+
.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%)); }
|
|
2449
|
+
.dvui-root .dvui-range::-moz-range-track { height: 6px; border-radius: 999px; background: var(--dsw-alias-interactive-bg-hover); }
|
|
2450
|
+
.dvui-root .dvui-range::-moz-range-progress { height: 6px; border-radius: 999px; background: var(--dsw-alias-state-business-primary); }
|
|
2451
|
+
.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); }
|
|
2452
|
+
.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); }
|
|
2453
|
+
.dvui-root .dvui-number { display: flex; align-items: center; gap: 6px; max-width: 200px; }
|
|
2454
|
+
.dvui-root .dvui-unit { flex: none; font-size: 13px; color: var(--dsw-alias-label-secondary); }
|
|
2455
|
+
.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); }
|
|
2456
|
+
.dvui-root .dvui-choice-seg { align-self: flex-start; max-width: 100%; }
|
|
2457
|
+
.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; }
|
|
2458
|
+
.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); }
|
|
2459
|
+
.dvui-root .dvui-radios { display: flex; flex-direction: column; gap: 2px; }
|
|
2460
|
+
.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; }
|
|
2461
|
+
.dvui-root .dvui-radio:hover:not(:disabled),.dvui-root .dvui-option:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
|
|
2462
|
+
.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); }
|
|
2463
|
+
.dvui-root .dvui-radio[aria-checked=true] .dvui-radio-dot { box-shadow: inset 0 0 0 5px var(--dsw-alias-state-business-primary); }
|
|
2464
|
+
.dvui-root .dvui-toggle { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 28px; }
|
|
2465
|
+
.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; }
|
|
2466
|
+
.dvui-root .dvui-switch[aria-checked=true] { background: var(--dsw-alias-button-primary-fill); }
|
|
2467
|
+
.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; }
|
|
2468
|
+
.dvui-root .dvui-switch[aria-checked=true] .dvui-switch-knob { transform: translateX(16px); }
|
|
2469
|
+
.dvui-root .dvui-options { display: flex; flex-direction: column; gap: 1px; min-width: 0; margin: 0 -8px; }
|
|
2470
|
+
.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; }
|
|
2471
|
+
.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; }
|
|
2472
|
+
.dvui-root .dvui-option-body { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 1px; padding-top: 2px; }
|
|
2473
|
+
.dvui-root .dvui-option-label { font-size: 14px; line-height: 20px; font-weight: 500; overflow-wrap: anywhere; }
|
|
2474
|
+
.dvui-root .dvui-option-desc { font-size: 12px; line-height: 17px; color: var(--dsw-alias-label-secondary); overflow-wrap: anywhere; }
|
|
2475
|
+
.dvui-root .dvui-check { flex: none; display: inline-flex; align-items: center; color: var(--dsw-alias-state-success-primary); }
|
|
2476
|
+
.dvui-root .dvui-form { display: flex; flex-direction: column; gap: 8px; margin: 0; }
|
|
2477
|
+
.dvui-root .dvui-field { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
|
|
2478
|
+
.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; }
|
|
2479
|
+
.dvui-root .dvui-input::placeholder { color: var(--dsw-alias-label-dimmed); }
|
|
2480
|
+
.dvui-root .dvui-input:focus-visible { border-color: var(--dsw-alias-state-business-primary); }
|
|
2481
|
+
.dvui-root .dvui-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
2482
|
+
.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; }
|
|
2483
|
+
.dvui-root .dvui-submit { background: var(--dsw-alias-button-primary-fill); color: var(--dsw-alias-label-primary-foreground); }
|
|
2484
|
+
.dvui-root .dvui-submit:hover:not(:disabled) { background: var(--dsw-alias-button-primary-hover); }
|
|
2485
|
+
.dvui-root .dvui-button { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
|
|
2486
|
+
.dvui-root .dvui-button:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-active); }
|
|
2487
|
+
.dvui-root button:disabled { opacity: .45; cursor: default; }
|
|
2488
|
+
.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; }
|
|
2489
|
+
.dvui-root .dvui-tools { position: relative; flex: none; align-self: flex-start; }
|
|
2490
|
+
.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; }
|
|
2491
|
+
.dvui-root .dvui-more:hover,.dvui-root .dvui-dismiss:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
|
|
2492
|
+
.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; }
|
|
2493
|
+
.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; }
|
|
2494
|
+
.dvui-root .dvui-menu button:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
2495
|
+
.dvui-root .dvui-menu-label { padding: 2px 8px; font-size: 11px; line-height: 14px; color: var(--dsw-alias-label-tertiary); }
|
|
2496
|
+
.dvui-root .dvui-local-note { margin: 0; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
|
|
2497
|
+
.dvui-root .dvui-chart { position: relative; margin: 0; min-width: 0; display: flex; flex-direction: column; gap: 8px; }
|
|
2498
|
+
.dvui-root .dvui-chart-plot { position: relative; min-width: 0; }
|
|
2499
|
+
.dvui-root .dvui-chart-svg { display: block; overflow: visible; outline: none; font-family: inherit; }
|
|
2500
|
+
.dvui-root .dvui-chart-svg text { pointer-events: none; }
|
|
2501
|
+
.dvui-root .dvui-chart-grid { stroke: var(--dsw-alias-border-l1); stroke-width: 1; stroke-dasharray: 3 3; }
|
|
2502
|
+
.dvui-root .dvui-chart-tick { font-size: 10px; fill: var(--dsw-alias-label-tertiary); font-variant-numeric: tabular-nums; }
|
|
2503
|
+
.dvui-root .dvui-chart-label,.dvui-root .dvui-chart-value { font-size: 11px; fill: var(--dsw-alias-label-secondary); }
|
|
2504
|
+
.dvui-root .dvui-chart-value { font-variant-numeric: tabular-nums; }
|
|
2505
|
+
.dvui-root .dvui-chart-center { font-size: 15px; font-weight: 600; fill: var(--dsw-alias-label-primary); font-variant-numeric: tabular-nums; }
|
|
2506
|
+
.dvui-root .dvui-chart-center-name { font-size: 11px; fill: var(--dsw-alias-label-secondary); }
|
|
2507
|
+
.dvui-root .dvui-chart-mark[data-dim] { opacity: .28; }
|
|
2508
|
+
.dvui-root .dvui-chart-line { fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
|
2509
|
+
.dvui-root .dvui-chart-area { opacity: .14; }
|
|
2510
|
+
.dvui-root .dvui-chart-guide { stroke: var(--dsw-alias-border-l3); stroke-width: 1; }
|
|
2511
|
+
.dvui-root .dvui-chart-hit { fill: transparent; }
|
|
2512
|
+
.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; }
|
|
2513
|
+
.dvui-root .dvui-chart-tip span { display: flex; align-items: center; gap: 6px; color: var(--dsw-alias-label-secondary); }
|
|
2514
|
+
.dvui-root .dvui-chart-tip i,.dvui-root .dvui-chart-key i { flex: none; width: 8px; height: 8px; border-radius: 50%; }
|
|
2515
|
+
.dvui-root .dvui-chart-legend { display: flex; flex-wrap: wrap; gap: 4px 12px; padding-right: 32px; }
|
|
2516
|
+
.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; }
|
|
2517
|
+
.dvui-root .dvui-chart-key[aria-pressed=false],.dvui-root .dvui-chart-key[data-dim] { opacity: .4; }
|
|
2518
|
+
.dvui-root .dvui-chart-legend-list { flex-direction: column; flex-wrap: nowrap; gap: 6px; padding: 0; min-width: 0; }
|
|
2519
|
+
.dvui-root .dvui-chart-legend-list .dvui-chart-key { cursor: default; width: 100%; }
|
|
2520
|
+
.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); }
|
|
2521
|
+
.dvui-root .dvui-chart-key-value { flex: none; font-variant-numeric: tabular-nums; }
|
|
2522
|
+
.dvui-root .dvui-chart-donut { display: flex; flex-direction: column; align-items: center; gap: 12px; }
|
|
2523
|
+
.dvui-root .dvui-chart-donut-beside { flex-direction: row; align-items: center; gap: 24px; }
|
|
2524
|
+
.dvui-root .dvui-chart-donut-beside .dvui-chart-legend-list { flex: 1 1 auto; max-width: 280px; }
|
|
2525
|
+
.dvui-root .dvui-chart-copied { position: absolute; top: 2px; right: 36px; font-size: 12px; line-height: 16px; color: var(--dsw-alias-label-secondary); }
|
|
2526
|
+
.dvui-root .dvui-reasoning { margin: 6px 0; color: var(--dsw-alias-label-secondary); }
|
|
2527
|
+
.dvui-root .dvui-reasoning > summary { cursor: pointer; font-size: 12px; line-height: 20px; }
|
|
2528
|
+
.dvui-root .dvui-reasoning-body { padding: 4px 0 4px 12px; border-left: 1px solid var(--dsw-alias-border-l2); }
|
|
2529
|
+
.dvui-root .dvui-unknown { white-space: pre-wrap; overflow-wrap: anywhere; color: var(--dsw-alias-label-secondary); font-size: 12px; }
|
|
2530
|
+
.dvui-root .dvui-tool-loading, .dvui-root.dvui-tool-loading { padding: 8px 0; color: var(--dsw-alias-label-secondary); font-size: 13px; }
|
|
2531
|
+
.dvui-root .dvui-tool-error, .dvui-root.dvui-tool-error { margin: 8px 0; color: var(--dsw-alias-label-error); font-size: 13px; }
|
|
2532
|
+
.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; }
|
|
2533
|
+
@media (hover: hover) { .dvui-root .dvui-tools:not(:focus-within):not([data-open]) { opacity: 0; } .dvui-root .dvui-view:hover .dvui-tools { opacity: 1; } }
|
|
2534
|
+
@media (prefers-reduced-motion: reduce) { .dvui-root * { transition: none !important; } }
|
|
2535
|
+
`;
|
|
2536
|
+
//#endregion
|
|
2537
|
+
//#region src/answer-text.ts
|
|
2538
|
+
function controls(view) {
|
|
2539
|
+
return [
|
|
2540
|
+
"options",
|
|
2541
|
+
"confirm",
|
|
2542
|
+
"form",
|
|
2543
|
+
"submit"
|
|
2544
|
+
].includes(view.type) ? [view] : (view.children ?? []).flatMap(controls);
|
|
2545
|
+
}
|
|
2546
|
+
function answerText(card, action, values) {
|
|
2547
|
+
const node = controls(card.view).find((node) => node.id === action);
|
|
2548
|
+
const lines = [card.title || "Deep View"];
|
|
2549
|
+
if (Array.isArray(values.selected)) lines.push(values.selected.map(String).join(", "));
|
|
2550
|
+
if (typeof values.confirmed === "boolean") lines.push(values.confirmed ? node?.confirmLabel ?? "Confirm" : node?.cancelLabel ?? "Cancel");
|
|
2551
|
+
if (isRecord(values.fields)) for (const field of node?.fields ?? []) lines.push(`${field.label}: ${String(values.fields[field.id] ?? "")}`);
|
|
2552
|
+
const inputs = inputsSummary(card.view, values.inputs);
|
|
2553
|
+
if (inputs) lines.push(inputs);
|
|
2554
|
+
if (node?.type === "submit") lines.push(node.label ?? "Submit");
|
|
2555
|
+
return lines.join("\n");
|
|
2556
|
+
}
|
|
2557
|
+
/** Legacy messages have no metadata. Restore only unambiguous, fully validated
|
|
2558
|
+
* answers that reproduce the exact text written by this plugin. Never guess
|
|
2559
|
+
* from ordinary prose, and never modify the stored message. */
|
|
2560
|
+
function legacyAnswer(card, text) {
|
|
2561
|
+
if (text.startsWith("{")) try {
|
|
2562
|
+
const data = JSON.parse(text);
|
|
2563
|
+
if (!isRecord(data) || data.kind !== "answer_to_pending_view" || data.tool !== "show_view" || !Array.isArray(data.answers)) return null;
|
|
2564
|
+
const answer = data.answers.find((value) => isRecord(value) && value.id === card.id);
|
|
2565
|
+
if (!isRecord(answer) || typeof answer.action !== "string") return null;
|
|
2566
|
+
const values = validateViewAnswer(card.view, answer.action, answer.values);
|
|
2567
|
+
return values.error === void 0 ? {
|
|
2568
|
+
id: card.id,
|
|
2569
|
+
action: answer.action,
|
|
2570
|
+
values
|
|
2571
|
+
} : null;
|
|
2572
|
+
} catch {
|
|
2573
|
+
return null;
|
|
2574
|
+
}
|
|
2575
|
+
const lines = text.split("\n");
|
|
2576
|
+
if (lines.shift() !== (card.title || "Deep View")) return null;
|
|
2577
|
+
const candidates = [];
|
|
2578
|
+
for (const node of controls(card.view)) {
|
|
2579
|
+
const rest = [...lines];
|
|
2580
|
+
const values = {};
|
|
2581
|
+
if (node.type === "options") {
|
|
2582
|
+
const line = rest.shift();
|
|
2583
|
+
if (line === void 0) continue;
|
|
2584
|
+
const labels = node.options?.map((option) => option.label) ?? [];
|
|
2585
|
+
if (node.multi && labels.some((label) => label.includes(", "))) continue;
|
|
2586
|
+
values.selected = node.multi ? line.split(", ") : [line];
|
|
2587
|
+
} else if (node.type === "confirm") {
|
|
2588
|
+
const line = rest.shift(), yes = node.confirmLabel ?? "Confirm", no = node.cancelLabel ?? "Cancel";
|
|
2589
|
+
if (yes === no || line !== yes && line !== no) continue;
|
|
2590
|
+
values.confirmed = line === yes;
|
|
2591
|
+
} else if (node.type === "form") {
|
|
2592
|
+
const fields = {};
|
|
2593
|
+
let valid = true;
|
|
2594
|
+
for (const field of node.fields ?? []) {
|
|
2595
|
+
const line = rest.shift(), prefix = `${field.label}: `;
|
|
2596
|
+
if (line === void 0 || !line.startsWith(prefix)) {
|
|
2597
|
+
valid = false;
|
|
2598
|
+
break;
|
|
2599
|
+
}
|
|
2600
|
+
fields[field.id] = line.slice(prefix.length);
|
|
2601
|
+
}
|
|
2602
|
+
if (!valid) continue;
|
|
2603
|
+
values.fields = fields;
|
|
2604
|
+
}
|
|
2605
|
+
const inputs = viewInputs(card.view);
|
|
2606
|
+
if (inputs.length) {
|
|
2607
|
+
const pairs = rest.shift()?.split("; ");
|
|
2608
|
+
if (!pairs || pairs.length !== inputs.length) continue;
|
|
2609
|
+
const sent = {};
|
|
2610
|
+
let valid = true;
|
|
2611
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
2612
|
+
const input = inputs[i], prefix = `${input.label || input.id}=`;
|
|
2613
|
+
if (!pairs[i].startsWith(prefix)) {
|
|
2614
|
+
valid = false;
|
|
2615
|
+
break;
|
|
2616
|
+
}
|
|
2617
|
+
const value = pairs[i].slice(prefix.length);
|
|
2618
|
+
if (input.type === "toggle") sent[input.id] = value === "on" ? true : value === "off" ? false : void 0;
|
|
2619
|
+
else if (input.type === "choice") {
|
|
2620
|
+
const choices = input.options?.filter((option) => option.label === value) ?? [];
|
|
2621
|
+
sent[input.id] = choices.length === 1 ? choiceValue(choices[0]) : void 0;
|
|
2622
|
+
} else {
|
|
2623
|
+
const unit = input.unit ? ` ${input.unit}` : "";
|
|
2624
|
+
sent[input.id] = !unit || value.endsWith(unit) ? Number(unit ? value.slice(0, -unit.length) : value) : void 0;
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
if (!valid) continue;
|
|
2628
|
+
values.inputs = sent;
|
|
2629
|
+
}
|
|
2630
|
+
if (node.type === "submit" && rest.shift() !== (node.label ?? "Submit")) continue;
|
|
2631
|
+
if (rest.length || !node.id) continue;
|
|
2632
|
+
const checked = validateViewAnswer(card.view, node.id, values);
|
|
2633
|
+
if (checked.error === void 0 && answerText(card, node.id, checked) === text) candidates.push({
|
|
2634
|
+
id: card.id,
|
|
2635
|
+
action: node.id,
|
|
2636
|
+
values: checked
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
return candidates.length === 1 ? candidates[0] : null;
|
|
2640
|
+
}
|
|
2641
|
+
//#endregion
|
|
2642
|
+
//#region src/answers.ts
|
|
2643
|
+
/** Read visible, durable user/steering messages only, including inherited history. */
|
|
2644
|
+
function replyAnswers(snapshot) {
|
|
2645
|
+
const answers = /* @__PURE__ */ new Map();
|
|
2646
|
+
let cards = [];
|
|
2647
|
+
const collect = (block) => {
|
|
2648
|
+
const meta = block.meta;
|
|
2649
|
+
if (block.kind === "tool-result" && !block.isError && isRecord(meta) && meta.owner === "dsh-deep-view" && meta.awaitReply === true && typeof meta.viewId === "string") {
|
|
2650
|
+
const shown = checkShownView(meta.title, meta.view);
|
|
2651
|
+
if (!("error" in shown)) cards.push({
|
|
2652
|
+
id: meta.viewId,
|
|
2653
|
+
title: shown.title,
|
|
2654
|
+
view: shown.view
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
2657
|
+
for (const child of block.subCalls ?? []) collect(child);
|
|
2658
|
+
};
|
|
2659
|
+
for (const key of snapshot.order) {
|
|
2660
|
+
const node = snapshot.nodes.get(key);
|
|
2661
|
+
if (node?.kind === "tool-call" && node.data?.root) collect(node.data.root);
|
|
2662
|
+
if (node?.kind !== "user" && node?.kind !== "steering") continue;
|
|
2663
|
+
const source = node.data?.source;
|
|
2664
|
+
if (!isRecord(source) || source.kind !== "user") continue;
|
|
2665
|
+
if (source.deepView === void 0) {
|
|
2666
|
+
const text = node.data?.content?.filter(isRecord).filter((block) => block.type === "text").map((block) => block.text).join("") ?? "";
|
|
2667
|
+
const candidates = cards.map((card) => legacyAnswer(card, text)).filter((answer) => answer !== null);
|
|
2668
|
+
if (candidates.length === 1 && !answers.has(candidates[0].id)) answers.set(candidates[0].id, candidates[0]);
|
|
2669
|
+
}
|
|
2670
|
+
cards = [];
|
|
2671
|
+
if (!isRecord(source.deepView)) continue;
|
|
2672
|
+
const meta = source.deepView;
|
|
2673
|
+
if (meta.owner !== "dsh-deep-view" || meta.version !== 1 || !isRecord(meta.answer)) continue;
|
|
2674
|
+
const answer = meta.answer;
|
|
2675
|
+
if (typeof answer.id !== "string" || typeof answer.action !== "string" || !isRecord(answer.values)) continue;
|
|
2676
|
+
if (!answers.has(answer.id)) answers.set(answer.id, answer);
|
|
2677
|
+
}
|
|
2678
|
+
return answers;
|
|
2679
|
+
}
|
|
2680
|
+
function checkedAnswer(answer, id, view) {
|
|
2681
|
+
if (!answer || !id || answer.id !== id) return null;
|
|
2682
|
+
const values = validateViewAnswer(view, answer.action, answer.values);
|
|
2683
|
+
return values.error === void 0 ? {
|
|
2684
|
+
id,
|
|
2685
|
+
action: answer.action,
|
|
2686
|
+
values
|
|
2687
|
+
} : null;
|
|
2688
|
+
}
|
|
2689
|
+
function sameAnswers(left, right) {
|
|
2690
|
+
return left.size === right.size && [...left].every(([id, answer]) => {
|
|
2691
|
+
const next = right.get(id);
|
|
2692
|
+
return next === answer || next?.action === answer.action && JSON.stringify(next.values) === JSON.stringify(answer.values);
|
|
2693
|
+
});
|
|
2694
|
+
}
|
|
2695
|
+
//#endregion
|
|
2696
|
+
//#region src/client.tsx
|
|
2697
|
+
const name = "dsh-deep-view-client";
|
|
2698
|
+
const inject = [
|
|
2699
|
+
"slots",
|
|
2700
|
+
"connection",
|
|
2701
|
+
"locale"
|
|
2702
|
+
];
|
|
2703
|
+
const LOCALE = "deep-view";
|
|
2704
|
+
const uiZh = {
|
|
2705
|
+
copy: "复制",
|
|
2706
|
+
copied: "已复制",
|
|
2707
|
+
footnotes: "脚注",
|
|
2708
|
+
code: "代码",
|
|
2709
|
+
wrap: "自动换行",
|
|
2710
|
+
unwrap: "不换行",
|
|
2711
|
+
prepared: "卡片将在最终回复中显示",
|
|
2712
|
+
preparing: "正在准备视图…",
|
|
2713
|
+
error: "视图未能显示",
|
|
2714
|
+
invalid: "视图数据无效",
|
|
2715
|
+
local: "仅本地预览;此卡片不会发送回答。",
|
|
2716
|
+
answerFailed: "回答未能发送,请重试。",
|
|
2717
|
+
chart: "图表",
|
|
2718
|
+
chartOptions: "图表选项",
|
|
2719
|
+
viewAsChart: "查看图表",
|
|
2720
|
+
viewAsTable: "查看表格",
|
|
2721
|
+
copyData: "复制数据",
|
|
2722
|
+
chartNumber: "图表 {n}",
|
|
2723
|
+
value: "值",
|
|
2724
|
+
series: "系列",
|
|
2725
|
+
view: "视图",
|
|
2726
|
+
sources: "来源",
|
|
2727
|
+
progress: "进度",
|
|
2728
|
+
yourAnswer: "你的回答",
|
|
2729
|
+
options: "选项",
|
|
2730
|
+
submit: "提交",
|
|
2731
|
+
cancel: "取消",
|
|
2732
|
+
confirm: "确认",
|
|
2733
|
+
send: "发送",
|
|
2734
|
+
selected: "已选择"
|
|
2735
|
+
};
|
|
2736
|
+
const uiEn = {
|
|
2737
|
+
copy: "Copy",
|
|
2738
|
+
copied: "Copied",
|
|
2739
|
+
footnotes: "Footnotes",
|
|
2740
|
+
code: "Code",
|
|
2741
|
+
wrap: "Wrap lines",
|
|
2742
|
+
unwrap: "Do not wrap lines",
|
|
2743
|
+
prepared: "The card will appear in the final reply",
|
|
2744
|
+
preparing: "Preparing view…",
|
|
2745
|
+
error: "The view could not be shown",
|
|
2746
|
+
invalid: "Invalid view data",
|
|
2747
|
+
local: "Local preview only; this card does not send an answer.",
|
|
2748
|
+
answerFailed: "The answer could not be sent; try again.",
|
|
2749
|
+
chart: "Chart",
|
|
2750
|
+
chartOptions: "Chart options",
|
|
2751
|
+
viewAsChart: "View as chart",
|
|
2752
|
+
viewAsTable: "View as table",
|
|
2753
|
+
copyData: "Copy data",
|
|
2754
|
+
chartNumber: "Chart {n}",
|
|
2755
|
+
value: "Value",
|
|
2756
|
+
series: "Series",
|
|
2757
|
+
view: "View",
|
|
2758
|
+
sources: "Sources",
|
|
2759
|
+
progress: "Progress",
|
|
2760
|
+
yourAnswer: "Your answer",
|
|
2761
|
+
options: "Options",
|
|
2762
|
+
submit: "Submit",
|
|
2763
|
+
cancel: "Cancel",
|
|
2764
|
+
confirm: "Confirm",
|
|
2765
|
+
send: "Send",
|
|
2766
|
+
selected: "Selected"
|
|
2767
|
+
};
|
|
2768
|
+
const viewT = (t) => (key, params) => {
|
|
2769
|
+
const keys = {
|
|
2770
|
+
Copy: "copy",
|
|
2771
|
+
Copied: "copied",
|
|
2772
|
+
Footnotes: "footnotes",
|
|
2773
|
+
Chart: "chart",
|
|
2774
|
+
"Chart options": "chartOptions",
|
|
2775
|
+
"View as chart": "viewAsChart",
|
|
2776
|
+
"View as table": "viewAsTable",
|
|
2777
|
+
"Copy data": "copyData",
|
|
2778
|
+
"Chart {n}": "chartNumber",
|
|
2779
|
+
Value: "value",
|
|
2780
|
+
Series: "series",
|
|
2781
|
+
View: "view",
|
|
2782
|
+
Sources: "sources",
|
|
2783
|
+
Progress: "progress",
|
|
2784
|
+
"Your answer": "yourAnswer",
|
|
2785
|
+
"Local preview only; this card does not send an answer.": "local",
|
|
2786
|
+
Options: "options",
|
|
2787
|
+
Submit: "submit",
|
|
2788
|
+
Cancel: "cancel",
|
|
2789
|
+
Confirm: "confirm",
|
|
2790
|
+
Send: "send",
|
|
2791
|
+
Selected: "selected"
|
|
2792
|
+
};
|
|
2793
|
+
return keys[key] ? t(keys[key], params) : key.replace(/\{(\w+)\}/g, (_, name) => String(params?.[name] ?? `{${name}}`));
|
|
2794
|
+
};
|
|
2795
|
+
function ViewMarkdown(props) {
|
|
2796
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, { ...props });
|
|
2797
|
+
}
|
|
2798
|
+
function useMarkdownLabels(t) {
|
|
2799
|
+
return (0, react.useMemo)(() => ({
|
|
2800
|
+
code: {
|
|
2801
|
+
copyLabel: t("copy"),
|
|
2802
|
+
copiedLabel: t("copied"),
|
|
2803
|
+
toolbarLabels: {
|
|
2804
|
+
codeLabel: t("code"),
|
|
2805
|
+
wrapLabel: t("wrap"),
|
|
2806
|
+
unwrapLabel: t("unwrap")
|
|
2807
|
+
}
|
|
2808
|
+
},
|
|
2809
|
+
footnotes: t("footnotes")
|
|
2810
|
+
}), [t]);
|
|
2811
|
+
}
|
|
2812
|
+
function isArgsView(value) {
|
|
2813
|
+
return isRecord(value) && typeof value.text === "function" && typeof value.value === "function";
|
|
2814
|
+
}
|
|
2815
|
+
function argField(args, key) {
|
|
2816
|
+
if (!isArgsView(args)) return void 0;
|
|
2817
|
+
const value = args.value(key);
|
|
2818
|
+
return value !== void 0 ? value : args.text(key);
|
|
2819
|
+
}
|
|
2820
|
+
function readToolCall(props) {
|
|
2821
|
+
const block = props.block;
|
|
2822
|
+
const phase = props.phase ?? (block.kind === "tool-result" ? "result" : "start");
|
|
2823
|
+
const isError = phase === "result" && block.isError === true;
|
|
2824
|
+
const meta = phase === "result" ? block.meta : void 0;
|
|
2825
|
+
if (isRecord(meta) && meta.owner === "dsh-deep-view") {
|
|
2826
|
+
const shown = checkShownView(meta.title, meta.view);
|
|
2827
|
+
if ("error" in shown) return {
|
|
2828
|
+
title: meta.title,
|
|
2829
|
+
view: meta.view,
|
|
2830
|
+
viewReady: true,
|
|
2831
|
+
phase,
|
|
2832
|
+
isError: true
|
|
2833
|
+
};
|
|
2834
|
+
return {
|
|
2835
|
+
presentation: {
|
|
2836
|
+
owner: VIEW_OWNER,
|
|
2837
|
+
...shown,
|
|
2838
|
+
...typeof meta.viewId === "string" ? { viewId: meta.viewId } : {},
|
|
2839
|
+
awaitReply: meta.awaitReply === true
|
|
2840
|
+
},
|
|
2841
|
+
title: meta.title,
|
|
2842
|
+
view: meta.view,
|
|
2843
|
+
viewReady: true,
|
|
2844
|
+
phase,
|
|
2845
|
+
isError
|
|
2846
|
+
};
|
|
2847
|
+
}
|
|
2848
|
+
const args = block.args;
|
|
2849
|
+
const title = argField(args, "title");
|
|
2850
|
+
const viewReady = phase !== "preparing" || isArgsView(args) && args.complete("view");
|
|
2851
|
+
return {
|
|
2852
|
+
title,
|
|
2853
|
+
view: viewReady ? argField(args, "view") : void 0,
|
|
2854
|
+
viewReady,
|
|
2855
|
+
phase,
|
|
2856
|
+
isError
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
function ToolCallView(props) {
|
|
2860
|
+
const t = props.t;
|
|
2861
|
+
const labels = useMarkdownLabels(t);
|
|
2862
|
+
const read = (0, react.useMemo)(() => readToolCall(props), [props.block, props.phase]);
|
|
2863
|
+
const shown = (0, react.useMemo)(() => {
|
|
2864
|
+
if (read.presentation) return read.presentation;
|
|
2865
|
+
if (!read.viewReady || read.phase === "preparing") return void 0;
|
|
2866
|
+
const checked = checkShownView(read.title, read.view);
|
|
2867
|
+
return "error" in checked ? void 0 : {
|
|
2868
|
+
...checked,
|
|
2869
|
+
awaitReply: checked.interactive
|
|
2870
|
+
};
|
|
2871
|
+
}, [
|
|
2872
|
+
read.presentation,
|
|
2873
|
+
read.viewReady,
|
|
2874
|
+
read.phase,
|
|
2875
|
+
read.title,
|
|
2876
|
+
read.view
|
|
2877
|
+
]);
|
|
2878
|
+
const [pending, setPending] = (0, react.useState)(null);
|
|
2879
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
2880
|
+
const [failed, setFailed] = (0, react.useState)(false);
|
|
2881
|
+
const [localAnswer, setAnswer] = (0, react.useState)(null);
|
|
2882
|
+
const restored = (0, react.useMemo)(() => shown ? checkedAnswer(props.restoredAnswer, read.presentation?.viewId, shown.view) : null, [
|
|
2883
|
+
props.restoredAnswer,
|
|
2884
|
+
read.presentation?.viewId,
|
|
2885
|
+
shown
|
|
2886
|
+
]);
|
|
2887
|
+
const answer = restored ?? localAnswer;
|
|
2888
|
+
const sequence = (0, react.useRef)(0);
|
|
2889
|
+
const callId = props.callId;
|
|
2890
|
+
(0, react.useEffect)(() => {
|
|
2891
|
+
const seq = ++sequence.current;
|
|
2892
|
+
setPending(null);
|
|
2893
|
+
setFailed(false);
|
|
2894
|
+
if (restored || !read.presentation?.awaitReply || !callId) return;
|
|
2895
|
+
props.call("pending", {
|
|
2896
|
+
sessionId: props.sessionId,
|
|
2897
|
+
callId
|
|
2898
|
+
}).then((result) => {
|
|
2899
|
+
if (sequence.current !== seq) return;
|
|
2900
|
+
const envelope = result;
|
|
2901
|
+
if (envelope?.ok) setPending(envelope.value);
|
|
2902
|
+
}).catch(() => {});
|
|
2903
|
+
return () => {
|
|
2904
|
+
sequence.current += 1;
|
|
2905
|
+
};
|
|
2906
|
+
}, [
|
|
2907
|
+
props.call,
|
|
2908
|
+
props.sessionId,
|
|
2909
|
+
callId,
|
|
2910
|
+
read.presentation?.awaitReply,
|
|
2911
|
+
read.presentation?.viewId,
|
|
2912
|
+
restored
|
|
2913
|
+
]);
|
|
2914
|
+
const submit = (0, react.useCallback)((action, values) => {
|
|
2915
|
+
if (!pending || busy) return;
|
|
2916
|
+
setBusy(true);
|
|
2917
|
+
setFailed(false);
|
|
2918
|
+
props.call("reply", {
|
|
2919
|
+
sessionId: props.sessionId,
|
|
2920
|
+
viewId: pending.id,
|
|
2921
|
+
action,
|
|
2922
|
+
values
|
|
2923
|
+
}).then((result) => {
|
|
2924
|
+
if (result?.ok) {
|
|
2925
|
+
setAnswer({
|
|
2926
|
+
id: pending.id,
|
|
2927
|
+
action,
|
|
2928
|
+
values
|
|
2929
|
+
});
|
|
2930
|
+
setPending(null);
|
|
2931
|
+
} else setFailed(true);
|
|
2932
|
+
}).catch(() => setFailed(true)).finally(() => setBusy(false));
|
|
2933
|
+
}, [
|
|
2934
|
+
props.call,
|
|
2935
|
+
props.sessionId,
|
|
2936
|
+
pending,
|
|
2937
|
+
busy
|
|
2938
|
+
]);
|
|
2939
|
+
if (read.phase === "preparing") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2940
|
+
className: "dvui-root dvui-tool-loading",
|
|
2941
|
+
role: "status",
|
|
2942
|
+
children: t("preparing")
|
|
2943
|
+
});
|
|
2944
|
+
if (read.isError || !shown) {
|
|
2945
|
+
if (read.phase !== "result" && !read.isError) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2946
|
+
className: "dvui-root dvui-tool-loading",
|
|
2947
|
+
role: "status",
|
|
2948
|
+
children: t("preparing")
|
|
2949
|
+
});
|
|
2950
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2951
|
+
className: "dvui-root dvui-tool-error",
|
|
2952
|
+
role: "status",
|
|
2953
|
+
children: t(read.isError ? "error" : "invalid")
|
|
2954
|
+
});
|
|
2955
|
+
}
|
|
2956
|
+
const canAnswer = shown.awaitReply && pending !== null && answer === null;
|
|
2957
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2958
|
+
className: "dvui-root",
|
|
2959
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidgetCard, {
|
|
2960
|
+
title: shown.title,
|
|
2961
|
+
view: shown.view,
|
|
2962
|
+
onSubmit: canAnswer ? submit : void 0,
|
|
2963
|
+
busy,
|
|
2964
|
+
settled: answer !== null || !canAnswer,
|
|
2965
|
+
answer,
|
|
2966
|
+
t: viewT(t),
|
|
2967
|
+
labels,
|
|
2968
|
+
Markdown: ViewMarkdown,
|
|
2969
|
+
localOnly: answer === null && !canAnswer && shown.interactive
|
|
2970
|
+
}), failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2971
|
+
className: "dvui-tool-error",
|
|
2972
|
+
role: "alert",
|
|
2973
|
+
children: t("answerFailed")
|
|
2974
|
+
}) : null]
|
|
2975
|
+
});
|
|
2976
|
+
}
|
|
2977
|
+
/** Collect only this turn's successfully persisted cards, including PTC children.
|
|
2978
|
+
* The turn index also covers hidden process rows; no all-history scan or DOM
|
|
2979
|
+
* relocation is needed. Result metadata works even when arguments were evicted.
|
|
2980
|
+
*/
|
|
2981
|
+
function replyCards(snapshot, turn) {
|
|
2982
|
+
const cards = [];
|
|
2983
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2984
|
+
const visit = (block) => {
|
|
2985
|
+
if (block.kind === "tool-result" && !block.isError && !seen.has(block.callId) && isRecord(block.meta) && block.meta.owner === "dsh-deep-view") {
|
|
2986
|
+
const read = readToolCall({
|
|
2987
|
+
phase: "result",
|
|
2988
|
+
block
|
|
2989
|
+
});
|
|
2990
|
+
if (read.presentation && !read.isError) {
|
|
2991
|
+
seen.add(block.callId);
|
|
2992
|
+
cards.push(block);
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
for (const child of block.subCalls ?? []) visit(child);
|
|
2996
|
+
};
|
|
2997
|
+
for (const key of snapshot.locations.getTurn(turn)) {
|
|
2998
|
+
const node = snapshot.nodes.get(key);
|
|
2999
|
+
if (node?.kind === "tool-call" && node.data?.root) visit(node.data.root);
|
|
3000
|
+
}
|
|
3001
|
+
return cards;
|
|
3002
|
+
}
|
|
3003
|
+
const sameCards = (left, right) => left.length === right.length && left.every((block, index) => block === right[index]);
|
|
3004
|
+
/** Native completed-turn surface: always outside the process disclosure. */
|
|
3005
|
+
function ReplyView(props) {
|
|
3006
|
+
const cards = props.useChat((snapshot) => replyCards(snapshot, props.turn.turn), sameCards);
|
|
3007
|
+
const answers = props.useChat(replyAnswers, sameAnswers);
|
|
3008
|
+
if (props.turn.status !== "closed" || cards.length === 0) return null;
|
|
3009
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3010
|
+
className: "dvui-root",
|
|
3011
|
+
"data-deep-view-reply": true,
|
|
3012
|
+
children: cards.map((block) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToolCallView, {
|
|
3013
|
+
sessionId: props.sessionId,
|
|
3014
|
+
callId: block.callId,
|
|
3015
|
+
phase: "result",
|
|
3016
|
+
block,
|
|
3017
|
+
t: props.t,
|
|
3018
|
+
call: props.call,
|
|
3019
|
+
restoredAnswer: isRecord(block.meta) && typeof block.meta.viewId === "string" ? answers.get(block.meta.viewId) : void 0
|
|
3020
|
+
}, `${props.sessionId}:${block.callId}`))
|
|
3021
|
+
});
|
|
3022
|
+
}
|
|
3023
|
+
/** Tool activity contains status only, never the user's interactive card. */
|
|
3024
|
+
function ToolCallStatus(props) {
|
|
3025
|
+
const read = readToolCall(props);
|
|
3026
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3027
|
+
className: "dvui-root",
|
|
3028
|
+
role: "status",
|
|
3029
|
+
children: props.t(read.isError ? "error" : read.phase === "result" && !read.presentation ? "invalid" : read.phase === "result" ? "prepared" : "preparing")
|
|
3030
|
+
});
|
|
3031
|
+
}
|
|
3032
|
+
function apply(ctx) {
|
|
3033
|
+
const client = ctx;
|
|
3034
|
+
ctx.effect(() => client.locale.register(LOCALE, {
|
|
3035
|
+
zh: { ...uiZh },
|
|
3036
|
+
en: { ...uiEn }
|
|
3037
|
+
}), "deep-view.locale");
|
|
3038
|
+
ctx.effect(() => {
|
|
3039
|
+
if (typeof document === "undefined") return () => {};
|
|
3040
|
+
const style = document.createElement("style");
|
|
3041
|
+
style.setAttribute("data-plugin", PLUGIN_NAME);
|
|
3042
|
+
style.textContent = viewStyles;
|
|
3043
|
+
document.head.appendChild(style);
|
|
3044
|
+
return () => style.remove();
|
|
3045
|
+
}, "deep-view.styles");
|
|
3046
|
+
ctx.effect(() => client.slots.inject("tool.call.toolview", () => client.slots.register({
|
|
3047
|
+
name: "tool.call.toolview",
|
|
3048
|
+
key: "show_view",
|
|
3049
|
+
locale: LOCALE
|
|
3050
|
+
}, ToolCallStatus)), "deep-view.tool");
|
|
3051
|
+
ctx.effect(() => client.slots.inject("conversation.chat.turnTail", () => client.slots.register({
|
|
3052
|
+
name: "conversation.chat.turnTail",
|
|
3053
|
+
id: "deep-view",
|
|
3054
|
+
order: 20,
|
|
3055
|
+
locale: LOCALE,
|
|
3056
|
+
inject: () => ({ call: (endpoint, payload) => client.connection.rpc.call(RPC_CHANNEL, endpoint, payload) })
|
|
3057
|
+
}, ReplyView)), "deep-view.reply");
|
|
3058
|
+
}
|
|
3059
|
+
//#endregion
|
|
3060
|
+
exports.LOCALE = LOCALE;
|
|
3061
|
+
exports.ReplyView = ReplyView;
|
|
3062
|
+
exports.ToolCallStatus = ToolCallStatus;
|
|
3063
|
+
exports.ToolCallView = ToolCallView;
|
|
3064
|
+
exports.apply = apply;
|
|
3065
|
+
exports.inject = inject;
|
|
3066
|
+
exports.name = name;
|
|
3067
|
+
exports.readToolCall = readToolCall;
|
|
3068
|
+
exports.replyCards = replyCards;
|
|
3069
|
+
exports.uiEn = uiEn;
|
|
3070
|
+
exports.uiZh = uiZh;
|
|
3071
|
+
exports.useMarkdownLabels = useMarkdownLabels;
|
|
3072
|
+
|
|
3073
|
+
//# sourceMappingURL=client.inner.cjs.map
|