dsh-motion-ui 0.8.2 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import {validateDocument} from './protocol.js';
2
2
 
3
3
  export const MOTION_CRAFT_GUIDE = `Motion craft: give movement a purpose (feedback, spatial continuity, causality or an occasional expressive moment). Choose one focal action; let connected objects react, then hold long enough to read. Keep identity/direction continuous across stages. Visual style controls composition/material/type; motion grammar controls how change is explained, across every style. Precise mechanisms need readable relationships, not arbitrary bounce. Quiet reading moments may be still. Use short interruptible transitions for frequent controls; never delay keyboard/click feedback. Avoid restarting entrances on state updates. Retrieve a targeted motion recipe for timelines, stages, path drawing or text; do not load the whole library.`;
4
- export const TIMELINE_CONTRACT = `Optional DSHUI.timeline({id?:"main",duration:seconds,draw(time,info),loop?:false,autoplay?:true,chapters?:[{time,label}]}) returns state(),seek(seconds),play(),pause(),replay(),chapter(index),subscribe(fn)->cleanup,dispose(). draw(time,info) receives seconds, not a Canvas context. For Canvas2D use const ctx=canvas.getContext("2d"); draw(t){drawFrame(ctx,t)}. draw uses ABSOLUTE time and current parameters: no accumulating phase, Date.now, random-per-frame, independent CSS animation or asynchronous draw for exportable films. Seed/cache procedural detail. Timelines pause offscreen/background and honor reduced motion; seeking remains available. Build labelled controls when requested, outside the exported stage if appropriate. Read an input value before pause/seek, since subscribers may synchronize its value. Transient time is not persisted each frame. ui_catalog topic=motion scene=timeline/stages/path/typography returns one example. Video export captures a single saved sandbox with timeline id main; author its duration to match export duration.`;
4
+ export const TIMELINE_CONTRACT = `Optional DSHUI.timeline({id?:"main",duration:seconds,draw(time,info),loop?:false,autoplay?:true,chapters?:[{time,label}]}) returns state(),seek(seconds),play(),pause(),replay(),chapter(index),subscribe(fn)->cleanup,dispose(). draw(time,info) receives seconds, not a Canvas context. For Canvas2D use const ctx=canvas.getContext("2d"); draw(t){drawFrame(ctx,t)}. draw uses ABSOLUTE time and current parameters: no accumulating phase, Date.now, random-per-frame, independent CSS animation or asynchronous draw for exportable films. Seed/cache procedural detail. Timelines pause offscreen/background and honor reduced motion; seeking remains available. Build labelled controls when requested, outside the exported stage if appropriate. Keep playback chrome on paired surface/text colors; scope cinematic dark backgrounds and artwork colors to #stage rather than mixing host text tokens with fixed opposite-theme controls. Read an input value before pause/seek, since subscribers may synchronize its value. Transient time is not persisted each frame. ui_catalog topic=motion scene=timeline/stages/path/typography returns one example. Video export captures a single saved sandbox with timeline id main; author its duration to match export duration.`;
5
5
  export const VIDEO_GUIDE = `Video tools: preview_video captures exact source frames/contact sheet before encoding; read them and patch first. inspect_video reads an actual local video and optionally extracts timestamped JPEG samples/contact sheet; frames are sparse observations, not full semantic analysis or ASR. Read returned images with the host image tool. render_video exports one saved canvas documentId/nodeId to editable source + MP4; call render_canvas first, author DSHUI.timeline id main with #stage, then render_video only for an actual video request. process_video trims/joins local clips. synthesize_voice produces real narration, duration and provider-measured captions/words using configured MiniMax authentication (paid); it does not transcribe input video. Fit the timeline AFTER measuring voice duration. render_video/process_video audioTracks:[{path,role:voice|music|effect,volume,offset,loop,fadeIn,fadeOut}] mixes up to four tracks and replaces original audio. Voice cannot repeat or be cut; music can loop/fade at low volume. Alternatively legacy audioPath/audioVolume replaces soundtrack. captions:[{start,end,text}] uses exact output seconds; do not stretch captions across silent gaps. Sources/audio/recipe stay editable. No API key is needed for HTML motion, local Chrome/Edge + FFmpeg/FFprobe required. No invented assets or guessed timestamps. For a promotional-film brief query ui_catalog topic=video scene=promo for the production workflow. Failed export retains assets; never label queued/HTML-only output as a finished film.`;
6
6
 
