dsh-motion-ui 0.5.2 → 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/index.js CHANGED
@@ -11,20 +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';
16
+ import {QUICK_EXAMPLES,QUICK_GUIDE} from './lib/quick-examples.js';
15
17
  import { PROTOCOL_GUIDE, calculatorExample, comparisonExample, budgetExample, curveExample, sceneExamples, bicycleExample, oscillatorExample } from './lib/catalog.js';
16
18
 
17
19
  export const name = 'motion-ui';
18
20
  export const inject = ['tools', 'connection', 'systemPrompt'];
19
- 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 侧栏。')}).volatile();
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();
20
34
  export function apply(ctx,config={}) {
21
35
  const store = new UiStore(dshHomePath('plugins', 'dsh-generative-ui'));
22
36
  const settings=()=>typeof config.get==='function'?config.get():config;
23
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\`\`\``:'';
24
- ctx.effect(() => ctx.tools.register(defineTool({
38
+ ctx.effect(() => ctx.tools.register(defineRenderTool({
25
39
  name: 'render_ui',
26
- description: 'Render an 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.',
27
- parameters: { document: { type: 'json', required: true, description: 'DSH UI v1 document. Follow the injected protocol and worked examples; ui_catalog supplies additional schema/examples when needed.' } },
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.',
41
+ parameters: { document: DOCUMENT_PARAMETER },
28
42
  output: {
29
43
  schema: { type: 'json' },
30
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) }],
@@ -38,19 +52,20 @@ export function apply(ctx,config={}) {
38
52
  }
39
53
  })));
40
54
  ctx.effect(()=>ctx.tools.register(defineTool({
41
- name:'render_canvas',description:'Create a freeform interactive HTML/SVG/CSS/JavaScript canvas in the conversation. You design all content, controls, layout and motion. No fixed UI composition. Same persistence, isolation and inspect/patch support as render_ui. Use the injected canvas contract; no external resources or host APIs. Direct native calls only.',
55
+ name:'render_canvas',description:'Create a static or interactive HTML/SVG/CSS/JavaScript canvas in the conversation. You design all content, controls, layout and motion. No fixed UI composition. Same persistence, isolation and inspect/patch support as render_ui. Use the injected canvas contract; no external resources or host APIs. Direct native calls only.',
42
56
  parameters:{title:{type:'string',required:true},fallback:{type:'string',required:true},state:{type:'json'},data:{type:'json'},derived:{type:'json'},height:{type:'number'},css:{type:'string',description:'Emit before html for styled static streaming preview; use an empty string when no custom styles are needed.'},html:{type:'string',required:true},script:{type:'string'}},
43
57
  output:{schema:{type:'json'},render:(_a,v)=>[{type:'text',text:fallbackText(v.document)+`\nUI documentId=${v.documentId}; documentRevision=${v.documentRevision??0}; canvas nodeId=canvas.`+placement(v)}],presentationMeta:(_a,v)=>({dshGenerativeUi:v})},
44
58
  async execute(args,exec){exec.signal.throwIfAborted();const record=await store.create(canvasDocument(args),{sessionId:exec.agent?.id??'unscoped',callId:exec.callId});exec.signal.throwIfAborted();return record;}
45
59
  })));
46
60
  ctx.effect(() => ctx.tools.register(defineTool({
47
- name: 'ui_catalog', description: 'Read the DSH UI protocol and registered components. Optional topic: overview (default, compact examples), canvas (freeform design guide and four visual examples), data, scenes, all (fourteen examples). Prefer targeted topics to avoid unnecessary context. Read-only; omit for ordinary text answers.',
48
- parameters: { topic: { type: 'string', enum: ['overview', 'canvas', 'data', 'scenes', 'all'], description: 'overview, canvas, data, scenes or all; default overview' } }, output: { schema: { type: 'json' }, render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }] },
61
+ name: 'ui_catalog', description: 'Read the DSH UI protocol and registered components. Optional topic: overview (default, compact static examples), quick (four minimal static examples), canvas (freeform design guide and four visual examples), data, scenes, all (fourteen examples). Prefer targeted topics to avoid unnecessary context. Read-only; omit for ordinary text answers.',
62
+ parameters: { topic: { type: 'string', enum: ['overview', 'quick', 'canvas', 'data', 'scenes', 'all'], description: 'overview, quick (four lightweight static layouts), canvas, data, scenes or all; default overview' } }, output: { schema: { type: 'json' }, render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }] },
49
63
  isConcurrencySafe: () => true,
50
64
  async execute({ topic = 'overview' } = {}) {
51
65
  const groups = { overview: [calculatorExample, oscillatorExample], canvas: [bicycleExample, oscillatorExample, architectureExample, scatterExample], data: [calculatorExample, comparisonExample, budgetExample, curveExample, scatterExample], scenes: sceneExamples, all: [calculatorExample, comparisonExample, budgetExample, curveExample, ...sceneExamples, bicycleExample, oscillatorExample, architectureExample, scatterExample] };
52
- if (!Object.hasOwn(groups, topic)) throw new Error('Unknown ui_catalog topic; use overview, canvas, data, scenes or all');
53
- return { build: BUILD_INFO, topic, protocol: PROTOCOL_GUIDE, components: componentCatalog(), design: topic === 'canvas' || topic === 'all' ? CANVAS_DESIGN_GUIDE : 'Choose composition by the question: editorial reading, technical relationships, analytical data, exploratory objects, aligned comparisons or ordered instruction. Keep theme, accessible labels and readable Chinese. Use topic=canvas for the full visual design guide.', examples: groups[topic] };
66
+ groups.quick=QUICK_EXAMPLES;groups.overview=QUICK_EXAMPLES.slice(0,2);
67
+ if (!Object.hasOwn(groups, topic)) throw new Error('Unknown ui_catalog topic; use overview, quick, canvas, data, scenes or all');
68
+ return { build: BUILD_INFO, topic, protocol: PROTOCOL_GUIDE, components: componentCatalog(), design: topic === 'quick' ? QUICK_GUIDE : topic === 'canvas' || topic === 'all' ? CANVAS_DESIGN_GUIDE : 'Choose composition by the question: editorial reading, technical relationships, analytical data, exploratory objects, aligned comparisons or ordered instruction. Keep theme, accessible labels and readable Chinese. Use topic=canvas for the full visual design guide.', examples: groups[topic] };
54
69
  }
55
70
  })));
