@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
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file video-comp.tsx — DDR-148 video-comp canvas kind.
|
|
3
|
+
* @scope apps/studio/video-comp.tsx (re-exported from canvas-lib.tsx as
|
|
4
|
+
* part of the `@maude/canvas-lib` surface).
|
|
5
|
+
* @purpose Mount a Remotion composition inside `@remotion/player`'s `<Player>`
|
|
6
|
+
* for free scrub/preview, and expose the ONE deterministic seek
|
|
7
|
+
* bridge (`window.__maude_seek__`) that the capture spine
|
|
8
|
+
* (`bin/_video-playwright.mjs`) and the Timeline panel drive.
|
|
9
|
+
*
|
|
10
|
+
* A video-comp is deterministic BY CONSTRUCTION — every animated
|
|
11
|
+
* value is a pure function of `useCurrentFrame()`, so seeking the
|
|
12
|
+
* Player's `frame` renders one exact frame with no free-running
|
|
13
|
+
* clock. The capture shim steps `__maude_seek__(0..N)` and
|
|
14
|
+
* screenshots each frame; two runs to frame N are pixel-identical.
|
|
15
|
+
*
|
|
16
|
+
* Runtime wiring: `remotion` + `@remotion/player` are externalised
|
|
17
|
+
* through the canvas importmap (RUNTIME_PACKAGES / DDR-148), so the
|
|
18
|
+
* Player's provider stack and the composition's `useCurrentFrame()`
|
|
19
|
+
* bind to the SAME `remotion` module instance (no dual-package
|
|
20
|
+
* frozen-at-frame-0 hazard).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { Player, type PlayerRef } from '@remotion/player';
|
|
24
|
+
import { type ComponentType, useEffect, useMemo, useRef, useState } from 'react';
|
|
25
|
+
|
|
26
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
// Comp meta contract + the module-level seek registry
|
|
28
|
+
|
|
29
|
+
/** The temporal contract a video-comp artboard declares. */
|
|
30
|
+
export interface VideoCompMeta {
|
|
31
|
+
/** Frames per second (Remotion timebase). */
|
|
32
|
+
fps: number;
|
|
33
|
+
/** Total composition length, in frames. */
|
|
34
|
+
durationInFrames: number;
|
|
35
|
+
/** Composition pixel width. */
|
|
36
|
+
width: number;
|
|
37
|
+
/** Composition pixel height. */
|
|
38
|
+
height: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A live registration — the Player handle + its meta, keyed by artboard id. */
|
|
42
|
+
interface CompEntry extends VideoCompMeta {
|
|
43
|
+
id: string;
|
|
44
|
+
ref: React.RefObject<PlayerRef | null>;
|
|
45
|
+
/** The Remotion composition component + its props — carried so the export
|
|
46
|
+
* capture's renderMediaOnWeb path (DDR-148 addendum, audio export) can
|
|
47
|
+
* render the SAME component tree Remotion owns the timeline math for
|
|
48
|
+
* (TransitionSeries offsets, <Audio volume={fn}> closures), rather than
|
|
49
|
+
* re-deriving it. Never serialized — component references don't survive
|
|
50
|
+
* postMessage/JSON, so this stays out of CompSnapshot. */
|
|
51
|
+
component: ComponentType<Record<string, unknown>>;
|
|
52
|
+
inputProps: Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Serializable meta the capture shim + Timeline panel read (no functions). */
|
|
56
|
+
export interface CompSnapshot extends VideoCompMeta {
|
|
57
|
+
id: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Result contract for `__maude_render_video__` — see exporters/video-render-lib.ts. */
|
|
61
|
+
export interface RenderVideoResult {
|
|
62
|
+
b64: string;
|
|
63
|
+
bytes: number;
|
|
64
|
+
container: string;
|
|
65
|
+
videoCodec: string | null;
|
|
66
|
+
audioCodec: string | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Options accepted by `__maude_render_video__`, forwarded to renderMediaOnWeb. */
|
|
70
|
+
export interface RenderVideoOptions {
|
|
71
|
+
container: 'mp4' | 'webm';
|
|
72
|
+
videoCodec?: string;
|
|
73
|
+
audioCodec?: string;
|
|
74
|
+
scale?: number;
|
|
75
|
+
muted?: boolean;
|
|
76
|
+
frameRange?: [number, number];
|
|
77
|
+
licenseKey?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface MaudeSeekWindow {
|
|
81
|
+
__maudeVideoComps?: Map<string, CompEntry>;
|
|
82
|
+
/** Seek every registered comp to `frame` (paused). Ordinary artboards fall
|
|
83
|
+
* back to a time-based WAAPI seek. Resolves once the frame has painted. */
|
|
84
|
+
__maude_seek__?: (frame: number, opts?: { fps?: number }) => Promise<void>;
|
|
85
|
+
/** Serializable comp list for the capture shim / Timeline panel. */
|
|
86
|
+
__maude_comps__?: () => CompSnapshot[];
|
|
87
|
+
/**
|
|
88
|
+
* DDR-148 addendum (audio export) — whole-comp render via renderMediaOnWeb
|
|
89
|
+
* (video+audio, one pass; Remotion owns the TransitionSeries/volume-closure
|
|
90
|
+
* timeline math). Looks up the component by compId and delegates to
|
|
91
|
+
* `window.__maudeRenderVideo__`, which the export capture injects via
|
|
92
|
+
* addScriptTag (exporters/video-render-lib.ts) — absent during normal
|
|
93
|
+
* preview, so this throws outside a capture session.
|
|
94
|
+
*/
|
|
95
|
+
__maude_render_video__?: (compId: string, opts: RenderVideoOptions) => Promise<RenderVideoResult>;
|
|
96
|
+
__maudeRenderVideo__?: (
|
|
97
|
+
opts: RenderVideoOptions & {
|
|
98
|
+
component: ComponentType<Record<string, unknown>>;
|
|
99
|
+
inputProps: Record<string, unknown>;
|
|
100
|
+
width: number;
|
|
101
|
+
height: number;
|
|
102
|
+
fps: number;
|
|
103
|
+
durationInFrames: number;
|
|
104
|
+
}
|
|
105
|
+
) => Promise<RenderVideoResult>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function seekWindow(): MaudeSeekWindow | null {
|
|
109
|
+
return typeof window === 'undefined' ? null : (window as unknown as MaudeSeekWindow);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function registry(): Map<string, CompEntry> {
|
|
113
|
+
const w = seekWindow();
|
|
114
|
+
if (!w) return new Map();
|
|
115
|
+
if (!w.__maudeVideoComps) w.__maudeVideoComps = new Map();
|
|
116
|
+
return w.__maudeVideoComps;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function clamp(n: number, lo: number, hi: number): number {
|
|
120
|
+
return Math.max(lo, Math.min(hi, n));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Two rAFs — enough for the Player to commit a seeked frame to the DOM. */
|
|
124
|
+
function afterPaint(): Promise<void> {
|
|
125
|
+
if (typeof requestAnimationFrame !== 'function') return Promise.resolve();
|
|
126
|
+
return new Promise((resolve) => {
|
|
127
|
+
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Install the singleton seek bridge on `window`. Idempotent — the first
|
|
133
|
+
* VideoComp to mount wins; later mounts reuse the same functions. The bridge
|
|
134
|
+
* reads the live registry, so it always reflects the currently-mounted comps.
|
|
135
|
+
* Exported so tests (and a defensive capture-shim call) can install it without
|
|
136
|
+
* mounting a Player.
|
|
137
|
+
*/
|
|
138
|
+
export function installMaudeSeekBridge(): void {
|
|
139
|
+
const w = seekWindow();
|
|
140
|
+
if (!w || w.__maude_seek__) return;
|
|
141
|
+
|
|
142
|
+
w.__maude_seek__ = async (frame, opts) => {
|
|
143
|
+
const entries = [...registry().values()];
|
|
144
|
+
if (entries.length > 0) {
|
|
145
|
+
for (const e of entries) {
|
|
146
|
+
const f = clamp(Math.round(frame), 0, Math.max(0, e.durationInFrames - 1));
|
|
147
|
+
try {
|
|
148
|
+
e.ref.current?.pause();
|
|
149
|
+
e.ref.current?.seekTo(f);
|
|
150
|
+
} catch {
|
|
151
|
+
/* Player not yet ready — a later seek settles it */
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
await afterPaint();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
// Ordinary-artboard fallback (no registered comp): drive WAAPI by time so a
|
|
158
|
+
// manual `__maude_seek__` still works. The capture shim owns the primary
|
|
159
|
+
// ordinary-artboard path (it seeks <video>.currentTime + getAnimations()).
|
|
160
|
+
const fps = opts?.fps ?? 30;
|
|
161
|
+
const ms = (frame / fps) * 1000;
|
|
162
|
+
if (typeof document !== 'undefined') {
|
|
163
|
+
for (const a of document.getAnimations()) {
|
|
164
|
+
try {
|
|
165
|
+
a.pause();
|
|
166
|
+
a.currentTime = ms;
|
|
167
|
+
} catch {
|
|
168
|
+
/* unseekable animation — skip */
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
await afterPaint();
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
w.__maude_comps__ = () =>
|
|
176
|
+
[...registry().values()].map((e) => ({
|
|
177
|
+
id: e.id,
|
|
178
|
+
fps: e.fps,
|
|
179
|
+
durationInFrames: e.durationInFrames,
|
|
180
|
+
width: e.width,
|
|
181
|
+
height: e.height,
|
|
182
|
+
}));
|
|
183
|
+
|
|
184
|
+
w.__maude_render_video__ = async (compId, opts) => {
|
|
185
|
+
const entry = registry().get(compId);
|
|
186
|
+
if (!entry) throw new Error(`no registered video-comp with id "${compId}"`);
|
|
187
|
+
if (typeof w.__maudeRenderVideo__ !== 'function') {
|
|
188
|
+
throw new Error(
|
|
189
|
+
'render-lib bundle not injected — the export capture must addScriptTag ' +
|
|
190
|
+
'exporters/video-render-lib.ts before calling __maude_render_video__'
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
return w.__maudeRenderVideo__({
|
|
194
|
+
...opts,
|
|
195
|
+
component: entry.component,
|
|
196
|
+
inputProps: entry.inputProps,
|
|
197
|
+
width: entry.width,
|
|
198
|
+
height: entry.height,
|
|
199
|
+
fps: entry.fps,
|
|
200
|
+
durationInFrames: entry.durationInFrames,
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Find the compId of the nearest VideoComp mounted inside `container` (the
|
|
206
|
+
* target artboard element) — the artboard's own `data-dc-screen` id and the
|
|
207
|
+
* VideoComp's `id` prop are independent (an author rarely sets the latter),
|
|
208
|
+
* so the capture shim resolves by DOM containment rather than assuming a
|
|
209
|
+
* 1:1 id match. Returns null when the artboard has no video-comp (an
|
|
210
|
+
* ordinary artboard, or a comp not yet mounted). */
|
|
211
|
+
export function findCompIdIn(container: Element): string | null {
|
|
212
|
+
return container.querySelector('[data-comp-id]')?.getAttribute('data-comp-id') ?? null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
216
|
+
// VideoComp — the canvas-lib wrapper
|
|
217
|
+
|
|
218
|
+
export interface VideoCompProps extends VideoCompMeta {
|
|
219
|
+
/** The Remotion composition component (frame-driven; NO CSS animations). */
|
|
220
|
+
component: ComponentType<Record<string, unknown>>;
|
|
221
|
+
/** Stable id (defaults to a mount-order slug) — keys the seek registry + the
|
|
222
|
+
* Timeline rows; match the enclosing DCArtboard id when there are several. */
|
|
223
|
+
id?: string;
|
|
224
|
+
/** Props forwarded to the composition (Remotion `inputProps`). */
|
|
225
|
+
inputProps?: Record<string, unknown>;
|
|
226
|
+
/** Show the Player's own transport chrome in PREVIEW. Auto-hidden during
|
|
227
|
+
* capture/export (the `?hide-chrome=1` shell flag). Default true. */
|
|
228
|
+
controls?: boolean;
|
|
229
|
+
/** Loop preview playback. Default true. */
|
|
230
|
+
loop?: boolean;
|
|
231
|
+
/**
|
|
232
|
+
* Autoplay in preview. Default: play unless the viewer prefers reduced motion
|
|
233
|
+
* (a11y) or the shell is in capture mode. Never autoplays during export.
|
|
234
|
+
*/
|
|
235
|
+
autoPlay?: boolean;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let mountCounter = 0;
|
|
239
|
+
|
|
240
|
+
function prefersReducedMotion(): boolean {
|
|
241
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false;
|
|
242
|
+
try {
|
|
243
|
+
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
244
|
+
} catch {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** True when the shell asked for a chrome-free render (export capture / diff). */
|
|
250
|
+
function hideChrome(): boolean {
|
|
251
|
+
if (typeof window === 'undefined') return false;
|
|
252
|
+
try {
|
|
253
|
+
return new URLSearchParams(window.location.search).get('hide-chrome') === '1';
|
|
254
|
+
} catch {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Mount a Remotion composition as a scrubbable video-comp artboard body. Place
|
|
261
|
+
* it inside a `DCArtboard` whose width/height match the composition:
|
|
262
|
+
*
|
|
263
|
+
* ```tsx
|
|
264
|
+
* const Intro = () => {
|
|
265
|
+
* const frame = useCurrentFrame();
|
|
266
|
+
* const opacity = interpolate(frame, [0, 20], [0, 1], { extrapolateRight: 'clamp' });
|
|
267
|
+
* return <AbsoluteFill style={{ opacity }}>…</AbsoluteFill>;
|
|
268
|
+
* };
|
|
269
|
+
* // …inside the canvas:
|
|
270
|
+
* <DCArtboard id="intro" label="Intro" width={640} height={360}>
|
|
271
|
+
* <VideoComp component={Intro} durationInFrames={90} fps={30} width={640} height={360} />
|
|
272
|
+
* </DCArtboard>
|
|
273
|
+
* ```
|
|
274
|
+
*/
|
|
275
|
+
export function VideoComp({
|
|
276
|
+
component,
|
|
277
|
+
durationInFrames,
|
|
278
|
+
fps,
|
|
279
|
+
width,
|
|
280
|
+
height,
|
|
281
|
+
id,
|
|
282
|
+
inputProps,
|
|
283
|
+
controls,
|
|
284
|
+
loop = true,
|
|
285
|
+
autoPlay,
|
|
286
|
+
}: VideoCompProps) {
|
|
287
|
+
const ref = useRef<PlayerRef | null>(null);
|
|
288
|
+
// Stable id across re-renders; unique per mount when the author omits one.
|
|
289
|
+
const [autoId] = useState(() => id ?? `videocomp-${++mountCounter}`);
|
|
290
|
+
const compId = id ?? autoId;
|
|
291
|
+
// Loop is live-controlled from the Timeline (timeline-loop message), not just
|
|
292
|
+
// the mount prop — so toggling loop OFF actually stops the replay.
|
|
293
|
+
const [loopState, setLoopState] = useState(loop);
|
|
294
|
+
const capturing = hideChrome();
|
|
295
|
+
// The Timeline panel is the control surface (transport + scrub + volume), so
|
|
296
|
+
// the Player's own chrome is redundant on the artboard — hidden by default.
|
|
297
|
+
// Opt back in per-comp with `controls`; never shown during capture. (DDR-148.)
|
|
298
|
+
const showControls = (controls ?? false) && !capturing;
|
|
299
|
+
// Default to PAUSED in preview so the comp is editable on open (a moving,
|
|
300
|
+
// frame-driven element can't be Cmd+Click-selected). The author can opt into
|
|
301
|
+
// autoplay; capture never autoplays (the shim drives frames). Opens on a
|
|
302
|
+
// poster frame ~35% in so a paused comp shows content, not an empty frame 0.
|
|
303
|
+
const play = (autoPlay ?? false) && !capturing;
|
|
304
|
+
const posterFrame = capturing
|
|
305
|
+
? 0
|
|
306
|
+
: Math.min(Math.max(0, durationInFrames - 1), Math.round(durationInFrames * 0.35));
|
|
307
|
+
|
|
308
|
+
// Register with the seek bridge for the lifetime of this mount.
|
|
309
|
+
useEffect(() => {
|
|
310
|
+
installMaudeSeekBridge();
|
|
311
|
+
const entry: CompEntry = {
|
|
312
|
+
id: compId,
|
|
313
|
+
fps,
|
|
314
|
+
durationInFrames,
|
|
315
|
+
width,
|
|
316
|
+
height,
|
|
317
|
+
ref,
|
|
318
|
+
component,
|
|
319
|
+
inputProps: inputProps ?? {},
|
|
320
|
+
};
|
|
321
|
+
registry().set(compId, entry);
|
|
322
|
+
// Announce comps to the shell so the Timeline panel can auto-suggest.
|
|
323
|
+
postToShell({ dgn: 'timeline-comps', comps: seekWindow()?.__maude_comps__?.() ?? [] });
|
|
324
|
+
return () => {
|
|
325
|
+
const cur = registry().get(compId);
|
|
326
|
+
if (cur === entry) registry().delete(compId);
|
|
327
|
+
postToShell({ dgn: 'timeline-comps', comps: seekWindow()?.__maude_comps__?.() ?? [] });
|
|
328
|
+
};
|
|
329
|
+
}, [compId, fps, durationInFrames, width, height, component, inputProps]);
|
|
330
|
+
|
|
331
|
+
// Shell → iframe transport: the Timeline panel drives preview scrub/playback.
|
|
332
|
+
useEffect(() => {
|
|
333
|
+
if (typeof window === 'undefined') return;
|
|
334
|
+
const onMessage = (e: MessageEvent) => {
|
|
335
|
+
const m = e.data as {
|
|
336
|
+
dgn?: string;
|
|
337
|
+
frame?: number;
|
|
338
|
+
id?: string;
|
|
339
|
+
muted?: boolean;
|
|
340
|
+
volume?: number;
|
|
341
|
+
loop?: boolean;
|
|
342
|
+
} | null;
|
|
343
|
+
if (!m || typeof m !== 'object' || typeof m.dgn !== 'string') return;
|
|
344
|
+
// The shell asks a (possibly already-mounted) canvas to re-announce its
|
|
345
|
+
// comps — answer regardless of Player readiness so a tab-switch / panel-
|
|
346
|
+
// open re-syncs the Timeline panel.
|
|
347
|
+
if (m.dgn === 'timeline-request-comps') {
|
|
348
|
+
postToShell({ dgn: 'timeline-comps', comps: seekWindow()?.__maude_comps__?.() ?? [] });
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (m.id && m.id !== compId) return; // targeted seek to another comp
|
|
352
|
+
const p = ref.current;
|
|
353
|
+
if (!p) return;
|
|
354
|
+
try {
|
|
355
|
+
if (m.dgn === 'timeline-seek' && typeof m.frame === 'number') {
|
|
356
|
+
p.pause();
|
|
357
|
+
p.seekTo(clamp(Math.round(m.frame), 0, Math.max(0, durationInFrames - 1)));
|
|
358
|
+
} else if (m.dgn === 'timeline-play') {
|
|
359
|
+
p.play();
|
|
360
|
+
} else if (m.dgn === 'timeline-pause') {
|
|
361
|
+
p.pause();
|
|
362
|
+
} else if (m.dgn === 'timeline-mute') {
|
|
363
|
+
// Sound is controlled from the Timeline (the artboard has no chrome).
|
|
364
|
+
if (m.muted) p.mute();
|
|
365
|
+
else p.unmute();
|
|
366
|
+
} else if (m.dgn === 'timeline-volume' && typeof m.volume === 'number') {
|
|
367
|
+
p.setVolume(clamp(m.volume, 0, 1));
|
|
368
|
+
} else if (m.dgn === 'timeline-loop') {
|
|
369
|
+
setLoopState(!!m.loop);
|
|
370
|
+
}
|
|
371
|
+
} catch {
|
|
372
|
+
/* Player transitional — ignore */
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
window.addEventListener('message', onMessage);
|
|
376
|
+
return () => window.removeEventListener('message', onMessage);
|
|
377
|
+
}, [compId, durationInFrames]);
|
|
378
|
+
|
|
379
|
+
// iframe → shell: mirror the playhead so the Timeline panel tracks preview.
|
|
380
|
+
useEffect(() => {
|
|
381
|
+
const p = ref.current;
|
|
382
|
+
if (!p) return;
|
|
383
|
+
const onFrame = () => {
|
|
384
|
+
try {
|
|
385
|
+
postToShell({ dgn: 'timeline-frame', id: compId, frame: p.getCurrentFrame() });
|
|
386
|
+
} catch {
|
|
387
|
+
/* ignore */
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
p.addEventListener('frameupdate', onFrame);
|
|
391
|
+
return () => {
|
|
392
|
+
try {
|
|
393
|
+
p.removeEventListener('frameupdate', onFrame);
|
|
394
|
+
} catch {
|
|
395
|
+
/* ignore */
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
}, [compId]);
|
|
399
|
+
|
|
400
|
+
const style = useMemo(() => ({ width: '100%', height: '100%', display: 'block' as const }), []);
|
|
401
|
+
|
|
402
|
+
return (
|
|
403
|
+
<div
|
|
404
|
+
className="dc-video-comp"
|
|
405
|
+
data-comp-id={compId}
|
|
406
|
+
style={{ width, height, maxWidth: '100%' }}
|
|
407
|
+
>
|
|
408
|
+
<Player
|
|
409
|
+
ref={ref}
|
|
410
|
+
component={component}
|
|
411
|
+
durationInFrames={Math.max(1, Math.round(durationInFrames))}
|
|
412
|
+
fps={fps}
|
|
413
|
+
compositionWidth={width}
|
|
414
|
+
compositionHeight={height}
|
|
415
|
+
inputProps={inputProps ?? {}}
|
|
416
|
+
controls={showControls}
|
|
417
|
+
loop={loopState}
|
|
418
|
+
autoPlay={play}
|
|
419
|
+
initialFrame={posterFrame}
|
|
420
|
+
// Clicking the artboard must NOT toggle playback — it selects elements
|
|
421
|
+
// for editing (the whole point of a canvas). Play only via the explicit
|
|
422
|
+
// transport button (Player controls / Timeline panel). Same for the
|
|
423
|
+
// spacebar, which is the canvas pan chord. (DDR-148 feedback #2.)
|
|
424
|
+
clickToPlay={false}
|
|
425
|
+
spaceKeyToPlayOrPause={false}
|
|
426
|
+
doubleClickToFullscreen={false}
|
|
427
|
+
// We ship Remotion in-house and disclose the license (DDR-148); ack in
|
|
428
|
+
// code so the Player never nags/watermarks the preview.
|
|
429
|
+
acknowledgeRemotionLicense
|
|
430
|
+
style={style}
|
|
431
|
+
/>
|
|
432
|
+
</div>
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
VideoComp.displayName = 'VideoComp';
|
|
436
|
+
|
|
437
|
+
function postToShell(payload: Record<string, unknown>): void {
|
|
438
|
+
if (typeof window === 'undefined' || window.parent === window) return;
|
|
439
|
+
try {
|
|
440
|
+
window.parent.postMessage(payload, '*');
|
|
441
|
+
} catch {
|
|
442
|
+
/* cross-origin parent without our listener — no-op */
|
|
443
|
+
}
|
|
444
|
+
}
|
|
@@ -1,6 +1,61 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./whats-new.schema.json",
|
|
3
3
|
"entries": [
|
|
4
|
+
{
|
|
5
|
+
"id": "desktop-intel-mac-support",
|
|
6
|
+
"version": "0.41.0",
|
|
7
|
+
"date": "2026-07-08",
|
|
8
|
+
"kind": "improvement",
|
|
9
|
+
"title": "Desktop app now runs on Intel Macs",
|
|
10
|
+
"summary": "The macOS desktop app was Apple-Silicon-only — it now ships as a universal binary, so Intel Mac users can download and run it natively too, no Rosetta workaround needed.",
|
|
11
|
+
"learnMore": "https://maude.sh/desktop",
|
|
12
|
+
"surface": "design-ui"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "element-editing-robustness",
|
|
16
|
+
"version": "0.41.0",
|
|
17
|
+
"date": "2026-07-08",
|
|
18
|
+
"kind": "feature",
|
|
19
|
+
"title": "Edit elements like Figma or Webflow",
|
|
20
|
+
"summary": "Selecting an element now opens the Inspector automatically, and drag handles let you resize it directly on the canvas (corner + edge, Shift to lock aspect, Alt to resize from center) — artboards get free-hand resize too. Delete an element with the Del key or the right-click menu, or insert a new div, text, or image straight into the artboard; add a whole new empty artboard from a Desktop/Laptop/Tablet/Mobile preset. The Inspector gained Position (with a constraints-style inset box), Transform, and Media sections (object-fit, aspect ratio), plus on-canvas dragging for padding and gaps. Swap a photo or video (or a background image) from a built-in asset picker. Editing now works on design-system specimens too, not just your UI canvases. A shared component always tells you plainly whether an edit is local to this artboard or shared across every place it's used, and moving one instance never drags the others along. Rounding it out: keyboard nudge + tree navigation, Cmd+D duplicate, copy/paste style, align and distribute a multi-selection, deep-select + \"Select layer\" for nested/overlapping elements, Alt-hover distance measurement, a free-hand rotate handle, and a proper Fixed/Hug/Fill sizing control for flex layouts. Every one of these is undoable with Cmd+Z.",
|
|
21
|
+
"surface": "design-ui"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "whiteboard-ai-toolkit",
|
|
25
|
+
"version": "0.41.0",
|
|
26
|
+
"date": "2026-07-08",
|
|
27
|
+
"kind": "feature",
|
|
28
|
+
"title": "The Assistant now reads and draws on your whiteboard",
|
|
29
|
+
"summary": "Sketch stickies, shapes, and arrows on a canvas and the Assistant understands not just which artboard a note sits on, but which button or element it's pointing at. Ask it to pin a note next to something specific, and it'll drop it right there with an arrow pointing at it — no guessing coordinates. It can also lay out a whole tidy board for you: a sprint retro, a kanban, a week of social posts, a roadmap, a brainstorm, a user-flow diagram — just ask.",
|
|
30
|
+
"surface": "design-ui"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "timeline-clip-editing",
|
|
34
|
+
"version": "0.40.0",
|
|
35
|
+
"date": "2026-07-05",
|
|
36
|
+
"kind": "feature",
|
|
37
|
+
"title": "Hand-edit your video comps on the timeline",
|
|
38
|
+
"summary": "After the Assistant generates a video, fine-tune it yourself. On the timeline you can drag a clip to move it, drag its right edge to trim (both snap to second-ticks and neighbour clip edges; hold Alt to drag freely), drop a video / image / audio file to add a clip, raise or lower a clip's stacking, swap a clip's footage (⇄), or remove a clip (×). Right-click any clip for the same menu — replace, move, hide, or remove — and hide keeps a clip in place while it stops rendering. Clips that layer a background over a title expand into per-layer rows, so you can see and replace the video separately from its caption. Transition-based showreels reorder and accept dropped clips too. Keyboard shortcuts help: Space plays/pauses, arrows step frames (Shift = a second), Home/End jump to the ends, and . / , hop between keyframes. Prefer to start from footage? Drop video/audio clips anywhere on the canvas as reference chips, then File then Assemble dropped clips into video builds a real, editable comp in one click. Editing is multi-comp-safe — a change always lands on the clip you meant, never one in another composition. And inline text / CSS tweaks you make on the canvas now actually stick.",
|
|
39
|
+
"surface": "design-ui"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": "video-comp-remotion",
|
|
43
|
+
"version": "0.40.0",
|
|
44
|
+
"date": "2026-07-05",
|
|
45
|
+
"kind": "feature",
|
|
46
|
+
"title": "Design videos & animations, right in the canvas",
|
|
47
|
+
"summary": "A canvas artboard can now be a real motion composition (powered by Remotion) — scrub it in the built-in player, drop clips and music onto it, direct edits from the Assistant (\"turn this into an animation\", \"join these 4 clips with a crossfade\"), and export the artboard as MP4 or GIF. Rendered by Maude's own capture engine — no extra install, no native downloads. Remotion is free for individuals and teams of up to 3; larger orgs need their own Remotion license.",
|
|
48
|
+
"surface": "design-ui"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"id": "file-tree-live-config-reload",
|
|
52
|
+
"version": "0.40.0",
|
|
53
|
+
"date": "2026-07-05",
|
|
54
|
+
"kind": "fix",
|
|
55
|
+
"title": "New design systems show up in the file tree instantly",
|
|
56
|
+
"summary": "Bootstrapping a design system from the Assistant (or editing your design config by hand) now updates the file tree live — new canvas groups and their files appear as they're created, no server restart needed.",
|
|
57
|
+
"surface": "design-ui"
|
|
58
|
+
},
|
|
4
59
|
{
|
|
5
60
|
"id": "acp-chat-cross-restart-resume",
|
|
6
61
|
"version": "0.39.1",
|
package/apps/studio/ws.ts
CHANGED
|
@@ -206,6 +206,11 @@ export function createWs(
|
|
|
206
206
|
broadcast({ type: 'canvas-list-update', payload })
|
|
207
207
|
);
|
|
208
208
|
|
|
209
|
+
// Config hot-reload (server.ts fs:json subscriber) — the shell refetches
|
|
210
|
+
// /_config so cfg-derived state (designSystems, tokensCssRel, canvasGroups)
|
|
211
|
+
// matches the reloaded server config. Inspector clients only.
|
|
212
|
+
ctx.bus.on('config-updated', () => broadcast({ type: 'config-updated' }));
|
|
213
|
+
|
|
209
214
|
// Phase 31 (DDR-123) — `/design:chat` → `maude design chat-open` → POST
|
|
210
215
|
// /_api/acp/focus emits this; the shell (app.jsx, native-only) opens the
|
|
211
216
|
// Assistant panel. Inspector clients only — same-origin shell, like the rest.
|
package/cli/commands/design.mjs
CHANGED
|
@@ -43,8 +43,10 @@ const BIN_VERBS = new Set([
|
|
|
43
43
|
'to-lottie',
|
|
44
44
|
'read-annotations',
|
|
45
45
|
'annotate',
|
|
46
|
+
'canvas-rects',
|
|
46
47
|
'chat-open',
|
|
47
48
|
'ensure-browser',
|
|
49
|
+
'fetch-asset',
|
|
48
50
|
]);
|
|
49
51
|
|
|
50
52
|
// Bin verbs that boot the dev-server (directly, or by shelling into server-up.sh).
|
|
@@ -136,8 +138,9 @@ Lifecycle:
|
|
|
136
138
|
|
|
137
139
|
Dev-tooling (dispatch to the dev-server bash helpers — DDR-062):
|
|
138
140
|
screenshot · server-up · prep · slug · bootstrap-check · runtime-health
|
|
139
|
-
smoke · canvas-edit · handoff · asset-sweep · visual-sanity
|
|
141
|
+
smoke · canvas-edit · handoff · asset-sweep · visual-sanity · fetch-asset
|
|
140
142
|
draw-build · draw-proof · svg-optimize · to-lottie · read-annotations · annotate
|
|
143
|
+
canvas-rects
|
|
141
144
|
Invoke the bundled helper of the same name. maude resolves it from its
|
|
142
145
|
own package root and sets CLAUDE_PLUGIN_ROOT for the child; stdout,
|
|
143
146
|
stderr, and exit code pass straight through (so command-substitution
|
|
@@ -147,6 +150,8 @@ Dev-tooling (dispatch to the dev-server bash helpers — DDR-062):
|
|
|
147
150
|
PORT=$(maude design server-up --root "$REPO")
|
|
148
151
|
eval "$(maude design prep --shell-export --shape edit --root "$REPO")"
|
|
149
152
|
maude design smoke --changed-only
|
|
153
|
+
REF=$(maude design fetch-asset "$URL" --root "$REPO") # download-first image
|
|
154
|
+
maude design canvas-rects "ui/Foo.tsx" # geometry manifest (whiteboard toolkit)
|
|
150
155
|
|
|
151
156
|
serve [--port N] [--root PATH]
|
|
152
157
|
Start the design plugin's dev server in the current repo. Equivalent
|
|
@@ -51,11 +51,59 @@ test('unknown verb exits 2', () => {
|
|
|
51
51
|
test('design help lists the dev-tooling verbs', () => {
|
|
52
52
|
const res = runDesign(['help']);
|
|
53
53
|
assert.equal(res.status, 0, res.stderr);
|
|
54
|
-
for (const verb of [
|
|
54
|
+
for (const verb of [
|
|
55
|
+
'screenshot',
|
|
56
|
+
'server-up',
|
|
57
|
+
'prep',
|
|
58
|
+
'slug',
|
|
59
|
+
'smoke',
|
|
60
|
+
'visual-sanity',
|
|
61
|
+
'canvas-rects',
|
|
62
|
+
]) {
|
|
55
63
|
assert.match(res.stdout, new RegExp(verb), `usage should mention ${verb}`);
|
|
56
64
|
}
|
|
57
65
|
});
|
|
58
66
|
|
|
67
|
+
// canvas-rects needs an explicit --root pinned to an isolated temp dir (not
|
|
68
|
+
// runDesign's ambient cwd) — its server-detection reads $CLAUDE_PROJECT_DIR /
|
|
69
|
+
// `git rev-parse` when --root is absent, which would escape the temp dir in
|
|
70
|
+
// an environment where either is set.
|
|
71
|
+
function runCanvasRects(args, root) {
|
|
72
|
+
const env = { ...process.env };
|
|
73
|
+
env.CLAUDE_PLUGIN_ROOT = undefined;
|
|
74
|
+
return spawnSync(process.execPath, [BIN, 'design', 'canvas-rects', ...args, '--root', root], {
|
|
75
|
+
cwd: root,
|
|
76
|
+
encoding: 'utf8',
|
|
77
|
+
env,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
test('canvas-rects dispatches to the bundled helper (--help)', () => {
|
|
82
|
+
const root = mkdtempSync(join(tmpdir(), 'maude-canvas-rects-'));
|
|
83
|
+
try {
|
|
84
|
+
const res = runCanvasRects(['--help'], root);
|
|
85
|
+
assert.equal(res.status, 0, res.stderr);
|
|
86
|
+
assert.match(res.stdout, /canvas-rects\.sh/);
|
|
87
|
+
} finally {
|
|
88
|
+
rmSync(root, { recursive: true, force: true });
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('canvas-rects with no live server falls back to the static lane', () => {
|
|
93
|
+
// No .design/_server.json under --root → static fallback, no browser
|
|
94
|
+
// needed. A missing canvas still emits a valid empty manifest (exit 0).
|
|
95
|
+
const root = mkdtempSync(join(tmpdir(), 'maude-canvas-rects-'));
|
|
96
|
+
try {
|
|
97
|
+
const res = runCanvasRects(['ui/Nonexistent.tsx'], root);
|
|
98
|
+
assert.equal(res.status, 0, res.stderr);
|
|
99
|
+
assert.match(res.stderr, /static/);
|
|
100
|
+
const manifest = JSON.parse(res.stdout.trim());
|
|
101
|
+
assert.deepEqual(manifest, { artboards: [], elements: [], elementsTruncated: false });
|
|
102
|
+
} finally {
|
|
103
|
+
rmSync(root, { recursive: true, force: true });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
59
107
|
// resolveServerBinary — the dev-server boot-runtime resolver (DDR-084). server-up
|
|
60
108
|
// must boot the compiled platform binary on a production install (it embeds yjs +
|
|
61
109
|
// every dep; `bun server.ts` from source can't resolve them — the historic
|