dsh-motion-ui 0.8.1 → 0.8.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/CHANGELOG.md +30 -0
- package/README.md +10 -2
- package/VERIFICATION.md +14 -0
- package/client.js +162 -9
- package/lib/build-info.js +1 -1
- package/lib/canvas-controls.js +53 -0
- package/lib/canvas-layout.js +64 -0
- package/lib/canvas-preview.js +3 -1
- package/lib/canvas-system.js +1 -0
- package/lib/export-runtime-bundle.js +1 -1
- package/lib/motion-craft.js +1 -1
- package/lib/refinement.js +1 -1
- package/lib/sandbox.js +10 -3
- package/lib/visual-audit.js +17 -2
- package/package.json +8 -7
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.8.
|
|
2
|
+
export const BUILD_INFO = Object.freeze({"version":"0.8.3","id":"0.8.3+da9b5c9a709d","sourceHash":"da9b5c9a709de333a5c0348d1e9544ced197459e293f22dcfdc302b8dbb5f3d0"});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Serialized into the opaque frame. Only repairs unreadable timeline chrome;
|
|
2
|
+
// the authored stage and ordinary interactive documents retain their styling.
|
|
3
|
+
export function createTimelineControlRepair() {
|
|
4
|
+
const originals = new WeakMap();
|
|
5
|
+
const rgb = text => {
|
|
6
|
+
const match = text.match(/^rgba?\(([^)]+)\)$/);
|
|
7
|
+
if (!match) return null;
|
|
8
|
+
const values = match[1].split(/[,\s/]+/).filter(Boolean).map(Number);
|
|
9
|
+
return values.length >= 3 ? [...values.slice(0, 3), values[3] ?? 1] : null;
|
|
10
|
+
};
|
|
11
|
+
const over = (a, b) => a.slice(0, 3).map((v, i) => v * a[3] + b[i] * (1 - a[3]));
|
|
12
|
+
const luminance = c => c.slice(0, 3).map(v => v / 255).map(v => v <= .04045 ? v / 12.92 : ((v + .055) / 1.055) ** 2.4).reduce((n, v, i) => n + v * [.2126, .7152, .0722][i], 0);
|
|
13
|
+
const contrast = (a, b) => { const x = luminance(a), y = luminance(b); return (Math.max(x, y) + .05) / (Math.min(x, y) + .05); };
|
|
14
|
+
return () => {
|
|
15
|
+
const stage = document.getElementById('stage'), root = document.getElementById('canvas');
|
|
16
|
+
if (!stage || !root) return;
|
|
17
|
+
for (const el of root.querySelectorAll('button,output,select,input:not([type=range]):not([type=checkbox]):not([type=radio])')) {
|
|
18
|
+
if (stage.contains(el)) continue;
|
|
19
|
+
let saved = originals.get(el);
|
|
20
|
+
if (saved) for (const [key, value, priority] of saved) value ? el.style.setProperty(key, value, priority) : el.style.removeProperty(key);
|
|
21
|
+
const style = getComputedStyle(el);
|
|
22
|
+
if (style.display === 'none' || style.visibility === 'hidden') continue;
|
|
23
|
+
const layers = []; let uncertain = false;
|
|
24
|
+
for (let parent = el; parent; parent = parent.parentElement) {
|
|
25
|
+
const backgroundStyle = getComputedStyle(parent);
|
|
26
|
+
// A gradient/image cannot be reduced to one reliable backdrop color.
|
|
27
|
+
if (backgroundStyle.backgroundImage !== 'none') { uncertain = true; break; }
|
|
28
|
+
const color = rgb(backgroundStyle.backgroundColor);
|
|
29
|
+
if (color) layers.push(color);
|
|
30
|
+
if (color?.[3] === 1) break;
|
|
31
|
+
}
|
|
32
|
+
if (uncertain) continue;
|
|
33
|
+
const background = layers.reverse().reduce((color, layer) => over(layer, color), [255, 255, 255]);
|
|
34
|
+
const foreground = rgb(style.color);
|
|
35
|
+
if (!foreground || contrast(over(foreground, background), background) >= 4.5) continue;
|
|
36
|
+
if (!saved) {
|
|
37
|
+
saved = ['color', 'border-color'].map(key => [key, el.style.getPropertyValue(key), el.style.getPropertyPriority(key)]);
|
|
38
|
+
originals.set(el, saved);
|
|
39
|
+
}
|
|
40
|
+
const light = [248, 250, 252], dark = [17, 24, 39];
|
|
41
|
+
el.style.setProperty('color', contrast(light, background) > contrast(dark, background) ? '#f8fafc' : '#111827', 'important');
|
|
42
|
+
const border = rgb(style.borderTopColor);
|
|
43
|
+
if (parseFloat(style.borderTopWidth) > 0 && border && contrast(over(border, background), background) < 1.5) el.style.setProperty('border-color', '#64748b', 'important');
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function canvasContentHeight(root) {
|
|
49
|
+
const body = getComputedStyle(document.body);
|
|
50
|
+
// getBoundingClientRect().height alone excludes author-defined body padding.
|
|
51
|
+
// Do not use scrollHeight: it is floored by the old iframe viewport and cannot shrink.
|
|
52
|
+
return Math.ceil(root.getBoundingClientRect().bottom + window.scrollY + (parseFloat(body.paddingBottom) || 0) + (parseFloat(body.marginBottom) || 0));
|
|
53
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Geometry only: no source rewriting, shadow removal, or clipping as a repair.
|
|
2
|
+
// Serialized into the isolated frame together with fitTextCards.
|
|
3
|
+
export function inspectTextCards(root, cards) {
|
|
4
|
+
const result = [];
|
|
5
|
+
const cache = new WeakMap(), styleOf = el => { if (!cache.has(el)) cache.set(el, getComputedStyle(el)); return cache.get(el); };
|
|
6
|
+
for (const card of cards ?? [...root.querySelectorAll('article,section,aside,figure,div')].slice(0, 256).reverse()) {
|
|
7
|
+
if (card.closest('#stage,svg,pre,code,[role=img],[data-motion-layout=preserve]')) continue;
|
|
8
|
+
const css = styleOf(card), box = card.getBoundingClientRect();
|
|
9
|
+
const radius = Math.max(parseFloat(css.borderBottomLeftRadius) || 0, parseFloat(css.borderBottomRightRadius) || 0);
|
|
10
|
+
if (box.width < 120 || box.height < 24 || css.display === 'none' || css.visibility === 'hidden') continue;
|
|
11
|
+
if (/auto|scroll/.test(css.overflowY)) continue;
|
|
12
|
+
if (!radius && css.boxShadow === 'none') continue;
|
|
13
|
+
const padding = parseFloat(css.paddingBottom) || 0, border = parseFloat(css.borderBottomWidth) || 0;
|
|
14
|
+
const bottom = box.bottom - padding - border;
|
|
15
|
+
let excess = 0, corner = false, target, flow = !['absolute', 'fixed'].includes(css.position) && css.transform === 'none' && !/auto|scroll/.test(css.overflowY) && !/size/.test(css.contain) && !css.borderBottomLeftRadius.includes('%') && !css.borderBottomRightRadius.includes('%') && !(css.transitionDuration.split(',').some(v => parseFloat(v) > 0) && /(^|,|\s)(height|max-height|all)(,|\s|$)/.test(css.transitionProperty));
|
|
16
|
+
const walker = document.createTreeWalker(card, NodeFilter.SHOW_TEXT); let text, count = 0;
|
|
17
|
+
while ((text = walker.nextNode()) && ++count <= 128) {
|
|
18
|
+
if (!text.textContent.trim()) continue;
|
|
19
|
+
const el = text.parentElement;
|
|
20
|
+
if (el.closest('svg,pre,code,script,style,textarea,[role=img]')) continue;
|
|
21
|
+
const style = styleOf(el);
|
|
22
|
+
if (style.visibility === 'hidden' || style.display === 'none') continue;
|
|
23
|
+
if (style.webkitLineClamp && style.webkitLineClamp !== 'none') continue;
|
|
24
|
+
let intentional = false;
|
|
25
|
+
for (let parent = el; parent && parent !== card; parent = parent.parentElement) {
|
|
26
|
+
const c = styleOf(parent);
|
|
27
|
+
if (/auto|scroll/.test(c.overflowY) || (c.webkitLineClamp && c.webkitLineClamp !== 'none')) { intentional = true; break; }
|
|
28
|
+
}
|
|
29
|
+
if (intentional) continue;
|
|
30
|
+
const range = document.createRange(); range.selectNode(text);
|
|
31
|
+
for (const rect of range.getClientRects()) {
|
|
32
|
+
if (!rect.width || !rect.height) continue;
|
|
33
|
+
const amount = rect.bottom - bottom;
|
|
34
|
+
// Keep the last line out of a rounded bottom corner, even if overflow is visible.
|
|
35
|
+
const r = Math.min(radius, box.height / 2, box.width / 2);
|
|
36
|
+
const progress = r ? Math.max(0, Math.min(1, (rect.bottom - (box.bottom - r)) / r)) : 0;
|
|
37
|
+
const inset = r * (1 - Math.sqrt(1 - progress * progress));
|
|
38
|
+
const touchesCorner = progress > 0 && (rect.left < box.left + inset - 1 || rect.right > box.right - inset + 1);
|
|
39
|
+
if (amount <= 1 && !touchesCorner) continue;
|
|
40
|
+
target = el; excess = Math.max(excess, amount); corner ||= touchesCorner;
|
|
41
|
+
for (let parent = el; parent && parent !== card; parent = parent.parentElement) {
|
|
42
|
+
const c = styleOf(parent);
|
|
43
|
+
if (['absolute', 'fixed'].includes(c.position) || c.transform !== 'none' || /auto|scroll/.test(c.overflowY) || (c.webkitLineClamp && c.webkitLineClamp !== 'none')) flow = false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (target) result.push({card, target, excess, corner, radius, flow});
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function fitTextCards(root, inspect) {
|
|
53
|
+
// Re-measure inner cards first so an outer container sees their final dimensions.
|
|
54
|
+
for (const {card} of inspect(root)) {
|
|
55
|
+
const issue = inspect(root, [card])[0];
|
|
56
|
+
if (!issue?.flow) continue;
|
|
57
|
+
const css = getComputedStyle(card), box = card.getBoundingClientRect();
|
|
58
|
+
if (issue.corner) card.style.setProperty('padding-bottom', Math.max(parseFloat(css.paddingBottom) || 0, Math.min(issue.radius, box.height / 2)) + 'px', 'important');
|
|
59
|
+
const inset = css.boxSizing === 'border-box' ? 0 : ['paddingTop', 'paddingBottom', 'borderTopWidth', 'borderBottomWidth'].reduce((n, key) => n + (parseFloat(css[key]) || 0), 0);
|
|
60
|
+
card.style.setProperty('min-height', Math.max(parseFloat(css.minHeight) || 0, box.height - inset) + 'px', 'important');
|
|
61
|
+
card.style.setProperty('height', 'auto', 'important');
|
|
62
|
+
card.style.setProperty('max-height', 'none', 'important');
|
|
63
|
+
}
|
|
64
|
+
}
|
package/lib/canvas-preview.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { CANVAS_SYSTEM_CSS } from './canvas-system.js';
|
|
2
|
+
import {canvasContentHeight} from './canvas-controls.js';
|
|
3
|
+
import {inspectTextCards, fitTextCards} from './canvas-layout.js';
|
|
2
4
|
// Executed only as trusted bootstrap code inside an opaque-origin preview frame.
|
|
3
5
|
export function updatePreview(html) {
|
|
4
6
|
const template=document.createElement('template');template.innerHTML=html;
|
|
@@ -27,5 +29,5 @@ export function updatePreview(html) {
|
|
|
27
29
|
morph(document.getElementById('canvas'),template.content);
|
|
28
30
|
}
|
|
29
31
|
export function previewFrameDocument(channel,nonce){
|
|
30
|
-
return `<!doctype html><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none';script-src 'nonce-${nonce}';style-src 'unsafe-inline';img-src data:;base-uri 'none';form-action 'none';frame-src 'none'"><style>${CANVAS_SYSTEM_CSS}</style><style id="custom"></style><style>#canvas,#canvas *,#canvas *::before,#canvas *::after{animation:none!important;transition:none!important}html,body{overflow:hidden}</style><main id="canvas" inert></main><script nonce="${nonce}">const update=${updatePreview.toString()};addEventListener('message',e=>{if(e.source!==parent||e.data?.channel!==${JSON.stringify(channel)})return;document.getElementById('custom').textContent=e.data.css;for(const [k,v] of Object.entries(e.data.theme||{}))document.documentElement.style.setProperty('--canvas-'+k,v);document.documentElement.dataset.canvasScheme=e.data.theme?.scheme||'light';update(e.data.html)});new ResizeObserver(()=>parent.postMessage({channel:${JSON.stringify(channel)},previewHeight:
|
|
32
|
+
return `<!doctype html><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none';script-src 'nonce-${nonce}';style-src 'unsafe-inline';img-src data:;base-uri 'none';form-action 'none';frame-src 'none'"><style>${CANVAS_SYSTEM_CSS}</style><style id="custom"></style><style>#canvas,#canvas *,#canvas *::before,#canvas *::after{animation:none!important;transition:none!important}html,body{overflow:hidden}</style><main id="canvas" inert></main><script nonce="${nonce}">const inspectCards=${inspectTextCards.toString()},fitCards=${fitTextCards.toString()};const contentHeight=${canvasContentHeight.toString()};const update=${updatePreview.toString()};addEventListener('message',e=>{if(e.source!==parent||e.data?.channel!==${JSON.stringify(channel)})return;document.getElementById('custom').textContent=e.data.css;for(const [k,v] of Object.entries(e.data.theme||{}))document.documentElement.style.setProperty('--canvas-'+k,v);document.documentElement.dataset.canvasScheme=e.data.theme?.scheme||'light';update(e.data.html);fitCards(document.getElementById('canvas'),inspectCards)});new ResizeObserver(()=>parent.postMessage({channel:${JSON.stringify(channel)},previewHeight:contentHeight(document.getElementById('canvas'))},'*')).observe(document.getElementById('canvas'));</script>`;
|
|
31
33
|
}
|
package/lib/canvas-system.js
CHANGED
|
@@ -12,6 +12,7 @@ svg .ui-node{fill:var(--canvas-soft,#f3f5f8);stroke:var(--canvas-line,#ced4df);s
|
|
|
12
12
|
|
|
13
13
|
export const CANVAS_SYSTEM_GUIDE = `The canvas includes optional .ui-stack, .ui-grid, .ui-surface, .ui-title, .ui-caption, .ui-value, .ui-controls, .ui-field, .ui-legend, .ui-status, .ui-error and .ui-primary classes. SVG helpers: .ui-node (shape), .ui-edge (unfilled connector), .ui-label (main text), .ui-note (annotation), .ui-leader (dashed annotation line), .ui-accent (theme-aware accent shape). Use them as building blocks, or override with your own composition; they do not prescribe cards or a fixed layout. Theme variables --canvas-bg/soft/ink/muted/line/accent follow DSH. Define --canvas-accent-ink yourself if overriding the accent with a pale color.
|
|
14
14
|
For a small static diagram, .ui-flow arranges .ui-step nodes separated by .ui-arrow; each step uses strong + span. .ui-callout emphasizes one sentence. These work with no custom CSS or JavaScript and adapt to narrow screens.
|
|
15
|
+
Text-card layout: let HTML card height grow with wrapped content, use real padding and a flow-root/flex/grid container so paragraph margins stay inside. Keep the last line above rounded bottom corners. Shadows are decorative paint, not content bounds: preserve them when fixing overflow. For SVG cards derive the background rect height and viewBox from actual wrapped text lines plus padding; do not fix the issue by removing shadows, hiding overflow or shrinking text.
|
|
15
16
|
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.
|
|
16
17
|
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.
|
|
17
18
|
Keep CSS small and put a useful initial view in html. Source field order is arbitrary. Completed static content previews progressively as read-only during generation; scripts/controls activate only after the whole assistant turn ends and state restoration. Do not claim a saved document has been visually checked without evidence.
|