56
71
  ctx.effect(() => ctx.connection.fetch.register({ path: '/api/plugins/dsh-generative-ui/state', methods: ['GET', 'POST'], requestBody: 'buffered', fetch: stateRoute(store) }));
@@ -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 = /\s*(?:(\d+(?:\.\d*)?|\.\d+)([eE][+-]?\d+)?|("(?:[^"\\]|\\.)*")|([a-zA-Z][a-zA-Z0-9_]*(?:\.[a-zA-Z][a-zA-Z0-9_]*)?)|(===|!==|<=|>=|==|!=|[+*/()?,:<>-]))/y;
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' && /^(state|data|derived|item)\.[a-zA-Z][a-zA-Z0-9_]*$/.test(t)) out = { ref: t };
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","id":"0.5.2+af2abef3c448","sourceHash":"af2abef3c4483e0ce89fb9438a032ad133c7039193778388acb821ce9d353449"});
2
+ export const BUILD_INFO = Object.freeze({"version":"0.5.4","id":"0.5.4+92e88973eabc","sourceHash":"92e88973eabc736e169fbb03366db183ca5cd1fbbe0e3a7e44e5742da708d96e"});
@@ -1,6 +1,6 @@
1
- export const CANVAS_DESIGN_GUIDE = `Design a useful interactive explanation, not a decorated tool card. Choose a visual plan before writing code; examples illustrate capabilities, not templates to copy.
2
- 1. Identify the user's question, the visual relationship and the one primary interaction. Reserve the largest region for the evidence or object being explored. Use a heading, a compact explanation and purposeful supporting details; don't repeat titles or put every paragraph inside another card.
3
- 2. Choose composition by task: mechanical explorer -> large SVG subject, continuous disassembly slider and part focus; parameter experiment -> visualization plus compact controls and exact values; architecture/process -> role-based nodes and clear labelled relationships; data analysis -> chart and exact table with units/source; comparison -> aligned criteria with one meaningful focus; reading/guide -> editorial text with a single supporting illustration. Combine native components and canvases where useful.
1
+ export const CANVAS_DESIGN_GUIDE = `Design a useful visual explanation, static by default and interactive when useful. Choose a visual plan before writing code; examples illustrate capabilities, not templates to copy.
2
+ 1. Identify the user's question, the visual relationship; add a primary interaction only if it helps. Reserve the largest region for the evidence or object being explored. Use a heading, a compact explanation and purposeful supporting details; don't repeat titles or put every paragraph inside another card.
3
+ 2. Choose composition by task: mechanical explorer -> large SVG subject, optional disassembly slider and part focus; parameter experiment -> visualization plus compact controls and exact values; architecture/process -> role-based nodes and clear labelled relationships; data analysis -> chart and exact table with units/source; comparison -> aligned criteria, normally static; reading/guide -> editorial text with a single supporting illustration. Combine native components and canvases where useful.
4
4
  3. Choose visual treatment by content: editorial (reading, minimal borders), technical (geometry, restrained grid, annotations), analytical (quiet surface, aligned numerals), exploratory (large interactive object), comparative (aligned criteria), instructional (ordered explanation). Variation must change hierarchy and composition, not randomly swap accent colors. Keep host theme and Chinese readability. Limit accent areas; semantic warning/success colors describe established meaning, not decoration. Check foreground/background contrast in both themes; pale dark-theme accents need dark foregrounds.
