dsh-motion-ui 0.6.0 → 0.7.1

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +55 -1
  3. package/VERIFICATION.md +46 -0
  4. package/client.js +2136 -1095
  5. package/examples/mechanism-assembly.json +50 -0
  6. package/examples/mechanism-crank.json +103 -0
  7. package/examples/mechanism-gears.json +103 -0
  8. package/index.js +132 -106
  9. package/lib/assets/standalone-runtime.js +3 -1
  10. package/lib/assets/visual.js +3 -1
  11. package/lib/author-expression.js +9 -8
  12. package/lib/authoring-guide.js +3 -16
  13. package/lib/build-info.js +1 -1
  14. package/lib/canvas-design.js +2 -2
  15. package/lib/canvas-system.js +1 -1
  16. package/lib/catalog.js +5 -6
  17. package/lib/components.js +2 -2
  18. package/lib/delivery.js +1 -1
  19. package/lib/export-runtime-bundle.js +1 -1
  20. package/lib/expressive-examples.js +6 -0
  21. package/lib/fence-feedback.js +8 -2
  22. package/lib/freeform.js +4 -9
  23. package/lib/inspection.js +11 -6
  24. package/lib/mechanism-catalog.js +24 -0
  25. package/lib/mechanism-css.js +13 -0
  26. package/lib/mechanism-entry.js +20 -0
  27. package/lib/mechanism-geometry.js +62 -0
  28. package/lib/mechanism-render.js +24 -0
  29. package/lib/mechanism-runtime-bundle.js +2 -0
  30. package/lib/mechanism-scenes.js +20 -0
  31. package/lib/motion-library.js +17 -0
  32. package/lib/normalize-authoring.js +15 -2
  33. package/lib/prompt-contract.js +22 -0
  34. package/lib/protocol.js +26 -11
  35. package/lib/refinement.js +3 -2
  36. package/lib/sandbox.js +2 -0
  37. package/lib/scene-entry.js +41 -0
  38. package/lib/style-system.js +28 -0
  39. package/lib/text-guide.js +9 -13
  40. package/lib/third-party/anatomy-LICENSE.txt +21 -0
  41. package/lib/third-party/anatomy-NOTICE.md +13 -0
  42. package/lib/third-party/anatomy-iso.mjs +674 -0
  43. package/lib/third-party/lucian-uxui-LICENSE.txt +21 -0
  44. package/lib/third-party/lucian-uxui-NOTICE.md +1 -0
  45. package/lib/third-party/taste-skill-NOTICE.md +10 -0
  46. package/lib/tool-schema.js +9 -9
  47. package/lib/ui-intent.js +4 -15
  48. package/lib/ui-skill.js +19 -25
  49. package/lib/visual-audit.js +15 -4
  50. package/lib/visual-styles.js +134 -22
  51. package/package.json +92 -92
  52. package/skills/intelligent-ui/SKILL.md +22 -102
package/lib/protocol.js CHANGED
@@ -56,7 +56,7 @@ export function validateState(doc, state) {
56
56
  }
57
57
  return result;
58
58
  }
59
- 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] };
59
+ const ARITY = { add: [1, 8], sub: [2, 2], mul: [1, 8], div: [2, 2], min: [1, 8], max: [1, 8], round: [1, 1], ceil: [1, 1], floor: [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] };
60
60
  function reference(doc, ref) {
61
61
  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`);
62
62
  const [group, k] = ref.split('.');
@@ -100,7 +100,9 @@ export function evaluate(doc, state) {
100
100
  case 'div': if (a[1] === 0) fail('division by zero'); out = a[0] / a[1]; break;
101
101
  case 'min': out = Math.min(...a); break;
102
102
  case 'max': out = Math.max(...a); break;
103
- case 'round': out = Math.round(a[0]); break;
103
+ case 'round': out = Math.round(a[0]); break;
104
+ case 'ceil': out = Math.ceil(a[0]); break;
105
+ case 'floor': out = Math.floor(a[0]); break;
104
106
  case 'abs': out = Math.abs(a[0]); break;
105
107
  case 'lt': return a[0] < a[1];
106
108
  case 'lte': return a[0] <= a[1];
@@ -112,7 +114,9 @@ export function evaluate(doc, state) {
112
114
  };
113
115
  for (const k of Object.keys(doc.derived)) resolve(`derived.${k}`);
114
116
  if (new TextEncoder().encode(JSON.stringify(derived)).length > LIMITS.bytes) fail('derived output exceeds 128 KiB');
117
+ const resolveCell=cell=>{const out=expression(cell);if(!scalar(out))fail('inline row formula cells must resolve to a string, boolean or finite number');return out};
115
118
  const result = { derived, resolveValue: value => {
119
+ if (Array.isArray(value)) return value.map(row => object(row) ? Object.fromEntries(Object.entries(row).map(([k,cell])=>[k,object(cell)&&['const','ref','op'].some(field=>own(cell,field))?resolveCell(cell):cell])) : row);
116
120
  if (!object(value)) return value;
117
121
  return expression(value);
118
122
  } };
@@ -123,7 +127,10 @@ function checkViewValues(doc, evaluated) {
123
127
  if (n.type === 'table' || n.type === 'chart') {
124
128
  const rows = evaluated.resolveValue(n.rows);
125
129
  if (!Array.isArray(rows) || rows.length > LIMITS.rows || !rows.every(object)) fail('table/chart rows must reference an array of objects');
126
- 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');
130
+ if (n.type === 'chart') {
131
+ const index=rows.findIndex(r=>!own(r,n.labelKey)||!scalar(r[n.labelKey])||!own(r,n.valueKey)||!finite(r[n.valueKey]));
132
+ if(index>=0){const row=rows[index],missing=[n.labelKey,n.valueKey].filter(k=>!own(row,k));fail('chart requires labels and finite numeric values. node.'+n.id+'.rows['+index+']: '+(missing.length?'missing field(s) '+missing.join(', '):n.valueKey+' must resolve to a finite number')+'. Available keys: '+Object.keys(row).slice(0,16).join(', ')+'. For live values use inline rows:[{'+n.labelKey+':"项目",'+n.valueKey+':{ref:"derived.amount"}}]; static data labels alone contain no amounts. Keep all nodes/state/derived when correcting the document');}
133
+ }
127
134
  if (n.type === 'chart' && n.xScale === 'linear' && rows.some(r => !finite(r[n.labelKey]))) fail('linear chart requires numeric x values');
128
135
  }
129
136
  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`);
