dsh-motion-ui 0.5.2 → 0.5.3

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
@@ -12,6 +12,7 @@ import { canvasDocument } from './lib/freeform.js';
12
12
  import z from '@deepseek-ai/schemastery';
13
13
  import { toolJson } from './lib/tool-json.js';
14
14
  import { inspectRecord } from './lib/inspection.js';
15
+ import {QUICK_EXAMPLES,QUICK_GUIDE} from './lib/quick-examples.js';
15
16
  import { PROTOCOL_GUIDE, calculatorExample, comparisonExample, budgetExample, curveExample, sceneExamples, bicycleExample, oscillatorExample } from './lib/catalog.js';
16
17
 
17
18
  export const name = 'motion-ui';
@@ -23,7 +24,7 @@ export function apply(ctx,config={}) {
23
24
  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
25
  ctx.effect(() => ctx.tools.register(defineTool({
25
26
  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
+ 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.',
27
28
  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.' } },
28
29
  output: {
29
30
  schema: { type: 'json' },
@@ -38,19 +39,20 @@ export function apply(ctx,config={}) {
38
39
  }
39
40
  })));
40
41
  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.',
42
+ 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
43
  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
44
  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
45
  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
46
  })));
46
47
  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) }] },
48
+ 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.',
49
+ 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
50
  isConcurrencySafe: () => true,
50
51
  async execute({ topic = 'overview' } = {}) {
51
52
  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] };
53
+ groups.quick=QUICK_EXAMPLES;groups.overview=QUICK_EXAMPLES.slice(0,2);
54
+ if (!Object.hasOwn(groups, topic)) throw new Error('Unknown ui_catalog topic; use overview, quick, canvas, data, scenes or all');
55
+ 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
56
  }
55
57
  })));
56
58
  ctx.effect(() => ctx.connection.fetch.register({ path: '/api/plugins/dsh-generative-ui/state', methods: ['GET', 'POST'], requestBody: 'buffered', fetch: stateRoute(store) }));
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.3","id":"0.5.3+634080c7e02a","sourceHash":"634080c7e02ae0667c17a2ce4f94d8999270f1827b7735058deee989caa7777a"});
@@ -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.
@@ -5,9 +5,12 @@ export const CANVAS_SYSTEM_CSS = `
5
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}
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
+ @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
10
  @media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}}`;
9
11
 
10
12
  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.
13
+ 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
14
  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
15
  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
16
  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