5
5
  4. Diagram grammar: distinguish hierarchy, flow, sequence, dependency and spatial assembly. Give each node one clear idea. Reserve space for labels before routing lines; avoid crossing text, ambiguity and clipped arrows. Prefer a focused overview plus details over dense spaghetti. Don't force every relation into identical cards. SVG labels and units must remain readable on mobile; use viewBox and responsive width. A diagram's numbers must not imply engineering or physical accuracy unless established by evidence.
6
6
  5. Interaction: show an informative initial state; updates need visible feedback and matching explanation. Important inputs use document.state. Read the shared DSHUI snapshot instead of independently recalculating a host-derived result. Use DSHUI.subscribe to drive views, not to echo the same state back. Prefer a few labelled controls. Don't duplicate every native input inside the canvas. Make SVG hotspots keyboard-operable or provide equivalent labelled buttons.
@@ -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
  }
@@ -1,13 +1,17 @@
1
1
  // Optional primitives for model-authored surfaces. Host theme remains authoritative.
2
2
  export const CANVAS_SYSTEM_CSS = `
3
- :root{--canvas-radius:12px;--canvas-gap:16px;--canvas-positive:#167456;--canvas-negative:#b13b3b;--canvas-warning:#875b15}
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
+ .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))}
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)}}
8
11
  @media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}}`;
9
12
 
10
13
  export const CANVAS_SYSTEM_GUIDE = `The canvas includes optional .ui-stack, .ui-grid, .ui-surface, .ui-title, .ui-caption, .ui-value, .ui-controls, .ui-field, .ui-legend, .ui-status, .ui-error and .ui-primary classes. SVG helpers: .ui-node (shape), .ui-edge (unfilled connector), .ui-label (main text), .ui-note (annotation), .ui-leader (dashed annotation line), .ui-accent (theme-aware accent shape). Use them as building blocks, or override with your own composition; they do not prescribe cards or a fixed layout. Theme variables --canvas-bg/soft/ink/muted/line/accent follow DSH. Define --canvas-accent-ink yourself if overriding the accent with a pale color.
14
+ For a small static diagram, .ui-flow arranges .ui-step nodes separated by .ui-arrow; each step uses strong + span. .ui-callout emphasizes one sentence. These work with no custom CSS or JavaScript and adapt to narrow screens.
11
15
  SVG construction: reserve label space first; center box labels with text-anchor=middle and dominant-baseline=central. Measure Chinese characters at roughly one font-size each, Latin letters about .6 font-size; mixed text needs combined width plus 24px padding on each side. These are estimates, not browser measurements. Give same-role nodes aligned dimensions. Route connectors around text and unrelated boxes, with explicit fill=none and visible arrowheads. Compute viewBox bounds from the furthest shape, label and arrow plus padding. A 680-unit diagram shown at 340px halves its fonts: reflow or move captions into HTML instead of shrinking important labels. Keep PC chat-column labels readable at actual rendered size. Use overview + focus detail for dense systems. Do not force this grammar on spatial/mechanical drawings.
12
16
  For calculators validate blanks, finite values, ranges and zero denominators; show an actionable error, never a stale result as valid. Use one state source for filtering/sorting/reset and one animation loop with cleanup. A reset restores inputs and outputs together. For explanation show relationships and one useful variable; for comparison expose criteria/units/assumptions; for a tool provide working inputs, calculation, outputs and reset.
13
17
  For fastest render_canvas preview emit title, fallback, state/data/derived if needed, height, css, html, script in that order. Keep CSS small; put a useful static initial view in html. Preview is non-interactive and scripts only run after successful commit and state restoration. Source order is a delivery optimization, not a thinking limit. Do not claim a preview is a working or saved result.
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 = `DSH UI v1. Call render_ui({document: ...}) directly when interaction helps. Do not call it inside run_code/PTC in this version.
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: ASCII letter followed by letters/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
+ 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 may reference data.key or derived.key containing object rows. 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 use ASCII identifiers such as powerW; Chinese text belongs 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
+ 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'], 'Numeric result with units', 'tone'),
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'], 'Exact supplied or derived values'),
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@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 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=`You have a freeform interactive UI surface 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
+ 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 and options:[string,...]. 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
+ 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);