@@ -150,7 +157,7 @@ export function validateDocument(input) {
150
157
  for (const [k, allowed] of Object.entries({ layout: UI_LAYOUTS, accent: UI_ACCENTS, density: ['comfortable', 'compact'] }))
151
158
  if (doc.presentation[k] !== undefined && !allowed.includes(doc.presentation[k])) fail(`unknown presentation ${k}`);
152
159
  }
153
- if (doc.version !== 1) fail('unsupported protocol version');
160
+ if (doc.version !== 1) fail(own(doc,'version') ? 'document.version must be 1' : 'document.version is missing. render_ui creates a complete document, not a partial patch: send {version:1,title,fallback,nodes:[...]} for shared controls/formulas, or {title,items:[...]} for extended visuals. Include all content, not only data/derived');
154
161
  text(doc.title, 'title'); text(doc.fallback, 'fallback');
155
162
  if (!doc.title.trim() || !doc.fallback.trim()) fail('title and fallback cannot be empty');
156
163
  for (const group of ['state', 'data', 'derived', 'actions']) {
@@ -171,11 +178,13 @@ export function validateDocument(input) {
171
178
  } else if (['min', 'max', 'step'].some(p => own(s, p))) fail(`numeric limits on non-number ${k}`);
172
179
  if (s.options !== undefined && (!Array.isArray(s.options) || !s.options.length || s.options.length > 64 || !s.options.every(scalar))) fail(`state.${k}.options must be a direct array of 1..64 scalar values, e.g. options:["3","5","7"], not {item:[...]}`);
173
180
  }
174
- const defaults = validateState(doc, initialState(doc)); let expressions = 0;
181
+ const defaults = validateState(doc, initialState(doc)); let expressions = 0; const expressionShapes=new Set();
175
182
  for (const [k, spec] of Object.entries(doc.state))
176
183
  for (const option of spec.options ?? []) validateState(doc, { ...defaults, [k]: option });
177
184
  function validateExpression(e, depth = 0, itemRows, at = 'expression') {
178
- if (++expressions > LIMITS.expressions || depth > LIMITS.depth) fail('expression limit exceeded');
185
+ if (depth > LIMITS.depth) fail(at+': expression nesting exceeds '+LIMITS.depth);
186
+ const shape=JSON.stringify(e);
187
+ if(!expressionShapes.has(shape)){expressionShapes.add(shape);if(++expressions>LIMITS.expressions)fail(at+': expression limit exceeded (128 distinct expressions). Reuse derived references instead of expanding long formulas; keep the complete document when retrying');}
179
188
  if (!object(e)) fail(`${at}: expression must be an object`);
180
189
  if (own(e, 'map')) {
181
190
  if (depth !== 0) fail('map is only allowed at the root of a derived value');
@@ -217,9 +226,10 @@ export function validateDocument(input) {
217
226
  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(doc.state, s))) fail('invalid action stateKeys');
218
227
  }
219
228
  const ids = new Map(); let count = 0;
