@1agh/maude 0.39.0 → 0.40.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/acp/bridge.ts +144 -5
- package/apps/studio/acp/index.ts +11 -1
- package/apps/studio/annotations-context-toolbar.tsx +44 -1
- package/apps/studio/annotations-layer.tsx +253 -3
- package/apps/studio/annotations-model.ts +107 -6
- package/apps/studio/api.ts +812 -64
- 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/prep.sh +8 -1
- package/apps/studio/canvas-edit.ts +1885 -104
- package/apps/studio/canvas-lib.tsx +19 -0
- package/apps/studio/canvas-list-watch.ts +6 -2
- package/apps/studio/canvas-shell.tsx +27 -0
- package/apps/studio/client/app.jsx +1029 -30
- package/apps/studio/client/github.js +7 -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 +107 -0
- package/apps/studio/config.schema.json +14 -0
- package/apps/studio/context-menu.tsx +1 -1
- package/apps/studio/context.ts +113 -1
- package/apps/studio/dist/client.bundle.js +88 -16
- package/apps/studio/dist/comment-mount.js +1 -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/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/http.ts +535 -27
- package/apps/studio/input-router.tsx +7 -1
- package/apps/studio/runtime-bundle.ts +30 -0
- package/apps/studio/server.ts +34 -9
- package/apps/studio/test/acp-bridge.test.ts +132 -0
- package/apps/studio/test/acp-commands.test.ts +8 -3
- package/apps/studio/test/annotations-roundtrip.test.ts +47 -0
- package/apps/studio/test/canvas-create-api.test.ts +76 -0
- package/apps/studio/test/canvas-edit.test.ts +90 -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 +36 -0
- package/apps/studio/test/clip-addressing.test.ts +732 -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/edit-persistence.test.ts +91 -0
- 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/mock-acp-agent.mjs +24 -1
- package/apps/studio/test/fixtures/video-comp-fixture.tsx +82 -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/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/use-annotation-resize.tsx +6 -3
- package/apps/studio/use-canvas-media-drop.tsx +66 -4
- package/apps/studio/video-comp.tsx +444 -0
- package/apps/studio/whats-new.json +36 -0
- package/apps/studio/ws.ts +5 -0
- package/package.json +8 -8
- 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,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./whats-new.schema.json",
|
|
3
3
|
"entries": [
|
|
4
|
+
{
|
|
5
|
+
"id": "timeline-clip-editing",
|
|
6
|
+
"version": "0.40.0",
|
|
7
|
+
"date": "2026-07-05",
|
|
8
|
+
"kind": "feature",
|
|
9
|
+
"title": "Hand-edit your video comps on the timeline",
|
|
10
|
+
"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.",
|
|
11
|
+
"surface": "design-ui"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"id": "video-comp-remotion",
|
|
15
|
+
"version": "0.40.0",
|
|
16
|
+
"date": "2026-07-05",
|
|
17
|
+
"kind": "feature",
|
|
18
|
+
"title": "Design videos & animations, right in the canvas",
|
|
19
|
+
"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.",
|
|
20
|
+
"surface": "design-ui"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "file-tree-live-config-reload",
|
|
24
|
+
"version": "0.40.0",
|
|
25
|
+
"date": "2026-07-05",
|
|
26
|
+
"kind": "fix",
|
|
27
|
+
"title": "New design systems show up in the file tree instantly",
|
|
28
|
+
"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.",
|
|
29
|
+
"surface": "design-ui"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "acp-chat-cross-restart-resume",
|
|
33
|
+
"version": "0.39.1",
|
|
34
|
+
"date": "2026-07-03",
|
|
35
|
+
"kind": "fix",
|
|
36
|
+
"title": "The Assistant remembers your conversation after a restart",
|
|
37
|
+
"summary": "Killed the app to grab an update, or restarted the dev server mid-chat? The Assistant now picks up the actual conversation where you left off instead of quietly starting fresh while the old messages just sat there.",
|
|
38
|
+
"surface": "design-ui"
|
|
39
|
+
},
|
|
4
40
|
{
|
|
5
41
|
"id": "acp-chat-image-thumbnails",
|
|
6
42
|
"version": "0.39.0",
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1agh/maude",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"description": "Marketplace of Claude Code plugins by Michal Dovrtěl: `design` (canvas-first design iteration) + `flow` (generic agentic workflow loop with .ai second brain). Ships the `maude` CLI (with `mdcc` legacy alias) to scaffold workspace, run the design dev server, and manage configs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -50,13 +50,13 @@
|
|
|
50
50
|
"test:e2e:desktop:onboarding": "pnpm --filter @maude/desktop-e2e e2e:onboarding"
|
|
51
51
|
},
|
|
52
52
|
"optionalDependencies": {
|
|
53
|
-
"@1agh/maude-darwin-arm64": "0.
|
|
54
|
-
"@1agh/maude-darwin-x64": "0.
|
|
55
|
-
"@1agh/maude-linux-arm64": "0.
|
|
56
|
-
"@1agh/maude-linux-arm64-musl": "0.
|
|
57
|
-
"@1agh/maude-linux-x64": "0.
|
|
58
|
-
"@1agh/maude-linux-x64-musl": "0.
|
|
59
|
-
"@1agh/maude-win32-x64": "0.
|
|
53
|
+
"@1agh/maude-darwin-arm64": "0.40.0",
|
|
54
|
+
"@1agh/maude-darwin-x64": "0.40.0",
|
|
55
|
+
"@1agh/maude-linux-arm64": "0.40.0",
|
|
56
|
+
"@1agh/maude-linux-arm64-musl": "0.40.0",
|
|
57
|
+
"@1agh/maude-linux-x64": "0.40.0",
|
|
58
|
+
"@1agh/maude-linux-x64-musl": "0.40.0",
|
|
59
|
+
"@1agh/maude-win32-x64": "0.40.0"
|
|
60
60
|
},
|
|
61
61
|
"files": [
|
|
62
62
|
"cli",
|
|
@@ -141,7 +141,17 @@
|
|
|
141
141
|
"y-protocols/sync": "/_canvas-runtime/y-protocols_sync.js",
|
|
142
142
|
"y-protocols/awareness": "/_canvas-runtime/y-protocols_awareness.js",
|
|
143
143
|
"lib0/decoding": "/_canvas-runtime/lib0_decoding.js",
|
|
144
|
-
"lib0/encoding": "/_canvas-runtime/lib0_encoding.js"
|
|
144
|
+
"lib0/encoding": "/_canvas-runtime/lib0_encoding.js",
|
|
145
|
+
"remotion": "/_canvas-runtime/remotion.js",
|
|
146
|
+
"@remotion/media": "/_canvas-runtime/@remotion_media.js",
|
|
147
|
+
"@remotion/player": "/_canvas-runtime/@remotion_player.js",
|
|
148
|
+
"@remotion/transitions": "/_canvas-runtime/@remotion_transitions.js",
|
|
149
|
+
"@remotion/transitions/fade": "/_canvas-runtime/@remotion_transitions_fade.js",
|
|
150
|
+
"@remotion/transitions/slide": "/_canvas-runtime/@remotion_transitions_slide.js",
|
|
151
|
+
"@remotion/transitions/wipe": "/_canvas-runtime/@remotion_transitions_wipe.js",
|
|
152
|
+
"@remotion/transitions/flip": "/_canvas-runtime/@remotion_transitions_flip.js",
|
|
153
|
+
"@remotion/transitions/clock-wipe": "/_canvas-runtime/@remotion_transitions_clock-wipe.js",
|
|
154
|
+
"@remotion/transitions/none": "/_canvas-runtime/@remotion_transitions_none.js"
|
|
145
155
|
}
|
|
146
156
|
}
|
|
147
157
|
</script>
|
|
@@ -350,7 +360,20 @@
|
|
|
350
360
|
// skipped. Mirrors the meta echo-guard below.
|
|
351
361
|
if (msg.mode === 'module') {
|
|
352
362
|
// read the parent-verified local (NOT a canvas-writable global)
|
|
353
|
-
|
|
363
|
+
//
|
|
364
|
+
// VIDEO-COMP EXCEPTION (DDR-150 dogfood): on a canvas with a
|
|
365
|
+
// mounted Remotion comp the optimistic DOM style CANNOT hold —
|
|
366
|
+
// the Player re-renders styles from the (old) module on every
|
|
367
|
+
// frame, wiping the edit at the next play-through. Skipping the
|
|
368
|
+
// module reload here left the OLD color playing until a manual
|
|
369
|
+
// Cmd+R even though the new value was on disk. So: skip the
|
|
370
|
+
// redundant reload only on comp-less canvases. (Reading the
|
|
371
|
+
// comp registry off `window` is canvas-influenced, but a canvas
|
|
372
|
+
// lying "I have comps" can only FORCE a reload — the safe
|
|
373
|
+
// direction; it can never suppress one.)
|
|
374
|
+
var hasComps = false;
|
|
375
|
+
try { hasComps = !!(window.__maude_comps__ && window.__maude_comps__().length); } catch (err) { hasComps = false; }
|
|
376
|
+
if (!hasComps && Date.now() - lastCssOptimisticAt < 1500) return;
|
|
354
377
|
}
|
|
355
378
|
// Phase 30 (F4) — prefer an in-place hot-swap for ANY canvas-body
|
|
356
379
|
// (`module`) change whenever the canvas runtime is mounted: the
|
|
@@ -170,7 +170,7 @@ The agent may also propose treatments outside this catalog — when it does, it
|
|
|
170
170
|
| `expressive` | `paired \| chromatic-3` | accent-tinted, gradients OK | `gradient-mesh`, `chromatic-blocks`, `frosted-blur` | `aesthetic` |
|
|
171
171
|
| `maximalist` | `chromatic-N` | bold, colour-as-structure | `chromatic-blocks`, `gradient-mesh`, `body-pattern` | `full` |
|
|
172
172
|
|
|
173
|
-
**Anti-funnel invariant:** absence of a clear brand-character signal does NOT mean `restrained` — it means low confidence, so Stage
|
|
173
|
+
**Anti-funnel invariant:** absence of a clear brand-character signal does NOT mean `restrained` — it means low confidence, so the flow surfaces the full scale to the user (on the multi-tile path the Stage-3 direction pick shows the ambition poles rendered; on the degraded-to-1 path the Stage-4 refinement ASKS across the full scale, incl. a multi-colour palette option — DDR-147). High-confidence skip is legitimate only when the character is unambiguous, at **both** ends of the scale. Q10 hard-NOs still override (e.g. "no gradients" disables `gradient-mesh`).
|
|
174
174
|
|
|
175
175
|
### Q10 hard NOs — sub-agent guardrails
|
|
176
176
|
|