dsh-motion-ui 0.5.3 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +10 -0
- package/VERIFICATION.md +25 -0
- package/client.js +401 -258
- package/index.js +15 -3
- package/lib/author-expression.js +3 -2
- package/lib/authoring-guide.js +10 -0
- package/lib/build-info.js +1 -1
- package/lib/canvas-preview.js +1 -1
- package/lib/canvas-system.js +3 -2
- package/lib/catalog.js +6 -5
- package/lib/components.js +2 -2
- package/lib/export-runtime-bundle.js +1 -1
- package/lib/freeform.js +3 -2
- package/lib/identifiers.js +4 -0
- package/lib/protocol.js +37 -19
- package/lib/stream.js +2 -1
- package/lib/tool-json.js +21 -2
- package/lib/tool-schema.js +39 -0
- package/package.json +1 -1
package/lib/protocol.js
CHANGED
|
@@ -3,11 +3,11 @@ import { SANDBOX_LIMITS } from './sandbox.js';
|
|
|
3
3
|
import { COMPONENTS, componentFields } from './components.js';
|
|
4
4
|
import { formatNumber } from './format-number.js';
|
|
5
5
|
import { expandExpressions } from './author-expression.js';
|
|
6
|
+
import { validIdentifier as key } from './identifiers.js';
|
|
6
7
|
export const LIMITS = Object.freeze({ bytes: 131072, nodes: 160, depth: 12, rows: 500, expressions: 128, steps: 4096 });
|
|
7
8
|
const own = (o, k) => Object.hasOwn(o, k);
|
|
8
9
|
const fail = message => { throw new Error(`DSH UI: ${message}`); };
|
|
9
10
|
const object = x => x !== null && typeof x === 'object' && !Array.isArray(x);
|
|
10
|
-
const key = x => typeof x === 'string' && /^[a-zA-Z][a-zA-Z0-9_]{0,63}$/.test(x) && !['constructor', 'prototype', '__proto__'].includes(x);
|
|
11
11
|
const finite = x => typeof x === 'number' && Number.isFinite(x);
|
|
12
12
|
const scalar = x => typeof x === 'string' || typeof x === 'boolean' || finite(x);
|
|
13
13
|
const text = (x, at) => { if (typeof x !== 'string' || x.length > 8192) fail(`${at} must be a string of at most 8192 characters`); };
|
|
@@ -41,6 +41,10 @@ export function validateState(doc, state) {
|
|
|
41
41
|
if (!finite(v) || v < s.min || v > s.max) fail(`state.${k} is outside its numeric range`);
|
|
42
42
|
const offset = (v - s.min) / s.step;
|
|
43
43
|
if (Math.abs(offset - Math.round(offset)) > 1e-6) fail(`state.${k} does not match its step`);
|
|
44
|
+
} else if (s.type === 'json') {
|
|
45
|
+
if (!object(v) && !Array.isArray(v)) fail(`state.${k} must be an array or object for type json`);
|
|
46
|
+
safeJson(v);
|
|
47
|
+
if (new TextEncoder().encode(JSON.stringify(v)).length > 16384) fail(`state.${k} JSON exceeds 16 KiB`);
|
|
44
48
|
} else if (s.type === 'string') text(v, `state.${k}`);
|
|
45
49
|
else if (typeof v !== 'boolean') fail(`state.${k} must be boolean`);
|
|
46
50
|
if (s.options && !s.options.includes(v)) fail(`invalid option for state.${k}`);
|
|
@@ -50,7 +54,7 @@ export function validateState(doc, state) {
|
|
|
50
54
|
}
|
|
51
55
|
const ARITY = { add: [1, 8], sub: [2, 2], mul: [1, 8], div: [2, 2], min: [1, 8], max: [1, 8], round: [1, 1], abs: [1, 1], eq: [2, 2], neq: [2, 2], lt: [2, 2], lte: [2, 2], gt: [2, 2], gte: [2, 2], if: [3, 3] };
|
|
52
56
|
function reference(doc, ref) {
|
|
53
|
-
if (typeof ref !== 'string' ||
|
|
57
|
+
if (typeof ref !== 'string' || !['state','data','derived'].includes(ref.split('.')[0]) || ref.split('.').length !== 2 || !key(ref.split('.')[1])) fail(`invalid reference ${JSON.stringify(ref)}; use state.key, data.key or derived.key with a letter/underscore identifier`);
|
|
54
58
|
const [group, k] = ref.split('.');
|
|
55
59
|
if (!own(doc[group], k)) fail(`unknown reference ${ref}`);
|
|
56
60
|
return [group, k];
|
|
@@ -118,7 +122,7 @@ function checkViewValues(doc, evaluated) {
|
|
|
118
122
|
if (n.type === 'chart' && rows.some(r => !own(r, n.labelKey) || !scalar(r[n.labelKey]) || !own(r, n.valueKey) || !finite(r[n.valueKey]))) fail('chart requires labels and finite numeric values');
|
|
119
123
|
if (n.type === 'chart' && n.xScale === 'linear' && rows.some(r => !finite(r[n.labelKey]))) fail('linear chart requires numeric x values');
|
|
120
124
|
}
|
|
121
|
-
if (n.type === 'metric' && !
|
|
125
|
+
if (n.type === 'metric' && !scalar(evaluated.resolveValue(n.value))) fail(`node.${n.id}.value must resolve to a number, string or boolean; use table for rows or a canvas for structured data`);
|
|
122
126
|
if (n.type === 'progress') {
|
|
123
127
|
const v = evaluated.resolveValue(n.value), max = evaluated.resolveValue(n.max);
|
|
124
128
|
if (!finite(v) || !finite(max) || max <= 0 || v < 0 || v > max) fail('progress requires 0 <= value <= positive max');
|
|
@@ -144,11 +148,14 @@ export function validateDocument(input) {
|
|
|
144
148
|
for (const group of ['state', 'data', 'derived', 'actions']) {
|
|
145
149
|
doc[group] ??= {};
|
|
146
150
|
if (!object(doc[group]) || Object.keys(doc[group]).length > 64) fail(`invalid ${group}`);
|
|
147
|
-
for (const k of Object.keys(doc[group])) if (!key(k)) fail(`invalid key in ${group}`);
|
|
151
|
+
for (const k of Object.keys(doc[group])) if (!key(k)) fail(`invalid key in ${group}: ${JSON.stringify(k)}; use 1..64 letters, numbers or underscores, starting with a letter/underscore (Chinese letters supported); no spaces, dots or hyphens. Put display labels in label/text`);
|
|
148
152
|
}
|
|
149
153
|
for (const [k, s] of Object.entries(doc.state)) {
|
|
150
154
|
fields(s, ['type', 'default', 'min', 'max', 'step', 'options'], `state.${k}`);
|
|
151
|
-
|
|
155
|
+
// Preserve structured checklist data instead of coercing it to a string.
|
|
156
|
+
if (s.type === 'string' && (Array.isArray(s.default) || object(s.default)) && !own(s,'options')) s.type = 'json';
|
|
157
|
+
if (!['number', 'string', 'boolean', 'json'].includes(s.type)) fail(`unsupported state type ${k}; use number, string, boolean or json (arrays/objects)`);
|
|
158
|
+
if (s.type === 'json' && own(s,'options')) fail(`state.${k}: json state does not support select options; use scalar options or a canvas`);
|
|
152
159
|
if (s.type === 'number') {
|
|
153
160
|
if (![s.min, s.max, s.step].every(finite) || s.min >= s.max || s.step <= 0) fail(`invalid numeric limits for ${k}`);
|
|
154
161
|
if ((s.max - s.min) / s.step > 1e9) fail(`numeric step too small for ${k}`);
|
|
@@ -168,7 +175,7 @@ export function validateDocument(input) {
|
|
|
168
175
|
const [, k] = reference(doc, e.map.source), rows = doc.data[k];
|
|
169
176
|
if (!Array.isArray(rows) || rows.length > LIMITS.rows || !rows.every(object)) fail('map source must be object rows');
|
|
170
177
|
if (!object(e.map.fields) || !Object.keys(e.map.fields).length || Object.keys(e.map.fields).length > 16) fail('map requires 1..16 fields');
|
|
171
|
-
for (const [k, value] of Object.entries(e.map.fields)) { if (!key(k)) fail(`invalid map field ${JSON.stringify(k).slice(0,100)}; use
|
|
178
|
+
for (const [k, value] of Object.entries(e.map.fields)) { if (!key(k)) fail(`invalid map field ${JSON.stringify(k).slice(0,100)}; use a letter/underscore identifier such as powerW or 功率, without spaces, dots or hyphens`); validateExpression(value, depth + 1, rows); }
|
|
172
179
|
return;
|
|
173
180
|
}
|
|
174
181
|
if (own(e, 'const')) { fields(e, ['const'], 'expression'); if (!scalar(e.const)) fail('constant must be scalar'); return; }
|
|
@@ -208,6 +215,7 @@ export function validateDocument(input) {
|
|
|
208
215
|
if (++count > LIMITS.nodes) fail('too many nodes');
|
|
209
216
|
if (!object(n) || !key(n.id) || ids.has(n.id)) fail('node ids must be unique identifiers'); ids.add(n.id);
|
|
210
217
|
if (!Object.hasOwn(COMPONENTS, n.type)) fail('unsupported component ' + n.type);
|
|
218
|
+
if (['table','chart'].includes(n.type) && typeof n.rows === 'string' && /^(data|derived)\./.test(n.rows)) n.rows={ref:n.rows};
|
|
211
219
|
switch (n.type) {
|
|
212
220
|
case 'sandbox': {
|
|
213
221
|
|
|
@@ -220,16 +228,25 @@ export function validateDocument(input) {
|
|
|
220
228
|
}
|
|
221
229
|
case 'row': case 'column': break;
|
|
222
230
|
case 'card': if (n.title !== undefined) text(n.title, 'card title'); if (n.surface !== undefined && !UI_SURFACES.includes(n.surface)) fail('unknown card surface'); break;
|
|
223
|
-
case 'details':
|
|
231
|
+
case 'details':
|
|
232
|
+
text(n.title, 'details title');
|
|
233
|
+
if (n.open === 'true' || n.open === 'false') n.open = n.open === 'true';
|
|
234
|
+
if (n.open !== undefined && typeof n.open !== 'boolean') fail(`node.${n.id}.open must be boolean true or false`);
|
|
235
|
+
break;
|
|
224
236
|
case 'badge': value(n.text, 'badge text'); break;
|
|
225
237
|
case 'progress': text(n.label, 'progress label'); value(n.value, 'progress value'); value(n.max, 'progress max'); if (n.unit !== undefined) text(n.unit, 'progress unit'); break;
|
|
226
238
|
case 'steps': {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
239
|
+
// Some function-call adapters emit an XML-style singular item wrapper.
|
|
240
|
+
// Unwrap only this exact, lossless shape; never drop sibling fields.
|
|
241
|
+
if (object(n.items) && Object.keys(n.items).length === 1 && Array.isArray(n.items.item)) n.items = n.items.item;
|
|
242
|
+
const at = `node.${n.id}.items`;
|
|
243
|
+
if (!Array.isArray(n.items)) fail(`${at} must be an array of 1..12 step objects, received ${n.items === null ? 'null' : typeof n.items}. Use items:[{"title":"First step","description":"One sentence"}], not items:{item:[...]}. status is optional for a static guide`);
|
|
244
|
+
if (!n.items.length || n.items.length > 12) fail(`${at} requires 1..12 items; received ${n.items.length}. Split a longer guide into separate steps nodes`);
|
|
245
|
+
for (const [i, item] of n.items.entries()) {
|
|
246
|
+
const stepAt = `${at}[${i}]`;
|
|
247
|
+
fields(item, ['title', 'description', 'status'], stepAt); text(item.title, `${stepAt}.title`);
|
|
248
|
+
if (item.description !== undefined) text(item.description, `${stepAt}.description`);
|
|
249
|
+
if (item.status !== undefined && !['pending', 'active', 'done', 'error'].includes(item.status)) fail(`${stepAt}.status must be pending, active, done or error; omit status for a static guide`);
|
|
233
250
|
} break;
|
|
234
251
|
}
|
|
235
252
|
case 'heading': case 'text': value(n.text, 'text'); break;
|
|
@@ -240,18 +257,19 @@ export function validateDocument(input) {
|
|
|
240
257
|
const [, k] = reference(doc, n.bind), spec = doc.state[k];
|
|
241
258
|
if (n.type === 'slider' && spec.type !== 'number') fail('slider requires numeric state');
|
|
242
259
|
if (n.type === 'toggle' && spec.type !== 'boolean') fail('toggle requires boolean state');
|
|
260
|
+
if (['input','select'].includes(n.type) && spec.type === 'json') fail(`node.${n.id}: structured state requires a sandbox with DSHUI.setState; use separate boolean states and toggle nodes for a native checklist`);
|
|
243
261
|
if (n.type === 'input' && spec.type === 'boolean') fail('use toggle for boolean state');
|
|
244
262
|
if (n.type === 'select' && !spec.options) fail('select requires options'); break;
|
|
245
263
|
}
|
|
246
264
|
case 'button': text(n.text, 'button text'); if (!own(doc.actions, n.action)) fail('unknown action'); break;
|
|
247
265
|
case 'table': {
|
|
248
|
-
if(
|
|
249
|
-
value(n.rows, 'table rows');
|
|
266
|
+
if (n.label !== undefined) text(n.label, 'table label');
|
|
267
|
+
if (!Array.isArray(n.rows)) value(n.rows, 'table rows');
|
|
250
268
|
if (!Array.isArray(n.columns) || !n.columns.length || n.columns.length > 16) fail('invalid table columns');
|
|
251
269
|
const cols = new Set();
|
|
252
270
|
for (const c of n.columns) { fields(c, ['key', 'label', 'unit'], 'column'); if (!key(c.key) || cols.has(c.key)) fail('invalid or duplicate column key'); cols.add(c.key); text(c.label, 'column label'); if (c.unit !== undefined) text(c.unit, 'column unit'); } break;
|
|
253
271
|
}
|
|
254
|
-
case 'chart': value(n.rows, 'chart rows'); if (!['bar', 'line'].includes(n.kind) || !key(n.labelKey) || !key(n.valueKey)) fail('invalid chart'); text(n.label, 'chart label'); if (n.unit !== undefined) text(n.unit, 'chart unit'); if (n.xScale !== undefined && (!['category', 'linear'].includes(n.xScale) || n.xScale === 'linear' && n.kind !== 'line')) fail('invalid xScale'); break;
|
|
272
|
+
case 'chart': if (!Array.isArray(n.rows)) value(n.rows, 'chart rows'); if (!['bar', 'line'].includes(n.kind) || !key(n.labelKey) || !key(n.valueKey)) fail('invalid chart'); text(n.label, 'chart label'); if (n.unit !== undefined) text(n.unit, 'chart unit'); if (n.xScale !== undefined && (!['category', 'linear'].includes(n.xScale) || n.xScale === 'linear' && n.kind !== 'line')) fail('invalid xScale'); break;
|
|
255
273
|
default: fail(`unsupported component ${n.type}`);
|
|
256
274
|
}
|
|
257
275
|
if (['heading', 'text', 'card', 'metric', 'button', 'badge', 'progress', 'details'].includes(n.type)) {
|
|
@@ -283,9 +301,9 @@ export function fallbackText(doc) {
|
|
|
283
301
|
if (n.type === 'sandbox') lines.push(n.title, n.fallback);
|
|
284
302
|
if ((n.type === 'card' || n.type === 'details') && n.title) lines.push(n.title);
|
|
285
303
|
if (n.type === 'progress') lines.push(`${n.label}: ${resolveValue(n.value)} / ${resolveValue(n.max)} ${n.unit ?? ''}`);
|
|
286
|
-
if (n.type === 'steps') for (const item of n.items) lines.push(`[${item.status}] ${item.title}${item.description ? ': ' + item.description : ''}`);
|
|
287
|
-
if (n.type === 'metric') lines.push(`${n.label}: ${formatNumber(
|
|
288
|
-
if (n.type === 'table') { lines.push(n.columns.map(c => c.label + (c.unit ? ` (${c.unit})` : '')).join(' | ')); for (const row of resolveValue(n.rows)) lines.push(n.columns.map(c => Object.hasOwn(row, c.key) ? String(row[c.key]) : '—').join(' | ')); }
|
|
304
|
+
if (n.type === 'steps') for (const [i,item] of n.items.entries()) lines.push(`${item.status ? `[${item.status}]` : `${i+1}.`} ${item.title}${item.description ? ': ' + item.description : ''}`);
|
|
305
|
+
if (n.type === 'metric') { const v=resolveValue(n.value); lines.push(`${n.label}: ${typeof v === 'number' ? formatNumber(v,n.decimals ?? 2) : String(v)} ${n.unit ?? ''}`); }
|
|
306
|
+
if (n.type === 'table') { if(n.label)lines.push(n.label); lines.push(n.columns.map(c => c.label + (c.unit ? ` (${c.unit})` : '')).join(' | ')); for (const row of resolveValue(n.rows)) lines.push(n.columns.map(c => Object.hasOwn(row, c.key) ? String(row[c.key]) : '—').join(' | ')); }
|
|
289
307
|
if (n.type === 'chart') { lines.push(n.label + (n.unit ? ` (${n.unit})` : '')); for (const r of resolveValue(n.rows)) lines.push(`${r[n.labelKey]}: ${r[n.valueKey]}`); }
|
|
290
308
|
if (n.children) walk(n.children);
|
|
291
309
|
} }
|
package/lib/stream.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { LIMITS, validateDocument } from './protocol.js';
|
|
2
|
+
import { toolJson } from './tool-json.js';
|
|
2
3
|
|
|
3
4
|
// Parse complete JSON tokens only. Never finish a string, number or model expression.
|
|
4
5
|
// Partial containers retain completed members, but only the document wrapper and
|
|
@@ -62,7 +63,7 @@ export function previewDocument(raw) {
|
|
|
62
63
|
try {
|
|
63
64
|
const root=parseToolArguments(raw);if(!root)return null;
|
|
64
65
|
const document = root.members?.get('document');
|
|
65
|
-
if(document?.complete&&typeof document.value==='string'&&root.members.size===1)return validateDocument(
|
|
66
|
+
if(document?.complete&&typeof document.value==='string'&&root.members.size===1)return validateDocument(toolJson(document.value,'document'));
|
|
66
67
|
if (!document?.members) return null;
|
|
67
68
|
if ([...root.members.keys()].some(k => k !== 'document')) return null;
|
|
68
69
|
const doc = { ...document.value }, nodes = document.members.get('nodes');
|
package/lib/tool-json.js
CHANGED
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
// DSH adapters may represent `type: json` fields as JSON strings.
|
|
2
2
|
// Decode only the tool field, once; strict protocol validation still applies.
|
|
3
|
+
function clean(source) {
|
|
4
|
+
let input=source.trim().replace(/^\uFEFF/, '');
|
|
5
|
+
const fence=input.match(/^```(?:json)?\s*\n([\s\S]*?)\n```$/i);
|
|
6
|
+
if(fence)input=fence[1];
|
|
7
|
+
let output='',quoted=false,escaped=false;
|
|
8
|
+
for(let i=0;i<input.length;i++){
|
|
9
|
+
const c=input[i];
|
|
10
|
+
if(quoted){output+=c;if(escaped)escaped=false;else if(c==='\\')escaped=true;else if(c==='"')quoted=false;continue;}
|
|
11
|
+
if(c==='"')quoted=true;
|
|
12
|
+
if(c===',' && /^\s*[}\]]/.test(input.slice(i+1)))continue;
|
|
13
|
+
output+=c;
|
|
14
|
+
}
|
|
15
|
+
return output;
|
|
16
|
+
}
|
|
3
17
|
export function toolJson(value, field, maxBytes=131072){
|
|
4
18
|
if(typeof value!=='string')return value;
|
|
5
|
-
if(
|
|
6
|
-
|
|
19
|
+
if(new TextEncoder().encode(value).length>maxBytes)throw new Error(`DSH UI: ${field} JSON exceeds size limit`);
|
|
20
|
+
const source=clean(value);
|
|
21
|
+
try{return JSON.parse(source)}catch(error){
|
|
22
|
+
const position=error.message.match(/position (\d+)/)?.[1];
|
|
23
|
+
const location=position===undefined?'':` at line ${source.slice(0,Number(position)).split('\n').length}, column ${Number(position)-source.lastIndexOf('\n',Number(position)-1)}`;
|
|
24
|
+
throw new Error(`DSH UI: ${field} must contain valid JSON${location}. Pass an object directly, not a manually escaped JSON string. Do not use single-quoted keys or raw newlines inside strings. For HTML/SVG use render_canvas with separate html/css/script fields. No partial document was saved.`);
|
|
25
|
+
}
|
|
7
26
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {COMPONENTS} from './components.js';
|
|
2
|
+
// Expose the document skeleton to native function calling, not just prose.
|
|
3
|
+
// Per-component and expression checks remain in the shared runtime validator.
|
|
4
|
+
const dictionary=description=>({type:'object',additionalProperties:true,description});
|
|
5
|
+
export const DOCUMENT_PARAMETER={
|
|
6
|
+
required:true,
|
|
7
|
+
description:'Pass the document as an OBJECT. Do not manually escape/serialize it. For HTML/SVG source prefer render_canvas.',
|
|
8
|
+
type:'object',additionalProperties:false,
|
|
9
|
+
properties:{
|
|
10
|
+
version:{type:'number',const:1,required:true},
|
|
11
|
+
title:{type:'string',required:true},
|
|
12
|
+
fallback:{type:'string',required:true,description:'Useful plain-text explanation if the UI is unavailable.'},
|
|
13
|
+
nodes:{type:'array',required:true,items:{type:'object',additionalProperties:true,properties:{
|
|
14
|
+
id:{type:'string',required:true,description:'Unique letter/underscore identifier; Chinese supported, max64.'},
|
|
15
|
+
type:{type:'string',required:true,enum:Object.keys(COMPONENTS)},
|
|
16
|
+
label:{type:'string'},title:{type:'string'},
|
|
17
|
+
items:{type:'array',description:'steps only: direct array, not {item:[...]}. 1..12 step objects. Omit status for a static process guide.',items:{type:'object',additionalProperties:false,properties:{
|
|
18
|
+
title:{type:'string',required:true},description:{type:'string'},status:{type:'string',enum:['pending','active','done','error']}
|
|
19
|
+
}}},
|
|
20
|
+
open:{type:'boolean',description:'details only; defaults to closed.'},
|
|
21
|
+
icon:{type:'string'},tone:{type:'string'},surface:{type:'string'},
|
|
22
|
+
max:{type:'json',description:'progress only: positive numeric value or binding.'},unit:{type:'string'},decimals:{type:'number'},
|
|
23
|
+
action:{type:'string',description:'button only: a key declared in document.actions.'},
|
|
24
|
+
kind:{type:'string',enum:['bar','line']},labelKey:{type:'string'},valueKey:{type:'string'},xScale:{type:'string'},
|
|
25
|
+
html:{type:'string'},css:{type:'string'},script:{type:'string'},fallback:{type:'string'},height:{type:'number'},
|
|
26
|
+
stateKeys:{type:'array',items:{type:'string'}},dataKeys:{type:'array',items:{type:'string'}},derivedKeys:{type:'array',items:{type:'string'}},
|
|
27
|
+
text:{type:'json',description:'Literal scalar or {ref}/{expr}.'},
|
|
28
|
+
value:{type:'json',description:'metric: number or display string/boolean, or {ref}/{expr}. progress: numeric.'},
|
|
29
|
+
rows:{type:'json',description:'table/chart: inline array of objects, {ref:"data.rows"}/{ref:"derived.rows"}, or scoped reference string.'},
|
|
30
|
+
columns:{type:'array',items:{type:'object',additionalProperties:false,properties:{key:{type:'string',required:true},label:{type:'string',required:true},unit:{type:'string'}}}},
|
|
31
|
+
bind:{type:'string',description:'Controls bind state.key.'},children:{type:'array',description:'row/column/card/details: array of registered nodes using the same fields and type-specific rules. steps uses items instead of children.',items:{type:'json'}}
|
|
32
|
+
}}},
|
|
33
|
+
state:dictionary('key -> {type:number/string/boolean/json,default,...}. Numeric state needs min,max,step. json is bounded array/object state for canvas controls.'),
|
|
34
|
+
data:dictionary('Static data; Chinese identifier keys supported. Omit if rows are inline.'),
|
|
35
|
+
derived:dictionary('Local expressions or maps. Omit when unused.'),
|
|
36
|
+
actions:dictionary('Declared agent.followup actions. Omit when unused.'),
|
|
37
|
+
presentation:dictionary('Optional layout/accent/density from registered tokens.')
|
|
38
|
+
}
|
|
39
|
+
};
|
package/package.json
CHANGED