@1agh/maude 0.39.1 → 0.41.0
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/README.md +1 -1
- package/apps/studio/ai-banner.tsx +2 -2
- package/apps/studio/annotations-context-toolbar.tsx +44 -1
- package/apps/studio/annotations-layer.tsx +306 -5
- package/apps/studio/annotations-model.ts +107 -6
- package/apps/studio/api.ts +1316 -67
- package/apps/studio/artboard-marquee.tsx +1 -1
- package/apps/studio/bin/_canvas-rects-playwright.mjs +55 -0
- package/apps/studio/bin/_canvas-rects-static.mjs +166 -0
- package/apps/studio/bin/_fetch-asset.mjs +556 -0
- package/apps/studio/bin/_html-playwright.mjs +9 -1
- package/apps/studio/bin/_pdf-playwright.mjs +8 -1
- package/apps/studio/bin/_png-playwright.mjs +8 -1
- package/apps/studio/bin/_pw-launch.mjs +54 -0
- package/apps/studio/bin/_svg-playwright.mjs +8 -1
- package/apps/studio/bin/_video-playwright.mjs +452 -0
- package/apps/studio/bin/annotate.mjs +576 -34
- package/apps/studio/bin/canvas-rects.sh +152 -0
- package/apps/studio/bin/fetch-asset.sh +34 -0
- package/apps/studio/bin/prep.sh +8 -1
- package/apps/studio/bin/read-annotations.mjs +138 -7
- package/apps/studio/bin/smoke.sh +42 -6
- package/apps/studio/build.ts +21 -0
- package/apps/studio/canvas-comment-mount.tsx +138 -4
- package/apps/studio/canvas-edit.ts +2625 -111
- package/apps/studio/canvas-lib.tsx +238 -2
- package/apps/studio/canvas-list-watch.ts +6 -2
- package/apps/studio/canvas-shell.tsx +514 -20
- package/apps/studio/client/app.jsx +2480 -52
- package/apps/studio/client/comments-overlay.css +130 -126
- package/apps/studio/client/github.js +15 -0
- package/apps/studio/client/panels/TimelinePanel.jsx +860 -0
- package/apps/studio/client/panels/timeline-parse.js +229 -0
- package/apps/studio/client/panels/timeline-snap.js +55 -0
- package/apps/studio/client/styles/3-shell-maude.css +155 -0
- package/apps/studio/comments-overlay.tsx +148 -41
- package/apps/studio/config.schema.json +14 -0
- package/apps/studio/context-menu.tsx +16 -6
- package/apps/studio/context.ts +113 -1
- package/apps/studio/contextual-toolbar.tsx +262 -4
- package/apps/studio/cursors-overlay.tsx +4 -4
- package/apps/studio/dist/client.bundle.js +89 -17
- package/apps/studio/dist/comment-mount.js +59 -1
- package/apps/studio/dist/runtime/.min-sizes.json +11 -1
- package/apps/studio/dist/runtime/@remotion_media.js +491 -0
- package/apps/studio/dist/runtime/@remotion_player.js +56 -0
- package/apps/studio/dist/runtime/@remotion_transitions.js +300 -0
- package/apps/studio/dist/runtime/@remotion_transitions_clock-wipe.js +1 -0
- package/apps/studio/dist/runtime/@remotion_transitions_fade.js +1 -0
- package/apps/studio/dist/runtime/@remotion_transitions_flip.js +1 -0
- package/apps/studio/dist/runtime/@remotion_transitions_none.js +1 -0
- package/apps/studio/dist/runtime/@remotion_transitions_slide.js +1 -0
- package/apps/studio/dist/runtime/@remotion_transitions_wipe.js +1 -0
- package/apps/studio/dist/runtime/REMOTION-LICENSE.md +28 -0
- package/apps/studio/dist/runtime/remotion.js +42 -0
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/dom-selection.ts +127 -1
- package/apps/studio/drag-state.ts +24 -0
- package/apps/studio/equal-spacing-detector.ts +205 -0
- package/apps/studio/export-dialog.tsx +1 -1
- package/apps/studio/exporters/_browser-bundles.ts +117 -0
- package/apps/studio/exporters/_runtime.ts +69 -0
- package/apps/studio/exporters/html.ts +4 -3
- package/apps/studio/exporters/index.ts +28 -2
- package/apps/studio/exporters/pdf.ts +4 -3
- package/apps/studio/exporters/png.ts +5 -3
- package/apps/studio/exporters/pptx.ts +6 -4
- package/apps/studio/exporters/svg.ts +9 -5
- package/apps/studio/exporters/video-encode-lib.ts +200 -0
- package/apps/studio/exporters/video-render-lib.ts +108 -0
- package/apps/studio/exporters/video.ts +184 -0
- package/apps/studio/history.ts +47 -1
- package/apps/studio/http.ts +758 -27
- package/apps/studio/input-router.tsx +19 -1
- package/apps/studio/marquee-overlay.tsx +1 -1
- package/apps/studio/measure-overlay.tsx +241 -0
- package/apps/studio/participants-chrome.tsx +3 -3
- package/apps/studio/runtime-bundle.ts +30 -0
- package/apps/studio/server.ts +34 -9
- package/apps/studio/sizing-mode.ts +117 -0
- package/apps/studio/spacing-handles.ts +166 -0
- package/apps/studio/test/annotate-write.test.ts +890 -0
- package/apps/studio/test/annotations-roundtrip.test.ts +47 -0
- package/apps/studio/test/camera-reveal.test.tsx +173 -0
- package/apps/studio/test/canvas-create-api.test.ts +76 -0
- package/apps/studio/test/canvas-edit.test.ts +140 -0
- package/apps/studio/test/canvas-list-watch.test.ts +49 -0
- package/apps/studio/test/canvas-media-drop.test.ts +30 -1
- package/apps/studio/test/canvas-origin-gate.test.ts +54 -0
- package/apps/studio/test/canvas-rects.test.ts +198 -0
- package/apps/studio/test/clip-addressing.test.ts +732 -0
- package/apps/studio/test/comments-overlay.test.ts +117 -0
- package/apps/studio/test/config-reload.test.ts +230 -0
- package/apps/studio/test/dns-rebinding-guard.test.ts +74 -0
- package/apps/studio/test/dom-selection.test.ts +130 -0
- package/apps/studio/test/edit-css-occurrence.test.ts +81 -0
- package/apps/studio/test/edit-persistence.test.ts +91 -0
- package/apps/studio/test/edit-scope-api.test.ts +115 -0
- package/apps/studio/test/element-resize.test.ts +136 -0
- package/apps/studio/test/element-structural-api.test.ts +360 -0
- package/apps/studio/test/element-structural-edit.test.ts +233 -0
- package/apps/studio/test/equal-spacing-detector.test.ts +165 -1
- package/apps/studio/test/exporters/runtime.test.ts +59 -0
- package/apps/studio/test/file-lock.test.ts +84 -0
- package/apps/studio/test/fixtures/video-comp-fixture.tsx +82 -0
- package/apps/studio/test/history-rollback.test.ts +26 -0
- package/apps/studio/test/knob-props-authored.test.ts +87 -0
- package/apps/studio/test/read-annotations.test.ts +154 -0
- package/apps/studio/test/sizing-mode.test.ts +102 -0
- package/apps/studio/test/spacing-handles.test.ts +138 -0
- package/apps/studio/test/specimen-select.test.ts +88 -0
- package/apps/studio/test/timeline-parse.test.ts +127 -0
- package/apps/studio/test/timeline-snap.test.ts +85 -0
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +84 -0
- package/apps/studio/test/undo-sequence-byte-compare.test.ts +211 -0
- package/apps/studio/test/video-asset.test.ts +163 -0
- package/apps/studio/test/video-comp-fixture.test.ts +50 -0
- package/apps/studio/test/video-comp.test.ts +168 -0
- package/apps/studio/test/video-render-bridge.test.ts +149 -0
- package/apps/studio/tool-palette.tsx +122 -2
- package/apps/studio/undo-hud.tsx +2 -2
- package/apps/studio/use-annotation-resize.tsx +6 -3
- package/apps/studio/use-canvas-media-drop.tsx +66 -4
- package/apps/studio/use-element-resize.tsx +732 -0
- package/apps/studio/use-keyboard-discipline.tsx +205 -15
- package/apps/studio/use-selection-set.tsx +14 -0
- package/apps/studio/use-spacing-handles.tsx +388 -0
- package/apps/studio/video-comp.tsx +444 -0
- package/apps/studio/whats-new.json +55 -0
- package/apps/studio/ws.ts +5 -0
- package/cli/commands/design.mjs +6 -1
- package/cli/commands/design.test.mjs +49 -1
- package/cli/lib/fetch-asset.test.mjs +213 -0
- package/package.json +8 -8
- package/plugins/design/dependencies.json +10 -2
- package/plugins/design/templates/_shell.html +25 -2
- package/plugins/design/templates/design-system-inspiration/_MAPPING.md +1 -1
|
@@ -345,7 +345,13 @@ export function isEditableTarget(t: EventTarget | null): boolean {
|
|
|
345
345
|
*/
|
|
346
346
|
export function isOverlayTarget(t: EventTarget | null): boolean {
|
|
347
347
|
if (!t || !(t as Element).closest) return false;
|
|
348
|
-
|
|
348
|
+
// [data-mediaref-player] — the inline <video>/<audio controls> on a media
|
|
349
|
+
// reference chip (DDR-150 dogfood #8). The router must never claim (and
|
|
350
|
+
// preventDefault) pointerdowns over the player, or its native controls
|
|
351
|
+
// (play button, scrubber drag, volume) die under the move tool.
|
|
352
|
+
return !!(t as Element).closest(
|
|
353
|
+
'.cm-composer, .cm-thread, .cm-mention-popup, .cm-pin, [data-mediaref-player]'
|
|
354
|
+
);
|
|
349
355
|
}
|
|
350
356
|
|
|
351
357
|
export function useInputRouter(opts: UseInputRouterOptions): void {
|
|
@@ -469,6 +475,18 @@ export function useInputRouter(opts: UseInputRouterOptions): void {
|
|
|
469
475
|
if (action.kind !== 'no-op') {
|
|
470
476
|
e.preventDefault();
|
|
471
477
|
e.stopImmediatePropagation();
|
|
478
|
+
// preventDefault above stops mousedown's default FOCUS, so a Cmd-click
|
|
479
|
+
// select would leave the iframe unfocused and every in-canvas keyboard
|
|
480
|
+
// shortcut (arrow-nudge, Cmd+D, copy/paste-style, Delete) dead until a
|
|
481
|
+
// drag happened to focus it (dogfood: "funguje jen když hnu myší").
|
|
482
|
+
// Restore canvas focus explicitly so the keydown listeners fire.
|
|
483
|
+
if (action.kind === 'select') {
|
|
484
|
+
try {
|
|
485
|
+
window.focus();
|
|
486
|
+
} catch {
|
|
487
|
+
/* focus may be rejected outside a user gesture */
|
|
488
|
+
}
|
|
489
|
+
}
|
|
472
490
|
}
|
|
473
491
|
};
|
|
474
492
|
|
|
@@ -54,7 +54,7 @@ function isChromeTarget(t: EventTarget | null): boolean {
|
|
|
54
54
|
const el = t as Element;
|
|
55
55
|
if (
|
|
56
56
|
el.closest(
|
|
57
|
-
'.dc-mm, .dc-zoom-tb, .dc-tool-palette, .dc-context-menu, .dc-cv-halo, .dc-cv-group-bbox, .cm-composer, .cm-thread, .cm-mention-popup, .cm-pin, .dc-annot-svg, .dc-annot-ctx, .dc-tp-popover, .dc-annot-resize-handle, .dc-multi-artboard-tb, .dc-elem-ctx-tb, .dc-cv-eq-spacing-layer'
|
|
57
|
+
'.dc-mm, .dc-zoom-tb, .dc-tool-palette, .dc-context-menu, .dc-cv-halo, .dc-cv-group-bbox, .cm-composer, .cm-thread, .cm-mention-popup, .cm-pin, .dc-annot-svg, .dc-annot-ctx, .dc-tp-popover, .dc-annot-resize-handle, .dc-el-resize-handle, .dc-multi-artboard-tb, .dc-elem-ctx-tb, .dc-cv-eq-spacing-layer'
|
|
58
58
|
)
|
|
59
59
|
) {
|
|
60
60
|
return true;
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file measure-overlay.tsx — Task L7 (feature-element-editing-robustness)
|
|
3
|
+
* @scope apps/studio/measure-overlay.tsx
|
|
4
|
+
* @purpose Alt-hover distance measurement (Figma parity): with exactly one
|
|
5
|
+
* element selected, holding Alt and hovering a DIFFERENT stamped
|
|
6
|
+
* element paints a red guide line + pixel-distance pill between
|
|
7
|
+
* the two boxes' facing edges, on whichever axis(es) actually
|
|
8
|
+
* have a gap. Read-only — never writes anything, no undo, no
|
|
9
|
+
* write-surface implications.
|
|
10
|
+
*
|
|
11
|
+
* Mirrors `equal-spacing-handles.tsx`'s fixed-rAF-follow scaffold
|
|
12
|
+
* (persistent DOM children, no React re-render per frame) and
|
|
13
|
+
* reuses its pairwise-gap sibling `computePairGap`
|
|
14
|
+
* (equal-spacing-detector.ts) for the math.
|
|
15
|
+
*
|
|
16
|
+
* Alt-tracking mirrors `use-cursor-modifiers.tsx`'s
|
|
17
|
+
* `reduceModifiers` reducer, but keeps the result in React state
|
|
18
|
+
* (gating hover RESOLUTION, a real cost) rather than a DOM
|
|
19
|
+
* attribute (that hook only drives CSS cursor swaps).
|
|
20
|
+
*
|
|
21
|
+
* Distances are printed in WORLD px (screen delta ÷ the world's
|
|
22
|
+
* zoom) so the number stays correct at any zoom level — the
|
|
23
|
+
* Stage-A host-scroll-0 invariant is what makes a screen-rect
|
|
24
|
+
* delta ÷ zoom equivalent to a true world-space measurement.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { useEffect, useRef, useState } from 'react';
|
|
28
|
+
import { useArtboardsContext } from './canvas-lib.tsx';
|
|
29
|
+
import { resolveSelectionEl } from './dom-selection.ts';
|
|
30
|
+
import { computePairGap } from './equal-spacing-detector.ts';
|
|
31
|
+
import { resolveHoverTarget } from './input-router.tsx';
|
|
32
|
+
import { type ModifierState, reduceModifiers } from './use-cursor-modifiers.tsx';
|
|
33
|
+
import { useSelectionSet } from './use-selection-set.tsx';
|
|
34
|
+
|
|
35
|
+
const MEASURE_CSS = `
|
|
36
|
+
.dc-cv-measure-layer {
|
|
37
|
+
position: fixed;
|
|
38
|
+
inset: 0;
|
|
39
|
+
pointer-events: none;
|
|
40
|
+
z-index: 7;
|
|
41
|
+
}
|
|
42
|
+
.dc-cv-measure-line {
|
|
43
|
+
position: absolute;
|
|
44
|
+
background: #FF3B30;
|
|
45
|
+
opacity: 0;
|
|
46
|
+
transition: opacity 120ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
47
|
+
}
|
|
48
|
+
.dc-cv-measure-line[data-axis="x"] { height: 1px; }
|
|
49
|
+
.dc-cv-measure-line[data-axis="y"] { width: 1px; }
|
|
50
|
+
.dc-cv-measure-line[data-on="true"] { opacity: 1; }
|
|
51
|
+
.dc-cv-measure-pill {
|
|
52
|
+
position: absolute;
|
|
53
|
+
transform: translate(-50%, -50%);
|
|
54
|
+
font-family: var(--maude-chrome-font-mono, ui-monospace, SFMono-Regular, monospace);
|
|
55
|
+
font-size: 10px;
|
|
56
|
+
padding: 2px 5px;
|
|
57
|
+
background: #FF3B30;
|
|
58
|
+
color: #ffffff;
|
|
59
|
+
border-radius: 2px;
|
|
60
|
+
letter-spacing: 0.02em;
|
|
61
|
+
white-space: nowrap;
|
|
62
|
+
opacity: 0;
|
|
63
|
+
transition: opacity 120ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
64
|
+
}
|
|
65
|
+
.dc-cv-measure-pill[data-on="true"] { opacity: 1; }
|
|
66
|
+
@media (prefers-reduced-motion: reduce) {
|
|
67
|
+
.dc-cv-measure-line, .dc-cv-measure-pill { transition-duration: 1ms; }
|
|
68
|
+
}
|
|
69
|
+
`.trim();
|
|
70
|
+
|
|
71
|
+
function ensureStyles(): void {
|
|
72
|
+
if (typeof document === 'undefined') return;
|
|
73
|
+
if (document.getElementById('dc-cv-measure-css')) return;
|
|
74
|
+
const s = document.createElement('style');
|
|
75
|
+
s.id = 'dc-cv-measure-css';
|
|
76
|
+
s.textContent = MEASURE_CSS;
|
|
77
|
+
document.head.appendChild(s);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface ScreenRect {
|
|
81
|
+
x: number;
|
|
82
|
+
y: number;
|
|
83
|
+
w: number;
|
|
84
|
+
h: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function rectOf(el: Element): ScreenRect | null {
|
|
88
|
+
const b = (el as HTMLElement).getBoundingClientRect();
|
|
89
|
+
if (b.width === 0 && b.height === 0) return null;
|
|
90
|
+
return { x: b.left, y: b.top, w: b.width, h: b.height };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Persistent line+pill pair for one axis — created once, toggled via
|
|
94
|
+
* `data-on` (opacity transition) rather than added/removed each frame. */
|
|
95
|
+
function ensureAxisNodes(layer: HTMLDivElement, axis: 'x' | 'y'): [HTMLDivElement, HTMLDivElement] {
|
|
96
|
+
let line = layer.querySelector<HTMLDivElement>(`.dc-cv-measure-line[data-axis="${axis}"]`);
|
|
97
|
+
let pill = layer.querySelector<HTMLDivElement>(`.dc-cv-measure-pill[data-axis="${axis}"]`);
|
|
98
|
+
if (!line) {
|
|
99
|
+
line = document.createElement('div');
|
|
100
|
+
line.className = 'dc-cv-measure-line';
|
|
101
|
+
line.dataset.axis = axis;
|
|
102
|
+
layer.appendChild(line);
|
|
103
|
+
}
|
|
104
|
+
if (!pill) {
|
|
105
|
+
pill = document.createElement('div');
|
|
106
|
+
pill.className = 'dc-cv-measure-pill';
|
|
107
|
+
pill.dataset.axis = axis;
|
|
108
|
+
layer.appendChild(pill);
|
|
109
|
+
}
|
|
110
|
+
return [line, pill];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function hideAxis(layer: HTMLDivElement, axis: 'x' | 'y'): void {
|
|
114
|
+
const [line, pill] = ensureAxisNodes(layer, axis);
|
|
115
|
+
line.dataset.on = 'false';
|
|
116
|
+
pill.dataset.on = 'false';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function paintAxis(
|
|
120
|
+
layer: HTMLDivElement,
|
|
121
|
+
axis: 'x' | 'y',
|
|
122
|
+
a: ScreenRect,
|
|
123
|
+
b: ScreenRect,
|
|
124
|
+
zoom: number
|
|
125
|
+
): void {
|
|
126
|
+
const g = computePairGap(a, b, axis);
|
|
127
|
+
const [line, pill] = ensureAxisNodes(layer, axis);
|
|
128
|
+
if (!g) {
|
|
129
|
+
line.dataset.on = 'false';
|
|
130
|
+
pill.dataset.on = 'false';
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const worldGap = Math.round(g.gap / (zoom || 1));
|
|
134
|
+
if (axis === 'x') {
|
|
135
|
+
line.style.left = `${Math.round(g.from)}px`;
|
|
136
|
+
line.style.top = `${Math.round(g.cross)}px`;
|
|
137
|
+
line.style.width = `${Math.round(g.gap)}px`;
|
|
138
|
+
pill.style.left = `${Math.round(g.from + g.gap / 2)}px`;
|
|
139
|
+
pill.style.top = `${Math.round(g.cross)}px`;
|
|
140
|
+
} else {
|
|
141
|
+
line.style.left = `${Math.round(g.cross)}px`;
|
|
142
|
+
line.style.top = `${Math.round(g.from)}px`;
|
|
143
|
+
line.style.height = `${Math.round(g.gap)}px`;
|
|
144
|
+
pill.style.left = `${Math.round(g.cross)}px`;
|
|
145
|
+
pill.style.top = `${Math.round(g.from + g.gap / 2)}px`;
|
|
146
|
+
}
|
|
147
|
+
pill.textContent = `${worldGap}`;
|
|
148
|
+
line.dataset.on = 'true';
|
|
149
|
+
pill.dataset.on = 'true';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function MeasureOverlay() {
|
|
153
|
+
ensureStyles();
|
|
154
|
+
const { selected } = useSelectionSet();
|
|
155
|
+
const world = useArtboardsContext();
|
|
156
|
+
const layerRef = useRef<HTMLDivElement | null>(null);
|
|
157
|
+
const [alt, setAlt] = useState(false);
|
|
158
|
+
const [hoverEl, setHoverEl] = useState<Element | null>(null);
|
|
159
|
+
|
|
160
|
+
// Alt + hover tracking, gated together: hover RESOLUTION (a real per-move
|
|
161
|
+
// cost — resolveHoverTarget walks the DOM) only runs while Alt is held.
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (typeof document === 'undefined') return;
|
|
164
|
+
let mods: ModifierState = { alt: false, shift: false, meta: false };
|
|
165
|
+
const onKeyChange = (e: KeyboardEvent) => {
|
|
166
|
+
const next = reduceModifiers(mods, e);
|
|
167
|
+
if (next === mods) return;
|
|
168
|
+
mods = next;
|
|
169
|
+
setAlt(mods.alt);
|
|
170
|
+
if (!mods.alt) setHoverEl(null);
|
|
171
|
+
};
|
|
172
|
+
const onPointer = (e: PointerEvent) => {
|
|
173
|
+
const next = reduceModifiers(mods, e);
|
|
174
|
+
if (next !== mods) {
|
|
175
|
+
mods = next;
|
|
176
|
+
setAlt(mods.alt);
|
|
177
|
+
}
|
|
178
|
+
if (!mods.alt) {
|
|
179
|
+
setHoverEl(null);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const t = resolveHoverTarget(document, e.clientX, e.clientY, { deep: true });
|
|
183
|
+
setHoverEl(t?.el ?? null);
|
|
184
|
+
};
|
|
185
|
+
const onBlur = () => {
|
|
186
|
+
mods = { alt: false, shift: false, meta: false };
|
|
187
|
+
setAlt(false);
|
|
188
|
+
setHoverEl(null);
|
|
189
|
+
};
|
|
190
|
+
document.addEventListener('keydown', onKeyChange, true);
|
|
191
|
+
document.addEventListener('keyup', onKeyChange, true);
|
|
192
|
+
document.addEventListener('pointermove', onPointer, { passive: true });
|
|
193
|
+
window.addEventListener('blur', onBlur);
|
|
194
|
+
return () => {
|
|
195
|
+
document.removeEventListener('keydown', onKeyChange, true);
|
|
196
|
+
document.removeEventListener('keyup', onKeyChange, true);
|
|
197
|
+
document.removeEventListener('pointermove', onPointer);
|
|
198
|
+
window.removeEventListener('blur', onBlur);
|
|
199
|
+
};
|
|
200
|
+
}, []);
|
|
201
|
+
|
|
202
|
+
const one = selected.length === 1 ? selected[0] : null;
|
|
203
|
+
const zoom = world?.viewport?.zoom || 1;
|
|
204
|
+
const active = alt && !!one && !!hoverEl;
|
|
205
|
+
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
const layer = layerRef.current;
|
|
208
|
+
if (!layer) return;
|
|
209
|
+
const hideBoth = () => {
|
|
210
|
+
hideAxis(layer, 'x');
|
|
211
|
+
hideAxis(layer, 'y');
|
|
212
|
+
};
|
|
213
|
+
if (!active || !one || !hoverEl) {
|
|
214
|
+
hideBoth();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
let alive = true;
|
|
218
|
+
let rafId: number | null = null;
|
|
219
|
+
const tick = () => {
|
|
220
|
+
rafId = null;
|
|
221
|
+
if (!alive) return;
|
|
222
|
+
const selEl = resolveSelectionEl(document, one);
|
|
223
|
+
const a = selEl && selEl !== hoverEl && hoverEl.isConnected ? rectOf(selEl) : null;
|
|
224
|
+
const b = a ? rectOf(hoverEl) : null;
|
|
225
|
+
if (a && b) {
|
|
226
|
+
paintAxis(layer, 'x', a, b, zoom);
|
|
227
|
+
paintAxis(layer, 'y', a, b, zoom);
|
|
228
|
+
} else {
|
|
229
|
+
hideBoth();
|
|
230
|
+
}
|
|
231
|
+
rafId = requestAnimationFrame(tick);
|
|
232
|
+
};
|
|
233
|
+
rafId = requestAnimationFrame(tick);
|
|
234
|
+
return () => {
|
|
235
|
+
alive = false;
|
|
236
|
+
if (rafId != null) cancelAnimationFrame(rafId);
|
|
237
|
+
};
|
|
238
|
+
}, [active, one, hoverEl, zoom]);
|
|
239
|
+
|
|
240
|
+
return <div ref={layerRef} className="dc-cv-measure-layer" aria-hidden="true" />;
|
|
241
|
+
}
|
|
@@ -29,7 +29,7 @@ const CHROME_CSS = `
|
|
|
29
29
|
display: flex;
|
|
30
30
|
align-items: center;
|
|
31
31
|
pointer-events: auto;
|
|
32
|
-
font-family:
|
|
32
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
33
33
|
}
|
|
34
34
|
.dc-participant {
|
|
35
35
|
width: 26px;
|
|
@@ -74,7 +74,7 @@ const CHROME_CSS = `
|
|
|
74
74
|
background: var(--maude-chrome-bg-1, #fff);
|
|
75
75
|
color: var(--maude-chrome-fg-0, #111);
|
|
76
76
|
border: 1px solid var(--maude-chrome-border, rgba(0,0,0,0.16));
|
|
77
|
-
border-radius:
|
|
77
|
+
border-radius: 4px;
|
|
78
78
|
font-size: 12px;
|
|
79
79
|
line-height: 1.35;
|
|
80
80
|
white-space: nowrap;
|
|
@@ -93,7 +93,7 @@ const CHROME_CSS = `
|
|
|
93
93
|
background: var(--maude-hud-accent, oklch(56% 0.170 50));
|
|
94
94
|
color: var(--maude-hud-accent-fg, #fff);
|
|
95
95
|
border: none;
|
|
96
|
-
border-radius:
|
|
96
|
+
border-radius: 2px;
|
|
97
97
|
font-family: inherit;
|
|
98
98
|
font-weight: 500;
|
|
99
99
|
font-size: 11px;
|
|
@@ -79,6 +79,36 @@ export const RUNTIME_PACKAGES = [
|
|
|
79
79
|
// every real canvas) returned HTTP 500 "Bundle failed".
|
|
80
80
|
'lib0/decoding',
|
|
81
81
|
'lib0/encoding',
|
|
82
|
+
// DDR-148 — Remotion video-comp authoring. `remotion` carries the React
|
|
83
|
+
// context stack (RemotionEnvironment / Sequence / Timeline) that a video-comp
|
|
84
|
+
// canvas AND the `<Player>` preview BOTH consume — so it MUST be a single
|
|
85
|
+
// externalised bundle (importmap-shared), exactly like React itself, or the
|
|
86
|
+
// Player's provider and the composition's `useCurrentFrame()` bind to
|
|
87
|
+
// different context instances and the comp renders frozen at frame 0 (the
|
|
88
|
+
// dual-package hazard). `@remotion/player` + `@remotion/transitions` each
|
|
89
|
+
// externalise `remotion` (→ the one bundle) and INLINE their own stateless
|
|
90
|
+
// helpers — `remotion/no-react` (interpolate/random/validators/NoReactInternals,
|
|
91
|
+
// all pure), plus `@remotion/paths`/`@remotion/shapes` for transitions — which
|
|
92
|
+
// hold no cross-package state, so a per-bundle copy is harmless. NOT shipping
|
|
93
|
+
// `@remotion/renderer`/`@remotion/web-renderer` (export goes through the
|
|
94
|
+
// capture spine). Pre-built into dist/runtime + floored in .min-sizes.json.
|
|
95
|
+
'remotion',
|
|
96
|
+
'@remotion/player',
|
|
97
|
+
'@remotion/media',
|
|
98
|
+
'@remotion/transitions',
|
|
99
|
+
// Transition presentations are separate subpath modules by design (Remotion
|
|
100
|
+
// tree-shakes to what you import). But a canvas import specifier that isn't
|
|
101
|
+
// in RUNTIME_PACKAGES tries to resolve against the USER's node_modules at
|
|
102
|
+
// request-time Bun.build — which doesn't exist on an npm/marketplace install
|
|
103
|
+
// — so EVERY presentation the skill teaches must be pre-bundled + importmap-
|
|
104
|
+
// routed. v1 ships the six core wipes ("join 4 clips + crossfade" vocabulary);
|
|
105
|
+
// exotic presentations (dreamy-zoom, film-burn, …) are a documented follow-up.
|
|
106
|
+
'@remotion/transitions/fade',
|
|
107
|
+
'@remotion/transitions/slide',
|
|
108
|
+
'@remotion/transitions/wipe',
|
|
109
|
+
'@remotion/transitions/flip',
|
|
110
|
+
'@remotion/transitions/clock-wipe',
|
|
111
|
+
'@remotion/transitions/none',
|
|
82
112
|
] as const;
|
|
83
113
|
|
|
84
114
|
export type RuntimePackage = (typeof RUNTIME_PACKAGES)[number];
|
package/apps/studio/server.ts
CHANGED
|
@@ -18,13 +18,13 @@ import { spawn } from 'node:child_process';
|
|
|
18
18
|
|
|
19
19
|
import { createAcp } from './acp/index.ts';
|
|
20
20
|
import { createActivity } from './activity.ts';
|
|
21
|
-
import { createApi } from './api.ts';
|
|
21
|
+
import { ASSET_MAX_VIDEO_BYTES, createApi } from './api.ts';
|
|
22
22
|
import { bootSelfHeal } from './boot-self-heal.ts';
|
|
23
23
|
import { createCanvasListWatch } from './canvas-list-watch.ts';
|
|
24
24
|
import { type AiActivityEntry, createAiActivity } from './collab/ai-activity.ts';
|
|
25
25
|
import { createGitLifecycle } from './collab/git-lifecycle.ts';
|
|
26
26
|
import { createCollab } from './collab/index.ts';
|
|
27
|
-
import { createContext } from './context.ts';
|
|
27
|
+
import { createContext, reloadConfig } from './context.ts';
|
|
28
28
|
import { createFsWatch } from './fs-watch.ts';
|
|
29
29
|
import { createGitWatch } from './git/watch.ts';
|
|
30
30
|
import { createHttp } from './http.ts';
|
|
@@ -124,13 +124,16 @@ const { port: BASE_PORT, explicit: PORT_EXPLICIT } = resolvePort();
|
|
|
124
124
|
|
|
125
125
|
type BunServer = ReturnType<typeof Bun.serve<WsData, never>>;
|
|
126
126
|
|
|
127
|
-
// Phase 23 security review (DDR-088
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
|
|
127
|
+
// Phase 23 security review (DDR-088) + DDR-148 — hard ceiling on a request
|
|
128
|
+
// body. Bun's 128 MB default would let the untrusted canvas origin send huge
|
|
129
|
+
// bodies to any route. DDR-088 pinned this at 16 MB. DDR-148 raises it to the
|
|
130
|
+
// video cap + 8 MB headroom so `POST /_api/asset` can accept a 100 MB clip —
|
|
131
|
+
// but that route now STREAMS the body to disk (saveAssetFromStream), so a big
|
|
132
|
+
// upload never lands as one ArrayBuffer in RAM. Other routes keep their small
|
|
133
|
+
// LOGICAL caps (annotation 1 MB, JSON) — a body over those still rejects; the
|
|
134
|
+
// only change is Bun accepts more bytes pre-handler (a bandwidth-bounded
|
|
135
|
+
// transient-buffer tradeoff on the buffering routes, noted in DDR-148).
|
|
136
|
+
const MAX_REQUEST_BODY = ASSET_MAX_VIDEO_BYTES + 8 * 1024 * 1024;
|
|
134
137
|
|
|
135
138
|
function startServer(port: number): BunServer {
|
|
136
139
|
return Bun.serve<WsData, never>({
|
|
@@ -401,6 +404,28 @@ const gitWatch = createGitWatch(ctx);
|
|
|
401
404
|
// `.ai/logs/rca/issue-acp-new-canvas-not-in-filetree.md`.
|
|
402
405
|
const canvasListWatch = createCanvasListWatch(ctx);
|
|
403
406
|
|
|
407
|
+
// Config hot-reload — `/design:setup-ds` (or a hand edit) rewrites
|
|
408
|
+
// `.design/config.json` mid-session; without a re-read the server keeps
|
|
409
|
+
// serving the boot snapshot and a newly added canvas group (`system`) never
|
|
410
|
+
// reaches /_index-data, so scaffolded DS files stay invisible even on a
|
|
411
|
+
// manual tree reload. On change: swap ctx.cfg in place (reloadConfig), let
|
|
412
|
+
// canvasListWatch's set-diff emit `canvas-list-update` for canvases the new
|
|
413
|
+
// groups uncover, and tell shells to refetch /_config.
|
|
414
|
+
// RCA: .ai/logs/rca/issue-ds-scaffold-files-not-in-filetree-stale-config.md
|
|
415
|
+
const CONFIG_RELOAD_DEBOUNCE_MS = 150;
|
|
416
|
+
let configReloadTimer: ReturnType<typeof setTimeout> | null = null;
|
|
417
|
+
ctx.bus.on('fs:json', (rel: string) => {
|
|
418
|
+
if (rel.replace(/\\/g, '/').replace(/^\/+/, '') !== 'config.json') return;
|
|
419
|
+
if (configReloadTimer) clearTimeout(configReloadTimer);
|
|
420
|
+
configReloadTimer = setTimeout(() => {
|
|
421
|
+
configReloadTimer = null;
|
|
422
|
+
if (!reloadConfig(ctx)) return;
|
|
423
|
+
console.log(' config.json changed — reloaded live.');
|
|
424
|
+
void canvasListWatch.refresh();
|
|
425
|
+
ctx.bus.emit('config-updated');
|
|
426
|
+
}, CONFIG_RELOAD_DEBOUNCE_MS);
|
|
427
|
+
});
|
|
428
|
+
|
|
404
429
|
// Phase 9 Task 4 — bidirectional sync agent. No-op when the project isn't
|
|
405
430
|
// linked to a hub (`.design/config.json` has no `linkedHub` field). Kicked
|
|
406
431
|
// off after fsWatch so the agent's bus subscription receives every fs event.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file sizing-mode.ts
|
|
3
|
+
* @purpose Pure Fixed / Hug / Fill sizing-mode logic for the CssKnobs Size
|
|
4
|
+
* control (feature-element-editing-robustness Stage M1). Kept framework-
|
|
5
|
+
* free + in a .ts file so it's unit-testable (app.jsx is bundled JSX).
|
|
6
|
+
*
|
|
7
|
+
* Figma auto-layout parity: a per-axis Fixed / Hug / Fill mode is the single
|
|
8
|
+
* biggest source of layout-behaviour clarity for React/flexbox mockups.
|
|
9
|
+
* - Fixed = an explicit width/height value.
|
|
10
|
+
* - Hug = shrink to children (`fit-content`).
|
|
11
|
+
* - Fill = stretch to fill the parent — and the CORRECT CSS depends on the
|
|
12
|
+
* parent's layout: a flex child fills its MAIN axis via `flex-grow`,
|
|
13
|
+
* its CROSS axis via `align-self: stretch`; a block/grid child fills
|
|
14
|
+
* via `width/height: 100%`. So the mode must know the parent context
|
|
15
|
+
* (captured at selection time in Selection.parentDisplay /
|
|
16
|
+
* .parentFlexDirection — the shell can't reach the iframe later).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export type SizeAxis = 'width' | 'height';
|
|
20
|
+
export type SizeMode = 'fixed' | 'hug' | 'fill';
|
|
21
|
+
|
|
22
|
+
export interface ParentLayout {
|
|
23
|
+
display?: string;
|
|
24
|
+
flexDirection?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const HUG_VALUES = new Set(['fit-content', 'max-content', 'min-content']);
|
|
28
|
+
const FILL_VALUES = new Set(['100%', 'fill', '-webkit-fill-available', 'stretch']);
|
|
29
|
+
|
|
30
|
+
function parentIsFlex(p: ParentLayout | undefined): boolean {
|
|
31
|
+
const d = (p?.display || '').trim();
|
|
32
|
+
return d === 'flex' || d === 'inline-flex';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** True when `axis` is the flex MAIN axis of the parent (row → width, column → height). */
|
|
36
|
+
export function isMainAxis(axis: SizeAxis, p: ParentLayout | undefined): boolean {
|
|
37
|
+
const dir = (p?.flexDirection || 'row').trim();
|
|
38
|
+
const rowMain = dir.startsWith('row'); // row / row-reverse
|
|
39
|
+
return rowMain ? axis === 'width' : axis === 'height';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Best-effort classification of the current sizing mode for the segmented
|
|
44
|
+
* control's active-state highlight. Reads AUTHORED values first (what the user
|
|
45
|
+
* set), so a value the panel wrote round-trips; defaults to `fixed`. Detection is
|
|
46
|
+
* advisory — the WRITE side (`sizingModePatch`) is what must be deterministic.
|
|
47
|
+
*/
|
|
48
|
+
export function sizingModeOf(
|
|
49
|
+
axis: SizeAxis,
|
|
50
|
+
authored: Record<string, string> = {},
|
|
51
|
+
_computed: Record<string, string> = {},
|
|
52
|
+
parent?: ParentLayout
|
|
53
|
+
): SizeMode {
|
|
54
|
+
const a = (authored[axis] || '').trim().toLowerCase();
|
|
55
|
+
if (HUG_VALUES.has(a)) return 'hug';
|
|
56
|
+
if (FILL_VALUES.has(a)) return 'fill';
|
|
57
|
+
if (parentIsFlex(parent)) {
|
|
58
|
+
const main = isMainAxis(axis, parent);
|
|
59
|
+
if (main && Number.parseFloat(authored['flex-grow'] || '0') >= 1) return 'fill';
|
|
60
|
+
if (!main && (authored['align-self'] || '').trim() === 'stretch') return 'fill';
|
|
61
|
+
}
|
|
62
|
+
// A numeric authored value (px/rem/%…) or nothing authored → Fixed.
|
|
63
|
+
return 'fixed';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface SizingPatch {
|
|
67
|
+
/** Properties to write (in order), each `[prop, value]`. */
|
|
68
|
+
set: Array<[string, string]>;
|
|
69
|
+
/** Fill-specific properties to REMOVE so a prior Fill doesn't linger. */
|
|
70
|
+
reset: string[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The deterministic CSS writes for switching `axis` to `mode`. `currentPx` seeds
|
|
75
|
+
* the Fixed value (the element's current rendered size). Fill is context-aware:
|
|
76
|
+
* a flex child fills its main axis via `flex-grow:1` + `flex-basis:0%`, its cross
|
|
77
|
+
* axis via `align-self:stretch`; a block/grid child via `<axis>: 100%`.
|
|
78
|
+
*/
|
|
79
|
+
export function sizingModePatch(
|
|
80
|
+
axis: SizeAxis,
|
|
81
|
+
mode: SizeMode,
|
|
82
|
+
parent: ParentLayout | undefined,
|
|
83
|
+
currentPx: number
|
|
84
|
+
): SizingPatch {
|
|
85
|
+
const flex = parentIsFlex(parent);
|
|
86
|
+
const main = isMainAxis(axis, parent);
|
|
87
|
+
// Which fill-specific props a non-fill mode must clear so the old Fill doesn't linger.
|
|
88
|
+
const fillProps = flex ? (main ? ['flex-grow', 'flex-basis'] : ['align-self']) : [];
|
|
89
|
+
if (mode === 'fixed') {
|
|
90
|
+
const px = Number.isFinite(currentPx) && currentPx > 0 ? Math.round(currentPx) : 0;
|
|
91
|
+
return { set: [[axis, `${px}px`]], reset: fillProps };
|
|
92
|
+
}
|
|
93
|
+
if (mode === 'hug') {
|
|
94
|
+
return { set: [[axis, 'fit-content']], reset: fillProps };
|
|
95
|
+
}
|
|
96
|
+
// fill
|
|
97
|
+
if (flex) {
|
|
98
|
+
if (main) {
|
|
99
|
+
return {
|
|
100
|
+
set: [
|
|
101
|
+
['flex-grow', '1'],
|
|
102
|
+
['flex-basis', '0%'],
|
|
103
|
+
[axis, 'auto'],
|
|
104
|
+
],
|
|
105
|
+
reset: [],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
set: [
|
|
110
|
+
['align-self', 'stretch'],
|
|
111
|
+
[axis, 'auto'],
|
|
112
|
+
],
|
|
113
|
+
reset: [],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return { set: [[axis, '100%']], reset: [] };
|
|
117
|
+
}
|