7
7
  export const PROMO_GUIDE = `One-brief film workflow (on demand, not everyday UI):
package/lib/refinement.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { componentFields } from './components.js';
2
2
  import { validateDocument } from './protocol.js';
3
3
  import {toolJson} from './tool-json.js';
4
- export const AUDIT_MESSAGES = Object.freeze({ overflow: '内容超出视口宽度', smallText: '文字实际显示尺寸小于 12px', contrast: '文字与背景对比度偏低', missingLabel: '交互控件缺少可访问名称',runtimeError:'画布报告运行异常',clippedText:'文字可能被隐藏或裁切',bottomWhitespace:'内容下方存在明显空白' });
4
+ export const AUDIT_MESSAGES = Object.freeze({ overflow: '内容超出视口宽度', smallText: '文字实际显示尺寸小于 12px', contrast: '文字与背景对比度偏低', missingLabel: '交互控件缺少可访问名称',runtimeError:'画布报告运行异常',clippedText:'文字可能被隐藏或裁切',cardTextOverflow:'文字超出卡片内容边界;请保留阴影并调整容器尺寸、内边距或文字排版',bottomWhitespace:'内容下方存在明显空白' });
5
5
  export function normalizeNodeEdits(changes) {
6
6
  changes=toolJson(changes,'changes');
7
7
  // Some providers serialize a JSON array as {item:{id,props}}. Only unwrap
package/lib/sandbox.js CHANGED
@@ -4,6 +4,8 @@ import { measureVisual } from './visual-audit.js';
4
4
  import { formatNumber } from './format-number.js';
5
5
  import { CANVAS_SYSTEM_CSS } from './canvas-system.js';
6
6
  import {createTimelineAPI} from './timeline.js';
7
+ import {createTimelineControlRepair, canvasContentHeight} from './canvas-controls.js';
8
+ import {inspectTextCards, fitTextCards} from './canvas-layout.js';
7
9
  // Shared bounds and document construction. Generated code runs only in an opaque-origin frame.
8
10
  export const SANDBOX_LIMITS = Object.freeze({ source: 49152, height: 1000, message: 16384, rate: 30 });
9
11
  export function canvasSnapshot(node, state, data, derived, disabled, theme) {
@@ -25,7 +27,12 @@ export function sandboxDocument(node, channel, nonce) {
25
27
  let snapshot={state:{},data:{},derived:{},disabled:true,theme:{}}, started=false;
26
28
  const listeners=new Set(), send=(type,payload)=>parent.postMessage({dshCanvas:1,channel,type,payload},'*');
27
29
  const report=e=>send('error',String(e?.message??e).slice(0,1000));
28
- const audit=${measureVisual.toString()};let auditTimer;const scheduleAudit=()=>{clearTimeout(auditTimer);auditTimer=setTimeout(()=>{const issues=audit(document.getElementById('canvas'),${json(node.id??'')});send('audit',{issues,coverage:issues.coverage})},400)};
30
+ const repairTimelineControls=(${createTimelineControlRepair.toString()})();
31
+ const inspectCards=${inspectTextCards.toString()}, fitCards=${fitTextCards.toString()};
32
+ const contentHeight=${canvasContentHeight.toString()};
33
+ const repairControls=()=>{if(!window.__DSHMotionRender&&timelineAPI.capture.list().length)repairTimelineControls()};
34
+ for(const type of ['click','input','change'])document.addEventListener(type,()=>queueMicrotask(repairControls));
35
+ const audit=${measureVisual.toString()};let auditTimer;const scheduleAudit=()=>{clearTimeout(auditTimer);auditTimer=setTimeout(()=>{repairControls();fitCards(document.getElementById('canvas'),inspectCards);const issues=audit(document.getElementById('canvas'),${json(node.id??'')},inspectCards);send('audit',{issues,coverage:issues.coverage})},400)};
29
36
  addEventListener('resize',scheduleAudit);
30
37
  addEventListener('error',e=>report(e.error??e.message));addEventListener('unhandledrejection',e=>report(e.reason));
31
38
  const animations=new Set(), motion=matchMedia('(prefers-reduced-motion: reduce)');
@@ -50,9 +57,9 @@ export function sandboxDocument(node, channel, nonce) {
50
57
  const wasStarted=started;
51
58
  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(nonce)};script.textContent=source.script;document.body.append(script)}
52
59
  if(wasStarted)for(const fn of listeners)try{fn(api.snapshot)}catch(e){report(e)}
53
- refresh();if(!wasStarted)send('mounted');scheduleAudit();
60
+ fitCards(document.getElementById('canvas'),inspectCards);repairControls();refresh();if(!wasStarted)send('mounted');scheduleAudit();
54
61
  });
55
- 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'));
62
+ let resizePending=false;new ResizeObserver(()=>{if(!started||resizePending)return;resizePending=true;requestAnimationFrame(()=>{resizePending=false;send('height',contentHeight(document.getElementById('canvas')))})}).observe(document.getElementById('canvas'));
56
63
  send('ready');
57
64
  })();</script></body></html>`;
