@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
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ A personal marketplace of Claude Code plugins. Two plugins today, plus a `maude`
|
|
|
11
11
|
|
|
12
12
|
| Plugin | What it does |
|
|
13
13
|
| ------ | ------------ |
|
|
14
|
-
| **`design`** | Canvas-first iteration on TSX/JSX mocks under `.design/` — element selection via Cmd+Click, auto-managed dev server, chained UX/DS critique. |
|
|
14
|
+
| **`design`** | Canvas-first iteration on TSX/JSX mocks under `.design/` — element selection via Cmd+Click, auto-managed dev server, chained UX/DS critique. Canvases can also be **video-comps** (Remotion compositions previewed in-app, exported to MP4/GIF through Maude's own capture engine — no extra install). |
|
|
15
15
|
| **`flow`** | Generic agentic workflow loop with a second-brain `.ai/` workspace. `/flow:plan`, `/flow:execute`, `/flow:utils-verify`, `/flow:validate`, `/flow:done`, `/flow:init`, `/flow:record-ddr`, `/flow:scenario`, …. Project-agnostic via `<project>` placeholders + per-repo `.ai/workflows.config.json`. |
|
|
16
16
|
|
|
17
17
|
Plus the **`maude`** CLI — `maude init` scaffolds a fresh `.ai/` workspace from the flow plugin skeleton; `maude design serve` boots the design dev server. The legacy `mdcc` alias still works (prints a deprecation warning) and will be removed in v0.17.x.
|
|
@@ -43,9 +43,9 @@ const BANNER_CSS = `
|
|
|
43
43
|
background: var(--maude-hud-accent-tint, oklch(92% 0.040 55));
|
|
44
44
|
color: var(--maude-chrome-fg-0, #111);
|
|
45
45
|
border: 1px solid var(--maude-chrome-border, rgba(0,0,0,0.16));
|
|
46
|
-
border-radius:
|
|
46
|
+
border-radius: 4px;
|
|
47
47
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
48
|
-
font-family:
|
|
48
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
49
49
|
font-weight: 500;
|
|
50
50
|
font-size: 12px;
|
|
51
51
|
line-height: 1.2;
|
|
@@ -827,9 +827,52 @@ export function AnnotationContextToolbar({
|
|
|
827
827
|
// fall through to the generic bar below.
|
|
828
828
|
const soleMedia =
|
|
829
829
|
selectedStrokes.length === 1 &&
|
|
830
|
-
(selectedStrokes[0]?.tool === 'image' ||
|
|
830
|
+
(selectedStrokes[0]?.tool === 'image' ||
|
|
831
|
+
selectedStrokes[0]?.tool === 'link' ||
|
|
832
|
+
selectedStrokes[0]?.tool === 'mediaref')
|
|
831
833
|
? selectedStrokes[0]
|
|
832
834
|
: null;
|
|
835
|
+
if (soleMedia?.tool === 'mediaref') {
|
|
836
|
+
// DDR-150 dogfood — a media-reference chip has no color to set (the generic
|
|
837
|
+
// swatch palette was meaningless noise). Focused panel: filename + delete.
|
|
838
|
+
const mr = soleMedia;
|
|
839
|
+
return (
|
|
840
|
+
<div
|
|
841
|
+
ref={ref}
|
|
842
|
+
className="dc-annot-ctx"
|
|
843
|
+
role="toolbar"
|
|
844
|
+
aria-label="Media reference properties"
|
|
845
|
+
style={{ display: 'flex', top: -9999, left: -9999 }}
|
|
846
|
+
>
|
|
847
|
+
<span
|
|
848
|
+
className="dc-annot-ctx-label"
|
|
849
|
+
style={{
|
|
850
|
+
padding: '0 8px',
|
|
851
|
+
fontSize: 12,
|
|
852
|
+
opacity: 0.85,
|
|
853
|
+
maxWidth: 220,
|
|
854
|
+
overflow: 'hidden',
|
|
855
|
+
textOverflow: 'ellipsis',
|
|
856
|
+
whiteSpace: 'nowrap',
|
|
857
|
+
}}
|
|
858
|
+
title={`${mr.mediaKind} · ${mr.src}`}
|
|
859
|
+
>
|
|
860
|
+
{mr.mediaKind === 'audio' ? '♪ ' : '▶ '}
|
|
861
|
+
{mr.title}
|
|
862
|
+
</span>
|
|
863
|
+
<div className="dc-annot-ctx-sep" />
|
|
864
|
+
<button
|
|
865
|
+
type="button"
|
|
866
|
+
className="dc-annot-ctx-ibtn dc-annot-ctx-ibtn--danger"
|
|
867
|
+
aria-label="Delete media reference"
|
|
868
|
+
title="Delete"
|
|
869
|
+
onClick={remove}
|
|
870
|
+
>
|
|
871
|
+
<IconTrash />
|
|
872
|
+
</button>
|
|
873
|
+
</div>
|
|
874
|
+
);
|
|
875
|
+
}
|
|
833
876
|
if (soleMedia?.tool === 'image') {
|
|
834
877
|
const img = soleMedia;
|
|
835
878
|
const commitAlt = (value: string) => {
|
|
@@ -93,6 +93,12 @@ import {
|
|
|
93
93
|
linkCardLayout,
|
|
94
94
|
listPrefixedBody,
|
|
95
95
|
listPrefixedLine,
|
|
96
|
+
MEDIAREF_AUDIO_GLYPH,
|
|
97
|
+
MEDIAREF_DEFAULT_H,
|
|
98
|
+
MEDIAREF_DEFAULT_W,
|
|
99
|
+
MEDIAREF_VIDEO_GLYPH,
|
|
100
|
+
MEDIAREF_VIDEO_H,
|
|
101
|
+
type MediaRefStroke,
|
|
96
102
|
normalizeBox,
|
|
97
103
|
normalizeRect,
|
|
98
104
|
normalizeSticky,
|
|
@@ -380,6 +386,19 @@ function resolveAssetHref(href: string): string {
|
|
|
380
386
|
return /^assets\//.test(href) ? `/${canvasDesignRel()}/${href}` : href;
|
|
381
387
|
}
|
|
382
388
|
|
|
389
|
+
/**
|
|
390
|
+
* DDR-150 dogfood #8 — true when a pointer/click event targets the inline
|
|
391
|
+
* media player inside a mediaref chip. Every document-capture annotation
|
|
392
|
+
* handler early-returns on it, so the player's native controls (and its
|
|
393
|
+
* click-to-toggle) work instead of starting a stroke select/drag/draw.
|
|
394
|
+
* (NOT a stopPropagation guard — that would also kill React's delegated
|
|
395
|
+
* listeners, which attach later on the capture path.)
|
|
396
|
+
*/
|
|
397
|
+
function isMediaPlayerTarget(e: Event): boolean {
|
|
398
|
+
const t = e.target as Element | null;
|
|
399
|
+
return !!(t && typeof t.closest === 'function' && t.closest('[data-mediaref-player]'));
|
|
400
|
+
}
|
|
401
|
+
|
|
383
402
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
384
403
|
// Styles
|
|
385
404
|
|
|
@@ -1057,11 +1076,36 @@ export function AnnotationsLayer() {
|
|
|
1057
1076
|
}
|
|
1058
1077
|
if (m.dgn === 'annotation-select-all') {
|
|
1059
1078
|
if (annotSel) annotSel.replace(strokes.map((s) => s.id));
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
// Stage F3 — the shell's AssetPicker (main-origin, DDR-054) resolved a new
|
|
1082
|
+
// asset for an ImageStroke/MediaRefStroke's "Replace…" context-menu entry;
|
|
1083
|
+
// the canvas iframe owns the annotation model, so the shell REQUESTS the
|
|
1084
|
+
// swap here rather than writing strokes itself. `href`/`src` mirrors each
|
|
1085
|
+
// stroke shape (ImageStroke.href / MediaRefStroke.src) — swap whichever key
|
|
1086
|
+
// the target stroke actually carries, everything else survives byte-for-byte.
|
|
1087
|
+
if (
|
|
1088
|
+
m.dgn === 'replace-annotation-media' &&
|
|
1089
|
+
typeof (m as { id?: unknown }).id === 'string' &&
|
|
1090
|
+
typeof (m as { path?: unknown }).path === 'string'
|
|
1091
|
+
) {
|
|
1092
|
+
const id = (m as { id: string }).id;
|
|
1093
|
+
const path = (m as { path: string }).path;
|
|
1094
|
+
const before = strokesRef.current;
|
|
1095
|
+
const target = before.find((s) => s.id === id);
|
|
1096
|
+
if (!target || (target.tool !== 'image' && target.tool !== 'mediaref')) return;
|
|
1097
|
+
const after = before.map((s) => {
|
|
1098
|
+
if (s.id !== id) return s;
|
|
1099
|
+
return target.tool === 'image'
|
|
1100
|
+
? ({ ...s, href: path } as ImageStroke)
|
|
1101
|
+
: ({ ...s, src: path } as MediaRefStroke);
|
|
1102
|
+
});
|
|
1103
|
+
commitStrokes(before, after, 'replace media');
|
|
1060
1104
|
}
|
|
1061
1105
|
};
|
|
1062
1106
|
window.addEventListener('message', onMessage);
|
|
1063
1107
|
return () => window.removeEventListener('message', onMessage);
|
|
1064
|
-
}, [annotSel, strokes, setVisible]);
|
|
1108
|
+
}, [annotSel, strokes, setVisible, commitStrokes]);
|
|
1065
1109
|
|
|
1066
1110
|
// Document-level toggle: Shift+P (presentation). Annotation-shortcut help is
|
|
1067
1111
|
// owned by the dev-server menubar (Help button); we no longer ship an
|
|
@@ -1167,15 +1211,63 @@ export function AnnotationsLayer() {
|
|
|
1167
1211
|
[commitStrokes, annotSel]
|
|
1168
1212
|
);
|
|
1169
1213
|
|
|
1214
|
+
// Media reference (DDR-150 P4): a video/audio file dropped on the canvas BODY
|
|
1215
|
+
// becomes a versioned reference chip (NOT a source insert, NOT a played
|
|
1216
|
+
// element) carrying its assets/ path — the "nahazet klipy → agent z toho udělá
|
|
1217
|
+
// video" artifact. Upload to assets/, then commit a MediaRefStroke; on failure
|
|
1218
|
+
// toast. No poster probe in v1 — a media glyph tile (▶/♪) + filename.
|
|
1219
|
+
const createMediaReference = useCallback(
|
|
1220
|
+
(file: File, mediaKind: 'video' | 'audio', world: [number, number]) => {
|
|
1221
|
+
const w = MEDIAREF_DEFAULT_W;
|
|
1222
|
+
// Video chips are taller — they host the inline 16:9 player (dogfood #8).
|
|
1223
|
+
const h = mediaKind === 'video' ? MEDIAREF_VIDEO_H : MEDIAREF_DEFAULT_H;
|
|
1224
|
+
void uploadAsset(file).then((res) => {
|
|
1225
|
+
if (!('path' in res)) {
|
|
1226
|
+
showCanvasToast(`Couldn't add ${mediaKind}: ${res.error}`);
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
const id = rid();
|
|
1230
|
+
const ref: MediaRefStroke = {
|
|
1231
|
+
id,
|
|
1232
|
+
tool: 'mediaref',
|
|
1233
|
+
x: world[0] - w / 2,
|
|
1234
|
+
y: world[1] - h / 2,
|
|
1235
|
+
w,
|
|
1236
|
+
h,
|
|
1237
|
+
src: res.path,
|
|
1238
|
+
mediaKind,
|
|
1239
|
+
title: (file.name || res.path).slice(0, 300),
|
|
1240
|
+
};
|
|
1241
|
+
const before = strokesRef.current;
|
|
1242
|
+
commitStrokes(before, [...before, ref], `add ${mediaKind} reference`);
|
|
1243
|
+
annotSel?.replace([id]);
|
|
1244
|
+
const sizeMb = file.size / (1024 * 1024);
|
|
1245
|
+
showCanvasToast(
|
|
1246
|
+
`Added ${mediaKind} reference · ${res.path}${sizeMb > 20 ? ' · ⚠ >20 MB rides git + sync' : ''}`
|
|
1247
|
+
);
|
|
1248
|
+
});
|
|
1249
|
+
},
|
|
1250
|
+
[commitStrokes, annotSel]
|
|
1251
|
+
);
|
|
1252
|
+
|
|
1170
1253
|
const mediaCallbacks = useMemo(
|
|
1171
|
-
() => ({ onImage: createImageFromFile, onLink: createLink }),
|
|
1172
|
-
[createImageFromFile, createLink]
|
|
1254
|
+
() => ({ onImage: createImageFromFile, onLink: createLink, onMedia: createMediaReference }),
|
|
1255
|
+
[createImageFromFile, createLink, createMediaReference]
|
|
1173
1256
|
);
|
|
1174
1257
|
// Media intake is paste/drop only (per product steer — no toolbar buttons):
|
|
1175
1258
|
// drop an image / URL or Cmd+V a clipboard image / link straight onto the
|
|
1176
1259
|
// canvas. The hook owns the dragover/drop/paste wiring; the create callbacks
|
|
1177
1260
|
// hold the commit/undo sink + screenToWorld.
|
|
1178
|
-
|
|
1261
|
+
//
|
|
1262
|
+
// DDR-150 dogfood #8 — intake was gated on `visible` (annotations toggled on),
|
|
1263
|
+
// so with annotations hidden (⇧P) a Finder drop silently did NOTHING. Intake
|
|
1264
|
+
// now stays live whenever we're not presenting; the committed stroke simply
|
|
1265
|
+
// shows once annotations are visible again.
|
|
1266
|
+
useCanvasMediaDrop({
|
|
1267
|
+
enabled: !(chrome?.present ?? false),
|
|
1268
|
+
screenToWorld,
|
|
1269
|
+
callbacks: mediaCallbacks,
|
|
1270
|
+
});
|
|
1179
1271
|
|
|
1180
1272
|
const eraseAt = useCallback(
|
|
1181
1273
|
(wx: number, wy: number) => {
|
|
@@ -1636,6 +1728,7 @@ export function AnnotationsLayer() {
|
|
|
1636
1728
|
t === 'sticky' ||
|
|
1637
1729
|
t === 'image' ||
|
|
1638
1730
|
t === 'link' ||
|
|
1731
|
+
t === 'mediaref' ||
|
|
1639
1732
|
t === 'section')
|
|
1640
1733
|
) {
|
|
1641
1734
|
return id;
|
|
@@ -1644,6 +1737,7 @@ export function AnnotationsLayer() {
|
|
|
1644
1737
|
};
|
|
1645
1738
|
|
|
1646
1739
|
const onDown = (e: PointerEvent) => {
|
|
1740
|
+
if (isMediaPlayerTarget(e)) return; // mediaref inline player owns this event
|
|
1647
1741
|
if (e.button !== 0) return;
|
|
1648
1742
|
if (e.metaKey || e.ctrlKey) return; // escape hatch into element-selection
|
|
1649
1743
|
const target = e.target as Element | null;
|
|
@@ -1937,6 +2031,7 @@ export function AnnotationsLayer() {
|
|
|
1937
2031
|
if (typeof document === 'undefined') return;
|
|
1938
2032
|
if (tool !== 'move') return;
|
|
1939
2033
|
const onDbl = (e: MouseEvent) => {
|
|
2034
|
+
if (isMediaPlayerTarget(e)) return; // mediaref inline player owns this event
|
|
1940
2035
|
const target = e.target as Element | null;
|
|
1941
2036
|
const node = target?.closest?.('[data-id][data-tool]');
|
|
1942
2037
|
if (!node) return;
|
|
@@ -2460,6 +2555,7 @@ export function AnnotationsLayer() {
|
|
|
2460
2555
|
if (typeof document === 'undefined') return;
|
|
2461
2556
|
if (tool !== 'move') return;
|
|
2462
2557
|
const onDown = (e: PointerEvent) => {
|
|
2558
|
+
if (isMediaPlayerTarget(e)) return; // mediaref inline player owns this event
|
|
2463
2559
|
if (e.button !== 0) return;
|
|
2464
2560
|
const dot = (e.target as Element | null)?.closest?.('.dc-annot-conn-dot');
|
|
2465
2561
|
if (!dot) return;
|
|
@@ -2575,6 +2671,7 @@ export function AnnotationsLayer() {
|
|
|
2575
2671
|
// propagation WITHOUT preventDefault, so the native contextmenu event
|
|
2576
2672
|
// (which opens OUR menu above) still follows.
|
|
2577
2673
|
const onDown = (e: PointerEvent) => {
|
|
2674
|
+
if (isMediaPlayerTarget(e)) return; // mediaref inline player owns this event
|
|
2578
2675
|
if (e.button !== 2) return;
|
|
2579
2676
|
if (!strokeAt(e.target as Element | null)) return;
|
|
2580
2677
|
e.stopImmediatePropagation();
|
|
@@ -2665,9 +2762,27 @@ export function AnnotationsLayer() {
|
|
|
2665
2762
|
if (members) annotSel.replace(members);
|
|
2666
2763
|
} else if (action === 'ungroup') {
|
|
2667
2764
|
store.ungroupSelection(sel);
|
|
2765
|
+
} else if (action === 'replace') {
|
|
2766
|
+
// Stage F3 — "Replace…" on an ImageStroke/MediaRefStroke. The canvas
|
|
2767
|
+
// REQUESTS (untrusted-origin postMessage, DDR-054); the main-origin shell
|
|
2768
|
+
// opens the AssetPicker and posts the picked path back down (handled by
|
|
2769
|
+
// the `replace-annotation-media` listener above, which owns the write —
|
|
2770
|
+
// this model has no data-cd-id, so it can't ride edit-attr like F2).
|
|
2771
|
+
if (sel.length !== 1) return;
|
|
2772
|
+
const target = strokesById.get(sel[0] as string);
|
|
2773
|
+
if (!target || (target.tool !== 'image' && target.tool !== 'mediaref')) return;
|
|
2774
|
+
const before = target.tool === 'image' ? target.href : target.src;
|
|
2775
|
+
try {
|
|
2776
|
+
window.parent.postMessage(
|
|
2777
|
+
{ dgn: 'replace-annotation-media-request', id: target.id, before },
|
|
2778
|
+
'*'
|
|
2779
|
+
);
|
|
2780
|
+
} catch {
|
|
2781
|
+
/* detached / cross-origin teardown */
|
|
2782
|
+
}
|
|
2668
2783
|
}
|
|
2669
2784
|
},
|
|
2670
|
-
[annotSel, copySelection, pasteStrokesText]
|
|
2785
|
+
[annotSel, copySelection, pasteStrokesText, strokesById]
|
|
2671
2786
|
);
|
|
2672
2787
|
|
|
2673
2788
|
// FigJam v3 — first time a multi-selection lands, surface the group /
|
|
@@ -2724,6 +2839,10 @@ export function AnnotationsLayer() {
|
|
|
2724
2839
|
onCancelEdit={cancelEditing}
|
|
2725
2840
|
/>
|
|
2726
2841
|
) : null}
|
|
2842
|
+
{/* DDR-150 dogfood #8 — inline players for media-reference chips (HTML
|
|
2843
|
+
overlay in the world div; see MediaRefPlayers for why not
|
|
2844
|
+
foreignObject). */}
|
|
2845
|
+
<MediaRefPlayers worldRef={worldRef} strokes={renderStrokes} visible={visible} />
|
|
2727
2846
|
<AnnotationContextToolbar
|
|
2728
2847
|
editingId={
|
|
2729
2848
|
editingTarget?.kind === 'anchored'
|
|
@@ -2740,6 +2859,10 @@ export function AnnotationsLayer() {
|
|
|
2740
2859
|
pos={ctxMenu}
|
|
2741
2860
|
selCount={annotSel.selectedIds.length}
|
|
2742
2861
|
canUngroup={selectedStrokes.some((s) => (s.groupIds?.length ?? 0) > 0)}
|
|
2862
|
+
canReplace={
|
|
2863
|
+
selectedStrokes.length === 1 &&
|
|
2864
|
+
(selectedStrokes[0]?.tool === 'image' || selectedStrokes[0]?.tool === 'mediaref')
|
|
2865
|
+
}
|
|
2743
2866
|
onAction={onMenuAction}
|
|
2744
2867
|
onClose={() => setCtxMenu(null)}
|
|
2745
2868
|
/>
|
|
@@ -3110,6 +3233,92 @@ function AnnotationsSvg({
|
|
|
3110
3233
|
);
|
|
3111
3234
|
}
|
|
3112
3235
|
|
|
3236
|
+
/**
|
|
3237
|
+
* DDR-150 dogfood #8 — the mediaref chips' inline players, rendered as PLAIN
|
|
3238
|
+
* HTML absolutely positioned in the world div (which carries the pan/zoom CSS
|
|
3239
|
+
* transform), NOT as SVG foreignObject: Chromium hit-tests foreignObject
|
|
3240
|
+
* content under a transformed ancestor in the un-transformed coordinate space,
|
|
3241
|
+
* so real clicks miss the player at most zoom levels. HTML children of the
|
|
3242
|
+
* transformed div hit-test correctly. The [data-mediaref-player] attr keeps
|
|
3243
|
+
* every document-capture annotation handler out (isMediaPlayerTarget guard).
|
|
3244
|
+
*/
|
|
3245
|
+
function MediaRefPlayers({
|
|
3246
|
+
worldRef,
|
|
3247
|
+
strokes,
|
|
3248
|
+
visible,
|
|
3249
|
+
}: {
|
|
3250
|
+
worldRef: React.RefObject<HTMLElement | null>;
|
|
3251
|
+
strokes: readonly Stroke[];
|
|
3252
|
+
visible: boolean;
|
|
3253
|
+
}) {
|
|
3254
|
+
const target = worldRef.current;
|
|
3255
|
+
if (!target || !visible) return null;
|
|
3256
|
+
const HEADER = 26;
|
|
3257
|
+
const refs = strokes.filter(
|
|
3258
|
+
(s): s is MediaRefStroke => s.tool === 'mediaref' && !!s.src && Math.abs(s.h) > HEADER + 12
|
|
3259
|
+
);
|
|
3260
|
+
if (refs.length === 0) return null;
|
|
3261
|
+
return createPortal(
|
|
3262
|
+
<>
|
|
3263
|
+
{refs.map((s) => {
|
|
3264
|
+
const x = Math.min(s.x, s.x + s.w);
|
|
3265
|
+
const y = Math.min(s.y, s.y + s.h);
|
|
3266
|
+
const w = Math.abs(s.w);
|
|
3267
|
+
const h = Math.abs(s.h);
|
|
3268
|
+
const mediaUrl = resolveAssetHref(s.src);
|
|
3269
|
+
const isAudio = s.mediaKind === 'audio';
|
|
3270
|
+
return (
|
|
3271
|
+
<div
|
|
3272
|
+
key={`mrp-${s.id}`}
|
|
3273
|
+
data-mediaref-player="1"
|
|
3274
|
+
style={{
|
|
3275
|
+
position: 'absolute',
|
|
3276
|
+
left: x + 4,
|
|
3277
|
+
top: y + HEADER,
|
|
3278
|
+
width: Math.max(8, w - 8),
|
|
3279
|
+
height: Math.max(8, h - HEADER - 4),
|
|
3280
|
+
borderRadius: 6,
|
|
3281
|
+
overflow: 'hidden',
|
|
3282
|
+
zIndex: 4,
|
|
3283
|
+
}}
|
|
3284
|
+
>
|
|
3285
|
+
{isAudio ? (
|
|
3286
|
+
// biome-ignore lint/a11y/useMediaCaption: user-dropped reference media — there is no caption source to point a <track> at.
|
|
3287
|
+
<audio
|
|
3288
|
+
controls
|
|
3289
|
+
preload="metadata"
|
|
3290
|
+
src={mediaUrl}
|
|
3291
|
+
style={{ width: '100%', height: '100%' }}
|
|
3292
|
+
/>
|
|
3293
|
+
) : (
|
|
3294
|
+
// biome-ignore lint/a11y/useMediaCaption: user-dropped reference media — no caption source exists for an arbitrary dragged-in clip.
|
|
3295
|
+
<video
|
|
3296
|
+
// Chromium's native controls already toggle play/pause on a
|
|
3297
|
+
// content-area click (a custom click listener would double-fire
|
|
3298
|
+
// against it and cancel itself out — verified live). The router
|
|
3299
|
+
// overlay-skip + the annotation-handler guards are what make
|
|
3300
|
+
// these native interactions reachable.
|
|
3301
|
+
controls
|
|
3302
|
+
preload="metadata"
|
|
3303
|
+
playsInline
|
|
3304
|
+
src={mediaUrl}
|
|
3305
|
+
style={{
|
|
3306
|
+
width: '100%',
|
|
3307
|
+
height: '100%',
|
|
3308
|
+
objectFit: 'contain',
|
|
3309
|
+
background: '#000',
|
|
3310
|
+
display: 'block',
|
|
3311
|
+
}}
|
|
3312
|
+
/>
|
|
3313
|
+
)}
|
|
3314
|
+
</div>
|
|
3315
|
+
);
|
|
3316
|
+
})}
|
|
3317
|
+
</>,
|
|
3318
|
+
target
|
|
3319
|
+
);
|
|
3320
|
+
}
|
|
3321
|
+
|
|
3113
3322
|
function TextEditor({
|
|
3114
3323
|
anchorId,
|
|
3115
3324
|
host,
|
|
@@ -3165,6 +3374,7 @@ function TextEditor({
|
|
|
3165
3374
|
useEffect(() => {
|
|
3166
3375
|
if (typeof document === 'undefined') return;
|
|
3167
3376
|
const onDown = (e: PointerEvent) => {
|
|
3377
|
+
if (isMediaPlayerTarget(e)) return; // mediaref inline player owns this event
|
|
3168
3378
|
const el = ref.current;
|
|
3169
3379
|
if (!el) return;
|
|
3170
3380
|
if (el.contains(e.target as Node)) return;
|
|
@@ -3427,6 +3637,7 @@ function StandaloneTextEditor({
|
|
|
3427
3637
|
useEffect(() => {
|
|
3428
3638
|
if (typeof document === 'undefined') return;
|
|
3429
3639
|
const onDown = (e: PointerEvent) => {
|
|
3640
|
+
if (isMediaPlayerTarget(e)) return; // mediaref inline player owns this event
|
|
3430
3641
|
const el = ref.current;
|
|
3431
3642
|
if (!el) return;
|
|
3432
3643
|
if (el.contains(e.target as Node)) return;
|
|
@@ -3586,12 +3797,15 @@ function AnnotationContextMenu({
|
|
|
3586
3797
|
pos,
|
|
3587
3798
|
selCount,
|
|
3588
3799
|
canUngroup,
|
|
3800
|
+
canReplace,
|
|
3589
3801
|
onAction,
|
|
3590
3802
|
onClose,
|
|
3591
3803
|
}: {
|
|
3592
3804
|
pos: { x: number; y: number };
|
|
3593
3805
|
selCount: number;
|
|
3594
3806
|
canUngroup: boolean;
|
|
3807
|
+
/** Stage F3 — exactly one ImageStroke/MediaRefStroke is selected. */
|
|
3808
|
+
canReplace: boolean;
|
|
3595
3809
|
onAction: (action: string) => void;
|
|
3596
3810
|
onClose: () => void;
|
|
3597
3811
|
}) {
|
|
@@ -3609,6 +3823,7 @@ function AnnotationContextMenu({
|
|
|
3609
3823
|
if (nx !== at.x || ny !== at.y) setAt({ x: nx, y: ny });
|
|
3610
3824
|
el.querySelector<HTMLButtonElement>('button.dc-menu-item:not([disabled])')?.focus();
|
|
3611
3825
|
const onDown = (e: PointerEvent) => {
|
|
3826
|
+
if (isMediaPlayerTarget(e)) return; // mediaref inline player owns this event
|
|
3612
3827
|
if (!el.contains(e.target as Node)) onClose();
|
|
3613
3828
|
};
|
|
3614
3829
|
const onKey = (e: KeyboardEvent) => {
|
|
@@ -3657,6 +3872,7 @@ function AnnotationContextMenu({
|
|
|
3657
3872
|
{item('cut', 'Cut', '⌘X')}
|
|
3658
3873
|
{item('paste', 'Paste', '⌘V')}
|
|
3659
3874
|
{item('duplicate', 'Duplicate', '⌘D')}
|
|
3875
|
+
{canReplace ? item('replace', 'Replace…') : null}
|
|
3660
3876
|
<div className="dc-menu-sep" aria-hidden="true" />
|
|
3661
3877
|
{item('front', 'Bring to front', ']')}
|
|
3662
3878
|
{item('forward', 'Bring forward', '⌘]')}
|
|
@@ -4176,6 +4392,91 @@ function StrokeNodeBase({
|
|
|
4176
4392
|
</g>
|
|
4177
4393
|
);
|
|
4178
4394
|
}
|
|
4395
|
+
if (stroke.tool === 'mediaref') {
|
|
4396
|
+
// DDR-150 P4 + dogfood #8 — reference chip with a REAL inline player.
|
|
4397
|
+
// LIVE-RENDER ONLY: the <foreignObject> + <video>/<audio> below never
|
|
4398
|
+
// persist — the model serializer still writes the sanitizer-safe data-*
|
|
4399
|
+
// card (foreignObject is stripped by sanitizeAnnotationSvg by design).
|
|
4400
|
+
// The 26px header strip (badge + filename) is the select/drag handle; the
|
|
4401
|
+
// player area is fenced off from the annotation handlers by the
|
|
4402
|
+
// [data-mediaref-player] window-capture guard.
|
|
4403
|
+
const x = Math.min(stroke.x, stroke.x + stroke.w);
|
|
4404
|
+
const y = Math.min(stroke.y, stroke.y + stroke.h);
|
|
4405
|
+
const w = Math.abs(stroke.w);
|
|
4406
|
+
const h = Math.abs(stroke.h);
|
|
4407
|
+
const HEADER = 26;
|
|
4408
|
+
const isAudio = stroke.mediaKind === 'audio';
|
|
4409
|
+
const mediaUrl = stroke.src ? resolveAssetHref(stroke.src) : '';
|
|
4410
|
+
const shownTitle = clampLinkTitle(stroke.title, Math.max(8, Math.floor((w - 40) / 7)));
|
|
4411
|
+
const textFont = {
|
|
4412
|
+
fontFamily: 'var(--u-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace)',
|
|
4413
|
+
} as const;
|
|
4414
|
+
return (
|
|
4415
|
+
<g
|
|
4416
|
+
data-id={stroke.id}
|
|
4417
|
+
data-tool="mediaref"
|
|
4418
|
+
data-src={stroke.src}
|
|
4419
|
+
data-media-kind={stroke.mediaKind}
|
|
4420
|
+
data-title={stroke.title}
|
|
4421
|
+
pointerEvents={hitMode}
|
|
4422
|
+
>
|
|
4423
|
+
<rect
|
|
4424
|
+
x={x}
|
|
4425
|
+
y={y}
|
|
4426
|
+
width={w}
|
|
4427
|
+
height={h}
|
|
4428
|
+
rx={8}
|
|
4429
|
+
ry={8}
|
|
4430
|
+
fill={LINK_CARD_FILL}
|
|
4431
|
+
stroke={LINK_CARD_STROKE}
|
|
4432
|
+
strokeWidth={1}
|
|
4433
|
+
vectorEffect="non-scaling-stroke"
|
|
4434
|
+
filter="url(#dc-sticky-shadow)"
|
|
4435
|
+
/>
|
|
4436
|
+
<svg
|
|
4437
|
+
x={x + 8}
|
|
4438
|
+
y={y + 5}
|
|
4439
|
+
width={16}
|
|
4440
|
+
height={16}
|
|
4441
|
+
viewBox="0 0 24 24"
|
|
4442
|
+
fill={LINK_GLYPH_STROKE}
|
|
4443
|
+
stroke="none"
|
|
4444
|
+
aria-hidden="true"
|
|
4445
|
+
>
|
|
4446
|
+
<path d={isAudio ? MEDIAREF_AUDIO_GLYPH : MEDIAREF_VIDEO_GLYPH} />
|
|
4447
|
+
</svg>
|
|
4448
|
+
<text
|
|
4449
|
+
x={x + 30}
|
|
4450
|
+
y={y + 9}
|
|
4451
|
+
fontSize={11}
|
|
4452
|
+
fill={LINK_TITLE_FILL}
|
|
4453
|
+
fontWeight={600}
|
|
4454
|
+
dominantBaseline="hanging"
|
|
4455
|
+
style={textFont}
|
|
4456
|
+
>
|
|
4457
|
+
{shownTitle}
|
|
4458
|
+
</text>
|
|
4459
|
+
{/* The inline player itself is an HTML overlay portaled beside this SVG
|
|
4460
|
+
(MediaRefPlayers below) — NOT a foreignObject: Chromium hit-tests
|
|
4461
|
+
foreignObject content under a CSS-transformed ancestor in the WRONG
|
|
4462
|
+
coordinate space (the un-panned/un-zoomed one), so real clicks miss
|
|
4463
|
+
the player at most zoom levels while elementFromPoint lies that
|
|
4464
|
+
they'd land. Plain HTML in the transformed world hit-tests right. */}
|
|
4465
|
+
{!mediaUrl ? (
|
|
4466
|
+
<text
|
|
4467
|
+
x={x + 30}
|
|
4468
|
+
y={y + HEADER + 10}
|
|
4469
|
+
fontSize={10}
|
|
4470
|
+
fill={LINK_DOMAIN_FILL}
|
|
4471
|
+
dominantBaseline="hanging"
|
|
4472
|
+
style={textFont}
|
|
4473
|
+
>
|
|
4474
|
+
(missing media reference)
|
|
4475
|
+
</text>
|
|
4476
|
+
) : null}
|
|
4477
|
+
</g>
|
|
4478
|
+
);
|
|
4479
|
+
}
|
|
4179
4480
|
if (stroke.tool === 'section') {
|
|
4180
4481
|
const x = Math.min(stroke.x, stroke.x + stroke.w);
|
|
4181
4482
|
const y = Math.min(stroke.y, stroke.y + stroke.h);
|