220
- const value = (v, at) => { if (object(v)) { if(own(v,'map'))fail('map belongs in derived');validateExpression(v); } else if (!scalar(v)) fail(`invalid value at ${at}`); };
229
+ const value = (v, at) => { if (object(v)) { if(own(v,'map'))fail('map belongs in derived');validateExpression(v,0,undefined,at); } else if (!scalar(v)) fail(`invalid value at ${at}`); };
221
230
  function nodes(list, depth = 0, path = 'nodes') {
222
- if (!Array.isArray(list) || depth > LIMITS.depth) fail('invalid node children or depth');
231
+ if (!Array.isArray(list)) fail(path==='nodes' ? 'document.nodes is missing or is not an array. State/derived define values but do not create visible UI. Include nodes:[{id:"heading",type:"heading",text:"..."},...] and actual controls in the SAME complete document; do not resend state/data/derived alone. Declared state keys: '+Object.keys(doc.state).join(', ') : path+' must be an array of child nodes; use children:[{id,type,...}]');
232
+ if (depth > LIMITS.depth) fail(path+' exceeds maximum node depth '+LIMITS.depth);
223
233
  for (const [index,n] of list.entries()) {
224
234
  const at = `${path}[${index}]`;
225
235
  if (++count > LIMITS.nodes) fail('too many nodes');
@@ -245,6 +255,7 @@ export function validateDocument(input) {
245
255
  if (!n.fallback.trim()) fail(`${at}.fallback must not be empty`);
246
256
  if (typeof n.html !== 'string' || !n.html.trim()) fail(`${at}.html must be a non-empty HTML string; script/css/fallback belong beside html on the sandbox node, not inside it`);
247
257
  for (const field of ['html', 'css', 'script']) if (n[field] !== undefined && (typeof n[field] !== 'string' || n[field].length > SANDBOX_LIMITS.source)) fail('canvas source exceeds 48 Ki characters');
258
+ if(n.libraries!==undefined&&(!Array.isArray(n.libraries)||n.libraries.length>1||n.libraries.some(v=>v!=='mechanism')))fail('canvas libraries supports only ["mechanism"] or []');
248
259
  if (n.height !== undefined && (!Number.isInteger(n.height) || n.height < 160 || n.height > SANDBOX_LIMITS.height)) fail('canvas height must be 160..1000');
249
260
  for (const [field, group] of [['stateKeys', 'state'], ['dataKeys', 'data'], ['derivedKeys', 'derived']]) if (n[field] !== undefined && (!Array.isArray(n[field]) || n[field].length > 64 || new Set(n[field]).size !== n[field].length || n[field].some(k => !own(doc[group], k)))) fail('invalid canvas ' + field);
250
261
  break;
@@ -289,7 +300,8 @@ export function validateDocument(input) {
289
300
  if (n.type === 'toggle' && spec.type !== 'boolean') fail('toggle requires boolean state');
290
301
  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`);
291
302
  if (n.type === 'input' && spec.type === 'boolean') fail('use toggle for boolean state');
292
- if (n.type === 'select' && !spec.options) fail('select requires options'); break;
303
+ if (n.type === 'select' && !spec.options) fail('select requires options');
304
+ if(n.type==='select'&&n.variant!==undefined&&!['dropdown','segmented'].includes(n.variant))fail(`node.${n.id}.variant must be dropdown or segmented`);break;
293
305
  }
294
306
  case 'code':
295
307
  if(typeof n.code!=='string'||n.code.length>SANDBOX_LIMITS.source)fail('code must be a string of at most 48 Ki characters');
@@ -298,18 +310,21 @@ export function validateDocument(input) {
298
310
  case 'divider': if(n.label!==undefined)text(n.label,'divider label');break;
299
311
  case 'table': {
300
312
  if (n.label !== undefined) text(n.label, 'table label');
301
- if (!Array.isArray(n.rows)) value(n.rows, 'table rows');
313
+ if (!Array.isArray(n.rows)) value(n.rows, 'node.'+n.id+'.rows');
302
314
  if (!Array.isArray(n.columns) || !n.columns.length || n.columns.length > 16) fail('invalid table columns');
303
315
  const cols = new Set();
304
316
  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;
305
317
  }
306
- 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;
318
+ case 'chart': if(n.rows===undefined)fail('node.'+n.id+'.rows is missing; supply rows:[{label:"项目",value:123}] or a rows binding, plus labelKey/valueKey matching its fields. Do not invent missing amounts'); if (!Array.isArray(n.rows)) value(n.rows, 'node.'+n.id+'.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;
307
319
  default: fail(`unsupported component ${n.type}`);
308
320
  }
309
321
  if (['heading', 'text','callout', 'card', 'metric', 'button','link', 'badge', 'progress', 'details'].includes(n.type)) {
310
322
  if (n.icon !== undefined && !UI_ICONS.includes(n.icon)) fail('unknown UI icon');
311
323
  if (!['button','link'].includes(n.type)) { if (n.tone !== undefined && !UI_TONES.includes(n.tone)) fail('unknown UI tone'); }
312
324
  }
325
+ if (['table','chart'].includes(n.type) && Array.isArray(n.rows)) {
326
+ for (const [i,row] of n.rows.entries()) if(object(row)) for(const [k,cell] of Object.entries(row)) if(object(cell)&&['const','ref','op'].some(field=>own(cell,field))) value(cell,at+'.rows['+i+'].'+k);
327
+ }
313
328
  fields(n, componentFields(n.type), `node.${n.id}`);
314
329
  if (['row', 'column', 'card', 'details'].includes(n.type)) nodes(n.children, depth + 1, `${at}.children`);
315
330
  }
package/lib/refinement.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { componentFields } from './components.js';
2
2
  import { validateDocument } from './protocol.js';
3
- export const AUDIT_MESSAGES = Object.freeze({ overflow: '内容超出视口宽度', smallText: '文字实际显示尺寸小于 12px', contrast: '文字与背景对比度偏低', missingLabel: '交互控件缺少可访问名称',runtimeError:'画布报告运行异常' });
3
+ export const AUDIT_MESSAGES = Object.freeze({ overflow: '内容超出视口宽度', smallText: '文字实际显示尺寸小于 12px', contrast: '文字与背景对比度偏低', missingLabel: '交互控件缺少可访问名称',runtimeError:'画布报告运行异常',clippedText:'文字可能被隐藏或裁切',bottomWhitespace:'内容下方存在明显空白' });
4
4
  export function refineDocument(document, changes) {
5
5
  if (!Array.isArray(changes) || !changes.length || changes.length > 32) throw new Error('DSH UI: changes requires 1..32 node edits');
6
6
  const next = structuredClone(document), nodes = new Map();
@@ -26,6 +26,7 @@ export function validateAudit(value){
26
26
  });
27
27
  return {width:value.width,theme:value.theme,coverage:checkedCoverage,issues:value.issues.map(i=>{
28
28
  if(!i || !Object.hasOwn(AUDIT_MESSAGES,i.code) || typeof i.nodeId!=='string' || i.nodeId.length>64 || (i.value!==undefined && (!Number.isFinite(i.value)||Math.abs(i.value)>100000)))throw new Error('DSH UI: invalid audit issue');
29
- return {code:i.code,nodeId:i.nodeId,message:AUDIT_MESSAGES[i.code],...(i.value===undefined?{}:{value:i.value})};
29
+ let target;if(i.target!==undefined){const t=i.target;if(!t||typeof t!=='object'||Array.isArray(t)||Object.keys(t).some(k=>!['selector','text','tag'].includes(k))||['selector','text','tag'].some(k=>typeof t[k]!=='string'||t[k].length>({selector:256,text:120,tag:32}[k])))throw new Error('DSH UI: invalid audit target');target={selector:t.selector,text:t.text,tag:t.tag};}
30
+ return {code:i.code,nodeId:i.nodeId,message:AUDIT_MESSAGES[i.code],...(target?{target}:{}),...(i.value===undefined?{}:{value:i.value})};
30
31
  })};
31
32
  }
package/lib/sandbox.js CHANGED
@@ -1,3 +1,4 @@
1
+ import {MECHANISM_RUNTIME} from './mechanism-runtime-bundle.js';
1
2
  import { measureVisual } from './visual-audit.js';
2
3
  import { formatNumber } from './format-number.js';
3
4
  import { CANVAS_SYSTEM_CSS } from './canvas-system.js';
@@ -35,6 +36,7 @@ export function sandboxDocument(node, channel, nonce) {
35
36
  motion.addEventListener('change',refresh);document.addEventListener('visibilitychange',refresh);
36
37
  const formatNumber=${formatNumber.toString()};
37
38
  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}}}};
39
+ ${node.libraries?.includes('mechanism')?MECHANISM_RUNTIME.replace(/<\/script/gi,'<\\/script')+';api.mechanism=__MotionMechanism.createMechanismAPI(api);':''}
38
40
  Object.defineProperty(window,'DSHUI',{value:Object.freeze(api),writable:false});
39
41
  for(const type of ['click','input','change','keydown','pointerdown'])document.addEventListener(type,e=>{if(snapshot.disabled){e.preventDefault();e.stopImmediatePropagation()}},true);
40
42
  addEventListener('message',event=>{
@@ -0,0 +1,41 @@
1
+ import{validateDocument,LIMITS}from'./protocol.js';
2
+ const object=v=>v!==null&&typeof v==='object'&&!Array.isArray(v);
3
+ const fail=(at,message)=>{throw Error('DSH UI: '+at+' '+message)};
4
+ const fields=(v,allowed,at)=>{if(!object(v))fail(at,'must be an object');for(const k of Object.keys(v))if(!allowed.includes(k))fail(at+'.'+k,'is not supported; use '+allowed.join(', '))};
5
+ const text=(v,at)=>{if(typeof v!=='string'||!v.trim()||v.length>8192)fail(at,'must be non-empty text of at most8192 characters');return v};
6
+ const num=(v,at,min,max,integer=false)=>{if(typeof v!=='number'||!Number.isFinite(v)||v<min||v>max||(integer&&!Number.isInteger(v)))fail(at,`must be ${integer?'an integer':'a number'} in ${min}..${max}; supply actual or explicitly assumed values`);return v};
7
+ const metric=(id,label,ref,unit='',decimals=0)=>({id,type:'metric',label,value:{ref:'derived.'+ref},unit,decimals});
8
+ const state=(value,max,step=1)=>({type:'number',default:value,min:0,max,step});
9
+ export const SCENE_IDS=Object.freeze(['flow','comparison','allocation','budget']);
10
+ export const SCENE_PARAMETERS={scene:{type:'string',required:true,enum:SCENE_IDS},title:{type:'string'},items:{type:'json',description:'flow: [{title,description?}] 1..12 (titles omit step numbers; Host numbers them); comparison: [{title,description?,value?,unit?,points?:[text]}] 2..4; allocation: [{label,percent}] 2..8, sum100. No fabricated facts.'},total:{type:'number',description:'allocation: positive initial total, max100000, up to3 decimals.'},unit:{type:'string',description:'allocation total unit; default 分钟.'},values:{type:'json',description:'budget requires {people:integer1..100,nights:integer1..365,roomRate:number0..100000,transportPerPerson:number0..100000}. Two adults per room, ceil(people/2). Monetary values max2 decimals; assumed values must be labeled in title or explanation.'}};
11
+ export function sceneDocument(args={}){
12
+ // Reject excessive input before traversing, including unsupported extras.
13
+ if(new TextEncoder().encode(JSON.stringify(args)).length>LIMITS.bytes)fail('render_scene','exceeds128KiB');
14
+ fields(args,['scene','title','items','total','unit','values'],'render_scene');
15
+ if(!SCENE_IDS.includes(args.scene))fail('scene','must be '+SCENE_IDS.join('/'));
16
+ const used={flow:['items'],comparison:['items'],allocation:['items','total','unit'],budget:['values']}[args.scene];
17
+ for(const k of Object.keys(args))if(!['scene','title',...used].includes(k))fail(k,'does not apply to '+args.scene);
18
+ const doc={version:1,title:args.title??{flow:'步骤指引',comparison:'方案对比',allocation:'时间与数量分配',budget:'旅行预算估算'}[args.scene],fallback:'',nodes:[]};text(doc.title,'title');
19
+ if(args.scene==='flow'||args.scene==='comparison'||args.scene==='allocation'){
20
+ const [min,max]=args.scene==='flow'?[1,12]:args.scene==='comparison'?[2,4]:[2,8];
21
+ if(!Array.isArray(args.items)||args.items.length<min||args.items.length>max)fail('items',`requires a direct array of ${min}..${max} complete objects`);
22
+ args.items.forEach((v,i)=>{fields(v,args.scene==='flow'?['title','description']:args.scene==='comparison'?['title','description','value','unit','points']:['label','percent'],'items['+i+']');text(v[args.scene==='allocation'?'label':'title'],'items['+i+'].'+(args.scene==='allocation'?'label':'title'));if(v.description!==undefined)text(v.description,'items['+i+'].description')});
23
+ }
24
+ if(args.scene==='flow'){
25
+ doc.presentation={layout:'guide',preset:'flow'};doc.nodes=[{id:'steps',type:'steps',items:structuredClone(args.items)}];doc.fallback=args.items.map((v,i)=>(i+1)+'. '+v.title+(v.description?':'+v.description:'')).join('\n');
26
+ }else if(args.scene==='comparison'){
27
+ doc.presentation={layout:'comparison',preset:'comparison'};doc.nodes=[{id:'options',type:'row',columns:args.items.length,children:args.items.map((v,i)=>{const children=[];if(v.description)children.push({id:'description_'+i,type:'text',text:v.description});if(v.value!==undefined){if(!['number','string','boolean'].includes(typeof v.value))fail('items['+i+'].value','must be scalar');if(v.unit!==undefined)text(v.unit,'items['+i+'].unit');children.push({id:'value_'+i,type:'metric',label:v.title,value:v.value,...(v.unit?{unit:v.unit}:{})})}else if(v.unit!==undefined)fail('items['+i+'].unit','requires a value');if(v.points!==undefined){if(!Array.isArray(v.points)||v.points.length>8)fail('items['+i+'].points','requires up to8 texts');v.points.forEach((t,j)=>children.push({id:'point_'+i+'_'+j,type:'text',text:text(t,'items['+i+'].points['+j+']')}))}if(!children.length)fail('items['+i+']','needs description, value or points, not only a title');return{id:'option_'+i,type:'card',title:v.title,children}})}];doc.fallback=args.items.map(v=>[v.title,v.description,v.value===undefined?'':String(v.value)+(v.unit??''),...(v.points??[])].filter(Boolean).join(';')).join('\n');
28
+ }else if(args.scene==='allocation'){
29
+ const total=num(args.total,'total',.001,100000);if(Math.abs(total*1000-Math.round(total*1000))>1e-7)fail('total','supports up to3 decimals');const unit=args.unit??'分钟';text(unit,'unit');let sum=0;args.items.forEach((v,i)=>sum+=num(v.percent,'items['+i+'].percent',0,100));if(Math.abs(sum-100)>1e-6)fail('items.percent','must sum to100; received '+sum);
30
+ doc.presentation={layout:'workbench'};doc.state={total:state(total,Math.max(total*3,100),Number.isInteger(total)?1:.001)};doc.derived=Object.fromEntries(args.items.map((v,i)=>['part_'+i,{expr:'state.total * '+v.percent+' / 100'}]));
31
+ doc.nodes=[{id:'total',type:'slider',label:'总量('+unit+')',bind:'state.total'},{id:'allocation',type:'row',columns:Math.min(args.items.length,4),children:args.items.map((v,i)=>metric('part_'+i,v.label,'part_'+i,unit,2))},{id:'distribution',type:'chart',kind:'bar',label:'分配结果',unit,labelKey:'label',valueKey:'value',rows:args.items.map((v,i)=>({label:v.label,value:{ref:'derived.part_'+i}}))}];doc.fallback='初始总量 '+total+unit+';'+args.items.map(v=>v.label+' '+v.percent+'%').join(',')+'。调节总量后按相同比例分配。';
32
+ }else{
33
+ fields(args.values,['people','nights','roomRate','transportPerPerson'],'values');const v=args.values;num(v.people,'values.people',1,100,true);num(v.nights,'values.nights',1,365,true);for(const k of['roomRate','transportPerPerson']){num(v[k],'values.'+k,0,100000);if(Math.abs(v[k]*100-Math.round(v[k]*100))>1e-6)fail('values.'+k,'supports up to2 decimal places')}
34
+ doc.presentation={layout:'workbench'};doc.state={people:{...state(v.people,100),min:1},nights:{...state(v.nights,365),min:1},roomRate:state(v.roomRate,100000,.01),transportPerPerson:state(v.transportPerPerson,100000,.01)};
35
+ doc.derived={rooms:{expr:'ceil(state.people / 2)'},lodging:{expr:'derived.rooms * state.nights * state.roomRate'},transport:{expr:'state.people * state.transportPerPerson'},total:{expr:'derived.lodging + derived.transport'},lodgingShare:{expr:'derived.total > 0 ? round(derived.lodging / derived.total * 1000) / 10 : 0'},transportShare:{expr:'derived.total > 0 ? round(derived.transport / derived.total * 1000) / 10 : 0'}};
36
+ doc.nodes=[{id:'parameters',type:'card',title:'调整参数',children:Object.entries({people:'人数(人)',nights:'入住晚数(晚)',roomRate:'每房每晚(元)',transportPerPerson:'每人交通(元)'}).map(([k,label])=>({id:k,type:k==='people'||k==='nights'?'slider':'input',label,bind:'state.'+k}))},{id:'totals',type:'row',columns:3,children:[metric('rooms','房间数','rooms','间'),metric('lodging','住宿','lodging','元',2),metric('total','总计','total','元',2)]},{id:'costs',type:'table',columns:[{key:'label',label:'项目'},{key:'value',label:'金额',unit:'元'},{key:'share',label:'占比',unit:'%'}],rows:[{label:'住宿',value:{ref:'derived.lodging'},share:{ref:'derived.lodgingShare'}},{label:'交通',value:{ref:'derived.transport'},share:{ref:'derived.transportShare'}}]},{id:'distribution',type:'chart',kind:'bar',label:'住宿与交通',unit:'元',labelKey:'label',valueKey:'value',rows:[{label:'住宿',value:{ref:'derived.lodging'}},{label:'交通',value:{ref:'derived.transport'}}]},{id:'basis',type:'text',text:'两位成人一间房,房间数向上取整;住宿 = 房间数 × 入住晚数 × 每房每晚,交通按人数计算。只计算输入的住宿与交通,未含其他费用;输入价格由用户或回答提供。'}];doc.fallback='按输入价格估算住宿与交通:'+v.people+'人,'+v.nights+'晚,房价'+v.roomRate+'元,每人交通'+v.transportPerPerson+'元。两位成人一间房,房间数向上取整;不是实时价格或报价。';
37
+ }
38
+ return validateDocument(doc);
39
+ }
40
+ export function previewScene(raw){try{if(typeof raw!=='string'||raw.length>LIMITS.bytes)return null;return sceneDocument(JSON.parse(raw))}catch{return null}}
41
+ export function sceneReference(){return{topic:'quick',tool:'render_scene',scenes:SCENE_IDS,examples:[{scene:'flow',items:[{title:'明确目标'},{title:'交付结果'}]},{scene:'comparison',items:[{title:'方案A',description:'适合日常使用'},{title:'方案B',description:'适合深入分析'}]},{scene:'allocation',total:60,unit:'分钟',items:[{label:'阅读',percent:40},{label:'练习',percent:40},{label:'复盘',percent:20}]},{scene:'budget',title:'假设旅行预算',values:{people:3,nights:3,roomRate:600,transportPerPerson:800}}],note:'Examples are illustrative, not measured data. Use supplied/explicitly assumed task values. Full custom UI remains available via render_ui/render_canvas.'}}
@@ -0,0 +1,28 @@
1
+ // Visual grammar, shared by native rendering, previews and model guidance.
2
+ // IDs are stable configuration values; no additional user switches are introduced.
3
+ export const STYLE_SYSTEM=Object.freeze({
4
+ editorial:{description:'开放正文 · 杂志层级 · 细线分组',grammar:'编辑式阅读',composition:'先给一个明确结论,再用开放正文、左右对齐比较和细线段落展开。用 column 组织文章,card 在此风格中是章节,不是封闭面板;不要给每段套框。指标嵌在解释旁,流程像文中的编号提纲。',typography:'标题有衬线阅读感,正文行距舒展;标题、正文、注释至少三个层级,强调依赖字重和位置。',surfaces:'透明正文、细分隔线、低阴影或无阴影;保持结构性留白,不加大面积渐变。',controls:'表单像文章中的设置行,标签与当前值左右对齐;按钮紧凑,代码像有边注的摘录。',avoid:'避免大彩色卡片、每个数字一个盒子、粗重边框和装饰性背景。',native:{title:27,weight:650,gap:24,radius:12,metric:32,feedback:210,gain:.25}},
5
+ soft:{description:'笔记色块 · 柔圆分组 · 舒缓节奏',grammar:'柔和彩色笔记',composition:'像清晰友好的笔记:把相关内容放在柔圆色块里,先提示主题,再给两三项要点。章节标题可以是小标签;比较项如并排便签,流程像一串有间距的笔记。',typography:'圆润标题和温和字重;正文清楚,数值与单位贴近,次要说明放在组内。',surfaces:'低饱和彩色便签、不同章节轻微交替色块、不对称的大圆角;使用实色底或少量柔和渐变。',controls:'控件内嵌在柔色分组,选择器像标签,操作有柔和回弹;避免每行都变成边框表单。',avoid:'避免工程网格、重黑海报、透明玻璃、泛白低对比度文字。',native:{title:25,weight:700,gap:20,radius:28,metric:36,feedback:390,gain:.7}},
6
+ technical:{description:'工程网格 · 数据条带 · 编号连接',grammar:'技术说明与工程图',composition:'像工程说明:按输入→机制→输出组织;优先对齐的数据条带、编号流程、表格和有意义的连接线。指标放成紧凑测量带而非一排装饰卡;比较围绕相同维度对齐。',typography:'中文正文保持正常字号,数值/单位/序号用等宽字体与 tabular-nums;标题简短精确,明确基准、单位和假设。',surfaces:'细线网格、规则的小圆角、清晰边界和表头、无浮动投影。网格仅在图解背景使用,文字区域保持干净。',controls:'标签和输入严格对齐,参数区像仪表设置行;按钮和分段选项采用规则矩形,保留键盘及全局反馈。',avoid:'避免胶囊堆叠、巨大海报数字、彩色便签、装饰箭头、靠小字硬塞信息。',native:{title:22,weight:750,gap:16,radius:10,metric:28,feedback:180,gain:.3}},
7
+ expressive:{description:'海报层级 · 非对称重点 · 醒目数字',grammar:'活力展示与重点海报',composition:'先选一个真正的视觉中心:大标题或关键数字,再用不对称分栏展开解释。主要结论占更大空间,次要比较退后;流程用醒目的序号与短句。不要把每一项都做成同等大小卡片。',typography:'短而有力的重标题、大数字、紧凑标题行距,正文仍易读。最多一个主要强调区,其余留白和细线辅助。',surfaces:'实色色块、粗细明确的边线、局部高对比强调;可用橙/珊瑚/暖黄,但不能整页争抢注意力。',controls:'主要操作醒目、次要操作轻量;表单和数据保持对齐,回弹比阅读风格明显但不空闲循环。',avoid:'避免六张等大的指标卡、满屏渐变、全屏大留白、无意义数字装饰。',native:{title:34,weight:850,gap:22,radius:20,metric:50,feedback:480,gain:1.1}},
8
+ prismframe:{description:'应用模块 · 实色控件 · 偏移底面',grammar:'明亮扁平的轻量应用',composition:'像可直接使用的轻量应用:任务标题、输入模块、结果模块、解释区职责清楚。模块使用稳定网格,操作靠近对应结果;比较、流程和数据看板各自采用合适结构,不固定成同一套卡片。',typography:'圆润清晰的系统字体、明确模块标题、中等大数字;数字绑定名称和单位,图形承担关系说明。',surfaces:'浅彩底、白色实心模块、细彩色边线和扁平偏移底面;局部多色用来区分职责,避免每块同色同形。',controls:'整齐且有面积的实色表单行、清晰状态标签,主要操作有轻微偏移底面;不是磨砂玻璃。',avoid:'避免文档式满屏细线、海报式超大字、透明层叠、视频页码和字幕、重复标题。',native:{title:26,weight:780,gap:20,radius:24,metric:36,feedback:320,gain:.65}},
9
+ aurora:{description:'产品层级 · 暖白曲面 · 局部渐彩',grammar:'柔光渐彩的精致产品界面',composition:'像精致的产品面板:暖白开放背景上安排少量实心表面,标题简洁、指标突出、控件顺手。用宽窄和疏密区分主要结果与次要说明;按钮、表单、表格、代码与文字要属于同一套语言。',typography:'近黑标题、清晰的字重差、大而利落的数字、柔和次要说明;不把文字都染成紫色。',surfaces:'暖白实色、24..30px 圆润表面、柔和投影、局部紫蓝青渐彩强调;不把整页铺成紫面板。',controls:'圆润内嵌输入、胶囊选择器、深色主操作与渐彩图标圆点;真实操作触发渐变扩散和弹性反馈。',avoid:'禁止玻璃/backdrop-filter、泛白低对比度、仅加一个演示按钮就算风格、空闲漂浮。',native:{title:28,weight:780,gap:24,radius:28,metric:42,feedback:460,gain:1}}
10
+ });
11
+ export function grammarPrompt(id){const g=STYLE_SYSTEM[id];return `视觉语言:${g.grammar}\n构图:${g.composition}\n文字层级:${g.typography}\n表面与边界:${g.surfaces}\n控件与反馈:${g.controls}\n避免:${g.avoid}\n同一任务应有可辨识的构图与层级,不能只换强调色。保持内容和DOM阅读顺序。原生交互共用,节奏随风格变化;禁止玻璃。示例展示语言,不要求每次使用全部模块。`;}
12
+ // Same facts, intentionally different content composition. Model examples are
13
+ // complete small documents; preview uses the same grammar without live controls.
14
+ export function grammarExample(id){
15
+ const heading={type:'heading',text:'先交付一个可用版本'},body={type:'text',text:'把目标缩小,把关键体验做好。'},note={type:'text',text:'先验证,再逐步完善。',color:'muted'};
16
+ const choice=(title,text)=>({type:'card',title,children:[{type:'text',text}]});
17
+ const choices=[choice('先做小版本','验证重点'),choice('先铺全功能','覆盖需求')];
18
+ const metrics=[{type:'metric',label:'推进阶段',value:3,unit:'步'},{type:'metric',label:'核心目标',value:1,unit:'个'}];
19
+ const flow={type:'steps',items:[{title:'明确目标',description:'先抓住重点。'},{title:'解释关系',description:'用图形说明。'},{title:'给出行动',description:'收成下一步。'}]};
20
+ let preset='brief',blocks;
21
+ if(id==='editorial'){preset='reading';blocks=[heading,body,{type:'row',columns:2,children:choices},{type:'row',columns:2,children:metrics},flow,note]}
22
+ else if(id==='soft'){blocks=[{type:'card',title:'今天的重点',children:[heading,body]},{type:'row',columns:2,children:choices},flow,{type:'row',columns:2,children:metrics},note]}
23
+ else if(id==='technical'){blocks=[heading,body,{type:'row',columns:2,children:metrics},flow,{type:'table',label:'推进方式',columns:[{key:'method',label:'方案'},{key:'focus',label:'重点'}],rows:[{method:'先做小版本',focus:'验证重点'},{method:'先铺全功能',focus:'覆盖需求'}]},note]}
24
+ else if(id==='expressive'){preset='minimal';blocks=[heading,{type:'row',columns:2,children:[{type:'column',children:[metrics[0],body]},{type:'column',children:[metrics[1],note]}]},{type:'row',columns:2,children:choices},flow]}
25
+ else if(id==='prismframe'){blocks=[heading,body,{type:'row',columns:2,children:choices},{type:'row',columns:2,children:metrics},{type:'card',title:'行动路径',children:[flow]},note]}
26
+ else{blocks=[heading,body,{type:'row',columns:2,children:metrics},{type:'row',columns:2,children:choices},flow,note]}
27
+ return {tool:'render_text',args:{title:'把复杂问题讲清楚',preset,blocks}};
28
+ }
package/lib/text-guide.js CHANGED
@@ -1,13 +1,9 @@
1
- export const TEXT_GUIDE=`## 日常对话的轻量排版
2
- 日常解释、建议、摘要、对照和资料入口可以直接用 render_text,不需要用户提醒“做 UI”,也不必写 HTML/CSS/JS。只有排版确实更易读时使用;一句话能说清的事实仍可直接回答。不要为了使用 UI 把短回答扩成看板、加无用交互或重复标题。用户要求多块内容时逐块交付,首次工具成功不代表整个任务完成;提交结论前核对是否遗漏用户指定的流程、对比、按钮或文件入口。
3
- render_text({title?:string,preset?:string,accent?:string,blocks:[string|node,...]})。纯字符串是一段文字;blocks 中的节点可省略 id,嵌套 children 的 id 也会自动补齐。render_ui 仍要求显式 id。state/data/derived/actions 不属于 render_text;需要本地计算时用 render_ui,需要自绘动画时用 render_canvas。
4
- 预设:reading 自然正文(无外框);brief 轻边框面板;comparison 并排比较;list 图标/标题/说明与分隔线;callout 少量强调;scorecard 标签与数值两端对齐;flow 纵向流程;minimal 最少留白。根据当前这一块内容选择,八种可在同一回复混排,不必统一用卡片。
5
- text/heading 支持 size(12..48px 或 sm/md/lg/xl/hero)、weight(400/500/600/700/800)、color(default/muted/accent/success/warning/danger/violet 或 #RGB/#RRGGBB)、align(left/center/right)、lineHeight(1.2..2.4)。正文通常 16..18px,标题 20..26px,脚注 14px;不用过小字号或整段彩色。主题色令牌自动适应深浅主题,自选十六进制色需要兼顾两种主题的对比度。局部强调用 spans:[{text:"普通文字"},{text:"关键结论",weight:700,color:"success"}] 替代 text,不能同时提供两者。只接受声明式字段,不输出 style、className、CSS 或 HTML。
6
- row 的 columns:1..4 可指定等宽列,gap:0..40 控制间距;column 也支持 gap。divider 可选 label。card 用于真实的信息分组;不要求每段文字包一层卡片。按钮使用 link:{type:"link",text,href:"https://...",variant:"primary"|"secondary"|"text"},本地文件用 path 替代 href。网页只接受 http/https;path 是文件路径,不能放命令、file: URL 或自定义启动协议。按钮只在用户点击后打开,文件走 DSH 原生 opener;打开文件不能伪称执行了脚本或工具。
7
- 同一回复可以多次 render_text/render_ui/render_canvas:每块各自保存文档、参数、主题和画布;相同节点 id 可在不同文档重复,单个文档内仍须唯一。按正文讲解顺序提交,多块小而完整的 UI 比一个巨大应用更适合混排。不要为了多样式复制相同内容。inline 模式下,每次成功结果都有自己的 documentId,各自放一个 fenced dsh-ui 标记到对应段落;同一个 ID 只标记一次,不用后一个结果覆盖前一个。生成期间逐步显示经过校验的完整内容;HTML/SVG 先提供静态预览,整个回复结束后恢复参数并启用交互。优先把 title、fallback、css 放在 html 前,原生 nodes/blocks 按阅读顺序输出,使预览更早可读。不要等待预览,也不要为了预览增加工具调用。
8
- 可直接复用的文案、提示词、邮件正文用 text 的 copyable:true,并可加 title;右上复制按钮只复制正文,spans 的样式不会混进剪贴板。代码、命令和配置用 {type:"code",lang:"bash",code:"git status",title?:string,wrap?:boolean};代码默认提供右上复制按钮,原样保留缩进、换行和反引号,只展示不执行。长行默认横向滚动,wrap:true 可换行。不必给每段普通讲解加复制框;用户需要直接使用的内容才用。不要为了可复制再把同一段内容在正文重复一遍。
9
- 最小示例:render_text({preset:"reading",blocks:[{type:"heading",text:"先看结论"},{type:"text",spans:[{text:"优先保证 "},{text:"可读性",weight:700,color:"accent"},{text:",再补充必要细节。"}]}]})。
10
- 对比示例:render_text({title:"两种推理方案(示意)",preset:"comparison",blocks:[{type:"row",columns:2,children:[{type:"column",children:["方案 A · 大 MoE",{type:"text",text:"30 tok/s",size:26,weight:700},"完成任务 180 秒"]},{type:"column",children:["方案 B · Dense",{type:"text",text:"10 tok/s",size:26,weight:700},"完成任务 120 秒"]}]},{type:"text",text:"示意数据,非模型实测。",size:14,color:"muted"}]})。
11
- 入口示例:render_text({preset:"minimal",blocks:[{type:"row",children:[{type:"link",text:"查看项目",href:"https://github.com/Scorp1o117/dsh-motion-ui",variant:"primary"},{type:"link",text:"打开报告",path:"report.html",variant:"secondary"}]}]})。文件必须来自当前任务的真实产物,不能臆造路径。`;
12
-
13
- export const TEXT_INTERFACE=`Native lightweight entry: render_text({title?,preset?,accent?,blocks:[]}). blocks are strings or native nodes with auto IDs; presets reading/brief/comparison/list/callout/scorecard/flow/minimal. Text/heading support size 12..48 or sm/md/lg/xl/hero, weight 400/500/600/700/800, color default/muted/accent/success/warning/danger/violet or hex, align left/center/right, lineHeight 1.2..2.4. Reusable text adds copyable:true and title?. Code nodes {type:"code",code:string,lang?,title?,wrap?} always have an upper-right copy button and display literal source without executing it. Inline spans:[{text,weight?,color?,size?}] replaces text. link:{text,href:http(s) URL OR path:local file,variant:primary/secondary/text} opens only on a user click through the Host. Several independent UI documents may accompany one answer, each with its own styling, state and successful documentId marker. No mandatory text preset or visual composition; use plain prose whenever it suffices.`;
1
+ export const TEXT_INTERFACE=`Native lightweight entry: render_text({title?,preset?,accent?,blocks:[]}). blocks are strings or native nodes with auto IDs; presets reading/brief/comparison/list/callout/scorecard/flow/minimal. Text/heading support size 12..48 or sm/md/lg/xl/hero, weight 400/500/600/700/800, color default/muted/accent/success/warning/danger/violet or hex, align left/center/right, lineHeight 1.2..2.4. Reusable text adds copyable:true and title?. Code nodes {type:"code",code:string,lang?,title?,wrap?} always have an upper-right copy button and display literal source without executing it. Inline spans:[{text,weight?,color?,size?}] replaces text. link:{text,href:http(s) URL OR path:local file,variant:primary/secondary/text} opens only on a user click through the Host. Several independent UI documents may accompany one answer, each with its own styling, state and successful documentId marker. No mandatory text preset or visual composition; use plain prose whenever it suffices.`;
2
+ export const TEXT_GUIDE=TEXT_INTERFACE+`
3
+ Preset meanings: reading=open prose; brief=compact panel; comparison=aligned alternatives; list=divided entries; callout=one emphasis; scorecard=label/value alignment; flow=ordered steps; minimal=lightweight text. A preset is per document and does not replace the selected visual style. row.columns1..4 and gap0..40 are optional. Native nodes accept registered fields only; state/derived/actions belong in render_ui.
4
+ Examples (call tool with args):
5
+ `+[
6
+ {tool:'render_text',args:{preset:'flow',blocks:[{type:'steps',items:[{title:'整理文件',description:'保留源码和说明。'},{title:'检查提交',description:'确认内容后提交。'}]}]}},
7
+ {tool:'render_text',args:{blocks:[{type:'text',title:'可复制文案',copyable:true,text:'这是可以直接使用的正文。'},{type:'code',lang:'bash',code:'git status\ngit diff'}]}},
8
+ {tool:'render_text',args:{preset:'minimal',blocks:[{type:'link',text:'查看项目',href:'https://github.com/Scorp1o117/dsh-motion-ui',variant:'primary'}]}}
9
+ ].map(v=>JSON.stringify(v)).join('\n');
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ryan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,13 @@
1
+ # Anatomy attribution
2
+
3
+ Selected isometric camera, projection, contour and fitting helpers are bundled from
4
+ Anatomy by Ryan, MIT licensed:
5
+ https://github.com/wheresryan22/anatomy
6
+
7
+ Source revision: 454a0ef801b6d9b905f08309d094491c7085b3dd
8
+ Original file: kit/iso-kit.mjs (retained as anatomy-iso.mjs).
9
+ License: anatomy-LICENSE.txt. Copyright (c) 2026 Ryan.
10
+
11
+ Motion UI's scene validation, renderer, state bridge, controls, simplified mechanism
12
+ models and examples are original integration code. This is a compact explanatory
13
+ renderer, not Anatomy's full collision/audit system or its beta 3D/WebGL runtime.