58
65
  }
@@ -1,5 +1,5 @@
1
1
  // Serialized into the opaque frame; it never reads host content.
2
- export function measureVisual(root, nodeId='') {
2
+ export function measureVisual(root, nodeId='', inspectCards=()=>[]) {
3
3
  const issues=[],seen=new Set(),coverage={nodeId,elements:0,truncated:false,contrastChecked:0,contrastUncovered:0};
4
4
  const styleCache=new WeakMap(),styleOf=el=>{let css=styleCache.get(el);if(!css){css=getComputedStyle(el);styleCache.set(el,css)}return css};
5
5
  const target=el=>{if(!el)return undefined;const tag=el.tagName.toLowerCase(),id=el.id;return{selector:id&&id.length<=64?'#'+CSS.escape(id):tag,text:(el.textContent??'').trim().replace(/\s+/g,' ').slice(0,120),tag}};
@@ -18,12 +18,27 @@ export function measureVisual(root, nodeId='') {
18
18
  const fg=color(css.color),bg=background(el);let opacitySafe=true,opacityDepth=0;for(let a=el;a;a=a.parentElement){if(++opacityDepth>64||parseFloat(styleOf(a).opacity)<1){opacitySafe=false;break}}
19
19
  if(fg&&bg&&opacitySafe){coverage.contrastChecked++;const a=luminance(blend(fg,bg)),b=luminance(bg),ratio=(Math.max(a,b)+.05)/(Math.min(a,b)+.05);if(ratio<(size>=24||(size>=18.66&&parseFloat(css.fontWeight)>=700)?3:4.5))add('contrast',ratio,el);}else coverage.contrastUncovered++;
20
20
  const range=document.createRange();range.selectNodeContents(el);const bounds=range.getBoundingClientRect();let a=el,depth=0;while(a&&a!==root.parentElement&&depth++<24){const c=styleOf(a),clip=a.getBoundingClientRect();if((['hidden','clip'].includes(c.overflowX)&&(bounds.left<clip.left-2||bounds.right>clip.right+2))||(['hidden','clip'].includes(c.overflowY)&&(bounds.top<clip.top-2||bounds.bottom>clip.bottom+2))){add('clippedText',undefined,el);break}a=a.parentElement}
21
- }else coverage.contrastUncovered++;
21
+ }else {
22
+ coverage.contrastUncovered++;
23
+ if (!el.closest('#stage')) {
24
+ let parent=el.parentElement,backgrounds=[];
25
+ for(let depth=0;parent instanceof SVGElement&&depth<4;parent=parent.parentElement,depth++){backgrounds=parent.querySelectorAll(':scope > rect');if(backgrounds.length)break}
26
+ for(const background of backgrounds){
27
+ const c=styleOf(background),rect=background.getBoundingClientRect();
28
+ if(c.fill==='none'||parseFloat(c.fillOpacity)===0||!parseFloat(background.getAttribute('rx'))||rect.width<box.width*.5||rect.height<box.height*2)continue;
29
+ const matrix=background.getScreenCTM(),radius=Math.min(parseFloat(background.getAttribute('rx'))*(matrix?Math.hypot(matrix.a,matrix.b):1),rect.height/2,rect.width/2);
30
+ const progress=radius?Math.max(0,Math.min(1,(box.bottom-(rect.bottom-radius))/radius)):0,inset=radius*(1-Math.sqrt(1-progress*progress));
31
+ const corner=progress>0&&(box.left<rect.left+inset-1||box.right>rect.right-inset+1);
32
+ if(box.left>=rect.left-1&&box.left<rect.right&&box.top>=rect.top&&box.top<=rect.bottom&&(box.bottom>rect.bottom+1||corner)){add('cardTextOverflow',Math.max(0,box.bottom-rect.bottom),el);break}
33
+ }
34
+ }
35
+ }
22
36
 
23
37
  }
24
38
  if(text||el.matches('svg,canvas,img,video,input,select,textarea,button'))contentBottom=Math.max(contentBottom,box.bottom);
25
39
  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',undefined,el);
26
40
  }
41
+ for(const issue of inspectCards(root))add('cardTextOverflow',Math.max(0,issue.excess),issue.target);
27
42
  const rect=root.getBoundingClientRect(),gap=rect.bottom-contentBottom;if(!coverage.truncated&&root.id==='canvas'&&rect.height>320&&gap>Math.max(96,rect.height*.25))add('bottomWhitespace',gap,root);
28
43
  Object.defineProperty(issues,'coverage',{value:coverage});return issues;
29
44
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-motion-ui",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "Readable conversational UI, interactive HTML/SVG, editable motion and local MP4 production for DeepSeek Harness.",
5
5
  "type": "module",
6
6
  "main": "index.js",