@@ -13,7 +13,7 @@ Freeform canvas: {id,type:"sandbox",title,html,css?:string,script?:string,height
13
13
  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
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
15
  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.
16
+ Text/metric values are literal scalars or {ref:...}. Table/chart rows MUST use {ref:"data.rows"} or {ref:"derived.rows"}, not an inline array or bare string. 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 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
17
  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
18
  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
19
  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.
@@ -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:12px;--canvas-gap:16px;--canvas-positive:#167456;--canvas-negative:#b13b3b;--canvas-warning:#875b15}\n:root[data-canvas-scheme=dark]{--canvas-positive:#78d9b4;--canvas-negative:#ffaaaa;--canvas-warning:#edc87e}\nhtml,body{margin:0;min-width:0;background:var(--canvas-bg,#fff);color:var(--canvas-ink,#182537);font:14px/1.6 system-ui,\"Microsoft YaHei\",sans-serif}*{box-sizing:border-box}svg{max-width:100%;height:auto}button,input,select,textarea{font:inherit;color:inherit}button,input,select,textarea{border:1px solid var(--canvas-line,#ced4df);border-radius:8px;background:var(--canvas-bg,#fff);padding:8px 12px;max-width:100%}button{cursor:pointer}button:disabled{cursor:default;opacity:.55}input[type=range]{padding:0;accent-color:var(--canvas-accent,#3e60d3)}input[type=checkbox],input[type=radio]{accent-color:var(--canvas-accent,#3e60d3)}button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--canvas-accent,#3e60d3);outline-offset:3px}\n.ui-stack{display:grid;gap:var(--canvas-gap)}.ui-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr));gap:var(--canvas-gap)}.ui-surface{padding:20px;border:1px solid var(--canvas-line,#ced4df);border-radius:var(--canvas-radius);background:var(--canvas-soft,#f3f5f8)}.ui-title{margin:0;font-size:clamp(20px,3vw,28px);line-height:1.3;letter-spacing:-.02em}.ui-caption{color:var(--canvas-muted,#526176);margin:8px 0;font-size:14px}.ui-value{font-size:clamp(24px,4vw,36px);font-variant-numeric:tabular-nums;line-height:1.2}.ui-controls{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.ui-field{display:grid;gap:6px;min-width:0}.ui-legend{display:flex;gap:16px;flex-wrap:wrap;font-size:13px}.ui-status{min-height:1.6em}.ui-error{color:var(--canvas-negative)}.ui-primary{background:var(--canvas-accent,#3e60d3);color:var(--canvas-accent-ink,#fff);border-color:transparent}\nsvg .ui-node{fill:var(--canvas-soft,#f3f5f8);stroke:var(--canvas-line,#ced4df);stroke-width:1}svg .ui-edge{fill:none;stroke:var(--canvas-muted,#526176);stroke-width:1.5}svg .ui-label{fill:var(--canvas-ink,#182537);font:500 14px system-ui,sans-serif}svg .ui-note{fill:var(--canvas-muted,#526176);font:400 13px system-ui,sans-serif}svg .ui-leader{fill:none;stroke:var(--canvas-muted,#526176);stroke-dasharray:3 4}svg .ui-accent{fill:color-mix(in srgb,var(--canvas-accent,#3e60d3) 12%,var(--canvas-bg,#fff));stroke:var(--canvas-accent,#3e60d3)}\n.ui-flow{display:flex;align-items:center;justify-content:center;gap:16px;padding:24px 8px;flex-wrap:wrap}.ui-step{display:grid;gap:6px;min-width:130px;max-width:240px;padding:16px 20px;border:1px solid var(--canvas-line);border-radius:6px;background:var(--canvas-bg)}.ui-step strong{font-size:16px;font-weight:600}.ui-step span{font-size:14px;color:var(--canvas-muted)}.ui-arrow{color:var(--canvas-muted);font-size:22px}.ui-callout{border-inline-start:3px solid var(--canvas-accent);padding:8px 16px;color:var(--canvas-ink)}\n@media(max-width:480px){.ui-flow{flex-direction:column;padding:16px 8px;gap:12px}.ui-step{width:100%;max-width:none}.ui-arrow{transform:rotate(90deg)}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important}}`;\n\n // lib/sandbox.js\n var SANDBOX_LIMITS = Object.freeze({ source: 49152, height: 1e3, message: 16384, rate: 30 });\n function canvasSnapshot(node2, state2, data, derived, disabled, theme) {\n const pick = (keys, values) => Object.fromEntries((keys ?? []).map((k) => [k, values[k]]));\n return { state: pick(node2.stateKeys, state2), data: pick(node2.dataKeys, data), derived: pick(node2.derivedKeys, derived), disabled, theme };\n }\n function canvasPatch(message, node2) {\n if (!message || typeof message !== \"object\" || Array.isArray(message)) throw new Error(\"Invalid canvas patch\");\n const keys = Object.keys(message);\n if (!keys.length || keys.length > 64 || keys.some((k) => !(node2.stateKeys ?? []).includes(k))) throw new Error(\"Canvas state key is not declared\");\n if (JSON.stringify(message).length > SANDBOX_LIMITS.message) throw new Error(\"Canvas patch is too large\");\n return message;\n }\n function sandboxDocument(node2, channel2, nonce2) {\n const json = (value) => JSON.stringify(value).replace(/</g, \"\\\\u003c\").replace(/\\u2028/g, \"\\\\u2028\").replace(/\\u2029/g, \"\\\\u2029\");\n return `<!doctype html><html><head><meta charset=\"utf-8\"><meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'none'; script-src 'nonce-${nonce2}'; style-src 'unsafe-inline'; img-src data:; font-src 'none'; connect-src 'none'; media-src 'none'; object-src 'none'; frame-src 'none'; worker-src 'none'; base-uri 'none'; form-action 'none'\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><style>${CANVAS_SYSTEM_CSS}</style></head><body><main id=\"canvas\"></main><script nonce=\"${nonce2}\">(()=>{\n const channel=${json(channel2)}, source=${json({ html: node2.html, css: node2.css ?? \"\", script: node2.script ?? \"\" })};\n let snapshot={state:{},data:{},derived:{},disabled:true,theme:{}}, started=false;\n const listeners=new Set(), send=(type,payload)=>parent.postMessage({dshCanvas:1,channel,type,payload},'*');\n const report=e=>send('error',String(e?.message??e).slice(0,1000));\n const audit=${measureVisual.toString()};let auditTimer;const scheduleAudit=()=>{clearTimeout(auditTimer);auditTimer=setTimeout(()=>{const issues=audit(document.getElementById('canvas'),${json(node2.id ?? \"\")});send('audit',{issues,coverage:issues.coverage})},400)};\n addEventListener('resize',scheduleAudit);\n addEventListener('error',e=>report(e.error??e.message));addEventListener('unhandledrejection',e=>report(e.reason));\n const animations=new Set(), motion=matchMedia('(prefers-reduced-motion: reduce)');\n let raf=0,last=0,time=0;\n const visible=()=>snapshot.visible!==false&&!document.hidden;\n const canAnimate=()=>visible()&&!snapshot.disabled&&!motion.matches;\n const draw=delta=>{for(const fn of animations)try{fn({time,delta,reducedMotion:motion.matches,visible:visible()})}catch(e){animations.delete(fn);report(e)}};\n const tick=t=>{raf=0;if(!canAnimate()||!animations.size){last=0;draw(0);return}const delta=last?Math.min(.05,(t-last)/1000):0;last=t;time+=delta;draw(delta);if(animations.size)raf=requestAnimationFrame(tick)};\n const refresh=()=>{draw(0);if(canAnimate()&&animations.size){if(!raf){last=0;raf=requestAnimationFrame(tick)}}else{cancelAnimationFrame(raf);raf=0;last=0}};\n motion.addEventListener('change',refresh);document.addEventListener('visibilitychange',refresh);\n const formatNumber=${formatNumber.toString()};\n const api={formatNumber,get snapshot(){return structuredClone(snapshot)},subscribe(fn){if(typeof fn!=='function')throw Error('Listener required');listeners.add(fn);fn(api.snapshot);return()=>listeners.delete(fn)},setState(patch){if(!snapshot.disabled)send('patch',patch)},animate(fn){if(typeof fn!=='function')throw Error('Animation callback required');animations.add(fn);refresh();return()=>{animations.delete(fn);if(!animations.size){cancelAnimationFrame(raf);raf=0;last=0}}}};\n Object.defineProperty(window,'DSHUI',{value:Object.freeze(api),writable:false});\n for(const type of ['click','input','change','keydown','pointerdown'])document.addEventListener(type,e=>{if(snapshot.disabled){e.preventDefault();e.stopImmediatePropagation()}},true);\n addEventListener('message',event=>{\n const m=event.data;if(event.source!==parent||m?.dshCanvas!==1||m.channel!==channel||m.type!=='snapshot')return;\n snapshot=m.payload;for(const [k,v] of Object.entries(snapshot.theme??{}))document.documentElement.style.setProperty('--canvas-'+k,v);\n document.documentElement.style.colorScheme=snapshot.theme?.scheme??'light';document.documentElement.dataset.canvasScheme=snapshot.theme?.scheme??'light';\n const wasStarted=started;\n if(!started){started=true;document.getElementById('canvas').innerHTML=source.html;const style=document.createElement('style');style.textContent=source.css;document.head.append(style);const script=document.createElement('script');script.nonce=${json(nonce2)};script.textContent=source.script;document.body.append(script)}\n if(wasStarted)for(const fn of listeners)try{fn(api.snapshot)}catch(e){report(e)}\n refresh();if(!wasStarted)send('mounted');scheduleAudit();\n });\n let resizePending=false;new ResizeObserver(()=>{if(!started||resizePending)return;resizePending=true;requestAnimationFrame(()=>{resizePending=false;send('height',Math.ceil(document.getElementById('canvas').getBoundingClientRect().height))})}).observe(document.getElementById('canvas'));\n send('ready');\n})();<\\/script></body></html>`;\n }\n\n // lib/components.js\n var define = (fields2, purpose, presentation = \"\") => Object.freeze({ fields: Object.freeze(fields2), purpose, presentation });\n var COMPONENTS = Object.freeze({\n heading: define([\"text\"], \"Section heading\", \"tone\"),\n text: define([\"text\"], \"Readable explanation or bound text\", \"tone\"),\n row: define([\"children\"], \"Wrapping parallel content\"),\n column: define([\"children\"], \"Ordered vertical content\"),\n card: define([\"title\", \"children\", \"surface\"], \"Meaningful grouped section\", \"tone\"),\n details: define([\"title\", \"children\", \"open\"], \"Collapsible supporting content\", \"tone\"),\n badge: define([\"text\"], \"Short explicit status label\", \"tone\"),\n progress: define([\"label\", \"value\", \"max\", \"unit\"], \"Declared numeric progress snapshot\", \"tone\"),\n steps: define([\"items\"], \"Ordered declared steps\"),\n metric: define([\"label\", \"value\", \"unit\", \"decimals\"], \"Numeric result with units\", \"tone\"),\n input: define([\"label\", \"bind\"], \"String or numeric state input\"),\n slider: define([\"label\", \"bind\"], \"Numeric state control\"),\n select: define([\"label\", \"bind\"], \"Selection from declared state options\"),\n toggle: define([\"label\", \"bind\"], \"Boolean state control\"),\n button: define([\"text\", \"action\"], \"Declared Agent follow-up\", \"icon\"),\n table: define([\"rows\", \"columns\"], \"Exact supplied or derived values\"),\n chart: define([\"kind\", \"rows\", \"labelKey\", \"valueKey\", \"label\", \"unit\", \"xScale\"], \"Bar/line comparison with accessible data\"),\n sandbox: define([\"title\", \"html\", \"css\", \"script\", \"fallback\", \"height\", \"stateKeys\", \"dataKeys\", \"derivedKeys\"], \"Freeform isolated HTML/SVG/CSS/JavaScript visualization\")\n });\n function componentFields(type) {\n const c = Object.hasOwn(COMPONENTS, type) ? COMPONENTS[type] : void 0;\n return c && [\"id\", \"type\", ...c.fields, ...c.presentation ? [\"icon\"] : [], ...c.presentation === \"tone\" ? [\"tone\"] : []];\n }\n\n // lib/author-expression.js\n function compileExpression(source) {\n const fail2 = () => {\n throw new Error(\"DSH UI: invalid formula; use state.x, data.x, derived.x, item.x, numbers, strings, + - * /, comparisons, ?: and min/max/round/abs\");\n };\n if (typeof source !== \"string\" || !source.trim() || source.length > 2048) fail2();\n const tokens = [];\n let offset = 0;\n const token2 = /\\s*(?:(\\d+(?:\\.\\d*)?|\\.\\d+)([eE][+-]?\\d+)?|(\"(?:[^\"\\\\]|\\\\.)*\")|([a-zA-Z][a-zA-Z0-9_]*(?:\\.[a-zA-Z][a-zA-Z0-9_]*)?)|(===|!==|<=|>=|==|!=|[+*/()?,:<>-]))/y;\n while (offset < source.length) {\n if (!source.slice(offset).trim()) break;\n token2.lastIndex = offset;\n const m = token2.exec(source);\n if (!m) fail2();\n offset = token2.lastIndex;\n tokens.push(m[1] ? { number: Number(m[1] + (m[2] ?? \"\")) } : m[3] ? { string: JSON.parse(m[3]) } : m[4] ?? m[5]);\n if (tokens.length > 256) fail2();\n }\n let pos = 0;\n const operators = { \"==\": [\"eq\", 1], \"===\": [\"eq\", 1], \"!=\": [\"neq\", 1], \"!==\": [\"neq\", 1], \"<\": [\"lt\", 2], \"<=\": [\"lte\", 2], \">\": [\"gt\", 2], \">=\": [\"gte\", 2], \"+\": [\"add\", 3], \"-\": [\"sub\", 3], \"*\": [\"mul\", 4], \"/\": [\"div\", 4] };\n const take = (t) => {\n if (tokens[pos++] !== t) fail2();\n };\n function parse(min = 0, depth = 0) {\n if (depth > 12) fail2();\n const t = tokens[pos++];\n let out;\n if (t && typeof t === \"object\") out = { const: Object.hasOwn(t, \"number\") ? t.number : t.string };\n else if (t === \"true\" || t === \"false\") out = { const: t === \"true\" };\n else if (t === \"(\") {\n out = parse(0, depth + 1);\n take(\")\");\n } else if (t === \"-\" || t === \"+\") {\n const arg = parse(5, depth + 1);\n out = t === \"-\" ? { op: \"sub\", args: [{ const: 0 }, arg] } : arg;\n } else if (typeof t === \"string\" && /^(state|data|derived|item)\\.[a-zA-Z][a-zA-Z0-9_]*$/.test(t)) out = { ref: t };\n else if ([\"min\", \"max\", \"round\", \"abs\"].includes(t)) {\n take(\"(\");\n const args = [parse(0, depth + 1)];\n while (tokens[pos] === \",\") {\n pos++;\n args.push(parse(0, depth + 1));\n if (args.length > 8) fail2();\n }\n take(\")\");\n out = { op: t, args };\n } else fail2();\n while (typeof tokens[pos] === \"string\" && Object.hasOwn(operators, tokens[pos]) && operators[tokens[pos]][1] >= min) {\n const [op, precedence] = operators[tokens[pos++]];\n out = { op, args: [out, parse(precedence + 1, depth + 1)] };\n }\n if (min === 0 && tokens[pos] === \"?\") {\n pos++;\n const yes = parse(0, depth + 1);\n take(\":\");\n out = { op: \"if\", args: [out, yes, parse(0, depth + 1)] };\n }\n return out;\n }\n const result = parse();\n if (pos !== tokens.length) fail2();\n return result;\n }\n function expandExpressions(doc2) {\n const expression = (e) => {\n if (!e || typeof e !== \"object\" || Array.isArray(e)) return e;\n if (Object.hasOwn(e, \"expr\")) {\n if (Object.keys(e).length !== 1) throw new Error(\"DSH UI: expr must be the only formula field\");\n return compileExpression(e.expr);\n }\n if (Array.isArray(e.args)) e.args = e.args.map(expression);\n if (e.map?.fields) for (const k of Object.keys(e.map.fields)) e.map.fields[k] = expression(e.map.fields[k]);\n return e;\n };\n if (doc2.derived && typeof doc2.derived === \"object\") for (const k of Object.keys(doc2.derived)) doc2.derived[k] = expression(doc2.derived[k]);\n const walk = (nodes) => {\n if (!Array.isArray(nodes)) return;\n for (const node2 of nodes) {\n if (!node2 || typeof node2 !== \"object\") continue;\n for (const field of [\"text\", \"value\", \"max\", \"rows\"]) if (Object.hasOwn(node2, field)) node2[field] = expression(node2[field]);\n walk(node2.children);\n }\n };\n walk(doc2.nodes);\n return doc2;\n }\n\n // lib/protocol.js\n var LIMITS = Object.freeze({ bytes: 131072, nodes: 160, depth: 12, rows: 500, expressions: 128, steps: 4096 });\n var own = (o, k) => Object.hasOwn(o, k);\n var fail = (message) => {\n throw new Error(`DSH UI: ${message}`);\n };\n var object = (x) => x !== null && typeof x === \"object\" && !Array.isArray(x);\n var key = (x) => typeof x === \"string\" && /^[a-zA-Z][a-zA-Z0-9_]{0,63}$/.test(x) && ![\"constructor\", \"prototype\", \"__proto__\"].includes(x);\n var finite = (x) => typeof x === \"number\" && Number.isFinite(x);\n var scalar = (x) => typeof x === \"string\" || typeof x === \"boolean\" || finite(x);\n var text = (x, at) => {\n if (typeof x !== \"string\" || x.length > 8192) fail(`${at} must be a string of at most 8192 characters`);\n };\n function fields(value, allowed, at) {\n if (!object(value)) fail(`${at} must be an object`);\n for (const k of Object.keys(value)) if (!allowed.includes(k)) {\n const hint = at === \"document\" ? \" Controls and visual components belong in nodes as registered typed nodes, not extra document fields.\" : \"\";\n fail(`unknown field ${at}.${k}; allowed fields at ${at}: ${allowed.join(\", \")}.${hint}`);\n }\n }\n function safeJson(value, depth = 0, source = false) {\n if (depth > 48) fail(\"JSON nesting is too deep\");\n if (value === null || typeof value === \"boolean\") return;\n if (typeof value === \"string\") {\n if (source) {\n if (value.length > SANDBOX_LIMITS.source) fail(\"canvas source exceeds 48 Ki characters\");\n return;\n }\n return text(value, \"value\");\n }\n if (typeof value === \"number\") {\n if (!finite(value)) fail(\"numbers must be finite\");\n return;\n }\n if (typeof value !== \"object\") fail(\"only JSON values are accepted\");\n if (Array.isArray(value) && value.length > LIMITS.rows) fail(\"too many array items\");\n for (const k of Object.keys(value)) {\n if ([\"__proto__\", \"prototype\", \"constructor\"].includes(k)) fail(\"unsafe property name\");\n safeJson(value[k], depth + 1, value.type === \"sandbox\" && [\"html\", \"css\", \"script\"].includes(k));\n }\n }\n function initialState(doc2) {\n return Object.fromEntries(Object.entries(doc2.state).map(([k, spec]) => [k, spec.default]));\n }\n function validateState(doc2, state2) {\n if (!object(state2) || Object.keys(state2).length !== Object.keys(doc2.state).length) fail(\"state keys do not match the document\");\n const result = {};\n for (const [k, s] of Object.entries(doc2.state)) {\n if (!own(state2, k)) fail(`missing state.${k}`);\n const v = state2[k];\n if (s.type === \"number\") {\n if (!finite(v) || v < s.min || v > s.max) fail(`state.${k} is outside its numeric range`);\n const offset = (v - s.min) / s.step;\n if (Math.abs(offset - Math.round(offset)) > 1e-6) fail(`state.${k} does not match its step`);\n } else if (s.type === \"string\") text(v, `state.${k}`);\n else if (typeof v !== \"boolean\") fail(`state.${k} must be boolean`);\n if (s.options && !s.options.includes(v)) fail(`invalid option for state.${k}`);\n result[k] = v;\n }\n return result;\n }\n var ARITY = { add: [1, 8], sub: [2, 2], mul: [1, 8], div: [2, 2], min: [1, 8], max: [1, 8], round: [1, 1], abs: [1, 1], eq: [2, 2], neq: [2, 2], lt: [2, 2], lte: [2, 2], gt: [2, 2], gte: [2, 2], if: [3, 3] };\n function reference(doc2, ref) {\n if (typeof ref !== \"string\" || !/^(state|data|derived)\\.[a-zA-Z][a-zA-Z0-9_]{0,63}$/.test(ref)) fail(\"invalid reference\");\n const [group, k] = ref.split(\".\");\n if (!own(doc2[group], k)) fail(`unknown reference ${ref}`);\n return [group, k];\n }\n function evaluate(doc2, state2) {\n const derived = {}, active = /* @__PURE__ */ new Set();\n let steps = 0, generatedRows = 0;\n const budget = () => {\n if (++steps > LIMITS.steps) fail(\"calculation budget exceeded\");\n };\n const resolve = (ref) => {\n budget();\n const [group, k] = reference(doc2, ref);\n if (group !== \"derived\") return (group === \"state\" ? state2 : doc2.data)[k];\n if (own(derived, k)) return derived[k];\n if (active.has(k)) fail(`cyclic calculation ${k}`);\n active.add(k);\n const result2 = expression(doc2.derived[k]);\n active.delete(k);\n derived[k] = result2;\n return result2;\n };\n const expression = (e, item) => {\n budget();\n if (own(e, \"map\")) {\n const rows = resolve(e.map.source);\n if ((generatedRows += rows.length) > LIMITS.rows) fail(\"generated row limit exceeded\");\n return rows.map((row) => Object.fromEntries(Object.entries(e.map.fields).map(([k, value]) => {\n const out2 = expression(value, row);\n if (!scalar(out2)) fail(\"generated cells must be scalar\");\n return [k, out2];\n })));\n }\n if (own(e, \"const\")) return e.const;\n if (own(e, \"ref\")) {\n if (e.ref.startsWith(\"item.\")) {\n const k = e.ref.slice(5);\n if (!item || !own(item, k) || !scalar(item[k])) fail(\"invalid item reference\");\n return item[k];\n }\n return resolve(e.ref);\n }\n if (e.op === \"if\") {\n const condition = expression(e.args[0], item);\n if (typeof condition !== \"boolean\") fail(\"if condition must be boolean\");\n return expression(e.args[condition ? 1 : 2], item);\n }\n const a = e.args.map((arg) => expression(arg, item));\n if (e.op === \"eq\") return a[0] === a[1];\n if (e.op === \"neq\") return a[0] !== a[1];\n if (!a.every(finite)) fail(`operator ${e.op} requires finite numbers`);\n let out;\n switch (e.op) {\n case \"add\":\n out = a.reduce((x, y) => x + y, 0);\n break;\n case \"sub\":\n out = a[0] - a[1];\n break;\n case \"mul\":\n out = a.reduce((x, y) => x * y, 1);\n break;\n case \"div\":\n if (a[1] === 0) fail(\"division by zero\");\n out = a[0] / a[1];\n break;\n case \"min\":\n out = Math.min(...a);\n break;\n case \"max\":\n out = Math.max(...a);\n break;\n case \"round\":\n out = Math.round(a[0]);\n break;\n case \"abs\":\n out = Math.abs(a[0]);\n break;\n case \"lt\":\n return a[0] < a[1];\n case \"lte\":\n return a[0] <= a[1];\n case \"gt\":\n return a[0] > a[1];\n case \"gte\":\n return a[0] >= a[1];\n default:\n fail(`unsupported operator ${e.op}`);\n }\n if (!finite(out)) fail(\"non-finite calculation result\");\n return out;\n };\n for (const k of Object.keys(doc2.derived)) resolve(`derived.${k}`);\n if (new TextEncoder().encode(JSON.stringify(derived)).length > LIMITS.bytes) fail(\"derived output exceeds 128 KiB\");\n const result = { derived, resolveValue: (value) => {\n if (!object(value)) return value;\n return expression(value);\n } };\n checkViewValues(doc2, result);\n return result;\n }\n function checkViewValues(doc2, evaluated) {\n function walk(list) {\n for (const n of list) {\n if (n.type === \"table\" || n.type === \"chart\") {\n const rows = evaluated.resolveValue(n.rows);\n if (!Array.isArray(rows) || rows.length > LIMITS.rows || !rows.every(object)) fail(\"table/chart rows must reference an array of objects\");\n if (n.type === \"chart\" && rows.some((r) => !own(r, n.labelKey) || !scalar(r[n.labelKey]) || !own(r, n.valueKey) || !finite(r[n.valueKey]))) fail(\"chart requires labels and finite numeric values\");\n if (n.type === \"chart\" && n.xScale === \"linear\" && rows.some((r) => !finite(r[n.labelKey]))) fail(\"linear chart requires numeric x values\");\n }\n if (n.type === \"metric\" && !finite(evaluated.resolveValue(n.value))) fail(\"metric requires a numeric value\");\n if (n.type === \"progress\") {\n const v = evaluated.resolveValue(n.value), max = evaluated.resolveValue(n.max);\n if (!finite(v) || !finite(max) || max <= 0 || v < 0 || v > max) fail(\"progress requires 0 <= value <= positive max\");\n }\n if (n.children) walk(n.children);\n }\n }\n walk(doc2.nodes);\n }\n function validateDocument(input) {\n safeJson(input);\n const json = JSON.stringify(input);\n if (new TextEncoder().encode(json).length > LIMITS.bytes) fail(\"document exceeds 128 KiB\");\n const doc2 = expandExpressions(JSON.parse(json));\n fields(doc2, [\"version\", \"title\", \"fallback\", \"state\", \"data\", \"derived\", \"nodes\", \"actions\", \"presentation\"], \"document\");\n if (doc2.presentation !== void 0) {\n fields(doc2.presentation, [\"layout\", \"accent\", \"density\"], \"presentation\");\n for (const [k, allowed] of Object.entries({ layout: UI_LAYOUTS, accent: UI_ACCENTS, density: [\"comfortable\", \"compact\"] }))\n if (doc2.presentation[k] !== void 0 && !allowed.includes(doc2.presentation[k])) fail(`unknown presentation ${k}`);\n }\n if (doc2.version !== 1) fail(\"unsupported protocol version\");\n text(doc2.title, \"title\");\n text(doc2.fallback, \"fallback\");\n if (!doc2.title.trim() || !doc2.fallback.trim()) fail(\"title and fallback cannot be empty\");\n for (const group of [\"state\", \"data\", \"derived\", \"actions\"]) {\n doc2[group] ??= {};\n if (!object(doc2[group]) || Object.keys(doc2[group]).length > 64) fail(`invalid ${group}`);\n for (const k of Object.keys(doc2[group])) if (!key(k)) fail(`invalid key in ${group}`);\n }\n for (const [k, s] of Object.entries(doc2.state)) {\n fields(s, [\"type\", \"default\", \"min\", \"max\", \"step\", \"options\"], `state.${k}`);\n if (![\"number\", \"string\", \"boolean\"].includes(s.type)) fail(`unsupported state type ${k}`);\n if (s.type === \"number\") {\n if (![s.min, s.max, s.step].every(finite) || s.min >= s.max || s.step <= 0) fail(`invalid numeric limits for ${k}`);\n if ((s.max - s.min) / s.step > 1e9) fail(`numeric step too small for ${k}`);\n } else if ([\"min\", \"max\", \"step\"].some((p) => own(s, p))) fail(`numeric limits on non-number ${k}`);\n if (s.options && (!Array.isArray(s.options) || !s.options.length || s.options.length > 64 || !s.options.every(scalar))) fail(`invalid options for ${k}`);\n }\n const defaults = validateState(doc2, initialState(doc2));\n let expressions = 0;\n for (const [k, spec] of Object.entries(doc2.state))\n for (const option of spec.options ?? []) validateState(doc2, { ...defaults, [k]: option });\n function validateExpression(e, depth = 0, itemRows) {\n if (++expressions > LIMITS.expressions || depth > LIMITS.depth) fail(\"expression limit exceeded\");\n if (!object(e)) fail(\"expression must be an object\");\n if (own(e, \"map\")) {\n if (depth !== 0) fail(\"map is only allowed at the root of a derived value\");\n fields(e, [\"map\"], \"expression\");\n fields(e.map, [\"source\", \"fields\"], \"map\");\n if (typeof e.map.source !== \"string\" || !e.map.source.startsWith(\"data.\")) fail('map source must reference static data using a string such as \"data.samples\"; put object rows in document.data.samples, not inline in map.source');\n const [, k] = reference(doc2, e.map.source), rows = doc2.data[k];\n if (!Array.isArray(rows) || rows.length > LIMITS.rows || !rows.every(object)) fail(\"map source must be object rows\");\n if (!object(e.map.fields) || !Object.keys(e.map.fields).length || Object.keys(e.map.fields).length > 16) fail(\"map requires 1..16 fields\");\n for (const [k2, value2] of Object.entries(e.map.fields)) {\n if (!key(k2)) fail(`invalid map field ${JSON.stringify(k2).slice(0, 100)}; use an ASCII identifier matching [a-zA-Z][a-zA-Z0-9_]{0,63}, such as powerW. Put Chinese display text in column/chart labels, not internal field keys`);\n validateExpression(value2, depth + 1, rows);\n }\n return;\n }\n if (own(e, \"const\")) {\n fields(e, [\"const\"], \"expression\");\n if (!scalar(e.const)) fail(\"constant must be scalar\");\n return;\n }\n if (own(e, \"ref\")) {\n fields(e, [\"ref\"], \"expression\");\n if (typeof e.ref === \"string\" && e.ref.startsWith(\"item.\")) {\n const k = e.ref.slice(5);\n if (!itemRows || !key(k) || itemRows.some((row) => !own(row, k) || !scalar(row[k]))) fail(\"invalid or unscoped item reference\");\n } else reference(doc2, e.ref);\n return;\n }\n fields(e, [\"op\", \"args\"], \"expression\");\n const arity = ARITY[e.op];\n if (!arity || !Array.isArray(e.args) || e.args.length < arity[0] || e.args.length > arity[1]) fail(\"invalid operator or argument count\");\n e.args.forEach((a) => validateExpression(a, depth + 1, itemRows));\n }\n Object.values(doc2.derived).forEach((e) => validateExpression(e));\n const visited = /* @__PURE__ */ new Set(), active = /* @__PURE__ */ new Set();\n function visit(k) {\n if (active.has(k)) fail(`cyclic calculation ${k}`);\n if (visited.has(k)) return;\n active.add(k);\n function walk(e) {\n if (e.ref?.startsWith(\"derived.\")) visit(e.ref.split(\".\")[1]);\n for (const a of e.args ?? []) walk(a);\n for (const value2 of Object.values(e.map?.fields ?? {})) walk(value2);\n }\n walk(doc2.derived[k]);\n active.delete(k);\n visited.add(k);\n }\n Object.keys(doc2.derived).forEach(visit);\n for (const [k, a] of Object.entries(doc2.actions)) {\n fields(a, [\"type\", \"intent\", \"stateKeys\"], `actions.${k}`);\n if (a.type !== \"agent.followup\") fail(\"only agent.followup actions are supported\");\n text(a.intent, \"action intent\");\n if (!a.intent.trim() || !Array.isArray(a.stateKeys) || a.stateKeys.length > 64 || new Set(a.stateKeys).size !== a.stateKeys.length || a.stateKeys.some((s) => !own(doc2.state, s))) fail(\"invalid action stateKeys\");\n }\n const ids = /* @__PURE__ */ new Set();\n let count2 = 0;\n const value = (v, at) => {\n if (object(v)) {\n if (own(v, \"map\")) fail(\"map belongs in derived\");\n validateExpression(v);\n } else if (!scalar(v)) fail(`invalid value at ${at}`);\n };\n function nodes(list, depth = 0) {\n if (!Array.isArray(list) || depth > LIMITS.depth) fail(\"invalid node children or depth\");\n for (const n of list) {\n if (++count2 > LIMITS.nodes) fail(\"too many nodes\");\n if (!object(n) || !key(n.id) || ids.has(n.id)) fail(\"node ids must be unique identifiers\");\n ids.add(n.id);\n if (!Object.hasOwn(COMPONENTS, n.type)) fail(\"unsupported component \" + n.type);\n switch (n.type) {\n case \"sandbox\": {\n text(n.title, \"canvas title\");\n text(n.fallback, \"canvas fallback\");\n if (!n.title.trim() || !n.fallback.trim() || typeof n.html !== \"string\" || !n.html.trim()) fail(\"canvas requires title, HTML and fallback\");\n for (const field of [\"html\", \"css\", \"script\"]) if (n[field] !== void 0 && (typeof n[field] !== \"string\" || n[field].length > SANDBOX_LIMITS.source)) fail(\"canvas source exceeds 48 Ki characters\");\n if (n.height !== void 0 && (!Number.isInteger(n.height) || n.height < 160 || n.height > SANDBOX_LIMITS.height)) fail(\"canvas height must be 160..1000\");\n for (const [field, group] of [[\"stateKeys\", \"state\"], [\"dataKeys\", \"data\"], [\"derivedKeys\", \"derived\"]]) if (n[field] !== void 0 && (!Array.isArray(n[field]) || n[field].length > 64 || new Set(n[field]).size !== n[field].length || n[field].some((k) => !own(doc2[group], k)))) fail(\"invalid canvas \" + field);\n break;\n }\n case \"row\":\n case \"column\":\n break;\n case \"card\":\n if (n.title !== void 0) text(n.title, \"card title\");\n if (n.surface !== void 0 && !UI_SURFACES.includes(n.surface)) fail(\"unknown card surface\");\n break;\n case \"details\":\n text(n.title, \"details title\");\n if (n.open !== void 0 && typeof n.open !== \"boolean\") fail(\"details open must be boolean\");\n break;\n case \"badge\":\n value(n.text, \"badge text\");\n break;\n case \"progress\":\n text(n.label, \"progress label\");\n value(n.value, \"progress value\");\n value(n.max, \"progress max\");\n if (n.unit !== void 0) text(n.unit, \"progress unit\");\n break;\n case \"steps\": {\n if (!Array.isArray(n.items) || !n.items.length || n.items.length > 12) fail(\"steps require 1..12 items\");\n for (const item of n.items) {\n fields(item, [\"title\", \"description\", \"status\"], \"step\");\n text(item.title, \"step title\");\n if (item.description !== void 0) text(item.description, \"step description\");\n if (![\"pending\", \"active\", \"done\", \"error\"].includes(item.status)) fail(\"unknown step status\");\n }\n break;\n }\n case \"heading\":\n case \"text\":\n value(n.text, \"text\");\n break;\n case \"metric\":\n text(n.label, \"metric label\");\n value(n.value, \"metric value\");\n if (n.unit !== void 0) text(n.unit, \"unit\");\n if (n.decimals !== void 0 && (!Number.isInteger(n.decimals) || n.decimals < 0 || n.decimals > 6)) fail(\"invalid decimals\");\n break;\n case \"input\":\n case \"slider\":\n case \"select\":\n case \"toggle\": {\n text(n.label, \"control label\");\n if (typeof n.bind !== \"string\" || !n.bind.startsWith(\"state.\")) fail(\"controls must bind state\");\n const [, k] = reference(doc2, n.bind), spec = doc2.state[k];\n if (n.type === \"slider\" && spec.type !== \"number\") fail(\"slider requires numeric state\");\n if (n.type === \"toggle\" && spec.type !== \"boolean\") fail(\"toggle requires boolean state\");\n if (n.type === \"input\" && spec.type === \"boolean\") fail(\"use toggle for boolean state\");\n if (n.type === \"select\" && !spec.options) fail(\"select requires options\");\n break;\n }\n case \"button\":\n text(n.text, \"button text\");\n if (!own(doc2.actions, n.action)) fail(\"unknown action\");\n break;\n case \"table\": {\n if (Array.isArray(n.rows)) fail('table.rows must use {ref:\"data.rows\"}; move the array into document.data.rows, not inline in the table');\n value(n.rows, \"table rows\");\n if (!Array.isArray(n.columns) || !n.columns.length || n.columns.length > 16) fail(\"invalid table columns\");\n const cols = /* @__PURE__ */ new Set();\n for (const c of n.columns) {\n fields(c, [\"key\", \"label\", \"unit\"], \"column\");\n if (!key(c.key) || cols.has(c.key)) fail(\"invalid or duplicate column key\");\n cols.add(c.key);\n text(c.label, \"column label\");\n if (c.unit !== void 0) text(c.unit, \"column unit\");\n }\n break;\n }\n case \"chart\":\n value(n.rows, \"chart rows\");\n if (![\"bar\", \"line\"].includes(n.kind) || !key(n.labelKey) || !key(n.valueKey)) fail(\"invalid chart\");\n text(n.label, \"chart label\");\n if (n.unit !== void 0) text(n.unit, \"chart unit\");\n if (n.xScale !== void 0 && (![\"category\", \"linear\"].includes(n.xScale) || n.xScale === \"linear\" && n.kind !== \"line\")) fail(\"invalid xScale\");\n break;\n default:\n fail(`unsupported component ${n.type}`);\n }\n if ([\"heading\", \"text\", \"card\", \"metric\", \"button\", \"badge\", \"progress\", \"details\"].includes(n.type)) {\n if (n.icon !== void 0 && !UI_ICONS.includes(n.icon)) fail(\"unknown UI icon\");\n if (n.type !== \"button\") {\n if (n.tone !== void 0 && !UI_TONES.includes(n.tone)) fail(\"unknown UI tone\");\n }\n }\n fields(n, componentFields(n.type), `node.${n.id}`);\n if ([\"row\", \"column\", \"card\", \"details\"].includes(n.type)) nodes(n.children, depth + 1);\n }\n }\n nodes(doc2.nodes);\n if (!count2) fail(\"at least one node is required\");\n evaluate(doc2, defaults);\n return doc2;\n }\n\n // src/export-runtime.js\n var payload = JSON.parse(document.getElementById(\"dsh-ui-export\").textContent);\n var doc = validateDocument(payload.document);\n var node = doc.nodes.flatMap(function flatten(n) {\n return [n, ...(n.children ?? []).flatMap(flatten)];\n }).find((n) => n.id === payload.nodeId && n.type === \"sandbox\");\n if (!node) throw Error(\"\\u5BFC\\u51FA\\u7684\\u753B\\u5E03\\u4E0D\\u5B58\\u5728\");\n var state = validateState(doc, payload.state);\n var connected = false;\n var start = Date.now();\n var count = 0;\n var token = () => Array.from(crypto.getRandomValues(new Uint8Array(16)), (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n var channel = token();\n var nonce = payload.nonce;\n var frame = document.createElement(\"iframe\");\n frame.title = node.title;\n frame.sandbox = \"allow-scripts\";\n frame.referrerPolicy = \"no-referrer\";\n frame.style.cssText = \"display:block;width:100%;height:\" + (node.height ?? 420) + \"px;border:0\";\n var status = document.getElementById(\"status\");\n var publish = () => {\n try {\n frame.contentWindow.postMessage({ dshCanvas: 1, channel, type: \"snapshot\", payload: { ...canvasSnapshot(node, state, doc.data, evaluate(doc, state).derived, false, payload.theme), visible: !document.hidden } }, \"*\");\n } catch (e) {\n status.textContent = \"\\u8BA1\\u7B97\\u5931\\u8D25\\uFF1A\" + e.message;\n }\n };\n addEventListener(\"message\", (event) => {\n const m = event.data;\n if (event.source !== frame.contentWindow || m?.channel !== channel || m.dshCanvas !== 1) return;\n if (Date.now() - start > 1e3) {\n start = Date.now();\n count = 0;\n }\n if (++count > SANDBOX_LIMITS.rate) return;\n if (m.type === \"ready\") {\n connected = true;\n publish();\n } else if (m.type === \"patch\" && connected) {\n try {\n const next = validateState(doc, { ...state, ...canvasPatch(m.payload, node) });\n evaluate(doc, next);\n state = next;\n status.textContent = \"\\u672C\\u5730\\u53C2\\u6570\\u5DF2\\u66F4\\u65B0\\uFF1B\\u5237\\u65B0\\u6587\\u4EF6\\u4F1A\\u56DE\\u5230\\u5BFC\\u51FA\\u65F6\\u7684\\u53C2\\u6570\\u3002\";\n } catch (e) {\n status.textContent = \"\\u53C2\\u6570\\u65E0\\u6548\\uFF1A\" + e.message;\n }\n publish();\n } else if (m.type === \"height\" && Number.isFinite(m.payload)) frame.style.height = Math.max(160, Math.min(1e3, Math.ceil(m.payload))) + \"px\";\n else if (m.type === \"error\") status.textContent = \"\\u753B\\u5E03\\u8FD0\\u884C\\u5F02\\u5E38\\uFF1A\" + String(m.payload).slice(0, 1e3);\n });\n document.addEventListener(\"visibilitychange\", () => {\n if (connected) publish();\n });\n document.getElementById(\"surface\").append(frame);\n frame.srcdoc = sandboxDocument(node, channel, nonce);\n})();\n";
package/lib/freeform.js CHANGED
@@ -5,7 +5,7 @@ export function canvasDocument({title,fallback,html,css,script,height,state,data
5
5
  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
6
  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
7
  }
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.
8
+ export const FREE_UI_CONTRACT=`You 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
9
  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
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
11
  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.
package/lib/protocol.js CHANGED
@@ -245,6 +245,7 @@ export function validateDocument(input) {
245
245
  }
246
246
  case 'button': text(n.text, 'button text'); if (!own(doc.actions, n.action)) fail('unknown action'); break;
247
247
  case 'table': {
248
+ if(Array.isArray(n.rows))fail('table.rows must use {ref:"data.rows"}; move the array into document.data.rows, not inline in the table');
248
249
  value(n.rows, 'table rows');
249
250
  if (!Array.isArray(n.columns) || !n.columns.length || n.columns.length > 16) fail('invalid table columns');
250
251
  const cols = new Set();
@@ -0,0 +1,8 @@
1
+ // Small authored starting points. Static documents need no state or script.
2
+ export const QUICK_GUIDE=`轻量可视化优先:先用一幅静态图、短流程、对齐对比或少量指标讲清楚;只有参数变化/选择本身有价值才加控件。默认不写 script、不建 state、不加动画。布局用宿主组件或已有 ui-* 类,不自建主题和绘图引擎。流程 3–5 步,架构 3–6 节点,比较 2–3 方案,每项 1–2 句;这些是起点,不是截断用户必要内容的硬限制。中性背景、细边框、一种强调色、清楚的标题与直接标注;不用阴影、发光或嵌套卡片装饰。复杂细节按用户后续问题扩展。`;
3
+ export const QUICK_EXAMPLES=[
4
+ {version:1,title:'发布流程',fallback:'建议流程:开发完成后先测试,通过后发布;这是规划,尚未执行。',presentation:{layout:'guide',density:'compact',accent:'teal'},nodes:[{id:'flow',type:'steps',items:[{title:'开发',description:'完成核心功能',status:'pending'},{title:'测试',description:'检查功能与边界',status:'pending'},{title:'发布',description:'交付并观察反馈',status:'pending'}]}]},
5
+ {version:1,title:'两种协作方式',fallback:'示例:表格便于记录,任务看板便于追踪流转;按工作方式选择。',presentation:{layout:'comparison',density:'compact',accent:'blue'},nodes:[{id:'choices',type:'row',children:[{id:'sheet',type:'card',title:'表格',children:[{id:'sheetText',type:'text',text:'适合清单与结构化记录。多人更新时需要约定字段。'}]},{id:'board',type:'card',title:'任务看板',children:[{id:'boardText',type:'text',text:'适合追踪任务阶段。复杂依赖需补充说明。'}]}]}]},
6
+ {version:1,title:'请求怎样到达数据',fallback:'简化架构:浏览器将请求交给服务,服务访问数据库后返回结果。',nodes:[{id:'diagram',type:'sandbox',title:'三层架构',height:180,html:'<div class="ui-flow" aria-label="浏览器到服务到数据库"><div class="ui-step"><strong>浏览器</strong><span>发起请求</span></div><span class="ui-arrow" aria-hidden="true">→</span><div class="ui-step"><strong>服务</strong><span>处理业务</span></div><span class="ui-arrow" aria-hidden="true">→</span><div class="ui-step"><strong>数据库</strong><span>保存与查询</span></div></div>',fallback:'浏览器 → 服务 → 数据库。简化架构示意。'}]},
7
+ {version:1,title:'本周概览',fallback:'示例数据:本周新增任务 12 项,已完成 9 项。',presentation:{layout:'dashboard',density:'compact',accent:'teal'},nodes:[{id:'metrics',type:'row',children:[{id:'new',type:'metric',label:'新增任务',value:12,unit:'项',decimals:0},{id:'done',type:'metric',label:'已完成',value:9,unit:'项',decimals:0}]},{id:'note',type:'text',text:'示例数据,仅展示看板排版。'}]}
8
+ ];
package/lib/ui-intent.js CHANGED
@@ -4,9 +4,9 @@ export const UI_INTENT_POLICY = `## 表达选择与首次交付
4
4
  无需等待用户说“UI”“可视化”或指定工具。先用一个简短判断选择表达方式,然后直接回答或提交界面,不向用户复述设计推理:
5
5
  - 用户明确要求交互界面、图解或动画:交付对应 UI;已要求纯文字、不要界面、只给代码时遵从用户。
6
6
  - 用户没提 UI,但问题的关键是参数变化如何影响结果、多个方案的权衡、时间趋势、空间结构或流程关系:当图形或操作比文字明显更易懂时主动使用 UI。不能只因出现数字、多个条目或技术名词就触发。
7
- - 简单事实、单次数值换算、短定义、普通代码排错:直接文字。一个静态小表格足够时不必创建交互应用。不确定有无收益时选更简单的表达,不询问“要不要我做个 UI”。
7
+ - 简单事实、单次数值换算、短定义、普通代码排错:直接文字。一个静态小表格足够时不必创建交互应用;静态图解、方案对比、流程和看板也都是 UI 的正常用途。不确定有无收益时选更简单的表达,不询问“要不要我做个 UI”。
8
8
  对照:问“复利是什么,简短解释”→文字;问“5万元按3%、5%、7%存10年,为什么后期差距更大”→可主动给增长曲线与年数调整,配准确数值和简短解释;问“解释七速自行车各档如何影响踏频和行进”→可主动给传动示意与档位选择;问“1 GiB是多少字节”→文字。例子只指导时机,不规定构图或配色。
9
- 首次交付只解决一个核心关系。立即选择最小完整方案并调用工具,不穷举设计选项,不先写长篇视觉规划,不等待“完美”再提交。默认一幅主图、1至3个必要控件、清楚的初始结果与说明;用户的必要要求不能省略。可以少而精,不能用漂亮静态壳冒充可交互成品。
9
+ 首次交付只解决一个核心关系。立即选择最小完整方案并调用工具,不穷举设计选项,不先写长篇视觉规划,不等待“完美”再提交。默认一幅静态主图、一个短流程、一个对齐对比或一个简洁看板即可;没有明确交互收益时不建 state、不写 script、不加控件和动画。静态可视化本身就是完整交付;只有用户需要操作时才保证对应交互。用户的必要要求不能省略。
10
10
  仅数值试算优先原生组件;计算超出 derived 白名单(例如幂、三角函数)时不要臆造算子或堆复杂表达式,改用小画布中的单一计算函数同步图与数值。结构或运动讲解选择一块自由画布,直接使用 render_canvas 的独立源码字段。避免首次创建多页应用、自制组件库或庞大绘图引擎。除非用户或宿主另有要求,对话内的简短交互讲解无需建立项目、编辑文件或制定任务清单。工具提交前检查绑定、初始图可见、改变参数有反馈、标签/单位和 fallback;不为简单任务反复读全部目录。
11
11
  必要的布局与排版从首次提交就做好:DSH 以 PC 横屏为主要使用场景,优先保证 1365px/1920px 桌面视口中实际聊天区域的构图与可读性,不假设画布等于整个浏览器宽度。主体突出、文字可读、控件有标签,装饰不挤占内容;手机适配是补充,不为窄屏牺牲桌面主体。Canvas 2D 按实际 CSS 像素布局,不整体缩放到文字难辨;动态数值和关键标签可放 HTML,绘图高度匹配内容,避免大块空白。
12
12
  首屏应能看见主要关系、关键操作及当前结果。用户问“为什么变化不同”,优先用能对照变化的图形;发光、粒子或装饰不能代替趋势或关系证据。主图紧邻控件,推导、完整表格和次要说明随后呈现;同一数值不在指标、图注、卡片和正文反复展示。不要把所有可用组件都用一遍,也不为减少滚动而缩小字。比较不同单位的量,分别标轴或明确使用相对同一基准的无量纲倍数,不能把原始数值放在同一未说明尺度上暗示快慢。这些是信息优先级要求,具体图形、构图与风格仍按问题自行设计。
package/lib/ui-skill.js CHANGED
@@ -4,6 +4,7 @@ import { CANVAS_DESIGN_GUIDE } from './canvas-design.js';
4
4
  import { FREE_UI_CONTRACT } from './freeform.js';
5
5
  import { UI_INTENT_POLICY } from './ui-intent.js';
6
6
  import { CANVAS_SYSTEM_GUIDE } from './canvas-system.js';
7
+ import {QUICK_EXAMPLES,QUICK_GUIDE} from './quick-examples.js';
7
8
 
8
9
  // Authored examples are deliberately small enough to be present in every model's context.
9
10
  export const SKILL_EXAMPLES = [
@@ -38,7 +39,7 @@ const skill = readFileSync(new URL('../skills/intelligent-ui/SKILL.md', import.m
38
39
  export const UI_SKILL_SECTIONS = Object.freeze([
39
40
  { name: 'generative-ui:design-skill', order: 850, interpolate: false, text: UI_INTENT_POLICY + '\n\n' + skill + '\n\nCanvas composition details:\n' + CANVAS_DESIGN_GUIDE + '\n\nConstruction handbook:\n' + CANVAS_SYSTEM_GUIDE },
40
41
  { name: 'generative-ui:protocol', order: 851, interpolate: false, text: PROTOCOL_GUIDE },
41
- { name: 'generative-ui:worked-examples', order: 852, interpolate: false, text: 'These two valid render_ui document examples teach composition and exact syntax. Adapt the idea to the user; do not copy their demo claims as real data. Submit as render_ui({document: ...}).\n' + SKILL_EXAMPLES.map(doc => JSON.stringify(doc)).join('\n') }
42
+ { name: 'generative-ui:worked-examples', order: 852, interpolate: false, text: QUICK_GUIDE+'\nThese valid render_ui examples start with static flow and comparison; the last adds a control only for a calculation. Adapt to the user, not their demo claims. Submit as render_ui({document: ...}). Animation examples remain available via ui_catalog({topic:"canvas"}).\n' + [QUICK_EXAMPLES[0],QUICK_EXAMPLES[1],SKILL_EXAMPLES[0]].map(doc => JSON.stringify(doc)).join('\n') }
42
43
  ]);
43
44
  export function promptSections(mode='guided'){
44
45
  if(!['guided','free'].includes(mode))throw new Error('DSH UI: designMode must be guided or free');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-motion-ui",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "Interactive UI, HTML/SVG animation and local calculations for DeepSeek Harness Web, with injected design guidance.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -10,7 +10,7 @@
10
10
 
11
11
  按问题选择构图,而不是每次复制仪表盘:
12
12
 
13
- | 任务 | 适合的构图 | 核心交互 |
13
+ | 任务 | 适合的构图 | 可选交互(并非必需) |
14
14
  | --- | --- | --- |
15
15
  | 机械/物体拆解 | 大幅 SVG 主体 + 部件说明,exploratory 处理 | 一个展开滑块 + 部件选择 |
16
16
  | 原理/参数实验 | 图形占主要空间 + 紧凑参数区,workbench | 参数改变,图与解释同步 |
@@ -21,6 +21,12 @@
21
21
 
22
22
  exploratory/analytical/editorial/technical 是构图思想,不是合法 layout 枚举;只能使用协议声明的 layout。状态面板仅在确有状态信息时使用 status。
23
23
 
24
+ ## 轻量图解优先
25
+
26
+ 默认先做静态可视化,不强行交互。一幅小图、3–5 步流程、2–3 方案的对齐对比或少量指标已经可以是完整作品。只有变化、选择或运动确实帮助理解时才添加 state、控件和 script;复杂交互与动画按需求启用,不能成为所有场景的默认。
27
+
28
+ 图解采用编辑式排版:中性底色、细线边框、少量强调色、直接标注、清晰层级。不叠阴影、发光或多重卡片。流程/架构用简单节点与有意义的箭头;可直接使用宿主提供的 ui-flow/ui-step/ui-arrow/ui-callout 类,通常无需自写 CSS 和 script。ui_catalog({topic:"quick"}) 提供流程、对比、架构和小看板四个短例子;优先参考已注入的静态例子,不反复查询目录。先提交足够讲清关系的小作品,按用户后续需求扩展。
29
+
24
30
  ## 视觉设计技能
25
31
 
26
32
  - 用大小、对齐、留白建立层级。让图形或证据成为主体;不把每段文字套进卡片,不堆 KPI,不重复标题。
@@ -34,7 +40,7 @@ exploratory/analytical/editorial/technical 是构图思想,不是合法 layout
34
40
 
35
41
  ## 正确生成的步骤
36
42
 
37
- 1. 确定问题、一个主要交互和可见反馈,不展开冗长设计复盘。
43
+ 1. 确定问题与核心可视化;仅有必要时加入一个主要交互和可见反馈,不展开冗长设计复盘。
38
44
  2. 优先使用下面已注入的协议和小例子。复杂功能或不确定字段,再调用 ui_catalog({topic:"canvas"|"data"|"scenes"});不必每次获取 all,也不必重复查询相同目录。
39
45
  3. 标准数据、控件使用原生组件;自由形状、SVG、HTML/CSS/JavaScript 动画使用 sandbox。它们可以混排,别因为固定组件不够就退回静态图片。
40
46
  4. 原生组件混排使用完整 render_ui({document:...}):先写元数据、state/data/derived/actions,再写 nodes。单独一块自由画布可用 render_canvas({title,fallback,html,css,script,state?...}),源码字段直接传字符串,避免把整份程序再嵌套进 JSON 文档字符串;该入口的画布节点 ID 固定为 canvas。不得在 PTC/run_code 中套用这两个工具。