dsh-motion-ui 0.5.3 → 0.5.4
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/CHANGELOG.md +9 -0
- package/README.md +6 -0
- package/VERIFICATION.md +18 -0
- package/client.js +386 -249
- package/index.js +16 -3
- package/lib/author-expression.js +3 -2
- package/lib/authoring-guide.js +9 -0
- package/lib/build-info.js +1 -1
- package/lib/canvas-preview.js +1 -1
- package/lib/canvas-system.js +3 -2
- package/lib/catalog.js +5 -4
- package/lib/components.js +2 -2
- package/lib/export-runtime-bundle.js +1 -1
- package/lib/freeform.js +3 -2
- package/lib/identifiers.js +4 -0
- package/lib/protocol.js +20 -11
- package/lib/stream.js +2 -1
- package/lib/tool-json.js +21 -2
- package/lib/tool-schema.js +29 -0
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -11,21 +11,34 @@ import { UI_SKILL_SECTIONS, promptSections } from './lib/ui-skill.js';
|
|
|
11
11
|
import { canvasDocument } from './lib/freeform.js';
|
|
12
12
|
import z from '@deepseek-ai/schemastery';
|
|
13
13
|
import { toolJson } from './lib/tool-json.js';
|
|
14
|
+
import { DOCUMENT_PARAMETER } from './lib/tool-schema.js';
|
|
14
15
|
import { inspectRecord } from './lib/inspection.js';
|
|
15
16
|
import {QUICK_EXAMPLES,QUICK_GUIDE} from './lib/quick-examples.js';
|
|
16
17
|
import { PROTOCOL_GUIDE, calculatorExample, comparisonExample, budgetExample, curveExample, sceneExamples, bicycleExample, oscillatorExample } from './lib/catalog.js';
|
|
17
18
|
|
|
18
19
|
export const name = 'motion-ui';
|
|
19
20
|
export const inject = ['tools', 'connection', 'systemPrompt'];
|
|
20
|
-
|
|
21
|
+
function defineRenderTool(options) {
|
|
22
|
+
const tool=defineTool(options),execute=tool.execute;
|
|
23
|
+
return {...tool,async execute(args,exec){
|
|
24
|
+
// Legacy adapters may stringify JSON. Decode here, without advertising a
|
|
25
|
+
// string branch that encourages models to manually escape whole documents.
|
|
26
|
+
const normalized=args&&Object.hasOwn(args,'document')?{...args,document:toolJson(args.document,'document')}:args;
|
|
27
|
+
try{return await execute(normalized,exec);}catch(error){
|
|
28
|
+
if(error.name==='ToolArgsError' && args && Object.hasOwn(args,'document')) validateDocument(toolJson(args.document,'document'));
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}};
|
|
32
|
+
}
|
|
33
|
+
export const Config=z.object({designMode:z.union(['guided','free']).default('guided').description('guided:设计技能与示例引导;free:只注入画布接口与交互约束,由模型自主设计。'),displayMode:z.union(['tail','inline','sidebar']).default('tail').description('tail:回答下方;inline:正文标记位置;sidebar:PC 侧栏。'),showDetails:z.boolean().default(false).description('显示参数状态、原始说明、视觉诊断和版本历史。默认关闭,保持界面简洁。')}).volatile();
|
|
21
34
|
export function apply(ctx,config={}) {
|
|
22
35
|
const store = new UiStore(dshHomePath('plugins', 'dsh-generative-ui'));
|
|
23
36
|
const settings=()=>typeof config.get==='function'?config.get():config;
|
|
24
37
|
const placement=value=>settings().displayMode==='inline'?`\nPlace this exact fenced marker in your answer at the intended UI position:\n\`\`\`dsh-ui\n${value.documentId}\n\`\`\``:'';
|
|
25
|
-
ctx.effect(() => ctx.tools.register(
|
|
38
|
+
ctx.effect(() => ctx.tools.register(defineRenderTool({
|
|
26
39
|
name: 'render_ui',
|
|
27
40
|
description: 'Render a static or interactive DSH UI v1 document in this conversation. Proactively use when visual relationships or changing parameters clearly explain the question better, even without an explicit UI request. Ordinary facts and short explanations stay text. Deliver the smallest complete useful UI first. ui_catalog supplies optional details. Direct native calls only; PTC returns text. Calculations stay local. document requires version:1,title,fallback,nodes. Repair errors at most twice or simplify/answer in text.',
|
|
28
|
-
parameters: { document:
|
|
41
|
+
parameters: { document: DOCUMENT_PARAMETER },
|
|
29
42
|
output: {
|
|
30
43
|
schema: { type: 'json' },
|
|
31
44
|
render: (_args, value) => [{ type: 'text', text: fallbackText(value.document)+`\nUI documentId=${value.documentId}; documentRevision=${value.documentRevision??0}. Use inspect_ui before refinements.`+placement(value) }],
|
package/lib/author-expression.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// A small authoring language compiled to the existing interpreter; never eval.
|
|
2
|
+
import { IDENTIFIER_SOURCE, validIdentifier } from './identifiers.js';
|
|
2
3
|
export function compileExpression(source) {
|
|
3
4
|
const fail = () => { throw new Error('DSH UI: invalid formula; use state.x, data.x, derived.x, item.x, numbers, strings, + - * /, comparisons, ?: and min/max/round/abs'); };
|
|
4
5
|
if (typeof source !== 'string' || !source.trim() || source.length > 2048) fail();
|
|
5
6
|
const tokens = []; let offset = 0;
|
|
6
|
-
const token =
|
|
7
|
+
const token = new RegExp(String.raw`\s*(?:(\d+(?:\.\d*)?|\.\d+)([eE][+-]?\d+)?|("(?:[^"\\]|\\.)*")|(${IDENTIFIER_SOURCE}(?:\.${IDENTIFIER_SOURCE})?)|(===|!==|<=|>=|==|!=|[+*/()?,:<>-]))`, 'uy');
|
|
7
8
|
while (offset < source.length) {
|
|
8
9
|
if (!source.slice(offset).trim()) break;
|
|
9
10
|
token.lastIndex = offset; const m = token.exec(source); if (!m) fail();
|
|
@@ -21,7 +22,7 @@ export function compileExpression(source) {
|
|
|
21
22
|
else if (t === 'true' || t === 'false') out = { const: t === 'true' };
|
|
22
23
|
else if (t === '(') { out = parse(0, depth + 1); take(')'); }
|
|
23
24
|
else if (t === '-' || t === '+') { const arg = parse(5, depth + 1); out = t === '-' ? { op: 'sub', args: [{ const: 0 }, arg] } : arg; }
|
|
24
|
-
else if (typeof t === 'string' &&
|
|
25
|
+
else if (typeof t === 'string' && t.split('.').length === 2 && ['state','data','derived','item'].includes(t.split('.')[0]) && validIdentifier(t.split('.')[1])) out = { ref: t };
|
|
25
26
|
else if (['min', 'max', 'round', 'abs'].includes(t)) {
|
|
26
27
|
take('('); const args = [parse(0, depth + 1)];
|
|
27
28
|
while (tokens[pos] === ',') { pos++; args.push(parse(0, depth + 1)); if (args.length > 8) fail(); }
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Keep this short and early in the injected contract, including free mode.
|
|
2
|
+
export const AUTHORING_GUIDE = `Authoring fast path:
|
|
3
|
+
- Pass tool JSON fields as objects, not hand-escaped JSON strings. For custom HTML/SVG use render_canvas({title,fallback,html,css,script}) to avoid nesting source in document JSON.
|
|
4
|
+
- Start with the smallest useful static UI. State/derived/actions are optional. A table can put its object rows directly in rows; reuse data/derived references only when useful. Example: {id:"t",type:"table",label:"方案比较",columns:[{key:"方案",label:"方案"}],rows:[{"方案":"A"}]}.
|
|
5
|
+
- Host defaults use rounded surfaces and soft theme-aware gradients. For guided canvas designs prefer 18..24px surface radii and subtle gradient fills with legible text. Let content determine height: no empty spacer sections, min-height:100vh or large fixed min-heights. A canvas auto-fits its rendered content after commit, including shrinking after filtering; height is only the initial placeholder. Use SVG viewBox bounds close to actual graphics plus reasonable padding.
|
|
6
|
+
- metric.value accepts a finite number or display string/boolean. Numbers use decimals and unit; strings display verbatim ("约 12 GiB", "待确认"). Calculations and chart series still require real numbers, never formatted strings.
|
|
7
|
+
- IDs, data/state/derived keys, column keys and formula references support Chinese letters and underscores. Use 1..64 letters/numbers/underscores, start with a letter/underscore, no spaces/dots/hyphens; never constructor/prototype/__proto__. Labels/text can be natural language.
|
|
8
|
+
- Persist a canvas checklist/list/object with state:{checklist:{type:"json",default:[]}} and DSHUI.setState({checklist:nextArray}). json state allows bounded arrays/objects, no options; native input/select cannot edit it. A native checklist uses separate boolean states and toggle nodes. Static lists belong in nodes/data, not state. String state with an array/object default and no options is canonicalized to json without losing its structure.
|
|
9
|
+
- Do not guess capabilities. On validation failure, fix the named field in the original document. Unknown properties are not discarded; unsupported code is not executed. Never claim successful delivery before the tool succeeds.`;
|
package/lib/build-info.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by npm run build.
|
|
2
|
-
export const BUILD_INFO = Object.freeze({"version":"0.5.
|
|
2
|
+
export const BUILD_INFO = Object.freeze({"version":"0.5.4","id":"0.5.4+92e88973eabc","sourceHash":"92e88973eabc736e169fbb03366db183ca5cd1fbbe0e3a7e44e5742da708d96e"});
|
package/lib/canvas-preview.js
CHANGED
|
@@ -27,5 +27,5 @@ export function updatePreview(html) {
|
|
|
27
27
|
morph(document.getElementById('canvas'),template.content);
|
|
28
28
|
}
|
|
29
29
|
export function previewFrameDocument(channel,nonce){
|
|
30
|
-
return `<!doctype html><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none';script-src 'nonce-${nonce}';style-src 'unsafe-inline';img-src data:;base-uri 'none';form-action 'none';frame-src 'none'"><style>${CANVAS_SYSTEM_CSS}</style><style id="custom"></style><style>#canvas,#canvas *,#canvas *::before,#canvas *::after{animation:none!important;transition:none!important}html,body{overflow:hidden}</style><main id="canvas" inert></main><script nonce="${nonce}">const update=${updatePreview.toString()};addEventListener('message',e=>{if(e.source!==parent||e.data?.channel!==${JSON.stringify(channel)})return;document.getElementById('custom').textContent=e.data.css;for(const [k,v] of Object.entries(e.data.theme||{}))document.documentElement.style.setProperty('--canvas-'+k,v);document.documentElement.dataset.canvasScheme=e.data.theme?.scheme||'light';update(e.data.html)});</script>`;
|
|
30
|
+
return `<!doctype html><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none';script-src 'nonce-${nonce}';style-src 'unsafe-inline';img-src data:;base-uri 'none';form-action 'none';frame-src 'none'"><style>${CANVAS_SYSTEM_CSS}</style><style id="custom"></style><style>#canvas,#canvas *,#canvas *::before,#canvas *::after{animation:none!important;transition:none!important}html,body{overflow:hidden}</style><main id="canvas" inert></main><script nonce="${nonce}">const update=${updatePreview.toString()};addEventListener('message',e=>{if(e.source!==parent||e.data?.channel!==${JSON.stringify(channel)})return;document.getElementById('custom').textContent=e.data.css;for(const [k,v] of Object.entries(e.data.theme||{}))document.documentElement.style.setProperty('--canvas-'+k,v);document.documentElement.dataset.canvasScheme=e.data.theme?.scheme||'light';update(e.data.html)});new ResizeObserver(()=>parent.postMessage({channel:${JSON.stringify(channel)},previewHeight:Math.ceil(document.getElementById('canvas').getBoundingClientRect().height)},'*')).observe(document.getElementById('canvas'));</script>`;
|
|
31
31
|
}
|
package/lib/canvas-system.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// Optional primitives for model-authored surfaces. Host theme remains authoritative.
|
|
2
2
|
export const CANVAS_SYSTEM_CSS = `
|
|
3
|
-
:root{--canvas-radius:
|
|
3
|
+
:root{--canvas-radius:20px;--canvas-gap:16px;--canvas-positive:#167456;--canvas-negative:#b13b3b;--canvas-warning:#875b15}
|
|
4
4
|
:root[data-canvas-scheme=dark]{--canvas-positive:#78d9b4;--canvas-negative:#ffaaaa;--canvas-warning:#edc87e}
|
|
5
|
-
html,body{margin:0;min-width:0;background:var(--canvas-bg,#fff);color:var(--canvas-ink,#182537);font:14px/1.6 system-ui,"Microsoft YaHei",sans-serif}*{box-sizing:border-box}svg{max-width:100%;height:auto}button,input,select,textarea{font:inherit;color:inherit}button,input,select,textarea{border:1px solid var(--canvas-line,#ced4df);border-radius:8px;background:var(--canvas-bg,#fff);padding:8px 12px;max-width:100%}button{cursor:pointer}button:disabled{cursor:default;opacity:.55}input[type=range]{padding:0;accent-color:var(--canvas-accent,#3e60d3)}input[type=checkbox],input[type=radio]{accent-color:var(--canvas-accent,#3e60d3)}button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--canvas-accent,#3e60d3);outline-offset:3px}
|
|
5
|
+
#canvas{display:flow-root}html,body{margin:0;min-width:0;background:var(--canvas-bg,#fff);color:var(--canvas-ink,#182537);font:14px/1.6 system-ui,"Microsoft YaHei",sans-serif}*{box-sizing:border-box}svg{max-width:100%;height:auto}button,input,select,textarea{font:inherit;color:inherit}button,input,select,textarea{border:1px solid var(--canvas-line,#ced4df);border-radius:8px;background:var(--canvas-bg,#fff);padding:8px 12px;max-width:100%}button{cursor:pointer}button:disabled{cursor:default;opacity:.55}input[type=range]{padding:0;accent-color:var(--canvas-accent,#3e60d3)}input[type=checkbox],input[type=radio]{accent-color:var(--canvas-accent,#3e60d3)}button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--canvas-accent,#3e60d3);outline-offset:3px}
|
|
6
6
|
.ui-stack{display:grid;gap:var(--canvas-gap)}.ui-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr));gap:var(--canvas-gap)}.ui-surface{padding:20px;border:1px solid var(--canvas-line,#ced4df);border-radius:var(--canvas-radius);background:var(--canvas-soft,#f3f5f8)}.ui-title{margin:0;font-size:clamp(20px,3vw,28px);line-height:1.3;letter-spacing:-.02em}.ui-caption{color:var(--canvas-muted,#526176);margin:8px 0;font-size:14px}.ui-value{font-size:clamp(24px,4vw,36px);font-variant-numeric:tabular-nums;line-height:1.2}.ui-controls{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.ui-field{display:grid;gap:6px;min-width:0}.ui-legend{display:flex;gap:16px;flex-wrap:wrap;font-size:13px}.ui-status{min-height:1.6em}.ui-error{color:var(--canvas-negative)}.ui-primary{background:var(--canvas-accent,#3e60d3);color:var(--canvas-accent-ink,#fff);border-color:transparent}
|
|
7
7
|
svg .ui-node{fill:var(--canvas-soft,#f3f5f8);stroke:var(--canvas-line,#ced4df);stroke-width:1}svg .ui-edge{fill:none;stroke:var(--canvas-muted,#526176);stroke-width:1.5}svg .ui-label{fill:var(--canvas-ink,#182537);font:500 14px system-ui,sans-serif}svg .ui-note{fill:var(--canvas-muted,#526176);font:400 13px system-ui,sans-serif}svg .ui-leader{fill:none;stroke:var(--canvas-muted,#526176);stroke-dasharray:3 4}svg .ui-accent{fill:color-mix(in srgb,var(--canvas-accent,#3e60d3) 12%,var(--canvas-bg,#fff));stroke:var(--canvas-accent,#3e60d3)}
|
|
8
8
|
.ui-flow{display:flex;align-items:center;justify-content:center;gap:16px;padding:24px 8px;flex-wrap:wrap}.ui-step{display:grid;gap:6px;min-width:130px;max-width:240px;padding:16px 20px;border:1px solid var(--canvas-line);border-radius:6px;background:var(--canvas-bg)}.ui-step strong{font-size:16px;font-weight:600}.ui-step span{font-size:14px;color:var(--canvas-muted)}.ui-arrow{color:var(--canvas-muted);font-size:22px}.ui-callout{border-inline-start:3px solid var(--canvas-accent);padding:8px 16px;color:var(--canvas-ink)}
|
|
9
|
+
.ui-surface,.ui-step{border-radius:var(--canvas-radius);background:linear-gradient(135deg,color-mix(in srgb,var(--canvas-accent,#3e60d3) 9%,var(--canvas-bg,#fff)),var(--canvas-soft,#f3f5f8))}button,input:not([type=checkbox]):not([type=radio]):not([type=range]),select,textarea{border-radius:14px}.ui-primary{background:linear-gradient(120deg,var(--canvas-accent,#3e60d3),color-mix(in srgb,var(--canvas-accent,#3e60d3) 85%,#152647))}.ui-callout{border-radius:14px;background:linear-gradient(120deg,color-mix(in srgb,var(--canvas-accent,#3e60d3) 7%,var(--canvas-bg,#fff)),var(--canvas-bg,#fff))}
|
|
9
10
|
@media(max-width:480px){.ui-flow{flex-direction:column;padding:16px 8px;gap:12px}.ui-step{width:100%;max-width:none}.ui-arrow{transform:rotate(90deg)}}
|
|
10
11
|
@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}}`;
|
|
11
12
|
|
package/lib/catalog.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { UI_ICONS, UI_TONES } from './presentation.js';
|
|
2
2
|
import { componentGuide } from './components.js';
|
|
3
|
+
import { AUTHORING_GUIDE } from './authoring-guide.js';
|
|
3
4
|
export { sceneExamples } from './scenes.js';
|
|
4
5
|
export { bicycleExample, oscillatorExample } from './canvas-example.js';
|
|
5
|
-
export const PROTOCOL_GUIDE =
|
|
6
|
+
export const PROTOCOL_GUIDE = AUTHORING_GUIDE+`\n\nDSH UI v1. Call render_ui({document: ...}) directly when visualization helps. Do not call it inside run_code/PTC in this version.
|
|
6
7
|
Formula shorthand: {expr:"state.V * state.V / state.R"} compiles to the same validated interpreter. Supports explicit state.x/data.x/derived.x/item.x references, + - * /, == != < <= > >=, parentheses, condition ? yes : no, min/max/round/abs, double-quoted strings and booleans. Use it in derived, node text/value/max/rows or map.fields. No arbitrary functions, property chains, statements or side effects; complex animation belongs in render_canvas. Example derived:{power:{expr:"state.V * state.V / state.R"}}, metric.value:{ref:"derived.power"}.
|
|
7
8
|
Document: {version:1,title:string,fallback:string,presentation?:{layout,accent,density},state?:{},data?:{},derived?:{},nodes:[],actions?:{}}.
|
|
8
9
|
Scene layouts: dashboard (metrics/trends), workbench (parameter controls/results), comparison (parallel choice cards), guide (ordered instructions), status (observed task snapshot), report (reading and evidence). Accent: blue/teal/violet/amber/rose; density: comfortable/compact. Omit to keep the standard design. Choose based on the task, not at random. Card surface: outline/soft/featured. Featured indicates discussion focus, not an automatic recommendation.
|
|
9
10
|
Additional nodes: badge {id,type,text,icon?,tone?}; progress {id,type,label,value,max,unit?,icon?,tone?} where values may be bindings and 0 <= value <= positive max; steps {id,type,items:[{title,description?,status:"pending"|"active"|"done"|"error"}]} (1..12 items); details {id,type,title,children,open?:boolean,icon?,tone?}. Progress and steps are declared snapshots, not a live host task subscription. Only claim real progress/completion from actual tool evidence; mark demo data visibly. Details keeps the content available in text fallback.
|
|
10
11
|
Streaming: use ONE normal native render_ui call. Write version,title,fallback,presentation,state,data,derived,actions BEFORE nodes where possible. The client previews completed, validated top-level subtrees as arguments stream; incomplete nodes/strings/calculations wait. Preview controls are disabled and never save state or send actions. Only a successful Host result activates the durable UI. No separate ui.begin/patch calls are required. A cancelled/failed generation has no interactive document. Prefer several small meaningful top-level sections over one enormous wrapping card.
|
|
11
|
-
All IDs and dictionary keys:
|
|
12
|
+
All IDs and dictionary keys: Unicode letters (including Chinese) or underscore, followed by letters/marks/digits/underscore, max64; unique node IDs. Standard components reject HTML, JS, URLs, styles and callbacks. The sandbox component below is the explicit exception; unlisted fields are always rejected.
|
|
12
13
|
Freeform canvas: {id,type:"sandbox",title,html,css?:string,script?:string,height?:160..1000,fallback,stateKeys?:string[],dataKeys?:string[],derivedKeys?:string[]}. Use it for custom SVG diagrams, animation, simulations and bespoke interfaces that fixed components cannot express. HTML is a body fragment. Put CSS in css and ALL JavaScript in script (not inline handlers, script tags or remote imports). Inline SVG is supported. Each source field allows up to 49152 characters; the total document remains 128KiB. The iframe has no same-origin privilege, remote resources/network APIs are blocked by CSP, and no host tools are exposed. Never put credentials or private unrelated data in a canvas. It is browser origin isolation, not a CPU/memory hard sandbox.
|
|
13
14
|
Canvas API: window.DSHUI.snapshot -> {state,data,derived,disabled,theme,visible}; DSHUI.animate(fn) returns cleanup and calls fn({time,delta,reducedMotion,visible}), time/delta in seconds, delta capped at 0.05. It pauses offscreen/background and supplies a static delta=0 callback for reduced motion; integrate delta for pause/resume without jumps. DSHUI.subscribe(snapshot=>{...}) immediately supplies current selected values and returns unsubscribe; DSHUI.setState({key:value,...}) requests an atomic Host-validated update of declared stateKeys. Use bare dictionary keys in stateKeys/dataKeys/derivedKeys. Only declared keys are exposed; state types/ranges/options remain enforced. Subscribe to drive DOM/SVG from host state; do not send the same state back from subscribe (feedback loop). Native controls and canvas share the saved state. Local animation may use requestAnimationFrame without saving every frame. No Agent/tool action API in this release. theme exposes bg,soft,ink,muted,line,accent,scheme; CSS variables --canvas-bg/soft/ink/muted/line/accent update automatically. Honor reduced motion, provide accessible labels and responsive layout. Canvas scripts activate only after successful tool commit and state restoration; previews show a placeholder. Use a complete useful fallback per canvas. Code or reset changes restart the frame, state changes do not. All transient internal animation state is reset on reload; declare important parameters in document.state.
|
|
14
|
-
state: key -> {type:"number",default,min,max,step} or {type:"string",default,options?:scalar[]} or {type:"boolean",default}. select requires options. Numeric defaults follow step from min.
|
|
15
|
+
state: key -> {type:"number",default,min,max,step} or {type:"string",default,options?:scalar[]} or {type:"boolean",default} or {type:"json",default:array/object} (canvas structured state, max16KiB per value). select requires scalar options. Numeric defaults follow step from min.
|
|
15
16
|
derived: key -> expression: {const:scalar}, {ref:"state.key"|"data.key"|"derived.key"}, or {op,args:expression[]}. Allowed ops add,mul,min,max (1..8 args); sub,div,eq,neq,lt,lte,gt,gte (2); round,abs (1); if (3, boolean condition). No cycles. Arithmetic requires finite numbers: add does NOT concatenate strings. For formatted labels use a metric's label/unit/decimals, or format text inside a sandbox; omit unused derived fields. if can return strings: {op:"if",args:[{op:"gt",args:[{ref:"derived.weightGiB"},{const:20}]},{const:"超过理论预算"},{const:"在理论预算内"}]} -> bind a text node to this derived result. No separate conditional-rendering field is needed.
|
|
16
|
-
Text/metric values are literal scalars or {ref:...}. Table/chart rows
|
|
17
|
+
Text/metric values are literal scalars or {ref:...}. Table/chart rows accept inline arrays of objects, {ref:"data.rows"}, {ref:"derived.rows"}, or the shorthand string "data.rows"/"derived.rows". Table label is an optional caption. Static table example: data:{rows:[{name:"A",note:"示例"}]}, node:{id:"t",type:"table",rows:{ref:"data.rows"},columns:[{key:"name",label:"方案"},{key:"note",label:"说明"}]}. Dynamic rows: a derived root may be {map:{source:"data.samples",fields:{contextK:{op:"mul",args:[{ref:"state.contextK"},{ref:"item.fraction"}]},value:{ref:"derived.total"}}}}. source must be the string "data.samples", not an inline array; declare those object rows in document.data.samples. Internal field keys support Chinese identifiers such as 功率 and ASCII identifiers such as powerW; spaces/dots/hyphens belong in display labels. item.key is available only inside map fields. Each field is a normal scalar expression. No nested maps/callbacks; 1..16 fields, max500 generated rows across all maps; normal expression and calculation budgets apply. Derived output max128KiB. DSHUI.formatNumber(value, decimals=2) formats finite numbers with 0..6 decimal places and comma grouping, exactly matching native metrics and fallback; use it for shared readouts instead of mixing toFixed rounding. Use this for live charts/tables, never model calls on slider changes.
|
|
17
18
|
Registered component fields (some optional; required values and types follow the rules above/below): ${componentGuide()}. Controls bind "state.key"; slider numeric, toggle boolean, select requires state options. Metric decimals 0..6. Table columns: [{key,label,unit?}]. Chart kind bar/line, finite numeric values, rows as a data/derived binding; line xScale linear positions numeric labelKey samples by their actual coordinate, category uses equal spacing. Use ascending samples for a curve.
|
|
18
19
|
actions: key -> {type:"agent.followup",intent:string,stateKeys:string[]}. Button submits current selected parameters as a new conversation request; it does not perform a host action itself. All host actions still use ordinary DSH permissions.
|
|
19
20
|
Presentation: heading/text/card/metric accept optional tone (${UI_TONES.join(', ')}); heading/text/card/metric/button accept optional icon (${UI_ICONS.join(', ')}). Fixed tokens only, no SVG/HTML/CSS/color strings. Defaults already include icons and themed spacing. Use accent for primary information, warning for assumptions/caveats, success/danger only for explicitly established facts. Tones are static, not conditional; use derived string text to report changing verdicts. Use row for 2-3 related metrics, card for a meaningful section, avoid repeated headings and deep decorative nesting. Do not use color as the only explanation.
|
package/lib/components.js
CHANGED
|
@@ -8,11 +8,11 @@ export const COMPONENTS = Object.freeze({
|
|
|
8
8
|
badge: define(['text'], 'Short explicit status label', 'tone'),
|
|
9
9
|
progress: define(['label', 'value', 'max', 'unit'], 'Declared numeric progress snapshot', 'tone'),
|
|
10
10
|
steps: define(['items'], 'Ordered declared steps'),
|
|
11
|
-
metric: define(['label', 'value', 'unit', 'decimals'], '
|
|
11
|
+
metric: define(['label', 'value', 'unit', 'decimals'], 'Number with units, or a literal/bound status string', 'tone'),
|
|
12
12
|
input: define(['label', 'bind'], 'String or numeric state input'), slider: define(['label', 'bind'], 'Numeric state control'),
|
|
13
13
|
select: define(['label', 'bind'], 'Selection from declared state options'), toggle: define(['label', 'bind'], 'Boolean state control'),
|
|
14
14
|
button: define(['text', 'action'], 'Declared Agent follow-up', 'icon'),
|
|
15
|
-
table: define(['rows', 'columns'], '
|
|
15
|
+
table: define(['label', 'rows', 'columns'], 'Supplied or derived values; inline rows or data/derived reference'),
|
|
16
16
|
chart: define(['kind', 'rows', 'labelKey', 'valueKey', 'label', 'unit', 'xScale'], 'Bar/line comparison with accessible data'),
|
|
17
17
|
sandbox: define(['title', 'html', 'css', 'script', 'fallback', 'height', 'stateKeys', 'dataKeys', 'derivedKeys'], 'Freeform isolated HTML/SVG/CSS/JavaScript visualization')
|
|
18
18
|
});
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/build.mjs.
|
|
2
|
-
export const EXPORT_RUNTIME="(() => {\n // lib/presentation.js\n var UI_ICONS = Object.freeze([\"sparkles\", \"sliders\", \"layers\", \"chart\", \"gauge\", \"cpu\", \"database\", \"info\", \"check\", \"warning\", \"arrow\", \"target\"]);\n var UI_TONES = Object.freeze([\"neutral\", \"accent\", \"success\", \"warning\", \"danger\"]);\n var UI_LAYOUTS = Object.freeze([\"dashboard\", \"workbench\", \"comparison\", \"guide\", \"status\", \"report\"]);\n var UI_ACCENTS = Object.freeze([\"blue\", \"teal\", \"violet\", \"amber\", \"rose\"]);\n var UI_SURFACES = Object.freeze([\"outline\", \"soft\", \"featured\"]);\n\n // lib/visual-audit.js\n function measureVisual(root, nodeId = \"\") {\n const issues = [], seen = /* @__PURE__ */ new Set(), coverage = { nodeId, elements: 0, truncated: false, contrastChecked: 0, contrastUncovered: 0 };\n const add = (code, value, el2) => {\n const id = el2?.closest(\"[data-ui-node]\")?.getAttribute(\"data-ui-node\") ?? nodeId;\n const k = id + \":\" + code + \":\" + Math.round(value ?? 0);\n if (!seen.has(k) && issues.length < 40) {\n seen.add(k);\n issues.push({ code, nodeId: id, ...value === void 0 ? {} : { value: Number(value.toFixed(2)) } });\n }\n };\n const color = (s) => {\n let m = s.match(/^rgba?\\(([^)]+)\\)/);\n if (m) {\n const a = m[1].split(/[\\s,\\/]+/).map(Number);\n return [a[0] / 255, a[1] / 255, a[2] / 255, a[3] ?? 1];\n }\n m = s.match(/^color\\(srgb ([^)]+)\\)/);\n if (m) {\n const a = m[1].split(/[\\s\\/]+/).map(Number);\n return [a[0], a[1], a[2], a[3] ?? 1];\n }\n return null;\n };\n const luminance = (c) => c.slice(0, 3).map((v) => v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4).reduce((s, v, i) => s + v * [0.2126, 0.7152, 0.0722][i], 0);\n const widthRoot = root.id === \"canvas\" ? document.documentElement : root;\n if (widthRoot.scrollWidth > widthRoot.clientWidth + 2) add(\"overflow\", widthRoot.scrollWidth - widthRoot.clientWidth);\n const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);\n let el;\n while (el = walker.nextNode()) {\n if (coverage.elements >= 2e3) {\n coverage.truncated = true;\n break;\n }\n coverage.elements++;\n if (el.matches(\"[data-canvas-phase=error]\")) add(\"runtimeError\", void 0, el);\n const css = getComputedStyle(el), box = el.getBoundingClientRect();\n if (!box.width || !box.height || css.visibility === \"hidden\" || css.display === \"none\") continue;\n const text2 = [...el.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim());\n if (text2) {\n let size = parseFloat(css.fontSize);\n if (el instanceof SVGElement && el.getScreenCTM) {\n const m = el.getScreenCTM();\n if (m) size *= Math.hypot(m.c, m.d);\n }\n if (size < 11.9) add(\"smallText\", size, el);\n const fg = color(css.color);\n let ancestor = el, bg = null, depth = 0;\n while (ancestor && depth++ < 64) {\n const style = getComputedStyle(ancestor);\n if (style.backgroundImage !== \"none\") break;\n const c = color(style.backgroundColor);\n if (c?.[3] === 1) {\n bg = c;\n break;\n }\n if (c?.[3] > 0) break;\n ancestor = ancestor.parentElement;\n }\n if (!(el instanceof SVGElement) && fg?.[3] === 1 && bg) {\n coverage.contrastChecked++;\n const a = luminance(fg), b = luminance(bg), ratio = (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);\n if (ratio < (size >= 24 || size >= 18.66 && parseFloat(css.fontWeight) >= 700 ? 3 : 4.5)) add(\"contrast\", ratio, el);\n } else coverage.contrastUncovered++;\n }\n if (el.matches(\"input:not([type=hidden]),select,textarea,button,[role=button]\") && !el.getAttribute(\"aria-label\") && !el.getAttribute(\"aria-labelledby\") && !el.labels?.length && !el.textContent.trim() && !el.getAttribute(\"title\")) add(\"missingLabel\", void 0, el);\n }\n Object.defineProperty(issues, \"coverage\", { value: coverage });\n return issues;\n }\n\n // lib/format-number.js\n function formatNumber(value, decimals = 2) {\n if (typeof value !== \"number\" || !Number.isFinite(value)) throw new Error(\"DSH UI: formatNumber requires a finite number\");\n if (!Number.isInteger(decimals) || decimals < 0 || decimals > 6) throw new Error(\"DSH UI: formatNumber decimals must be an integer from 0 to 6\");\n return value.toLocaleString(\"en-US\", { minimumFractionDigits: decimals, maximumFractionDigits: decimals });\n }\n\n // lib/canvas-system.js\n var CANVAS_SYSTEM_CSS = `\n:root{--canvas-radius:12px;--canvas-gap:16px;--canvas-positive:#167456;--canvas-negative:#b13b3b;--canvas-warning:#875b15}\n:root[data-canvas-scheme=dark]{--canvas-positive:#78d9b4;--canvas-negative:#ffaaaa;--canvas-warning:#edc87e}\nhtml,body{margin:0;min-width:0;background:var(--canvas-bg,#fff);color:var(--canvas-ink,#182537);font:14px/1.6 system-ui,\"Microsoft YaHei\",sans-serif}*{box-sizing:border-box}svg{max-width:100%;height:auto}button,input,select,textarea{font:inherit;color:inherit}button,input,select,textarea{border:1px solid var(--canvas-line,#ced4df);border-radius:8px;background:var(--canvas-bg,#fff);padding:8px 12px;max-width:100%}button{cursor:pointer}button:disabled{cursor:default;opacity:.55}input[type=range]{padding:0;accent-color:var(--canvas-accent,#3e60d3)}input[type=checkbox],input[type=radio]{accent-color:var(--canvas-accent,#3e60d3)}button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--canvas-accent,#3e60d3);outline-offset:3px}\n.ui-stack{display:grid;gap:var(--canvas-gap)}.ui-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr));gap:var(--canvas-gap)}.ui-surface{padding:20px;border:1px solid var(--canvas-line,#ced4df);border-radius:var(--canvas-radius);background:var(--canvas-soft,#f3f5f8)}.ui-title{margin:0;font-size:clamp(20px,3vw,28px);line-height:1.3;letter-spacing:-.02em}.ui-caption{color:var(--canvas-muted,#526176);margin:8px 0;font-size:14px}.ui-value{font-size:clamp(24px,4vw,36px);font-variant-numeric:tabular-nums;line-height:1.2}.ui-controls{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.ui-field{display:grid;gap:6px;min-width:0}.ui-legend{display:flex;gap:16px;flex-wrap:wrap;font-size:13px}.ui-status{min-height:1.6em}.ui-error{color:var(--canvas-negative)}.ui-primary{background:var(--canvas-accent,#3e60d3);color:var(--canvas-accent-ink,#fff);border-color:transparent}\nsvg .ui-node{fill:var(--canvas-soft,#f3f5f8);stroke:var(--canvas-line,#ced4df);stroke-width:1}svg .ui-edge{fill:none;stroke:var(--canvas-muted,#526176);stroke-width:1.5}svg .ui-label{fill:var(--canvas-ink,#182537);font:500 14px system-ui,sans-serif}svg .ui-note{fill:var(--canvas-muted,#526176);font:400 13px system-ui,sans-serif}svg .ui-leader{fill:none;stroke:var(--canvas-muted,#526176);stroke-dasharray:3 4}svg .ui-accent{fill:color-mix(in srgb,var(--canvas-accent,#3e60d3) 12%,var(--canvas-bg,#fff));stroke:var(--canvas-accent,#3e60d3)}\n.ui-flow{display:flex;align-items:center;justify-content:center;gap:16px;padding:24px 8px;flex-wrap:wrap}.ui-step{display:grid;gap:6px;min-width:130px;max-width:240px;padding:16px 20px;border:1px solid var(--canvas-line);border-radius:6px;background:var(--canvas-bg)}.ui-step strong{font-size:16px;font-weight:600}.ui-step span{font-size:14px;color:var(--canvas-muted)}.ui-arrow{color:var(--canvas-muted);font-size:22px}.ui-callout{border-inline-start:3px solid var(--canvas-accent);padding:8px 16px;color:var(--canvas-ink)}\n@media(max-width:480px){.ui-flow{flex-direction:column;padding:16px 8px;gap:12px}.ui-step{width:100%;max-width:none}.ui-arrow{transform:rotate(90deg)}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}}`;\n\n // lib/sandbox.js\n var SANDBOX_LIMITS = Object.freeze({ source: 49152, height: 1e3, message: 16384, rate: 30 });\n function canvasSnapshot(node2, state2, data, derived, disabled, theme) {\n const pick = (keys, values) => Object.fromEntries((keys ?? []).map((k) => [k, values[k]]));\n return { state: pick(node2.stateKeys, state2), data: pick(node2.dataKeys, data), derived: pick(node2.derivedKeys, derived), disabled, theme };\n }\n function canvasPatch(message, node2) {\n if (!message || typeof message !== \"object\" || Array.isArray(message)) throw new Error(\"Invalid canvas patch\");\n const keys = Object.keys(message);\n if (!keys.length || keys.length > 64 || keys.some((k) => !(node2.stateKeys ?? []).includes(k))) throw new Error(\"Canvas state key is not declared\");\n if (JSON.stringify(message).length > SANDBOX_LIMITS.message) throw new Error(\"Canvas patch is too large\");\n return message;\n }\n function sandboxDocument(node2, channel2, nonce2) {\n const json = (value) => JSON.stringify(value).replace(/</g, \"\\\\u003c\").replace(/\\u2028/g, \"\\\\u2028\").replace(/\\u2029/g, \"\\\\u2029\");\n return `<!doctype html><html><head><meta charset=\"utf-8\"><meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; script-src 'nonce-${nonce2}'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; connect-src 'none'; media-src 'none'; object-src 'none'; frame-src 'none'; worker-src 'none'; base-uri 'none'; form-action 'none'\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><style>${CANVAS_SYSTEM_CSS}</style></head><body><main id=\"canvas\"></main><script nonce=\"${nonce2}\">(()=>{\n const channel=${json(channel2)}, source=${json({ html: node2.html, css: node2.css ?? \"\", script: node2.script ?? \"\" })};\n let snapshot={state:{},data:{},derived:{},disabled:true,theme:{}}, started=false;\n const listeners=new Set(), send=(type,payload)=>parent.postMessage({dshCanvas:1,channel,type,payload},'*');\n const report=e=>send('error',String(e?.message??e).slice(0,1000));\n const audit=${measureVisual.toString()};let auditTimer;const scheduleAudit=()=>{clearTimeout(auditTimer);auditTimer=setTimeout(()=>{const issues=audit(document.getElementById('canvas'),${json(node2.id ?? \"\")});send('audit',{issues,coverage:issues.coverage})},400)};\n addEventListener('resize',scheduleAudit);\n addEventListener('error',e=>report(e.error??e.message));addEventListener('unhandledrejection',e=>report(e.reason));\n const animations=new Set(), motion=matchMedia('(prefers-reduced-motion: reduce)');\n let raf=0,last=0,time=0;\n const visible=()=>snapshot.visible!==false&&!document.hidden;\n const canAnimate=()=>visible()&&!snapshot.disabled&&!motion.matches;\n const draw=delta=>{for(const fn of animations)try{fn({time,delta,reducedMotion:motion.matches,visible:visible()})}catch(e){animations.delete(fn);report(e)}};\n const tick=t=>{raf=0;if(!canAnimate()||!animations.size){last=0;draw(0);return}const delta=last?Math.min(.05,(t-last)/1000):0;last=t;time+=delta;draw(delta);if(animations.size)raf=requestAnimationFrame(tick)};\n const refresh=()=>{draw(0);if(canAnimate()&&animations.size){if(!raf){last=0;raf=requestAnimationFrame(tick)}}else{cancelAnimationFrame(raf);raf=0;last=0}};\n motion.addEventListener('change',refresh);document.addEventListener('visibilitychange',refresh);\n const formatNumber=${formatNumber.toString()};\n const api={formatNumber,get snapshot(){return structuredClone(snapshot)},subscribe(fn){if(typeof fn!=='function')throw Error('Listener required');listeners.add(fn);fn(api.snapshot);return()=>listeners.delete(fn)},setState(patch){if(!snapshot.disabled)send('patch',patch)},animate(fn){if(typeof fn!=='function')throw Error('Animation callback required');animations.add(fn);refresh();return()=>{animations.delete(fn);if(!animations.size){cancelAnimationFrame(raf);raf=0;last=0}}}};\n Object.defineProperty(window,'DSHUI',{value:Object.freeze(api),writable:false});\n for(const type of ['click','input','change','keydown','pointerdown'])document.addEventListener(type,e=>{if(snapshot.disabled){e.preventDefault();e.stopImmediatePropagation()}},true);\n addEventListener('message',event=>{\n const m=event.data;if(event.source!==parent||m?.dshCanvas!==1||m.channel!==channel||m.type!=='snapshot')return;\n snapshot=m.payload;for(const [k,v] of Object.entries(snapshot.theme??{}))document.documentElement.style.setProperty('--canvas-'+k,v);\n document.documentElement.style.colorScheme=snapshot.theme?.scheme??'light';document.documentElement.dataset.canvasScheme=snapshot.theme?.scheme??'light';\n const wasStarted=started;\n if(!started){started=true;document.getElementById('canvas').innerHTML=source.html;const style=document.createElement('style');style.textContent=source.css;document.head.append(style);const script=document.createElement('script');script.nonce=${json(nonce2)};script.textContent=source.script;document.body.append(script)}\n if(wasStarted)for(const fn of listeners)try{fn(api.snapshot)}catch(e){report(e)}\n refresh();if(!wasStarted)send('mounted');scheduleAudit();\n });\n let resizePending=false;new ResizeObserver(()=>{if(!started||resizePending)return;resizePending=true;requestAnimationFrame(()=>{resizePending=false;send('height',Math.ceil(document.getElementById('canvas').getBoundingClientRect().height))})}).observe(document.getElementById('canvas'));\n send('ready');\n})();<\\/script></body></html>`;\n }\n\n // lib/components.js\n var define = (fields2, purpose, presentation = \"\") => Object.freeze({ fields: Object.freeze(fields2), purpose, presentation });\n var COMPONENTS = Object.freeze({\n heading: define([\"text\"], \"Section heading\", \"tone\"),\n text: define([\"text\"], \"Readable explanation or bound text\", \"tone\"),\n row: define([\"children\"], \"Wrapping parallel content\"),\n column: define([\"children\"], \"Ordered vertical content\"),\n card: define([\"title\", \"children\", \"surface\"], \"Meaningful grouped section\", \"tone\"),\n details: define([\"title\", \"children\", \"open\"], \"Collapsible supporting content\", \"tone\"),\n badge: define([\"text\"], \"Short explicit status label\", \"tone\"),\n progress: define([\"label\", \"value\", \"max\", \"unit\"], \"Declared numeric progress snapshot\", \"tone\"),\n steps: define([\"items\"], \"Ordered declared steps\"),\n metric: define([\"label\", \"value\", \"unit\", \"decimals\"], \"Numeric result with units\", \"tone\"),\n input: define([\"label\", \"bind\"], \"String or numeric state input\"),\n slider: define([\"label\", \"bind\"], \"Numeric state control\"),\n select: define([\"label\", \"bind\"], \"Selection from declared state options\"),\n toggle: define([\"label\", \"bind\"], \"Boolean state control\"),\n button: define([\"text\", \"action\"], \"Declared Agent follow-up\", \"icon\"),\n table: define([\"rows\", \"columns\"], \"Exact supplied or derived values\"),\n chart: define([\"kind\", \"rows\", \"labelKey\", \"valueKey\", \"label\", \"unit\", \"xScale\"], \"Bar/line comparison with accessible data\"),\n sandbox: define([\"title\", \"html\", \"css\", \"script\", \"fallback\", \"height\", \"stateKeys\", \"dataKeys\", \"derivedKeys\"], \"Freeform isolated HTML/SVG/CSS/JavaScript visualization\")\n });\n function componentFields(type) {\n const c = Object.hasOwn(COMPONENTS, type) ? COMPONENTS[type] : void 0;\n return c && [\"id\", \"type\", ...c.fields, ...c.presentation ? [\"icon\"] : [], ...c.presentation === \"tone\" ? [\"tone\"] : []];\n }\n\n // lib/author-expression.js\n function compileExpression(source) {\n const fail2 = () => {\n throw new Error(\"DSH UI: invalid formula; use state.x, data.x, derived.x, item.x, numbers, strings, + - * /, comparisons, ?: and min/max/round/abs\");\n };\n if (typeof source !== \"string\" || !source.trim() || source.length > 2048) fail2();\n const tokens = [];\n let offset = 0;\n const token2 = /\\s*(?:(\\d+(?:\\.\\d*)?|\\.\\d+)([eE][+-]?\\d+)?|(\"(?:[^\"\\\\]|\\\\.)*\")|([a-zA-Z][a-zA-Z0-9_]*(?:\\.[a-zA-Z][a-zA-Z0-9_]*)?)|(===|!==|<=|>=|==|!=|[+*/()?,:<>-]))/y;\n while (offset < source.length) {\n if (!source.slice(offset).trim()) break;\n token2.lastIndex = offset;\n const m = token2.exec(source);\n if (!m) fail2();\n offset = token2.lastIndex;\n tokens.push(m[1] ? { number: Number(m[1] + (m[2] ?? \"\")) } : m[3] ? { string: JSON.parse(m[3]) } : m[4] ?? m[5]);\n if (tokens.length > 256) fail2();\n }\n let pos = 0;\n const operators = { \"==\": [\"eq\", 1], \"===\": [\"eq\", 1], \"!=\": [\"neq\", 1], \"!==\": [\"neq\", 1], \"<\": [\"lt\", 2], \"<=\": [\"lte\", 2], \">\": [\"gt\", 2], \">=\": [\"gte\", 2], \"+\": [\"add\", 3], \"-\": [\"sub\", 3], \"*\": [\"mul\", 4], \"/\": [\"div\", 4] };\n const take = (t) => {\n if (tokens[pos++] !== t) fail2();\n };\n function parse(min = 0, depth = 0) {\n if (depth > 12) fail2();\n const t = tokens[pos++];\n let out;\n if (t && typeof t === \"object\") out = { const: Object.hasOwn(t, \"number\") ? t.number : t.string };\n else if (t === \"true\" || t === \"false\") out = { const: t === \"true\" };\n else if (t === \"(\") {\n out = parse(0, depth + 1);\n take(\")\");\n } else if (t === \"-\" || t === \"+\") {\n const arg = parse(5, depth + 1);\n out = t === \"-\" ? { op: \"sub\", args: [{ const: 0 }, arg] } : arg;\n } else if (typeof t === \"string\" && /^(state|data|derived|item)\\.[a-zA-Z][a-zA-Z0-9_]*$/.test(t)) out = { ref: t };\n else if ([\"min\", \"max\", \"round\", \"abs\"].includes(t)) {\n take(\"(\");\n const args = [parse(0, depth + 1)];\n while (tokens[pos] === \",\") {\n pos++;\n args.push(parse(0, depth + 1));\n if (args.length > 8) fail2();\n }\n take(\")\");\n out = { op: t, args };\n } else fail2();\n while (typeof tokens[pos] === \"string\" && Object.hasOwn(operators, tokens[pos]) && operators[tokens[pos]][1] >= min) {\n const [op, precedence] = operators[tokens[pos++]];\n out = { op, args: [out, parse(precedence + 1, depth + 1)] };\n }\n if (min === 0 && tokens[pos] === \"?\") {\n pos++;\n const yes = parse(0, depth + 1);\n take(\":\");\n out = { op: \"if\", args: [out, yes, parse(0, depth + 1)] };\n }\n return out;\n }\n const result = parse();\n if (pos !== tokens.length) fail2();\n return result;\n }\n function expandExpressions(doc2) {\n const expression = (e) => {\n if (!e || typeof e !== \"object\" || Array.isArray(e)) return e;\n if (Object.hasOwn(e, \"expr\")) {\n if (Object.keys(e).length !== 1) throw new Error(\"DSH UI: expr must be the only formula field\");\n return compileExpression(e.expr);\n }\n if (Array.isArray(e.args)) e.args = e.args.map(expression);\n if (e.map?.fields) for (const k of Object.keys(e.map.fields)) e.map.fields[k] = expression(e.map.fields[k]);\n return e;\n };\n if (doc2.derived && typeof doc2.derived === \"object\") for (const k of Object.keys(doc2.derived)) doc2.derived[k] = expression(doc2.derived[k]);\n const walk = (nodes) => {\n if (!Array.isArray(nodes)) return;\n for (const node2 of nodes) {\n if (!node2 || typeof node2 !== \"object\") continue;\n for (const field of [\"text\", \"value\", \"max\", \"rows\"]) if (Object.hasOwn(node2, field)) node2[field] = expression(node2[field]);\n walk(node2.children);\n }\n };\n walk(doc2.nodes);\n return doc2;\n }\n\n // lib/protocol.js\n var LIMITS = Object.freeze({ bytes: 131072, nodes: 160, depth: 12, rows: 500, expressions: 128, steps: 4096 });\n var own = (o, k) => Object.hasOwn(o, k);\n var fail = (message) => {\n throw new Error(`DSH UI: ${message}`);\n };\n var object = (x) => x !== null && typeof x === \"object\" && !Array.isArray(x);\n var key = (x) => typeof x === \"string\" && /^[a-zA-Z][a-zA-Z0-9_]{0,63}$/.test(x) && ![\"constructor\", \"prototype\", \"__proto__\"].includes(x);\n var finite = (x) => typeof x === \"number\" && Number.isFinite(x);\n var scalar = (x) => typeof x === \"string\" || typeof x === \"boolean\" || finite(x);\n var text = (x, at) => {\n if (typeof x !== \"string\" || x.length > 8192) fail(`${at} must be a string of at most 8192 characters`);\n };\n function fields(value, allowed, at) {\n if (!object(value)) fail(`${at} must be an object`);\n for (const k of Object.keys(value)) if (!allowed.includes(k)) {\n const hint = at === \"document\" ? \" Controls and visual components belong in nodes as registered typed nodes, not extra document fields.\" : \"\";\n fail(`unknown field ${at}.${k}; allowed fields at ${at}: ${allowed.join(\", \")}.${hint}`);\n }\n }\n function safeJson(value, depth = 0, source = false) {\n if (depth > 48) fail(\"JSON nesting is too deep\");\n if (value === null || typeof value === \"boolean\") return;\n if (typeof value === \"string\") {\n if (source) {\n if (value.length > SANDBOX_LIMITS.source) fail(\"canvas source exceeds 48 Ki characters\");\n return;\n }\n return text(value, \"value\");\n }\n if (typeof value === \"number\") {\n if (!finite(value)) fail(\"numbers must be finite\");\n return;\n }\n if (typeof value !== \"object\") fail(\"only JSON values are accepted\");\n if (Array.isArray(value) && value.length > LIMITS.rows) fail(\"too many array items\");\n for (const k of Object.keys(value)) {\n if ([\"__proto__\", \"prototype\", \"constructor\"].includes(k)) fail(\"unsafe property name\");\n safeJson(value[k], depth + 1, value.type === \"sandbox\" && [\"html\", \"css\", \"script\"].includes(k));\n }\n }\n function initialState(doc2) {\n return Object.fromEntries(Object.entries(doc2.state).map(([k, spec]) => [k, spec.default]));\n }\n function validateState(doc2, state2) {\n if (!object(state2) || Object.keys(state2).length !== Object.keys(doc2.state).length) fail(\"state keys do not match the document\");\n const result = {};\n for (const [k, s] of Object.entries(doc2.state)) {\n if (!own(state2, k)) fail(`missing state.${k}`);\n const v = state2[k];\n if (s.type === \"number\") {\n if (!finite(v) || v < s.min || v > s.max) fail(`state.${k} is outside its numeric range`);\n const offset = (v - s.min) / s.step;\n if (Math.abs(offset - Math.round(offset)) > 1e-6) fail(`state.${k} does not match its step`);\n } else if (s.type === \"string\") text(v, `state.${k}`);\n else if (typeof v !== \"boolean\") fail(`state.${k} must be boolean`);\n if (s.options && !s.options.includes(v)) fail(`invalid option for state.${k}`);\n result[k] = v;\n }\n return result;\n }\n var ARITY = { add: [1, 8], sub: [2, 2], mul: [1, 8], div: [2, 2], min: [1, 8], max: [1, 8], round: [1, 1], abs: [1, 1], eq: [2, 2], neq: [2, 2], lt: [2, 2], lte: [2, 2], gt: [2, 2], gte: [2, 2], if: [3, 3] };\n function reference(doc2, ref) {\n if (typeof ref !== \"string\" || !/^(state|data|derived)\\.[a-zA-Z][a-zA-Z0-9_]{0,63}$/.test(ref)) fail(\"invalid reference\");\n const [group, k] = ref.split(\".\");\n if (!own(doc2[group], k)) fail(`unknown reference ${ref}`);\n return [group, k];\n }\n function evaluate(doc2, state2) {\n const derived = {}, active = /* @__PURE__ */ new Set();\n let steps = 0, generatedRows = 0;\n const budget = () => {\n if (++steps > LIMITS.steps) fail(\"calculation budget exceeded\");\n };\n const resolve = (ref) => {\n budget();\n const [group, k] = reference(doc2, ref);\n if (group !== \"derived\") return (group === \"state\" ? state2 : doc2.data)[k];\n if (own(derived, k)) return derived[k];\n if (active.has(k)) fail(`cyclic calculation ${k}`);\n active.add(k);\n const result2 = expression(doc2.derived[k]);\n active.delete(k);\n derived[k] = result2;\n return result2;\n };\n const expression = (e, item) => {\n budget();\n if (own(e, \"map\")) {\n const rows = resolve(e.map.source);\n if ((generatedRows += rows.length) > LIMITS.rows) fail(\"generated row limit exceeded\");\n return rows.map((row) => Object.fromEntries(Object.entries(e.map.fields).map(([k, value]) => {\n const out2 = expression(value, row);\n if (!scalar(out2)) fail(\"generated cells must be scalar\");\n return [k, out2];\n })));\n }\n if (own(e, \"const\")) return e.const;\n if (own(e, \"ref\")) {\n if (e.ref.startsWith(\"item.\")) {\n const k = e.ref.slice(5);\n if (!item || !own(item, k) || !scalar(item[k])) fail(\"invalid item reference\");\n return item[k];\n }\n return resolve(e.ref);\n }\n if (e.op === \"if\") {\n const condition = expression(e.args[0], item);\n if (typeof condition !== \"boolean\") fail(\"if condition must be boolean\");\n return expression(e.args[condition ? 1 : 2], item);\n }\n const a = e.args.map((arg) => expression(arg, item));\n if (e.op === \"eq\") return a[0] === a[1];\n if (e.op === \"neq\") return a[0] !== a[1];\n if (!a.every(finite)) fail(`operator ${e.op} requires finite numbers`);\n let out;\n switch (e.op) {\n case \"add\":\n out = a.reduce((x, y) => x + y, 0);\n break;\n case \"sub\":\n out = a[0] - a[1];\n break;\n case \"mul\":\n out = a.reduce((x, y) => x * y, 1);\n break;\n case \"div\":\n if (a[1] === 0) fail(\"division by zero\");\n out = a[0] / a[1];\n break;\n case \"min\":\n out = Math.min(...a);\n break;\n case \"max\":\n out = Math.max(...a);\n break;\n case \"round\":\n out = Math.round(a[0]);\n break;\n case \"abs\":\n out = Math.abs(a[0]);\n break;\n case \"lt\":\n return a[0] < a[1];\n case \"lte\":\n return a[0] <= a[1];\n case \"gt\":\n return a[0] > a[1];\n case \"gte\":\n return a[0] >= a[1];\n default:\n fail(`unsupported operator ${e.op}`);\n }\n if (!finite(out)) fail(\"non-finite calculation result\");\n return out;\n };\n for (const k of Object.keys(doc2.derived)) resolve(`derived.${k}`);\n if (new TextEncoder().encode(JSON.stringify(derived)).length > LIMITS.bytes) fail(\"derived output exceeds 128 KiB\");\n const result = { derived, resolveValue: (value) => {\n if (!object(value)) return value;\n return expression(value);\n } };\n checkViewValues(doc2, result);\n return result;\n }\n function checkViewValues(doc2, evaluated) {\n function walk(list) {\n for (const n of list) {\n if (n.type === \"table\" || n.type === \"chart\") {\n const rows = evaluated.resolveValue(n.rows);\n if (!Array.isArray(rows) || rows.length > LIMITS.rows || !rows.every(object)) fail(\"table/chart rows must reference an array of objects\");\n if (n.type === \"chart\" && rows.some((r) => !own(r, n.labelKey) || !scalar(r[n.labelKey]) || !own(r, n.valueKey) || !finite(r[n.valueKey]))) fail(\"chart requires labels and finite numeric values\");\n if (n.type === \"chart\" && n.xScale === \"linear\" && rows.some((r) => !finite(r[n.labelKey]))) fail(\"linear chart requires numeric x values\");\n }\n if (n.type === \"metric\" && !finite(evaluated.resolveValue(n.value))) fail(\"metric requires a numeric value\");\n if (n.type === \"progress\") {\n const v = evaluated.resolveValue(n.value), max = evaluated.resolveValue(n.max);\n if (!finite(v) || !finite(max) || max <= 0 || v < 0 || v > max) fail(\"progress requires 0 <= value <= positive max\");\n }\n if (n.children) walk(n.children);\n }\n }\n walk(doc2.nodes);\n }\n function validateDocument(input) {\n safeJson(input);\n const json = JSON.stringify(input);\n if (new TextEncoder().encode(json).length > LIMITS.bytes) fail(\"document exceeds 128 KiB\");\n const doc2 = expandExpressions(JSON.parse(json));\n fields(doc2, [\"version\", \"title\", \"fallback\", \"state\", \"data\", \"derived\", \"nodes\", \"actions\", \"presentation\"], \"document\");\n if (doc2.presentation !== void 0) {\n fields(doc2.presentation, [\"layout\", \"accent\", \"density\"], \"presentation\");\n for (const [k, allowed] of Object.entries({ layout: UI_LAYOUTS, accent: UI_ACCENTS, density: [\"comfortable\", \"compact\"] }))\n if (doc2.presentation[k] !== void 0 && !allowed.includes(doc2.presentation[k])) fail(`unknown presentation ${k}`);\n }\n if (doc2.version !== 1) fail(\"unsupported protocol version\");\n text(doc2.title, \"title\");\n text(doc2.fallback, \"fallback\");\n if (!doc2.title.trim() || !doc2.fallback.trim()) fail(\"title and fallback cannot be empty\");\n for (const group of [\"state\", \"data\", \"derived\", \"actions\"]) {\n doc2[group] ??= {};\n if (!object(doc2[group]) || Object.keys(doc2[group]).length > 64) fail(`invalid ${group}`);\n for (const k of Object.keys(doc2[group])) if (!key(k)) fail(`invalid key in ${group}`);\n }\n for (const [k, s] of Object.entries(doc2.state)) {\n fields(s, [\"type\", \"default\", \"min\", \"max\", \"step\", \"options\"], `state.${k}`);\n if (![\"number\", \"string\", \"boolean\"].includes(s.type)) fail(`unsupported state type ${k}`);\n if (s.type === \"number\") {\n if (![s.min, s.max, s.step].every(finite) || s.min >= s.max || s.step <= 0) fail(`invalid numeric limits for ${k}`);\n if ((s.max - s.min) / s.step > 1e9) fail(`numeric step too small for ${k}`);\n } else if ([\"min\", \"max\", \"step\"].some((p) => own(s, p))) fail(`numeric limits on non-number ${k}`);\n if (s.options && (!Array.isArray(s.options) || !s.options.length || s.options.length > 64 || !s.options.every(scalar))) fail(`invalid options for ${k}`);\n }\n const defaults = validateState(doc2, initialState(doc2));\n let expressions = 0;\n for (const [k, spec] of Object.entries(doc2.state))\n for (const option of spec.options ?? []) validateState(doc2, { ...defaults, [k]: option });\n function validateExpression(e, depth = 0, itemRows) {\n if (++expressions > LIMITS.expressions || depth > LIMITS.depth) fail(\"expression limit exceeded\");\n if (!object(e)) fail(\"expression must be an object\");\n if (own(e, \"map\")) {\n if (depth !== 0) fail(\"map is only allowed at the root of a derived value\");\n fields(e, [\"map\"], \"expression\");\n fields(e.map, [\"source\", \"fields\"], \"map\");\n if (typeof e.map.source !== \"string\" || !e.map.source.startsWith(\"data.\")) fail('map source must reference static data using a string such as \"data.samples\"; put object rows in document.data.samples, not inline in map.source');\n const [, k] = reference(doc2, e.map.source), rows = doc2.data[k];\n if (!Array.isArray(rows) || rows.length > LIMITS.rows || !rows.every(object)) fail(\"map source must be object rows\");\n if (!object(e.map.fields) || !Object.keys(e.map.fields).length || Object.keys(e.map.fields).length > 16) fail(\"map requires 1..16 fields\");\n for (const [k2, value2] of Object.entries(e.map.fields)) {\n if (!key(k2)) fail(`invalid map field ${JSON.stringify(k2).slice(0, 100)}; use an ASCII identifier matching [a-zA-Z][a-zA-Z0-9_]{0,63}, such as powerW. Put Chinese display text in column/chart labels, not internal field keys`);\n validateExpression(value2, depth + 1, rows);\n }\n return;\n }\n if (own(e, \"const\")) {\n fields(e, [\"const\"], \"expression\");\n if (!scalar(e.const)) fail(\"constant must be scalar\");\n return;\n }\n if (own(e, \"ref\")) {\n fields(e, [\"ref\"], \"expression\");\n if (typeof e.ref === \"string\" && e.ref.startsWith(\"item.\")) {\n const k = e.ref.slice(5);\n if (!itemRows || !key(k) || itemRows.some((row) => !own(row, k) || !scalar(row[k]))) fail(\"invalid or unscoped item reference\");\n } else reference(doc2, e.ref);\n return;\n }\n fields(e, [\"op\", \"args\"], \"expression\");\n const arity = ARITY[e.op];\n if (!arity || !Array.isArray(e.args) || e.args.length < arity[0] || e.args.length > arity[1]) fail(\"invalid operator or argument count\");\n e.args.forEach((a) => validateExpression(a, depth + 1, itemRows));\n }\n Object.values(doc2.derived).forEach((e) => validateExpression(e));\n const visited = /* @__PURE__ */ new Set(), active = /* @__PURE__ */ new Set();\n function visit(k) {\n if (active.has(k)) fail(`cyclic calculation ${k}`);\n if (visited.has(k)) return;\n active.add(k);\n function walk(e) {\n if (e.ref?.startsWith(\"derived.\")) visit(e.ref.split(\".\")[1]);\n for (const a of e.args ?? []) walk(a);\n for (const value2 of Object.values(e.map?.fields ?? {})) walk(value2);\n }\n walk(doc2.derived[k]);\n active.delete(k);\n visited.add(k);\n }\n Object.keys(doc2.derived).forEach(visit);\n for (const [k, a] of Object.entries(doc2.actions)) {\n fields(a, [\"type\", \"intent\", \"stateKeys\"], `actions.${k}`);\n if (a.type !== \"agent.followup\") fail(\"only agent.followup actions are supported\");\n text(a.intent, \"action intent\");\n if (!a.intent.trim() || !Array.isArray(a.stateKeys) || a.stateKeys.length > 64 || new Set(a.stateKeys).size !== a.stateKeys.length || a.stateKeys.some((s) => !own(doc2.state, s))) fail(\"invalid action stateKeys\");\n }\n const ids = /* @__PURE__ */ new Set();\n let count2 = 0;\n const value = (v, at) => {\n if (object(v)) {\n if (own(v, \"map\")) fail(\"map belongs in derived\");\n validateExpression(v);\n } else if (!scalar(v)) fail(`invalid value at ${at}`);\n };\n function nodes(list, depth = 0) {\n if (!Array.isArray(list) || depth > LIMITS.depth) fail(\"invalid node children or depth\");\n for (const n of list) {\n if (++count2 > LIMITS.nodes) fail(\"too many nodes\");\n if (!object(n) || !key(n.id) || ids.has(n.id)) fail(\"node ids must be unique identifiers\");\n ids.add(n.id);\n if (!Object.hasOwn(COMPONENTS, n.type)) fail(\"unsupported component \" + n.type);\n switch (n.type) {\n case \"sandbox\": {\n text(n.title, \"canvas title\");\n text(n.fallback, \"canvas fallback\");\n if (!n.title.trim() || !n.fallback.trim() || typeof n.html !== \"string\" || !n.html.trim()) fail(\"canvas requires title, HTML and fallback\");\n for (const field of [\"html\", \"css\", \"script\"]) if (n[field] !== void 0 && (typeof n[field] !== \"string\" || n[field].length > SANDBOX_LIMITS.source)) fail(\"canvas source exceeds 48 Ki characters\");\n if (n.height !== void 0 && (!Number.isInteger(n.height) || n.height < 160 || n.height > SANDBOX_LIMITS.height)) fail(\"canvas height must be 160..1000\");\n for (const [field, group] of [[\"stateKeys\", \"state\"], [\"dataKeys\", \"data\"], [\"derivedKeys\", \"derived\"]]) if (n[field] !== void 0 && (!Array.isArray(n[field]) || n[field].length > 64 || new Set(n[field]).size !== n[field].length || n[field].some((k) => !own(doc2[group], k)))) fail(\"invalid canvas \" + field);\n break;\n }\n case \"row\":\n case \"column\":\n break;\n case \"card\":\n if (n.title !== void 0) text(n.title, \"card title\");\n if (n.surface !== void 0 && !UI_SURFACES.includes(n.surface)) fail(\"unknown card surface\");\n break;\n case \"details\":\n text(n.title, \"details title\");\n if (n.open !== void 0 && typeof n.open !== \"boolean\") fail(\"details open must be boolean\");\n break;\n case \"badge\":\n value(n.text, \"badge text\");\n break;\n case \"progress\":\n text(n.label, \"progress label\");\n value(n.value, \"progress value\");\n value(n.max, \"progress max\");\n if (n.unit !== void 0) text(n.unit, \"progress unit\");\n break;\n case \"steps\": {\n if (!Array.isArray(n.items) || !n.items.length || n.items.length > 12) fail(\"steps require 1..12 items\");\n for (const item of n.items) {\n fields(item, [\"title\", \"description\", \"status\"], \"step\");\n text(item.title, \"step title\");\n if (item.description !== void 0) text(item.description, \"step description\");\n if (![\"pending\", \"active\", \"done\", \"error\"].includes(item.status)) fail(\"unknown step status\");\n }\n break;\n }\n case \"heading\":\n case \"text\":\n value(n.text, \"text\");\n break;\n case \"metric\":\n text(n.label, \"metric label\");\n value(n.value, \"metric value\");\n if (n.unit !== void 0) text(n.unit, \"unit\");\n if (n.decimals !== void 0 && (!Number.isInteger(n.decimals) || n.decimals < 0 || n.decimals > 6)) fail(\"invalid decimals\");\n break;\n case \"input\":\n case \"slider\":\n case \"select\":\n case \"toggle\": {\n text(n.label, \"control label\");\n if (typeof n.bind !== \"string\" || !n.bind.startsWith(\"state.\")) fail(\"controls must bind state\");\n const [, k] = reference(doc2, n.bind), spec = doc2.state[k];\n if (n.type === \"slider\" && spec.type !== \"number\") fail(\"slider requires numeric state\");\n if (n.type === \"toggle\" && spec.type !== \"boolean\") fail(\"toggle requires boolean state\");\n if (n.type === \"input\" && spec.type === \"boolean\") fail(\"use toggle for boolean state\");\n if (n.type === \"select\" && !spec.options) fail(\"select requires options\");\n break;\n }\n case \"button\":\n text(n.text, \"button text\");\n if (!own(doc2.actions, n.action)) fail(\"unknown action\");\n break;\n case \"table\": {\n if (Array.isArray(n.rows)) fail('table.rows must use {ref:\"data.rows\"}; move the array into document.data.rows, not inline in the table');\n value(n.rows, \"table rows\");\n if (!Array.isArray(n.columns) || !n.columns.length || n.columns.length > 16) fail(\"invalid table columns\");\n const cols = /* @__PURE__ */ new Set();\n for (const c of n.columns) {\n fields(c, [\"key\", \"label\", \"unit\"], \"column\");\n if (!key(c.key) || cols.has(c.key)) fail(\"invalid or duplicate column key\");\n cols.add(c.key);\n text(c.label, \"column label\");\n if (c.unit !== void 0) text(c.unit, \"column unit\");\n }\n break;\n }\n case \"chart\":\n value(n.rows, \"chart rows\");\n if (![\"bar\", \"line\"].includes(n.kind) || !key(n.labelKey) || !key(n.valueKey)) fail(\"invalid chart\");\n text(n.label, \"chart label\");\n if (n.unit !== void 0) text(n.unit, \"chart unit\");\n if (n.xScale !== void 0 && (![\"category\", \"linear\"].includes(n.xScale) || n.xScale === \"linear\" && n.kind !== \"line\")) fail(\"invalid xScale\");\n break;\n default:\n fail(`unsupported component ${n.type}`);\n }\n if ([\"heading\", \"text\", \"card\", \"metric\", \"button\", \"badge\", \"progress\", \"details\"].includes(n.type)) {\n if (n.icon !== void 0 && !UI_ICONS.includes(n.icon)) fail(\"unknown UI icon\");\n if (n.type !== \"button\") {\n if (n.tone !== void 0 && !UI_TONES.includes(n.tone)) fail(\"unknown UI tone\");\n }\n }\n fields(n, componentFields(n.type), `node.${n.id}`);\n if ([\"row\", \"column\", \"card\", \"details\"].includes(n.type)) nodes(n.children, depth + 1);\n }\n }\n nodes(doc2.nodes);\n if (!count2) fail(\"at least one node is required\");\n evaluate(doc2, defaults);\n return doc2;\n }\n\n // src/export-runtime.js\n var payload = JSON.parse(document.getElementById(\"dsh-ui-export\").textContent);\n var doc = validateDocument(payload.document);\n var node = doc.nodes.flatMap(function flatten(n) {\n return [n, ...(n.children ?? []).flatMap(flatten)];\n }).find((n) => n.id === payload.nodeId && n.type === \"sandbox\");\n if (!node) throw Error(\"\\u5BFC\\u51FA\\u7684\\u753B\\u5E03\\u4E0D\\u5B58\\u5728\");\n var state = validateState(doc, payload.state);\n var connected = false;\n var start = Date.now();\n var count = 0;\n var token = () => Array.from(crypto.getRandomValues(new Uint8Array(16)), (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n var channel = token();\n var nonce = payload.nonce;\n var frame = document.createElement(\"iframe\");\n frame.title = node.title;\n frame.sandbox = \"allow-scripts\";\n frame.referrerPolicy = \"no-referrer\";\n frame.style.cssText = \"display:block;width:100%;height:\" + (node.height ?? 420) + \"px;border:0\";\n var status = document.getElementById(\"status\");\n var publish = () => {\n try {\n frame.contentWindow.postMessage({ dshCanvas: 1, channel, type: \"snapshot\", payload: { ...canvasSnapshot(node, state, doc.data, evaluate(doc, state).derived, false, payload.theme), visible: !document.hidden } }, \"*\");\n } catch (e) {\n status.textContent = \"\\u8BA1\\u7B97\\u5931\\u8D25\\uFF1A\" + e.message;\n }\n };\n addEventListener(\"message\", (event) => {\n const m = event.data;\n if (event.source !== frame.contentWindow || m?.channel !== channel || m.dshCanvas !== 1) return;\n if (Date.now() - start > 1e3) {\n start = Date.now();\n count = 0;\n }\n if (++count > SANDBOX_LIMITS.rate) return;\n if (m.type === \"ready\") {\n connected = true;\n publish();\n } else if (m.type === \"patch\" && connected) {\n try {\n const next = validateState(doc, { ...state, ...canvasPatch(m.payload, node) });\n evaluate(doc, next);\n state = next;\n status.textContent = \"\\u672C\\u5730\\u53C2\\u6570\\u5DF2\\u66F4\\u65B0\\uFF1B\\u5237\\u65B0\\u6587\\u4EF6\\u4F1A\\u56DE\\u5230\\u5BFC\\u51FA\\u65F6\\u7684\\u53C2\\u6570\\u3002\";\n } catch (e) {\n status.textContent = \"\\u53C2\\u6570\\u65E0\\u6548\\uFF1A\" + e.message;\n }\n publish();\n } else if (m.type === \"height\" && Number.isFinite(m.payload)) frame.style.height = Math.max(160, Math.min(1e3, Math.ceil(m.payload))) + \"px\";\n else if (m.type === \"error\") status.textContent = \"\\u753B\\u5E03\\u8FD0\\u884C\\u5F02\\u5E38\\uFF1A\" + String(m.payload).slice(0, 1e3);\n });\n document.addEventListener(\"visibilitychange\", () => {\n if (connected) publish();\n });\n document.getElementById(\"surface\").append(frame);\n frame.srcdoc = sandboxDocument(node, channel, nonce);\n})();\n";
|
|
2
|
+
export const EXPORT_RUNTIME="(() => {\n // lib/presentation.js\n var UI_ICONS = Object.freeze([\"sparkles\", \"sliders\", \"layers\", \"chart\", \"gauge\", \"cpu\", \"database\", \"info\", \"check\", \"warning\", \"arrow\", \"target\"]);\n var UI_TONES = Object.freeze([\"neutral\", \"accent\", \"success\", \"warning\", \"danger\"]);\n var UI_LAYOUTS = Object.freeze([\"dashboard\", \"workbench\", \"comparison\", \"guide\", \"status\", \"report\"]);\n var UI_ACCENTS = Object.freeze([\"blue\", \"teal\", \"violet\", \"amber\", \"rose\"]);\n var UI_SURFACES = Object.freeze([\"outline\", \"soft\", \"featured\"]);\n\n // lib/visual-audit.js\n function measureVisual(root, nodeId = \"\") {\n const issues = [], seen = /* @__PURE__ */ new Set(), coverage = { nodeId, elements: 0, truncated: false, contrastChecked: 0, contrastUncovered: 0 };\n const add = (code, value, el2) => {\n const id = el2?.closest(\"[data-ui-node]\")?.getAttribute(\"data-ui-node\") ?? nodeId;\n const k = id + \":\" + code + \":\" + Math.round(value ?? 0);\n if (!seen.has(k) && issues.length < 40) {\n seen.add(k);\n issues.push({ code, nodeId: id, ...value === void 0 ? {} : { value: Number(value.toFixed(2)) } });\n }\n };\n const color = (s) => {\n let m = s.match(/^rgba?\\(([^)]+)\\)/);\n if (m) {\n const a = m[1].split(/[\\s,\\/]+/).map(Number);\n return [a[0] / 255, a[1] / 255, a[2] / 255, a[3] ?? 1];\n }\n m = s.match(/^color\\(srgb ([^)]+)\\)/);\n if (m) {\n const a = m[1].split(/[\\s\\/]+/).map(Number);\n return [a[0], a[1], a[2], a[3] ?? 1];\n }\n return null;\n };\n const luminance = (c) => c.slice(0, 3).map((v) => v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4).reduce((s, v, i) => s + v * [0.2126, 0.7152, 0.0722][i], 0);\n const widthRoot = root.id === \"canvas\" ? document.documentElement : root;\n if (widthRoot.scrollWidth > widthRoot.clientWidth + 2) add(\"overflow\", widthRoot.scrollWidth - widthRoot.clientWidth);\n const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);\n let el;\n while (el = walker.nextNode()) {\n if (coverage.elements >= 2e3) {\n coverage.truncated = true;\n break;\n }\n coverage.elements++;\n if (el.matches(\"[data-canvas-phase=error]\")) add(\"runtimeError\", void 0, el);\n const css = getComputedStyle(el), box = el.getBoundingClientRect();\n if (!box.width || !box.height || css.visibility === \"hidden\" || css.display === \"none\") continue;\n const text2 = [...el.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim());\n if (text2) {\n let size = parseFloat(css.fontSize);\n if (el instanceof SVGElement && el.getScreenCTM) {\n const m = el.getScreenCTM();\n if (m) size *= Math.hypot(m.c, m.d);\n }\n if (size < 11.9) add(\"smallText\", size, el);\n const fg = color(css.color);\n let ancestor = el, bg = null, depth = 0;\n while (ancestor && depth++ < 64) {\n const style = getComputedStyle(ancestor);\n if (style.backgroundImage !== \"none\") break;\n const c = color(style.backgroundColor);\n if (c?.[3] === 1) {\n bg = c;\n break;\n }\n if (c?.[3] > 0) break;\n ancestor = ancestor.parentElement;\n }\n if (!(el instanceof SVGElement) && fg?.[3] === 1 && bg) {\n coverage.contrastChecked++;\n const a = luminance(fg), b = luminance(bg), ratio = (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);\n if (ratio < (size >= 24 || size >= 18.66 && parseFloat(css.fontWeight) >= 700 ? 3 : 4.5)) add(\"contrast\", ratio, el);\n } else coverage.contrastUncovered++;\n }\n if (el.matches(\"input:not([type=hidden]),select,textarea,button,[role=button]\") && !el.getAttribute(\"aria-label\") && !el.getAttribute(\"aria-labelledby\") && !el.labels?.length && !el.textContent.trim() && !el.getAttribute(\"title\")) add(\"missingLabel\", void 0, el);\n }\n Object.defineProperty(issues, \"coverage\", { value: coverage });\n return issues;\n }\n\n // lib/format-number.js\n function formatNumber(value, decimals = 2) {\n if (typeof value !== \"number\" || !Number.isFinite(value)) throw new Error(\"DSH UI: formatNumber requires a finite number\");\n if (!Number.isInteger(decimals) || decimals < 0 || decimals > 6) throw new Error(\"DSH UI: formatNumber decimals must be an integer from 0 to 6\");\n return value.toLocaleString(\"en-US\", { minimumFractionDigits: decimals, maximumFractionDigits: decimals });\n }\n\n // lib/canvas-system.js\n var CANVAS_SYSTEM_CSS = `\n:root{--canvas-radius:20px;--canvas-gap:16px;--canvas-positive:#167456;--canvas-negative:#b13b3b;--canvas-warning:#875b15}\n:root[data-canvas-scheme=dark]{--canvas-positive:#78d9b4;--canvas-negative:#ffaaaa;--canvas-warning:#edc87e}\n#canvas{display:flow-root}html,body{margin:0;min-width:0;background:var(--canvas-bg,#fff);color:var(--canvas-ink,#182537);font:14px/1.6 system-ui,\"Microsoft YaHei\",sans-serif}*{box-sizing:border-box}svg{max-width:100%;height:auto}button,input,select,textarea{font:inherit;color:inherit}button,input,select,textarea{border:1px solid var(--canvas-line,#ced4df);border-radius:8px;background:var(--canvas-bg,#fff);padding:8px 12px;max-width:100%}button{cursor:pointer}button:disabled{cursor:default;opacity:.55}input[type=range]{padding:0;accent-color:var(--canvas-accent,#3e60d3)}input[type=checkbox],input[type=radio]{accent-color:var(--canvas-accent,#3e60d3)}button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--canvas-accent,#3e60d3);outline-offset:3px}\n.ui-stack{display:grid;gap:var(--canvas-gap)}.ui-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr));gap:var(--canvas-gap)}.ui-surface{padding:20px;border:1px solid var(--canvas-line,#ced4df);border-radius:var(--canvas-radius);background:var(--canvas-soft,#f3f5f8)}.ui-title{margin:0;font-size:clamp(20px,3vw,28px);line-height:1.3;letter-spacing:-.02em}.ui-caption{color:var(--canvas-muted,#526176);margin:8px 0;font-size:14px}.ui-value{font-size:clamp(24px,4vw,36px);font-variant-numeric:tabular-nums;line-height:1.2}.ui-controls{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.ui-field{display:grid;gap:6px;min-width:0}.ui-legend{display:flex;gap:16px;flex-wrap:wrap;font-size:13px}.ui-status{min-height:1.6em}.ui-error{color:var(--canvas-negative)}.ui-primary{background:var(--canvas-accent,#3e60d3);color:var(--canvas-accent-ink,#fff);border-color:transparent}\nsvg .ui-node{fill:var(--canvas-soft,#f3f5f8);stroke:var(--canvas-line,#ced4df);stroke-width:1}svg .ui-edge{fill:none;stroke:var(--canvas-muted,#526176);stroke-width:1.5}svg .ui-label{fill:var(--canvas-ink,#182537);font:500 14px system-ui,sans-serif}svg .ui-note{fill:var(--canvas-muted,#526176);font:400 13px system-ui,sans-serif}svg .ui-leader{fill:none;stroke:var(--canvas-muted,#526176);stroke-dasharray:3 4}svg .ui-accent{fill:color-mix(in srgb,var(--canvas-accent,#3e60d3) 12%,var(--canvas-bg,#fff));stroke:var(--canvas-accent,#3e60d3)}\n.ui-flow{display:flex;align-items:center;justify-content:center;gap:16px;padding:24px 8px;flex-wrap:wrap}.ui-step{display:grid;gap:6px;min-width:130px;max-width:240px;padding:16px 20px;border:1px solid var(--canvas-line);border-radius:6px;background:var(--canvas-bg)}.ui-step strong{font-size:16px;font-weight:600}.ui-step span{font-size:14px;color:var(--canvas-muted)}.ui-arrow{color:var(--canvas-muted);font-size:22px}.ui-callout{border-inline-start:3px solid var(--canvas-accent);padding:8px 16px;color:var(--canvas-ink)}\n.ui-surface,.ui-step{border-radius:var(--canvas-radius);background:linear-gradient(135deg,color-mix(in srgb,var(--canvas-accent,#3e60d3) 9%,var(--canvas-bg,#fff)),var(--canvas-soft,#f3f5f8))}button,input:not([type=checkbox]):not([type=radio]):not([type=range]),select,textarea{border-radius:14px}.ui-primary{background:linear-gradient(120deg,var(--canvas-accent,#3e60d3),color-mix(in srgb,var(--canvas-accent,#3e60d3) 85%,#152647))}.ui-callout{border-radius:14px;background:linear-gradient(120deg,color-mix(in srgb,var(--canvas-accent,#3e60d3) 7%,var(--canvas-bg,#fff)),var(--canvas-bg,#fff))}\n@media(max-width:480px){.ui-flow{flex-direction:column;padding:16px 8px;gap:12px}.ui-step{width:100%;max-width:none}.ui-arrow{transform:rotate(90deg)}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}}`;\n\n // lib/sandbox.js\n var SANDBOX_LIMITS = Object.freeze({ source: 49152, height: 1e3, message: 16384, rate: 30 });\n function canvasSnapshot(node2, state2, data, derived, disabled, theme) {\n const pick = (keys, values) => Object.fromEntries((keys ?? []).map((k) => [k, values[k]]));\n return { state: pick(node2.stateKeys, state2), data: pick(node2.dataKeys, data), derived: pick(node2.derivedKeys, derived), disabled, theme };\n }\n function canvasPatch(message, node2) {\n if (!message || typeof message !== \"object\" || Array.isArray(message)) throw new Error(\"Invalid canvas patch\");\n const keys = Object.keys(message);\n if (!keys.length || keys.length > 64 || keys.some((k) => !(node2.stateKeys ?? []).includes(k))) throw new Error(\"Canvas state key is not declared\");\n if (JSON.stringify(message).length > SANDBOX_LIMITS.message) throw new Error(\"Canvas patch is too large\");\n return message;\n }\n function sandboxDocument(node2, channel2, nonce2) {\n const json = (value) => JSON.stringify(value).replace(/</g, \"\\\\u003c\").replace(/\\u2028/g, \"\\\\u2028\").replace(/\\u2029/g, \"\\\\u2029\");\n return `<!doctype html><html><head><meta charset=\"utf-8\"><meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; script-src 'nonce-${nonce2}'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; connect-src 'none'; media-src 'none'; object-src 'none'; frame-src 'none'; worker-src 'none'; base-uri 'none'; form-action 'none'\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><style>${CANVAS_SYSTEM_CSS}</style></head><body><main id=\"canvas\"></main><script nonce=\"${nonce2}\">(()=>{\n const channel=${json(channel2)}, source=${json({ html: node2.html, css: node2.css ?? \"\", script: node2.script ?? \"\" })};\n let snapshot={state:{},data:{},derived:{},disabled:true,theme:{}}, started=false;\n const listeners=new Set(), send=(type,payload)=>parent.postMessage({dshCanvas:1,channel,type,payload},'*');\n const report=e=>send('error',String(e?.message??e).slice(0,1000));\n const audit=${measureVisual.toString()};let auditTimer;const scheduleAudit=()=>{clearTimeout(auditTimer);auditTimer=setTimeout(()=>{const issues=audit(document.getElementById('canvas'),${json(node2.id ?? \"\")});send('audit',{issues,coverage:issues.coverage})},400)};\n addEventListener('resize',scheduleAudit);\n addEventListener('error',e=>report(e.error??e.message));addEventListener('unhandledrejection',e=>report(e.reason));\n const animations=new Set(), motion=matchMedia('(prefers-reduced-motion: reduce)');\n let raf=0,last=0,time=0;\n const visible=()=>snapshot.visible!==false&&!document.hidden;\n const canAnimate=()=>visible()&&!snapshot.disabled&&!motion.matches;\n const draw=delta=>{for(const fn of animations)try{fn({time,delta,reducedMotion:motion.matches,visible:visible()})}catch(e){animations.delete(fn);report(e)}};\n const tick=t=>{raf=0;if(!canAnimate()||!animations.size){last=0;draw(0);return}const delta=last?Math.min(.05,(t-last)/1000):0;last=t;time+=delta;draw(delta);if(animations.size)raf=requestAnimationFrame(tick)};\n const refresh=()=>{draw(0);if(canAnimate()&&animations.size){if(!raf){last=0;raf=requestAnimationFrame(tick)}}else{cancelAnimationFrame(raf);raf=0;last=0}};\n motion.addEventListener('change',refresh);document.addEventListener('visibilitychange',refresh);\n const formatNumber=${formatNumber.toString()};\n const api={formatNumber,get snapshot(){return structuredClone(snapshot)},subscribe(fn){if(typeof fn!=='function')throw Error('Listener required');listeners.add(fn);fn(api.snapshot);return()=>listeners.delete(fn)},setState(patch){if(!snapshot.disabled)send('patch',patch)},animate(fn){if(typeof fn!=='function')throw Error('Animation callback required');animations.add(fn);refresh();return()=>{animations.delete(fn);if(!animations.size){cancelAnimationFrame(raf);raf=0;last=0}}}};\n Object.defineProperty(window,'DSHUI',{value:Object.freeze(api),writable:false});\n for(const type of ['click','input','change','keydown','pointerdown'])document.addEventListener(type,e=>{if(snapshot.disabled){e.preventDefault();e.stopImmediatePropagation()}},true);\n addEventListener('message',event=>{\n const m=event.data;if(event.source!==parent||m?.dshCanvas!==1||m.channel!==channel||m.type!=='snapshot')return;\n snapshot=m.payload;for(const [k,v] of Object.entries(snapshot.theme??{}))document.documentElement.style.setProperty('--canvas-'+k,v);\n document.documentElement.style.colorScheme=snapshot.theme?.scheme??'light';document.documentElement.dataset.canvasScheme=snapshot.theme?.scheme??'light';\n const wasStarted=started;\n if(!started){started=true;document.getElementById('canvas').innerHTML=source.html;const style=document.createElement('style');style.textContent=source.css;document.head.append(style);const script=document.createElement('script');script.nonce=${json(nonce2)};script.textContent=source.script;document.body.append(script)}\n if(wasStarted)for(const fn of listeners)try{fn(api.snapshot)}catch(e){report(e)}\n refresh();if(!wasStarted)send('mounted');scheduleAudit();\n });\n let resizePending=false;new ResizeObserver(()=>{if(!started||resizePending)return;resizePending=true;requestAnimationFrame(()=>{resizePending=false;send('height',Math.ceil(document.getElementById('canvas').getBoundingClientRect().height))})}).observe(document.getElementById('canvas'));\n send('ready');\n})();<\\/script></body></html>`;\n }\n\n // lib/components.js\n var define = (fields2, purpose, presentation = \"\") => Object.freeze({ fields: Object.freeze(fields2), purpose, presentation });\n var COMPONENTS = Object.freeze({\n heading: define([\"text\"], \"Section heading\", \"tone\"),\n text: define([\"text\"], \"Readable explanation or bound text\", \"tone\"),\n row: define([\"children\"], \"Wrapping parallel content\"),\n column: define([\"children\"], \"Ordered vertical content\"),\n card: define([\"title\", \"children\", \"surface\"], \"Meaningful grouped section\", \"tone\"),\n details: define([\"title\", \"children\", \"open\"], \"Collapsible supporting content\", \"tone\"),\n badge: define([\"text\"], \"Short explicit status label\", \"tone\"),\n progress: define([\"label\", \"value\", \"max\", \"unit\"], \"Declared numeric progress snapshot\", \"tone\"),\n steps: define([\"items\"], \"Ordered declared steps\"),\n metric: define([\"label\", \"value\", \"unit\", \"decimals\"], \"Number with units, or a literal/bound status string\", \"tone\"),\n input: define([\"label\", \"bind\"], \"String or numeric state input\"),\n slider: define([\"label\", \"bind\"], \"Numeric state control\"),\n select: define([\"label\", \"bind\"], \"Selection from declared state options\"),\n toggle: define([\"label\", \"bind\"], \"Boolean state control\"),\n button: define([\"text\", \"action\"], \"Declared Agent follow-up\", \"icon\"),\n table: define([\"label\", \"rows\", \"columns\"], \"Supplied or derived values; inline rows or data/derived reference\"),\n chart: define([\"kind\", \"rows\", \"labelKey\", \"valueKey\", \"label\", \"unit\", \"xScale\"], \"Bar/line comparison with accessible data\"),\n sandbox: define([\"title\", \"html\", \"css\", \"script\", \"fallback\", \"height\", \"stateKeys\", \"dataKeys\", \"derivedKeys\"], \"Freeform isolated HTML/SVG/CSS/JavaScript visualization\")\n });\n function componentFields(type) {\n const c = Object.hasOwn(COMPONENTS, type) ? COMPONENTS[type] : void 0;\n return c && [\"id\", \"type\", ...c.fields, ...c.presentation ? [\"icon\"] : [], ...c.presentation === \"tone\" ? [\"tone\"] : []];\n }\n\n // lib/identifiers.js\n var IDENTIFIER_SOURCE = \"[\\\\p{L}_][\\\\p{L}\\\\p{M}\\\\p{N}_]{0,63}\";\n var pattern = new RegExp(`^${IDENTIFIER_SOURCE}$`, \"u\");\n var validIdentifier = (value) => typeof value === \"string\" && pattern.test(value) && ![\"constructor\", \"prototype\", \"__proto__\"].includes(value);\n\n // lib/author-expression.js\n function compileExpression(source) {\n const fail2 = () => {\n throw new Error(\"DSH UI: invalid formula; use state.x, data.x, derived.x, item.x, numbers, strings, + - * /, comparisons, ?: and min/max/round/abs\");\n };\n if (typeof source !== \"string\" || !source.trim() || source.length > 2048) fail2();\n const tokens = [];\n let offset = 0;\n const token2 = new RegExp(String.raw`\\s*(?:(\\d+(?:\\.\\d*)?|\\.\\d+)([eE][+-]?\\d+)?|(\"(?:[^\"\\\\]|\\\\.)*\")|(${IDENTIFIER_SOURCE}(?:\\.${IDENTIFIER_SOURCE})?)|(===|!==|<=|>=|==|!=|[+*/()?,:<>-]))`, \"uy\");\n while (offset < source.length) {\n if (!source.slice(offset).trim()) break;\n token2.lastIndex = offset;\n const m = token2.exec(source);\n if (!m) fail2();\n offset = token2.lastIndex;\n tokens.push(m[1] ? { number: Number(m[1] + (m[2] ?? \"\")) } : m[3] ? { string: JSON.parse(m[3]) } : m[4] ?? m[5]);\n if (tokens.length > 256) fail2();\n }\n let pos = 0;\n const operators = { \"==\": [\"eq\", 1], \"===\": [\"eq\", 1], \"!=\": [\"neq\", 1], \"!==\": [\"neq\", 1], \"<\": [\"lt\", 2], \"<=\": [\"lte\", 2], \">\": [\"gt\", 2], \">=\": [\"gte\", 2], \"+\": [\"add\", 3], \"-\": [\"sub\", 3], \"*\": [\"mul\", 4], \"/\": [\"div\", 4] };\n const take = (t) => {\n if (tokens[pos++] !== t) fail2();\n };\n function parse(min = 0, depth = 0) {\n if (depth > 12) fail2();\n const t = tokens[pos++];\n let out;\n if (t && typeof t === \"object\") out = { const: Object.hasOwn(t, \"number\") ? t.number : t.string };\n else if (t === \"true\" || t === \"false\") out = { const: t === \"true\" };\n else if (t === \"(\") {\n out = parse(0, depth + 1);\n take(\")\");\n } else if (t === \"-\" || t === \"+\") {\n const arg = parse(5, depth + 1);\n out = t === \"-\" ? { op: \"sub\", args: [{ const: 0 }, arg] } : arg;\n } else if (typeof t === \"string\" && t.split(\".\").length === 2 && [\"state\", \"data\", \"derived\", \"item\"].includes(t.split(\".\")[0]) && validIdentifier(t.split(\".\")[1])) out = { ref: t };\n else if ([\"min\", \"max\", \"round\", \"abs\"].includes(t)) {\n take(\"(\");\n const args = [parse(0, depth + 1)];\n while (tokens[pos] === \",\") {\n pos++;\n args.push(parse(0, depth + 1));\n if (args.length > 8) fail2();\n }\n take(\")\");\n out = { op: t, args };\n } else fail2();\n while (typeof tokens[pos] === \"string\" && Object.hasOwn(operators, tokens[pos]) && operators[tokens[pos]][1] >= min) {\n const [op, precedence] = operators[tokens[pos++]];\n out = { op, args: [out, parse(precedence + 1, depth + 1)] };\n }\n if (min === 0 && tokens[pos] === \"?\") {\n pos++;\n const yes = parse(0, depth + 1);\n take(\":\");\n out = { op: \"if\", args: [out, yes, parse(0, depth + 1)] };\n }\n return out;\n }\n const result = parse();\n if (pos !== tokens.length) fail2();\n return result;\n }\n function expandExpressions(doc2) {\n const expression = (e) => {\n if (!e || typeof e !== \"object\" || Array.isArray(e)) return e;\n if (Object.hasOwn(e, \"expr\")) {\n if (Object.keys(e).length !== 1) throw new Error(\"DSH UI: expr must be the only formula field\");\n return compileExpression(e.expr);\n }\n if (Array.isArray(e.args)) e.args = e.args.map(expression);\n if (e.map?.fields) for (const k of Object.keys(e.map.fields)) e.map.fields[k] = expression(e.map.fields[k]);\n return e;\n };\n if (doc2.derived && typeof doc2.derived === \"object\") for (const k of Object.keys(doc2.derived)) doc2.derived[k] = expression(doc2.derived[k]);\n const walk = (nodes) => {\n if (!Array.isArray(nodes)) return;\n for (const node2 of nodes) {\n if (!node2 || typeof node2 !== \"object\") continue;\n for (const field of [\"text\", \"value\", \"max\", \"rows\"]) if (Object.hasOwn(node2, field)) node2[field] = expression(node2[field]);\n walk(node2.children);\n }\n };\n walk(doc2.nodes);\n return doc2;\n }\n\n // lib/protocol.js\n var LIMITS = Object.freeze({ bytes: 131072, nodes: 160, depth: 12, rows: 500, expressions: 128, steps: 4096 });\n var own = (o, k) => Object.hasOwn(o, k);\n var fail = (message) => {\n throw new Error(`DSH UI: ${message}`);\n };\n var object = (x) => x !== null && typeof x === \"object\" && !Array.isArray(x);\n var finite = (x) => typeof x === \"number\" && Number.isFinite(x);\n var scalar = (x) => typeof x === \"string\" || typeof x === \"boolean\" || finite(x);\n var text = (x, at) => {\n if (typeof x !== \"string\" || x.length > 8192) fail(`${at} must be a string of at most 8192 characters`);\n };\n function fields(value, allowed, at) {\n if (!object(value)) fail(`${at} must be an object`);\n for (const k of Object.keys(value)) if (!allowed.includes(k)) {\n const hint = at === \"document\" ? \" Controls and visual components belong in nodes as registered typed nodes, not extra document fields.\" : \"\";\n fail(`unknown field ${at}.${k}; allowed fields at ${at}: ${allowed.join(\", \")}.${hint}`);\n }\n }\n function safeJson(value, depth = 0, source = false) {\n if (depth > 48) fail(\"JSON nesting is too deep\");\n if (value === null || typeof value === \"boolean\") return;\n if (typeof value === \"string\") {\n if (source) {\n if (value.length > SANDBOX_LIMITS.source) fail(\"canvas source exceeds 48 Ki characters\");\n return;\n }\n return text(value, \"value\");\n }\n if (typeof value === \"number\") {\n if (!finite(value)) fail(\"numbers must be finite\");\n return;\n }\n if (typeof value !== \"object\") fail(\"only JSON values are accepted\");\n if (Array.isArray(value) && value.length > LIMITS.rows) fail(\"too many array items\");\n for (const k of Object.keys(value)) {\n if ([\"__proto__\", \"prototype\", \"constructor\"].includes(k)) fail(\"unsafe property name\");\n safeJson(value[k], depth + 1, value.type === \"sandbox\" && [\"html\", \"css\", \"script\"].includes(k));\n }\n }\n function initialState(doc2) {\n return Object.fromEntries(Object.entries(doc2.state).map(([k, spec]) => [k, spec.default]));\n }\n function validateState(doc2, state2) {\n if (!object(state2) || Object.keys(state2).length !== Object.keys(doc2.state).length) fail(\"state keys do not match the document\");\n const result = {};\n for (const [k, s] of Object.entries(doc2.state)) {\n if (!own(state2, k)) fail(`missing state.${k}`);\n const v = state2[k];\n if (s.type === \"number\") {\n if (!finite(v) || v < s.min || v > s.max) fail(`state.${k} is outside its numeric range`);\n const offset = (v - s.min) / s.step;\n if (Math.abs(offset - Math.round(offset)) > 1e-6) fail(`state.${k} does not match its step`);\n } else if (s.type === \"json\") {\n if (!object(v) && !Array.isArray(v)) fail(`state.${k} must be an array or object for type json`);\n safeJson(v);\n if (new TextEncoder().encode(JSON.stringify(v)).length > 16384) fail(`state.${k} JSON exceeds 16 KiB`);\n } else if (s.type === \"string\") text(v, `state.${k}`);\n else if (typeof v !== \"boolean\") fail(`state.${k} must be boolean`);\n if (s.options && !s.options.includes(v)) fail(`invalid option for state.${k}`);\n result[k] = v;\n }\n return result;\n }\n var ARITY = { add: [1, 8], sub: [2, 2], mul: [1, 8], div: [2, 2], min: [1, 8], max: [1, 8], round: [1, 1], abs: [1, 1], eq: [2, 2], neq: [2, 2], lt: [2, 2], lte: [2, 2], gt: [2, 2], gte: [2, 2], if: [3, 3] };\n function reference(doc2, ref) {\n if (typeof ref !== \"string\" || ![\"state\", \"data\", \"derived\"].includes(ref.split(\".\")[0]) || ref.split(\".\").length !== 2 || !validIdentifier(ref.split(\".\")[1])) fail(`invalid reference ${JSON.stringify(ref)}; use state.key, data.key or derived.key with a letter/underscore identifier`);\n const [group, k] = ref.split(\".\");\n if (!own(doc2[group], k)) fail(`unknown reference ${ref}`);\n return [group, k];\n }\n function evaluate(doc2, state2) {\n const derived = {}, active = /* @__PURE__ */ new Set();\n let steps = 0, generatedRows = 0;\n const budget = () => {\n if (++steps > LIMITS.steps) fail(\"calculation budget exceeded\");\n };\n const resolve = (ref) => {\n budget();\n const [group, k] = reference(doc2, ref);\n if (group !== \"derived\") return (group === \"state\" ? state2 : doc2.data)[k];\n if (own(derived, k)) return derived[k];\n if (active.has(k)) fail(`cyclic calculation ${k}`);\n active.add(k);\n const result2 = expression(doc2.derived[k]);\n active.delete(k);\n derived[k] = result2;\n return result2;\n };\n const expression = (e, item) => {\n budget();\n if (own(e, \"map\")) {\n const rows = resolve(e.map.source);\n if ((generatedRows += rows.length) > LIMITS.rows) fail(\"generated row limit exceeded\");\n return rows.map((row) => Object.fromEntries(Object.entries(e.map.fields).map(([k, value]) => {\n const out2 = expression(value, row);\n if (!scalar(out2)) fail(\"generated cells must be scalar\");\n return [k, out2];\n })));\n }\n if (own(e, \"const\")) return e.const;\n if (own(e, \"ref\")) {\n if (e.ref.startsWith(\"item.\")) {\n const k = e.ref.slice(5);\n if (!item || !own(item, k) || !scalar(item[k])) fail(\"invalid item reference\");\n return item[k];\n }\n return resolve(e.ref);\n }\n if (e.op === \"if\") {\n const condition = expression(e.args[0], item);\n if (typeof condition !== \"boolean\") fail(\"if condition must be boolean\");\n return expression(e.args[condition ? 1 : 2], item);\n }\n const a = e.args.map((arg) => expression(arg, item));\n if (e.op === \"eq\") return a[0] === a[1];\n if (e.op === \"neq\") return a[0] !== a[1];\n if (!a.every(finite)) fail(`operator ${e.op} requires finite numbers`);\n let out;\n switch (e.op) {\n case \"add\":\n out = a.reduce((x, y) => x + y, 0);\n break;\n case \"sub\":\n out = a[0] - a[1];\n break;\n case \"mul\":\n out = a.reduce((x, y) => x * y, 1);\n break;\n case \"div\":\n if (a[1] === 0) fail(\"division by zero\");\n out = a[0] / a[1];\n break;\n case \"min\":\n out = Math.min(...a);\n break;\n case \"max\":\n out = Math.max(...a);\n break;\n case \"round\":\n out = Math.round(a[0]);\n break;\n case \"abs\":\n out = Math.abs(a[0]);\n break;\n case \"lt\":\n return a[0] < a[1];\n case \"lte\":\n return a[0] <= a[1];\n case \"gt\":\n return a[0] > a[1];\n case \"gte\":\n return a[0] >= a[1];\n default:\n fail(`unsupported operator ${e.op}`);\n }\n if (!finite(out)) fail(\"non-finite calculation result\");\n return out;\n };\n for (const k of Object.keys(doc2.derived)) resolve(`derived.${k}`);\n if (new TextEncoder().encode(JSON.stringify(derived)).length > LIMITS.bytes) fail(\"derived output exceeds 128 KiB\");\n const result = { derived, resolveValue: (value) => {\n if (!object(value)) return value;\n return expression(value);\n } };\n checkViewValues(doc2, result);\n return result;\n }\n function checkViewValues(doc2, evaluated) {\n function walk(list) {\n for (const n of list) {\n if (n.type === \"table\" || n.type === \"chart\") {\n const rows = evaluated.resolveValue(n.rows);\n if (!Array.isArray(rows) || rows.length > LIMITS.rows || !rows.every(object)) fail(\"table/chart rows must reference an array of objects\");\n if (n.type === \"chart\" && rows.some((r) => !own(r, n.labelKey) || !scalar(r[n.labelKey]) || !own(r, n.valueKey) || !finite(r[n.valueKey]))) fail(\"chart requires labels and finite numeric values\");\n if (n.type === \"chart\" && n.xScale === \"linear\" && rows.some((r) => !finite(r[n.labelKey]))) fail(\"linear chart requires numeric x values\");\n }\n if (n.type === \"metric\" && !scalar(evaluated.resolveValue(n.value))) fail(`node.${n.id}.value must resolve to a number, string or boolean; use table for rows or a canvas for structured data`);\n if (n.type === \"progress\") {\n const v = evaluated.resolveValue(n.value), max = evaluated.resolveValue(n.max);\n if (!finite(v) || !finite(max) || max <= 0 || v < 0 || v > max) fail(\"progress requires 0 <= value <= positive max\");\n }\n if (n.children) walk(n.children);\n }\n }\n walk(doc2.nodes);\n }\n function validateDocument(input) {\n safeJson(input);\n const json = JSON.stringify(input);\n if (new TextEncoder().encode(json).length > LIMITS.bytes) fail(\"document exceeds 128 KiB\");\n const doc2 = expandExpressions(JSON.parse(json));\n fields(doc2, [\"version\", \"title\", \"fallback\", \"state\", \"data\", \"derived\", \"nodes\", \"actions\", \"presentation\"], \"document\");\n if (doc2.presentation !== void 0) {\n fields(doc2.presentation, [\"layout\", \"accent\", \"density\"], \"presentation\");\n for (const [k, allowed] of Object.entries({ layout: UI_LAYOUTS, accent: UI_ACCENTS, density: [\"comfortable\", \"compact\"] }))\n if (doc2.presentation[k] !== void 0 && !allowed.includes(doc2.presentation[k])) fail(`unknown presentation ${k}`);\n }\n if (doc2.version !== 1) fail(\"unsupported protocol version\");\n text(doc2.title, \"title\");\n text(doc2.fallback, \"fallback\");\n if (!doc2.title.trim() || !doc2.fallback.trim()) fail(\"title and fallback cannot be empty\");\n for (const group of [\"state\", \"data\", \"derived\", \"actions\"]) {\n doc2[group] ??= {};\n if (!object(doc2[group]) || Object.keys(doc2[group]).length > 64) fail(`invalid ${group}`);\n for (const k of Object.keys(doc2[group])) if (!validIdentifier(k)) fail(`invalid key in ${group}: ${JSON.stringify(k)}; use 1..64 letters, numbers or underscores, starting with a letter/underscore (Chinese letters supported); no spaces, dots or hyphens. Put display labels in label/text`);\n }\n for (const [k, s] of Object.entries(doc2.state)) {\n fields(s, [\"type\", \"default\", \"min\", \"max\", \"step\", \"options\"], `state.${k}`);\n if (s.type === \"string\" && (Array.isArray(s.default) || object(s.default)) && !own(s, \"options\")) s.type = \"json\";\n if (![\"number\", \"string\", \"boolean\", \"json\"].includes(s.type)) fail(`unsupported state type ${k}; use number, string, boolean or json (arrays/objects)`);\n if (s.type === \"json\" && own(s, \"options\")) fail(`state.${k}: json state does not support select options; use scalar options or a canvas`);\n if (s.type === \"number\") {\n if (![s.min, s.max, s.step].every(finite) || s.min >= s.max || s.step <= 0) fail(`invalid numeric limits for ${k}`);\n if ((s.max - s.min) / s.step > 1e9) fail(`numeric step too small for ${k}`);\n } else if ([\"min\", \"max\", \"step\"].some((p) => own(s, p))) fail(`numeric limits on non-number ${k}`);\n if (s.options && (!Array.isArray(s.options) || !s.options.length || s.options.length > 64 || !s.options.every(scalar))) fail(`invalid options for ${k}`);\n }\n const defaults = validateState(doc2, initialState(doc2));\n let expressions = 0;\n for (const [k, spec] of Object.entries(doc2.state))\n for (const option of spec.options ?? []) validateState(doc2, { ...defaults, [k]: option });\n function validateExpression(e, depth = 0, itemRows) {\n if (++expressions > LIMITS.expressions || depth > LIMITS.depth) fail(\"expression limit exceeded\");\n if (!object(e)) fail(\"expression must be an object\");\n if (own(e, \"map\")) {\n if (depth !== 0) fail(\"map is only allowed at the root of a derived value\");\n fields(e, [\"map\"], \"expression\");\n fields(e.map, [\"source\", \"fields\"], \"map\");\n if (typeof e.map.source !== \"string\" || !e.map.source.startsWith(\"data.\")) fail('map source must reference static data using a string such as \"data.samples\"; put object rows in document.data.samples, not inline in map.source');\n const [, k] = reference(doc2, e.map.source), rows = doc2.data[k];\n if (!Array.isArray(rows) || rows.length > LIMITS.rows || !rows.every(object)) fail(\"map source must be object rows\");\n if (!object(e.map.fields) || !Object.keys(e.map.fields).length || Object.keys(e.map.fields).length > 16) fail(\"map requires 1..16 fields\");\n for (const [k2, value2] of Object.entries(e.map.fields)) {\n if (!validIdentifier(k2)) fail(`invalid map field ${JSON.stringify(k2).slice(0, 100)}; use a letter/underscore identifier such as powerW or \\u529F\\u7387, without spaces, dots or hyphens`);\n validateExpression(value2, depth + 1, rows);\n }\n return;\n }\n if (own(e, \"const\")) {\n fields(e, [\"const\"], \"expression\");\n if (!scalar(e.const)) fail(\"constant must be scalar\");\n return;\n }\n if (own(e, \"ref\")) {\n fields(e, [\"ref\"], \"expression\");\n if (typeof e.ref === \"string\" && e.ref.startsWith(\"item.\")) {\n const k = e.ref.slice(5);\n if (!itemRows || !validIdentifier(k) || itemRows.some((row) => !own(row, k) || !scalar(row[k]))) fail(\"invalid or unscoped item reference\");\n } else reference(doc2, e.ref);\n return;\n }\n fields(e, [\"op\", \"args\"], \"expression\");\n const arity = ARITY[e.op];\n if (!arity || !Array.isArray(e.args) || e.args.length < arity[0] || e.args.length > arity[1]) fail(\"invalid operator or argument count\");\n e.args.forEach((a) => validateExpression(a, depth + 1, itemRows));\n }\n Object.values(doc2.derived).forEach((e) => validateExpression(e));\n const visited = /* @__PURE__ */ new Set(), active = /* @__PURE__ */ new Set();\n function visit(k) {\n if (active.has(k)) fail(`cyclic calculation ${k}`);\n if (visited.has(k)) return;\n active.add(k);\n function walk(e) {\n if (e.ref?.startsWith(\"derived.\")) visit(e.ref.split(\".\")[1]);\n for (const a of e.args ?? []) walk(a);\n for (const value2 of Object.values(e.map?.fields ?? {})) walk(value2);\n }\n walk(doc2.derived[k]);\n active.delete(k);\n visited.add(k);\n }\n Object.keys(doc2.derived).forEach(visit);\n for (const [k, a] of Object.entries(doc2.actions)) {\n fields(a, [\"type\", \"intent\", \"stateKeys\"], `actions.${k}`);\n if (a.type !== \"agent.followup\") fail(\"only agent.followup actions are supported\");\n text(a.intent, \"action intent\");\n if (!a.intent.trim() || !Array.isArray(a.stateKeys) || a.stateKeys.length > 64 || new Set(a.stateKeys).size !== a.stateKeys.length || a.stateKeys.some((s) => !own(doc2.state, s))) fail(\"invalid action stateKeys\");\n }\n const ids = /* @__PURE__ */ new Set();\n let count2 = 0;\n const value = (v, at) => {\n if (object(v)) {\n if (own(v, \"map\")) fail(\"map belongs in derived\");\n validateExpression(v);\n } else if (!scalar(v)) fail(`invalid value at ${at}`);\n };\n function nodes(list, depth = 0) {\n if (!Array.isArray(list) || depth > LIMITS.depth) fail(\"invalid node children or depth\");\n for (const n of list) {\n if (++count2 > LIMITS.nodes) fail(\"too many nodes\");\n if (!object(n) || !validIdentifier(n.id) || ids.has(n.id)) fail(\"node ids must be unique identifiers\");\n ids.add(n.id);\n if (!Object.hasOwn(COMPONENTS, n.type)) fail(\"unsupported component \" + n.type);\n if ([\"table\", \"chart\"].includes(n.type) && typeof n.rows === \"string\" && /^(data|derived)\\./.test(n.rows)) n.rows = { ref: n.rows };\n switch (n.type) {\n case \"sandbox\": {\n text(n.title, \"canvas title\");\n text(n.fallback, \"canvas fallback\");\n if (!n.title.trim() || !n.fallback.trim() || typeof n.html !== \"string\" || !n.html.trim()) fail(\"canvas requires title, HTML and fallback\");\n for (const field of [\"html\", \"css\", \"script\"]) if (n[field] !== void 0 && (typeof n[field] !== \"string\" || n[field].length > SANDBOX_LIMITS.source)) fail(\"canvas source exceeds 48 Ki characters\");\n if (n.height !== void 0 && (!Number.isInteger(n.height) || n.height < 160 || n.height > SANDBOX_LIMITS.height)) fail(\"canvas height must be 160..1000\");\n for (const [field, group] of [[\"stateKeys\", \"state\"], [\"dataKeys\", \"data\"], [\"derivedKeys\", \"derived\"]]) if (n[field] !== void 0 && (!Array.isArray(n[field]) || n[field].length > 64 || new Set(n[field]).size !== n[field].length || n[field].some((k) => !own(doc2[group], k)))) fail(\"invalid canvas \" + field);\n break;\n }\n case \"row\":\n case \"column\":\n break;\n case \"card\":\n if (n.title !== void 0) text(n.title, \"card title\");\n if (n.surface !== void 0 && !UI_SURFACES.includes(n.surface)) fail(\"unknown card surface\");\n break;\n case \"details\":\n text(n.title, \"details title\");\n if (n.open !== void 0 && typeof n.open !== \"boolean\") fail(\"details open must be boolean\");\n break;\n case \"badge\":\n value(n.text, \"badge text\");\n break;\n case \"progress\":\n text(n.label, \"progress label\");\n value(n.value, \"progress value\");\n value(n.max, \"progress max\");\n if (n.unit !== void 0) text(n.unit, \"progress unit\");\n break;\n case \"steps\": {\n if (!Array.isArray(n.items) || !n.items.length || n.items.length > 12) fail(\"steps require 1..12 items\");\n for (const item of n.items) {\n fields(item, [\"title\", \"description\", \"status\"], \"step\");\n text(item.title, \"step title\");\n if (item.description !== void 0) text(item.description, \"step description\");\n if (![\"pending\", \"active\", \"done\", \"error\"].includes(item.status)) fail(\"unknown step status\");\n }\n break;\n }\n case \"heading\":\n case \"text\":\n value(n.text, \"text\");\n break;\n case \"metric\":\n text(n.label, \"metric label\");\n value(n.value, \"metric value\");\n if (n.unit !== void 0) text(n.unit, \"unit\");\n if (n.decimals !== void 0 && (!Number.isInteger(n.decimals) || n.decimals < 0 || n.decimals > 6)) fail(\"invalid decimals\");\n break;\n case \"input\":\n case \"slider\":\n case \"select\":\n case \"toggle\": {\n text(n.label, \"control label\");\n if (typeof n.bind !== \"string\" || !n.bind.startsWith(\"state.\")) fail(\"controls must bind state\");\n const [, k] = reference(doc2, n.bind), spec = doc2.state[k];\n if (n.type === \"slider\" && spec.type !== \"number\") fail(\"slider requires numeric state\");\n if (n.type === \"toggle\" && spec.type !== \"boolean\") fail(\"toggle requires boolean state\");\n if ([\"input\", \"select\"].includes(n.type) && spec.type === \"json\") fail(`node.${n.id}: structured state requires a sandbox with DSHUI.setState; use separate boolean states and toggle nodes for a native checklist`);\n if (n.type === \"input\" && spec.type === \"boolean\") fail(\"use toggle for boolean state\");\n if (n.type === \"select\" && !spec.options) fail(\"select requires options\");\n break;\n }\n case \"button\":\n text(n.text, \"button text\");\n if (!own(doc2.actions, n.action)) fail(\"unknown action\");\n break;\n case \"table\": {\n if (n.label !== void 0) text(n.label, \"table label\");\n if (!Array.isArray(n.rows)) value(n.rows, \"table rows\");\n if (!Array.isArray(n.columns) || !n.columns.length || n.columns.length > 16) fail(\"invalid table columns\");\n const cols = /* @__PURE__ */ new Set();\n for (const c of n.columns) {\n fields(c, [\"key\", \"label\", \"unit\"], \"column\");\n if (!validIdentifier(c.key) || cols.has(c.key)) fail(\"invalid or duplicate column key\");\n cols.add(c.key);\n text(c.label, \"column label\");\n if (c.unit !== void 0) text(c.unit, \"column unit\");\n }\n break;\n }\n case \"chart\":\n if (!Array.isArray(n.rows)) value(n.rows, \"chart rows\");\n if (![\"bar\", \"line\"].includes(n.kind) || !validIdentifier(n.labelKey) || !validIdentifier(n.valueKey)) fail(\"invalid chart\");\n text(n.label, \"chart label\");\n if (n.unit !== void 0) text(n.unit, \"chart unit\");\n if (n.xScale !== void 0 && (![\"category\", \"linear\"].includes(n.xScale) || n.xScale === \"linear\" && n.kind !== \"line\")) fail(\"invalid xScale\");\n break;\n default:\n fail(`unsupported component ${n.type}`);\n }\n if ([\"heading\", \"text\", \"card\", \"metric\", \"button\", \"badge\", \"progress\", \"details\"].includes(n.type)) {\n if (n.icon !== void 0 && !UI_ICONS.includes(n.icon)) fail(\"unknown UI icon\");\n if (n.type !== \"button\") {\n if (n.tone !== void 0 && !UI_TONES.includes(n.tone)) fail(\"unknown UI tone\");\n }\n }\n fields(n, componentFields(n.type), `node.${n.id}`);\n if ([\"row\", \"column\", \"card\", \"details\"].includes(n.type)) nodes(n.children, depth + 1);\n }\n }\n nodes(doc2.nodes);\n if (!count2) fail(\"at least one node is required\");\n evaluate(doc2, defaults);\n return doc2;\n }\n\n // src/export-runtime.js\n var payload = JSON.parse(document.getElementById(\"dsh-ui-export\").textContent);\n var doc = validateDocument(payload.document);\n var node = doc.nodes.flatMap(function flatten(n) {\n return [n, ...(n.children ?? []).flatMap(flatten)];\n }).find((n) => n.id === payload.nodeId && n.type === \"sandbox\");\n if (!node) throw Error(\"\\u5BFC\\u51FA\\u7684\\u753B\\u5E03\\u4E0D\\u5B58\\u5728\");\n var state = validateState(doc, payload.state);\n var connected = false;\n var start = Date.now();\n var count = 0;\n var token = () => Array.from(crypto.getRandomValues(new Uint8Array(16)), (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n var channel = token();\n var nonce = payload.nonce;\n var frame = document.createElement(\"iframe\");\n frame.title = node.title;\n frame.sandbox = \"allow-scripts\";\n frame.referrerPolicy = \"no-referrer\";\n frame.style.cssText = \"display:block;width:100%;height:\" + (node.height ?? 420) + \"px;border:0\";\n var status = document.getElementById(\"status\");\n var publish = () => {\n try {\n frame.contentWindow.postMessage({ dshCanvas: 1, channel, type: \"snapshot\", payload: { ...canvasSnapshot(node, state, doc.data, evaluate(doc, state).derived, false, payload.theme), visible: !document.hidden } }, \"*\");\n } catch (e) {\n status.textContent = \"\\u8BA1\\u7B97\\u5931\\u8D25\\uFF1A\" + e.message;\n }\n };\n addEventListener(\"message\", (event) => {\n const m = event.data;\n if (event.source !== frame.contentWindow || m?.channel !== channel || m.dshCanvas !== 1) return;\n if (Date.now() - start > 1e3) {\n start = Date.now();\n count = 0;\n }\n if (++count > SANDBOX_LIMITS.rate) return;\n if (m.type === \"ready\") {\n connected = true;\n publish();\n } else if (m.type === \"patch\" && connected) {\n try {\n const next = validateState(doc, { ...state, ...canvasPatch(m.payload, node) });\n evaluate(doc, next);\n state = next;\n status.textContent = \"\\u672C\\u5730\\u53C2\\u6570\\u5DF2\\u66F4\\u65B0\\uFF1B\\u5237\\u65B0\\u6587\\u4EF6\\u4F1A\\u56DE\\u5230\\u5BFC\\u51FA\\u65F6\\u7684\\u53C2\\u6570\\u3002\";\n } catch (e) {\n status.textContent = \"\\u53C2\\u6570\\u65E0\\u6548\\uFF1A\" + e.message;\n }\n publish();\n } else if (m.type === \"height\" && Number.isFinite(m.payload) && m.payload >= 0) frame.style.height = Math.max(32, Math.min(1e3, Math.ceil(m.payload))) + \"px\";\n else if (m.type === \"error\") status.textContent = \"\\u753B\\u5E03\\u8FD0\\u884C\\u5F02\\u5E38\\uFF1A\" + String(m.payload).slice(0, 1e3);\n });\n document.addEventListener(\"visibilitychange\", () => {\n if (connected) publish();\n });\n document.getElementById(\"surface\").append(frame);\n frame.srcdoc = sandboxDocument(node, channel, nonce);\n})();\n";
|
package/lib/freeform.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { toolJson } from './tool-json.js';
|
|
2
2
|
import { validateDocument } from './protocol.js';
|
|
3
|
+
import { AUTHORING_GUIDE } from './authoring-guide.js';
|
|
3
4
|
export function canvasDocument({title,fallback,html,css,script,height,state,data,derived}){
|
|
4
5
|
const values={state:toolJson(state??{},'state'),data:toolJson(data??{},'data'),derived:toolJson(derived??{},'derived')};
|
|
5
6
|
for(const [key,value] of Object.entries(values))if(!value||typeof value!=='object'||Array.isArray(value))throw new Error('DSH UI: '+key+' must be an object');
|
|
6
7
|
return validateDocument({version:1,title,fallback,...values,nodes:[{id:'canvas',type:'sandbox',title,fallback,html,...(css===undefined?{}:{css}),...(script===undefined?{}:{script}),...(height===undefined?{}:{height}),stateKeys:Object.keys(values.state),dataKeys:Object.keys(values.data),derivedKeys:Object.keys(values.derived)}]});
|
|
7
8
|
}
|
|
8
|
-
export const FREE_UI_CONTRACT
|
|
9
|
+
export const FREE_UI_CONTRACT=AUTHORING_GUIDE+`\nYou have a freeform visual UI surface (static or interactive) in the conversation. Choose the graphics, structure, typography, colors, layout and animation yourself to suit the user's intent. No design template, card composition, accent palette, example UI or dashboard convention is prescribed. Use ordinary text when a UI would not help.
|
|
9
10
|
Call render_canvas({title,fallback,html,css?,script?,height?,state?,data?,derived?}) directly, not inside PTC/run_code. For styled streaming preview emit css (empty string if unused) before html, then script. Preview is static until commit and parameter restoration. Optional host .ui-* primitives exist but no composition is required. html is a body fragment supporting HTML/SVG; put all CSS in css and JavaScript in script, use addEventListener rather than HTML event attributes. Each source field max 49152 characters, whole document max128KiB, height160..1000. Code runs only after commit and parameter restoration. No external resources, imports, eval or host APIs; the isolated frame is not a CPU/memory hard limit. The existing frame is updated with inspect_ui/patch_ui, never arbitrary host access.
|
|
10
|
-
The user-facing content and all controls may be authored inside your canvas. State important to the user belongs in state, e.g. {speed:{type:"number",default:1,min:0.5,max:3,step:0.5},running:{type:"boolean",default:false}}. Strings require type:"string",default
|
|
11
|
+
The user-facing content and all controls may be authored inside your canvas. State important to the user belongs in state, e.g. {speed:{type:"number",default:1,min:0.5,max:3,step:0.5},running:{type:"boolean",default:false}}. Strings require type:"string",default; options:[string,...] is optional. Structured lists/objects use type:"json",default:[]/{} (16KiB per value). Bind custom controls through DSHUI.setState({speed:2}); use DSHUI.subscribe(snapshot=>...) to draw snapshot.state/data/derived. Do not write state from each animation frame or echo subscribe updates back into setState. State updates are validated and persisted. Reload may reset transient animation phase.
|
|
11
12
|
Prefer DSHUI.animate(({time,delta,reducedMotion,visible})=>...) for motion; seconds, delta capped0.05, returns unsubscribe. Integrate delta only while running; reducedMotion needs a meaningful static view. Stop/replacement disposes the frame. DSHUI.formatNumber(value, decimals=2) formats finite numbers with 0..6 decimal places and comma grouping, exactly matching native metrics and fallback; use it for shared readouts instead of mixing toFixed rounding. Theme is in snapshot.theme and CSS variables --canvas-bg/soft/ink/muted/line/accent. Use readable text and named keyboard-operable controls, adapt to narrow and wide widths; ensure information remains available in a useful fallback. Be truthful about data, assumptions and simulation accuracy.
|
|
12
13
|
If native controls or mixed component layouts would help, render_ui and ui_catalog remain available; query the catalog for their exact contract when needed. They are optional, not the default layout.
|
|
13
14
|
inspect_ui defaults to a compact summary; detail:"nodes",nodeIds:["canvas"] reads canvas source. patch_ui requires documentId,current documentRevision,changes:[{id:"canvas",props:{html/css/script/...}}]. It preserves declared state and other nodes; replacing source restarts that canvas. Observations are heuristic and scoped to recorded viewport/theme/revision, not a beauty or safety score. Empty reports mean unobserved, do not poll or invent success. Limit automatic correction to two rounds per user request, then simplify or explain unresolved problems. Explicit new user edits start a new request. This is a behavior budget, not a provider inference timeout.`;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Shared by validation and the formula tokenizer; never a property-chain grammar.
|
|
2
|
+
export const IDENTIFIER_SOURCE = '[\\p{L}_][\\p{L}\\p{M}\\p{N}_]{0,63}';
|
|
3
|
+
const pattern = new RegExp(`^${IDENTIFIER_SOURCE}$`, 'u');
|
|
4
|
+
export const validIdentifier = value => typeof value === 'string' && pattern.test(value) && !['constructor', 'prototype', '__proto__'].includes(value);
|
package/lib/protocol.js
CHANGED
|
@@ -3,11 +3,11 @@ import { SANDBOX_LIMITS } from './sandbox.js';
|
|
|
3
3
|
import { COMPONENTS, componentFields } from './components.js';
|
|
4
4
|
import { formatNumber } from './format-number.js';
|
|
5
5
|
import { expandExpressions } from './author-expression.js';
|
|
6
|
+
import { validIdentifier as key } from './identifiers.js';
|
|
6
7
|
export const LIMITS = Object.freeze({ bytes: 131072, nodes: 160, depth: 12, rows: 500, expressions: 128, steps: 4096 });
|
|
7
8
|
const own = (o, k) => Object.hasOwn(o, k);
|
|
8
9
|
const fail = message => { throw new Error(`DSH UI: ${message}`); };
|
|
9
10
|
const object = x => x !== null && typeof x === 'object' && !Array.isArray(x);
|
|
10
|
-
const key = x => typeof x === 'string' && /^[a-zA-Z][a-zA-Z0-9_]{0,63}$/.test(x) && !['constructor', 'prototype', '__proto__'].includes(x);
|
|
11
11
|
const finite = x => typeof x === 'number' && Number.isFinite(x);
|
|
12
12
|
const scalar = x => typeof x === 'string' || typeof x === 'boolean' || finite(x);
|
|
13
13
|
const text = (x, at) => { if (typeof x !== 'string' || x.length > 8192) fail(`${at} must be a string of at most 8192 characters`); };
|
|
@@ -41,6 +41,10 @@ export function validateState(doc, state) {
|
|
|
41
41
|
if (!finite(v) || v < s.min || v > s.max) fail(`state.${k} is outside its numeric range`);
|
|
42
42
|
const offset = (v - s.min) / s.step;
|
|
43
43
|
if (Math.abs(offset - Math.round(offset)) > 1e-6) fail(`state.${k} does not match its step`);
|
|
44
|
+
} else if (s.type === 'json') {
|
|
45
|
+
if (!object(v) && !Array.isArray(v)) fail(`state.${k} must be an array or object for type json`);
|
|
46
|
+
safeJson(v);
|
|
47
|
+
if (new TextEncoder().encode(JSON.stringify(v)).length > 16384) fail(`state.${k} JSON exceeds 16 KiB`);
|
|
44
48
|
} else if (s.type === 'string') text(v, `state.${k}`);
|
|
45
49
|
else if (typeof v !== 'boolean') fail(`state.${k} must be boolean`);
|
|
46
50
|
if (s.options && !s.options.includes(v)) fail(`invalid option for state.${k}`);
|
|
@@ -50,7 +54,7 @@ export function validateState(doc, state) {
|
|
|
50
54
|
}
|
|
51
55
|
const ARITY = { add: [1, 8], sub: [2, 2], mul: [1, 8], div: [2, 2], min: [1, 8], max: [1, 8], round: [1, 1], abs: [1, 1], eq: [2, 2], neq: [2, 2], lt: [2, 2], lte: [2, 2], gt: [2, 2], gte: [2, 2], if: [3, 3] };
|
|
52
56
|
function reference(doc, ref) {
|
|
53
|
-
if (typeof ref !== 'string' ||
|
|
57
|
+
if (typeof ref !== 'string' || !['state','data','derived'].includes(ref.split('.')[0]) || ref.split('.').length !== 2 || !key(ref.split('.')[1])) fail(`invalid reference ${JSON.stringify(ref)}; use state.key, data.key or derived.key with a letter/underscore identifier`);
|
|
54
58
|
const [group, k] = ref.split('.');
|
|
55
59
|
if (!own(doc[group], k)) fail(`unknown reference ${ref}`);
|
|
56
60
|
return [group, k];
|
|
@@ -118,7 +122,7 @@ function checkViewValues(doc, evaluated) {
|
|
|
118
122
|
if (n.type === 'chart' && rows.some(r => !own(r, n.labelKey) || !scalar(r[n.labelKey]) || !own(r, n.valueKey) || !finite(r[n.valueKey]))) fail('chart requires labels and finite numeric values');
|
|
119
123
|
if (n.type === 'chart' && n.xScale === 'linear' && rows.some(r => !finite(r[n.labelKey]))) fail('linear chart requires numeric x values');
|
|
120
124
|
}
|
|
121
|
-
if (n.type === 'metric' && !
|
|
125
|
+
if (n.type === 'metric' && !scalar(evaluated.resolveValue(n.value))) fail(`node.${n.id}.value must resolve to a number, string or boolean; use table for rows or a canvas for structured data`);
|
|
122
126
|
if (n.type === 'progress') {
|
|
123
127
|
const v = evaluated.resolveValue(n.value), max = evaluated.resolveValue(n.max);
|
|
124
128
|
if (!finite(v) || !finite(max) || max <= 0 || v < 0 || v > max) fail('progress requires 0 <= value <= positive max');
|
|
@@ -144,11 +148,14 @@ export function validateDocument(input) {
|
|
|
144
148
|
for (const group of ['state', 'data', 'derived', 'actions']) {
|
|
145
149
|
doc[group] ??= {};
|
|
146
150
|
if (!object(doc[group]) || Object.keys(doc[group]).length > 64) fail(`invalid ${group}`);
|
|
147
|
-
for (const k of Object.keys(doc[group])) if (!key(k)) fail(`invalid key in ${group}`);
|
|
151
|
+
for (const k of Object.keys(doc[group])) if (!key(k)) fail(`invalid key in ${group}: ${JSON.stringify(k)}; use 1..64 letters, numbers or underscores, starting with a letter/underscore (Chinese letters supported); no spaces, dots or hyphens. Put display labels in label/text`);
|
|
148
152
|
}
|
|
149
153
|
for (const [k, s] of Object.entries(doc.state)) {
|
|
150
154
|
fields(s, ['type', 'default', 'min', 'max', 'step', 'options'], `state.${k}`);
|
|
151
|
-
|
|
155
|
+
// Preserve structured checklist data instead of coercing it to a string.
|
|
156
|
+
if (s.type === 'string' && (Array.isArray(s.default) || object(s.default)) && !own(s,'options')) s.type = 'json';
|
|
157
|
+
if (!['number', 'string', 'boolean', 'json'].includes(s.type)) fail(`unsupported state type ${k}; use number, string, boolean or json (arrays/objects)`);
|
|
158
|
+
if (s.type === 'json' && own(s,'options')) fail(`state.${k}: json state does not support select options; use scalar options or a canvas`);
|
|
152
159
|
if (s.type === 'number') {
|
|
153
160
|
if (![s.min, s.max, s.step].every(finite) || s.min >= s.max || s.step <= 0) fail(`invalid numeric limits for ${k}`);
|
|
154
161
|
if ((s.max - s.min) / s.step > 1e9) fail(`numeric step too small for ${k}`);
|
|
@@ -168,7 +175,7 @@ export function validateDocument(input) {
|
|
|
168
175
|
const [, k] = reference(doc, e.map.source), rows = doc.data[k];
|
|
169
176
|
if (!Array.isArray(rows) || rows.length > LIMITS.rows || !rows.every(object)) fail('map source must be object rows');
|
|
170
177
|
if (!object(e.map.fields) || !Object.keys(e.map.fields).length || Object.keys(e.map.fields).length > 16) fail('map requires 1..16 fields');
|
|
171
|
-
for (const [k, value] of Object.entries(e.map.fields)) { if (!key(k)) fail(`invalid map field ${JSON.stringify(k).slice(0,100)}; use
|
|
178
|
+
for (const [k, value] of Object.entries(e.map.fields)) { if (!key(k)) fail(`invalid map field ${JSON.stringify(k).slice(0,100)}; use a letter/underscore identifier such as powerW or 功率, without spaces, dots or hyphens`); validateExpression(value, depth + 1, rows); }
|
|
172
179
|
return;
|
|
173
180
|
}
|
|
174
181
|
if (own(e, 'const')) { fields(e, ['const'], 'expression'); if (!scalar(e.const)) fail('constant must be scalar'); return; }
|
|
@@ -208,6 +215,7 @@ export function validateDocument(input) {
|
|
|
208
215
|
if (++count > LIMITS.nodes) fail('too many nodes');
|
|
209
216
|
if (!object(n) || !key(n.id) || ids.has(n.id)) fail('node ids must be unique identifiers'); ids.add(n.id);
|
|
210
217
|
if (!Object.hasOwn(COMPONENTS, n.type)) fail('unsupported component ' + n.type);
|
|
218
|
+
if (['table','chart'].includes(n.type) && typeof n.rows === 'string' && /^(data|derived)\./.test(n.rows)) n.rows={ref:n.rows};
|
|
211
219
|
switch (n.type) {
|
|
212
220
|
case 'sandbox': {
|
|
213
221
|
|
|
@@ -240,18 +248,19 @@ export function validateDocument(input) {
|
|
|
240
248
|
const [, k] = reference(doc, n.bind), spec = doc.state[k];
|
|
241
249
|
if (n.type === 'slider' && spec.type !== 'number') fail('slider requires numeric state');
|
|
242
250
|
if (n.type === 'toggle' && spec.type !== 'boolean') fail('toggle requires boolean state');
|
|
251
|
+
if (['input','select'].includes(n.type) && spec.type === 'json') fail(`node.${n.id}: structured state requires a sandbox with DSHUI.setState; use separate boolean states and toggle nodes for a native checklist`);
|
|
243
252
|
if (n.type === 'input' && spec.type === 'boolean') fail('use toggle for boolean state');
|
|
244
253
|
if (n.type === 'select' && !spec.options) fail('select requires options'); break;
|
|
245
254
|
}
|
|
246
255
|
case 'button': text(n.text, 'button text'); if (!own(doc.actions, n.action)) fail('unknown action'); break;
|
|
247
256
|
case 'table': {
|
|
248
|
-
if(
|
|
249
|
-
value(n.rows, 'table rows');
|
|
257
|
+
if (n.label !== undefined) text(n.label, 'table label');
|
|
258
|
+
if (!Array.isArray(n.rows)) value(n.rows, 'table rows');
|
|
250
259
|
if (!Array.isArray(n.columns) || !n.columns.length || n.columns.length > 16) fail('invalid table columns');
|
|
251
260
|
const cols = new Set();
|
|
252
261
|
for (const c of n.columns) { fields(c, ['key', 'label', 'unit'], 'column'); if (!key(c.key) || cols.has(c.key)) fail('invalid or duplicate column key'); cols.add(c.key); text(c.label, 'column label'); if (c.unit !== undefined) text(c.unit, 'column unit'); } break;
|
|
253
262
|
}
|
|
254
|
-
case 'chart': value(n.rows, 'chart rows'); if (!['bar', 'line'].includes(n.kind) || !key(n.labelKey) || !key(n.valueKey)) fail('invalid chart'); text(n.label, 'chart label'); if (n.unit !== undefined) text(n.unit, 'chart unit'); if (n.xScale !== undefined && (!['category', 'linear'].includes(n.xScale) || n.xScale === 'linear' && n.kind !== 'line')) fail('invalid xScale'); break;
|
|
263
|
+
case 'chart': if (!Array.isArray(n.rows)) value(n.rows, 'chart rows'); if (!['bar', 'line'].includes(n.kind) || !key(n.labelKey) || !key(n.valueKey)) fail('invalid chart'); text(n.label, 'chart label'); if (n.unit !== undefined) text(n.unit, 'chart unit'); if (n.xScale !== undefined && (!['category', 'linear'].includes(n.xScale) || n.xScale === 'linear' && n.kind !== 'line')) fail('invalid xScale'); break;
|
|
255
264
|
default: fail(`unsupported component ${n.type}`);
|
|
256
265
|
}
|
|
257
266
|
if (['heading', 'text', 'card', 'metric', 'button', 'badge', 'progress', 'details'].includes(n.type)) {
|
|
@@ -284,8 +293,8 @@ export function fallbackText(doc) {
|
|
|
284
293
|
if ((n.type === 'card' || n.type === 'details') && n.title) lines.push(n.title);
|
|
285
294
|
if (n.type === 'progress') lines.push(`${n.label}: ${resolveValue(n.value)} / ${resolveValue(n.max)} ${n.unit ?? ''}`);
|
|
286
295
|
if (n.type === 'steps') for (const item of n.items) lines.push(`[${item.status}] ${item.title}${item.description ? ': ' + item.description : ''}`);
|
|
287
|
-
if (n.type === 'metric') lines.push(`${n.label}: ${formatNumber(
|
|
288
|
-
if (n.type === 'table') { lines.push(n.columns.map(c => c.label + (c.unit ? ` (${c.unit})` : '')).join(' | ')); for (const row of resolveValue(n.rows)) lines.push(n.columns.map(c => Object.hasOwn(row, c.key) ? String(row[c.key]) : '—').join(' | ')); }
|
|
296
|
+
if (n.type === 'metric') { const v=resolveValue(n.value); lines.push(`${n.label}: ${typeof v === 'number' ? formatNumber(v,n.decimals ?? 2) : String(v)} ${n.unit ?? ''}`); }
|
|
297
|
+
if (n.type === 'table') { if(n.label)lines.push(n.label); lines.push(n.columns.map(c => c.label + (c.unit ? ` (${c.unit})` : '')).join(' | ')); for (const row of resolveValue(n.rows)) lines.push(n.columns.map(c => Object.hasOwn(row, c.key) ? String(row[c.key]) : '—').join(' | ')); }
|
|
289
298
|
if (n.type === 'chart') { lines.push(n.label + (n.unit ? ` (${n.unit})` : '')); for (const r of resolveValue(n.rows)) lines.push(`${r[n.labelKey]}: ${r[n.valueKey]}`); }
|
|
290
299
|
if (n.children) walk(n.children);
|
|
291
300
|
} }
|