@1agh/maude 0.41.0 → 0.43.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/apps/studio/.ai/cache/_stats.json +7 -0
- package/apps/studio/acp/bootstrap-brief.ts +12 -2
- package/apps/studio/annotations-layer.tsx +1055 -278
- package/apps/studio/annotations-model.ts +58 -0
- package/apps/studio/api.ts +173 -63
- package/apps/studio/bin/_html-playwright.mjs +26 -4
- package/apps/studio/bin/_ingest-footage.mjs +386 -0
- package/apps/studio/bin/_pdf-playwright.mjs +13 -2
- package/apps/studio/bin/_png-playwright.mjs +15 -2
- package/apps/studio/bin/_pptx-playwright.mjs +17 -4
- package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
- package/apps/studio/bin/_screenshot-playwright.mjs +17 -2
- package/apps/studio/bin/_svg-playwright.mjs +26 -4
- package/apps/studio/bin/_video-playwright.mjs +141 -71
- package/apps/studio/bin/ingest-footage.sh +27 -0
- package/apps/studio/bin/photo-adjust.sh +163 -0
- package/apps/studio/bin/photo-bg-remove.sh +160 -0
- package/apps/studio/bin/probe-footage.sh +28 -0
- package/apps/studio/bin/read-annotations.mjs +125 -2
- package/apps/studio/bin/screenshot.sh +53 -4
- package/apps/studio/canvas-edit.ts +459 -10
- package/apps/studio/canvas-lib.tsx +523 -5
- package/apps/studio/canvas-pipeline.ts +65 -7
- package/apps/studio/canvas-shell.tsx +156 -16
- package/apps/studio/client/app.jsx +476 -88
- package/apps/studio/client/export-center.jsx +460 -0
- package/apps/studio/client/github.js +4 -0
- package/apps/studio/client/panels/GitPanel.jsx +8 -0
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
- package/apps/studio/client/panels/StickerPicker.jsx +160 -0
- package/apps/studio/client/panels/TimelinePanel.jsx +15 -2
- package/apps/studio/client/photo-knobs.jsx +437 -0
- package/apps/studio/client/styles/3-shell-maude.css +59 -0
- package/apps/studio/client/styles/4-components.css +150 -0
- package/apps/studio/commands/edit-source-command.ts +31 -4
- package/apps/studio/config.schema.json +2 -2
- package/apps/studio/context-menu.tsx +31 -6
- package/apps/studio/dist/client.bundle.js +5561 -19
- package/apps/studio/dist/comment-mount.js +2 -2
- package/apps/studio/dist/runtime/.min-sizes.json +1 -0
- package/apps/studio/dist/runtime/@imgly_background-removal.js +5543 -0
- package/apps/studio/dist/runtime/pixi-js.js +519 -519
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/dom-selection.ts +25 -0
- package/apps/studio/export-dialog.tsx +18 -25
- package/apps/studio/exporters/_runtime.ts +104 -0
- package/apps/studio/exporters/html.ts +12 -20
- package/apps/studio/exporters/index.ts +14 -2
- package/apps/studio/exporters/jobs.ts +376 -0
- package/apps/studio/exporters/pdf.ts +16 -20
- package/apps/studio/exporters/png.ts +12 -20
- package/apps/studio/exporters/pptx.ts +22 -23
- package/apps/studio/exporters/scope.ts +1 -0
- package/apps/studio/exporters/svg.ts +14 -22
- package/apps/studio/exporters/video.ts +37 -19
- package/apps/studio/footage/schema.test.ts +195 -0
- package/apps/studio/footage/schema.ts +484 -0
- package/apps/studio/footage-store.ts +235 -0
- package/apps/studio/fs-watch.ts +10 -0
- package/apps/studio/git/endpoints.ts +68 -7
- package/apps/studio/git/service.ts +198 -20
- package/apps/studio/github/service.ts +67 -0
- package/apps/studio/handoff.ts +3 -2
- package/apps/studio/http.ts +318 -64
- package/apps/studio/input-router.tsx +10 -0
- package/apps/studio/inspect.ts +53 -1
- package/apps/studio/paths.ts +10 -0
- package/apps/studio/photo/filters.ts +285 -0
- package/apps/studio/photo/pipeline.ts +607 -0
- package/apps/studio/photo/schema.ts +367 -0
- package/apps/studio/photo-store.ts +154 -0
- package/apps/studio/runtime-bundle.ts +29 -6
- package/apps/studio/server.ts +8 -2
- package/apps/studio/stickers/figjam-doodle/manifest.json +104 -0
- package/apps/studio/stickers/figjam-doodle/slice1.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice10.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice11.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice12.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice13.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice14.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice15.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice16.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice17.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice18.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice19.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice2.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice20.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice21.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice22.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice23.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice24.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice3.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice4.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice5.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice6.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice7.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice8.png +0 -0
- package/apps/studio/stickers/figjam-doodle/slice9.png +0 -0
- package/apps/studio/stickers/life-style/best.png +0 -0
- package/apps/studio/stickers/life-style/come-on.png +0 -0
- package/apps/studio/stickers/life-style/cut.png +0 -0
- package/apps/studio/stickers/life-style/detox.png +0 -0
- package/apps/studio/stickers/life-style/just.png +0 -0
- package/apps/studio/stickers/life-style/manifest.json +68 -0
- package/apps/studio/stickers/life-style/mirror.png +0 -0
- package/apps/studio/stickers/life-style/nice.png +0 -0
- package/apps/studio/stickers/life-style/objects.png +0 -0
- package/apps/studio/stickers/life-style/ok.png +0 -0
- package/apps/studio/stickers/life-style/queen.png +0 -0
- package/apps/studio/stickers/life-style/star.png +0 -0
- package/apps/studio/stickers/life-style/sun.png +0 -0
- package/apps/studio/stickers/life-style/water.png +0 -0
- package/apps/studio/stickers/life-style/yeah.png +0 -0
- package/apps/studio/stickers/life-style/you-can.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/check-the-deets.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/cut-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/dope.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/fresh.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/hot.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/idea.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/keep-exploring.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/killed-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/love-it.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/manifest.json +88 -0
- package/apps/studio/stickers/opposing-thoughts/not-sure.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/ok.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/pure-gold.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/save-for-later.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/steal-this.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/take-a-peek.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/this-or-that.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/thoughts.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/vibes.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/winner.png +0 -0
- package/apps/studio/stickers/opposing-thoughts/wip.png +0 -0
- package/apps/studio/stickers/project-status/group-100.png +0 -0
- package/apps/studio/stickers/project-status/group-101.png +0 -0
- package/apps/studio/stickers/project-status/group-102.png +0 -0
- package/apps/studio/stickers/project-status/group-103.png +0 -0
- package/apps/studio/stickers/project-status/group-104.png +0 -0
- package/apps/studio/stickers/project-status/group-105.png +0 -0
- package/apps/studio/stickers/project-status/group-106.png +0 -0
- package/apps/studio/stickers/project-status/group-107.png +0 -0
- package/apps/studio/stickers/project-status/group-108.png +0 -0
- package/apps/studio/stickers/project-status/group-109.png +0 -0
- package/apps/studio/stickers/project-status/group-110.png +0 -0
- package/apps/studio/stickers/project-status/group-111.png +0 -0
- package/apps/studio/stickers/project-status/group-112.png +0 -0
- package/apps/studio/stickers/project-status/group-113.png +0 -0
- package/apps/studio/stickers/project-status/group-114.png +0 -0
- package/apps/studio/stickers/project-status/group-115.png +0 -0
- package/apps/studio/stickers/project-status/group-117.png +0 -0
- package/apps/studio/stickers/project-status/group-118.png +0 -0
- package/apps/studio/stickers/project-status/group-119.png +0 -0
- package/apps/studio/stickers/project-status/group-120.png +0 -0
- package/apps/studio/stickers/project-status/group-121.png +0 -0
- package/apps/studio/stickers/project-status/group-122.png +0 -0
- package/apps/studio/stickers/project-status/group-41.png +0 -0
- package/apps/studio/stickers/project-status/group-42.png +0 -0
- package/apps/studio/stickers/project-status/group-43.png +0 -0
- package/apps/studio/stickers/project-status/group-44.png +0 -0
- package/apps/studio/stickers/project-status/group-45.png +0 -0
- package/apps/studio/stickers/project-status/group-46.png +0 -0
- package/apps/studio/stickers/project-status/group-47.png +0 -0
- package/apps/studio/stickers/project-status/group-48.png +0 -0
- package/apps/studio/stickers/project-status/group-49.png +0 -0
- package/apps/studio/stickers/project-status/group-50.png +0 -0
- package/apps/studio/stickers/project-status/group-51.png +0 -0
- package/apps/studio/stickers/project-status/group-52.png +0 -0
- package/apps/studio/stickers/project-status/group-53.png +0 -0
- package/apps/studio/stickers/project-status/group-54.png +0 -0
- package/apps/studio/stickers/project-status/group-55.png +0 -0
- package/apps/studio/stickers/project-status/group-56.png +0 -0
- package/apps/studio/stickers/project-status/group-57.png +0 -0
- package/apps/studio/stickers/project-status/group-58.png +0 -0
- package/apps/studio/stickers/project-status/group-59.png +0 -0
- package/apps/studio/stickers/project-status/group-60.png +0 -0
- package/apps/studio/stickers/project-status/group-61.png +0 -0
- package/apps/studio/stickers/project-status/group-62.png +0 -0
- package/apps/studio/stickers/project-status/group-63.png +0 -0
- package/apps/studio/stickers/project-status/group-64.png +0 -0
- package/apps/studio/stickers/project-status/group-65.png +0 -0
- package/apps/studio/stickers/project-status/group-66.png +0 -0
- package/apps/studio/stickers/project-status/group-67.png +0 -0
- package/apps/studio/stickers/project-status/group-68.png +0 -0
- package/apps/studio/stickers/project-status/group-69.png +0 -0
- package/apps/studio/stickers/project-status/group-70.png +0 -0
- package/apps/studio/stickers/project-status/group-71.png +0 -0
- package/apps/studio/stickers/project-status/group-72.png +0 -0
- package/apps/studio/stickers/project-status/group-73.png +0 -0
- package/apps/studio/stickers/project-status/group-74.png +0 -0
- package/apps/studio/stickers/project-status/group-75.png +0 -0
- package/apps/studio/stickers/project-status/group-76.png +0 -0
- package/apps/studio/stickers/project-status/group-77.png +0 -0
- package/apps/studio/stickers/project-status/group-78.png +0 -0
- package/apps/studio/stickers/project-status/group-79.png +0 -0
- package/apps/studio/stickers/project-status/group-80.png +0 -0
- package/apps/studio/stickers/project-status/group-81.png +0 -0
- package/apps/studio/stickers/project-status/group-82.png +0 -0
- package/apps/studio/stickers/project-status/group-83.png +0 -0
- package/apps/studio/stickers/project-status/group-84.png +0 -0
- package/apps/studio/stickers/project-status/group-85.png +0 -0
- package/apps/studio/stickers/project-status/group-86.png +0 -0
- package/apps/studio/stickers/project-status/group-87.png +0 -0
- package/apps/studio/stickers/project-status/group-88.png +0 -0
- package/apps/studio/stickers/project-status/group-89.png +0 -0
- package/apps/studio/stickers/project-status/group-90.png +0 -0
- package/apps/studio/stickers/project-status/group-91.png +0 -0
- package/apps/studio/stickers/project-status/group-92.png +0 -0
- package/apps/studio/stickers/project-status/group-93.png +0 -0
- package/apps/studio/stickers/project-status/group-94.png +0 -0
- package/apps/studio/stickers/project-status/group-96.png +0 -0
- package/apps/studio/stickers/project-status/group-97.png +0 -0
- package/apps/studio/stickers/project-status/group-98.png +0 -0
- package/apps/studio/stickers/project-status/group-99.png +0 -0
- package/apps/studio/stickers/project-status/manifest.json +328 -0
- package/apps/studio/test/acp-bootstrap-brief.test.ts +11 -0
- package/apps/studio/test/acp-commands.test.ts +2 -1
- package/apps/studio/test/asset-api.test.ts +3 -3
- package/apps/studio/test/canvas-edit.test.ts +17 -6
- package/apps/studio/test/canvas-origin-gate.test.ts +19 -0
- package/apps/studio/test/dns-rebinding-guard.test.ts +27 -0
- package/apps/studio/test/dynamic-text-edit.test.ts +162 -0
- package/apps/studio/test/edit-source-command.test.ts +23 -0
- package/apps/studio/test/element-structural-edit.test.ts +46 -0
- package/apps/studio/test/export-center.test.tsx +287 -0
- package/apps/studio/test/export-shim-multi-capture.test.ts +83 -0
- package/apps/studio/test/exporters/history.test.ts +32 -3
- package/apps/studio/test/exporters/jobs.test.ts +263 -0
- package/apps/studio/test/footage-store.test.ts +100 -0
- package/apps/studio/test/git-branches.test.ts +97 -0
- package/apps/studio/test/github-api.test.ts +56 -1
- package/apps/studio/test/input-router.test.ts +33 -0
- package/apps/studio/test/inspect-script-syntax.test.ts +53 -0
- package/apps/studio/test/photo-canvas-bundle.test.ts +61 -0
- package/apps/studio/test/photo-edit-api.test.ts +201 -0
- package/apps/studio/test/photo-filters.test.ts +149 -0
- package/apps/studio/test/photo-pipeline.test.ts +106 -0
- package/apps/studio/test/photo-taxonomy.test.ts +96 -0
- package/apps/studio/test/read-annotations.test.ts +164 -0
- package/apps/studio/test/shell-importmap.test.ts +49 -0
- package/apps/studio/test/text-editability-stamp.test.ts +131 -0
- package/apps/studio/test/timeline-parse.test.ts +57 -0
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +5 -3
- package/apps/studio/test/undo-stack.test.ts +33 -0
- package/apps/studio/test/video-asset.test.ts +5 -5
- package/apps/studio/test/video-render-bridge.test.ts +23 -1
- package/apps/studio/text-caret.ts +239 -0
- package/apps/studio/tool-palette.tsx +43 -20
- package/apps/studio/undo-stack.ts +94 -2
- package/apps/studio/use-annotation-resize.tsx +4 -4
- package/apps/studio/use-canvas-media-drop.tsx +38 -1
- package/apps/studio/use-selection-set.tsx +21 -0
- package/apps/studio/video-comp.tsx +48 -7
- package/apps/studio/whats-new.json +70 -0
- package/apps/studio/ws.ts +6 -0
- package/cli/commands/design.mjs +34 -3
- package/cli/lib/gitignore-block.mjs +2 -0
- package/package.json +10 -10
- package/plugins/design/templates/_shell.html +1 -0
|
@@ -23,17 +23,19 @@ import { ReadinessDialog } from './panels/ReadinessList.jsx';
|
|
|
23
23
|
import TimelinePanel from './panels/TimelinePanel.jsx';
|
|
24
24
|
import { parseCompTimeline } from './panels/timeline-parse.js';
|
|
25
25
|
import RepoBranchSwitcher from './panels/RepoBranchSwitcher.jsx';
|
|
26
|
+
import StickerPicker from './panels/StickerPicker.jsx';
|
|
27
|
+
import { PhotoKnobs } from './photo-knobs.jsx';
|
|
26
28
|
import {
|
|
27
29
|
appIsFirstRun,
|
|
28
30
|
isNativeApp,
|
|
29
31
|
onUpdateReady,
|
|
30
32
|
pickMediaFile,
|
|
31
33
|
restartToUpdate,
|
|
32
|
-
saveExport,
|
|
33
34
|
} from './github.js';
|
|
34
35
|
import { COLLAB_TOUR } from './tour/collab-tour.js';
|
|
35
36
|
import { TourOverlay } from './tour/overlay.jsx';
|
|
36
37
|
import { USAGE_TOUR } from './tour/usage-tour.js';
|
|
38
|
+
import { ExportBadge, ExportPanel, ExportToast, useExportCenter } from './export-center.jsx';
|
|
37
39
|
import { useWhatsNew, WhatsNewPanel, WhatsNewToast } from './whats-new.jsx';
|
|
38
40
|
|
|
39
41
|
const USAGE_TOUR_STORE = 'mdcc-usage-tour-seen';
|
|
@@ -45,6 +47,8 @@ const THEME_STORE = 'mdcc-theme';
|
|
|
45
47
|
const SHOW_HIDDEN_STORE = 'mdcc-show-hidden';
|
|
46
48
|
const SECTIONS_STORE = 'mdcc-sections-expanded';
|
|
47
49
|
const SIDEBAR_STORE = 'mdcc-sidebar-open';
|
|
50
|
+
const MINIMAP_STORE = 'mdcc-minimap-visible';
|
|
51
|
+
const ZOOMCTL_STORE = 'mdcc-zoomctl-visible';
|
|
48
52
|
const CANVAS_EXT_RE = /\.(tsx|html?)$/i;
|
|
49
53
|
// Bun's `define` substitutes this at build time (see build.ts); falls back when
|
|
50
54
|
// the bundle is consumed in a context that hasn't run the build.
|
|
@@ -1079,7 +1083,10 @@ function ExportDialog({
|
|
|
1079
1083
|
if (activeArtboardId) options.artboardId = activeArtboardId;
|
|
1080
1084
|
if (selection?.selector) options.selection = selection;
|
|
1081
1085
|
try {
|
|
1082
|
-
|
|
1086
|
+
// feature-background-export-notification-center — enqueue and close
|
|
1087
|
+
// immediately; the menubar notification center owns status, progress,
|
|
1088
|
+
// and completion (download / native Save…) from here on.
|
|
1089
|
+
const r = await fetch('/_api/export-jobs', {
|
|
1083
1090
|
method: 'POST',
|
|
1084
1091
|
headers: { 'content-type': 'application/json' },
|
|
1085
1092
|
body: JSON.stringify({ format: card.format, scope, options }),
|
|
@@ -1089,38 +1096,11 @@ function ExportDialog({
|
|
|
1089
1096
|
setBusy(false);
|
|
1090
1097
|
return;
|
|
1091
1098
|
}
|
|
1092
|
-
|
|
1093
|
-
const fn = /filename="([^"]+)"/.exec(disp);
|
|
1094
|
-
const filename = (fn && fn[1]) || `export.${card.format}`;
|
|
1095
|
-
const blob = await r.blob();
|
|
1096
|
-
if (isNativeApp()) {
|
|
1097
|
-
// Native: WKWebView swallows the `<a download>` blob (the file lands in
|
|
1098
|
-
// an unknown default with no prompt). Route through a real OS save dialog
|
|
1099
|
-
// so the export OFFERS a location. Cancel → soft no-op.
|
|
1100
|
-
const bytes = Array.from(new Uint8Array(await blob.arrayBuffer()));
|
|
1101
|
-
const savedPath = await saveExport(filename, bytes);
|
|
1102
|
-
if (savedPath) {
|
|
1103
|
-
setStatus({ ok: true, msg: `Saved to ${savedPath}` });
|
|
1104
|
-
loadRecent();
|
|
1105
|
-
} else {
|
|
1106
|
-
setStatus({ ok: true, msg: 'Save cancelled' });
|
|
1107
|
-
}
|
|
1108
|
-
} else {
|
|
1109
|
-
const url = URL.createObjectURL(blob);
|
|
1110
|
-
const a = document.createElement('a');
|
|
1111
|
-
a.href = url;
|
|
1112
|
-
a.download = filename;
|
|
1113
|
-
document.body.appendChild(a);
|
|
1114
|
-
a.click();
|
|
1115
|
-
a.remove();
|
|
1116
|
-
URL.revokeObjectURL(url);
|
|
1117
|
-
setStatus({ ok: true, msg: `Exported ${filename}` });
|
|
1118
|
-
loadRecent();
|
|
1119
|
-
}
|
|
1099
|
+
onClose();
|
|
1120
1100
|
} catch (err) {
|
|
1121
1101
|
setStatus({ ok: false, msg: err && err.message ? err.message : String(err) });
|
|
1102
|
+
setBusy(false);
|
|
1122
1103
|
}
|
|
1123
|
-
setBusy(false);
|
|
1124
1104
|
}
|
|
1125
1105
|
|
|
1126
1106
|
return (
|
|
@@ -1672,6 +1652,8 @@ function Sidebar({
|
|
|
1672
1652
|
dirtyByPath,
|
|
1673
1653
|
project,
|
|
1674
1654
|
gitBranch,
|
|
1655
|
+
remoteSync,
|
|
1656
|
+
onGetLatest,
|
|
1675
1657
|
}) {
|
|
1676
1658
|
const filteredGroups = useMemo(() => {
|
|
1677
1659
|
if (!search) return groups;
|
|
@@ -1913,7 +1895,7 @@ function Sidebar({
|
|
|
1913
1895
|
{/* Phase 29 (E4) — the project + draft switcher: a compact one-line dock that
|
|
1914
1896
|
opens UPWARD, sitting directly above the GitHub identity avatar so the two
|
|
1915
1897
|
form one bottom dock. Renders nothing until the project is a git repo. */}
|
|
1916
|
-
<RepoBranchSwitcher project={project} liveBranch={gitBranch} />
|
|
1898
|
+
<RepoBranchSwitcher project={project} liveBranch={gitBranch} remoteSync={remoteSync} onGetLatest={onGetLatest} />
|
|
1917
1899
|
{/* Phase 28 (E3) — GitHub identity as a compact avatar docked at the BOTTOM:
|
|
1918
1900
|
sign in, connected account + New/Pull/Share, sign out. Self-contained
|
|
1919
1901
|
(owns its device-code + CreateProject dialogs). Renders nothing in browser. */}
|
|
@@ -2451,12 +2433,9 @@ function ViewDropdown({ panels, onToggle, onClose, onZoom, hasCanvas }) {
|
|
|
2451
2433
|
<span className="st-dd-lead">
|
|
2452
2434
|
<span className="st-dd-check">{p.checked ? <StIcon name="check" size={13} /> : null}</span>
|
|
2453
2435
|
<span>{p.label}</span>
|
|
2436
|
+
{p.phase ? <span className="st-dd-phase">{p.phase}</span> : null}
|
|
2454
2437
|
</span>
|
|
2455
|
-
{p.
|
|
2456
|
-
<span className="st-dd-phase">{p.phase}</span>
|
|
2457
|
-
) : p.shortcut ? (
|
|
2458
|
-
<Kbd>{p.shortcut}</Kbd>
|
|
2459
|
-
) : null}
|
|
2438
|
+
{p.shortcut ? <Kbd>{p.shortcut}</Kbd> : null}
|
|
2460
2439
|
</button>
|
|
2461
2440
|
))}
|
|
2462
2441
|
<div className="st-dd-sep" />
|
|
@@ -2680,6 +2659,7 @@ function Menubar({
|
|
|
2680
2659
|
onOpenWhatsNew,
|
|
2681
2660
|
onOpenReadiness,
|
|
2682
2661
|
whatsNewCount,
|
|
2662
|
+
exportCenter,
|
|
2683
2663
|
artboardCount = 0,
|
|
2684
2664
|
presence = null,
|
|
2685
2665
|
inspectorOpen,
|
|
@@ -2763,6 +2743,7 @@ function Menubar({
|
|
|
2763
2743
|
{
|
|
2764
2744
|
id: 'timeline',
|
|
2765
2745
|
label: 'Timeline',
|
|
2746
|
+
shortcut: '⌘ ⇧ T',
|
|
2766
2747
|
phase: hasComps ? 'video' : undefined,
|
|
2767
2748
|
checked: timelineOpen,
|
|
2768
2749
|
disabled: false,
|
|
@@ -2993,6 +2974,7 @@ function Menubar({
|
|
|
2993
2974
|
<StIcon name="sparkle" size={15} />
|
|
2994
2975
|
</button>
|
|
2995
2976
|
)}
|
|
2977
|
+
{exportCenter && <ExportBadge center={exportCenter} />}
|
|
2996
2978
|
<button
|
|
2997
2979
|
type="button"
|
|
2998
2980
|
className="st-whatsnew"
|
|
@@ -3981,6 +3963,12 @@ function cssSplitUnit(v) {
|
|
|
3981
3963
|
// is for the SAME element we already hold locally, preserve those fields instead
|
|
3982
3964
|
// of clobbering them to empty (else the server round-trip wipes the custom-CSS /
|
|
3983
3965
|
// custom-attr rows + computed readout right after selection).
|
|
3966
|
+
// feature-photo-editor — `photoKind`/`photoAsset` (dom-selection.ts) are the SAME
|
|
3967
|
+
// class of client-only DOM-derived field and belong in this list for the same
|
|
3968
|
+
// reason: without it, every server-pushed `selected`/`snapshot` restore (a canvas
|
|
3969
|
+
// switch, a reconnect, the persisted `_active.json` on boot — none of which carry
|
|
3970
|
+
// these fields) silently drops the Inspector's Photo tab until the next fresh
|
|
3971
|
+
// click re-derives it live — the "tab is there, then it's gone" flicker.
|
|
3984
3972
|
function mergeSelClientFields(incoming, prev) {
|
|
3985
3973
|
if (!incoming || Array.isArray(incoming) || Array.isArray(prev) || !prev) return incoming;
|
|
3986
3974
|
if (!incoming.id || incoming.id !== prev.id) return incoming;
|
|
@@ -3990,6 +3978,8 @@ function mergeSelClientFields(incoming, prev) {
|
|
|
3990
3978
|
computed: incoming.computed ?? prev.computed,
|
|
3991
3979
|
customStyles: incoming.customStyles ?? prev.customStyles,
|
|
3992
3980
|
attrs: incoming.attrs ?? prev.attrs,
|
|
3981
|
+
photoKind: incoming.photoKind ?? prev.photoKind,
|
|
3982
|
+
photoAsset: incoming.photoAsset ?? prev.photoAsset,
|
|
3993
3983
|
};
|
|
3994
3984
|
}
|
|
3995
3985
|
|
|
@@ -6094,6 +6084,31 @@ function ArtboardKnobs({ el, onResizeArtboard }) {
|
|
|
6094
6084
|
);
|
|
6095
6085
|
}
|
|
6096
6086
|
|
|
6087
|
+
// feature-photo-editor (Task 13/14) — derive the content-addressed photo asset a
|
|
6088
|
+
// DOM selection points at (an artboard `<img src="assets/<sha8>.<ext>">`). Prefers
|
|
6089
|
+
// the `photoAsset` the resolver stamped (dom-selection.ts), falling back to a scan
|
|
6090
|
+
// of the selection's src/html for a selection that reached the panel via a code
|
|
6091
|
+
// path predating that field. Returns null for a non-photo element.
|
|
6092
|
+
const PHOTO_ASSET_RE = /assets\/[0-9a-f]{8}\.[a-z0-9]+/i;
|
|
6093
|
+
function photoAssetOfSelection(el) {
|
|
6094
|
+
if (!el || Array.isArray(el)) return null;
|
|
6095
|
+
if (el.photoAsset) return el.photoAsset;
|
|
6096
|
+
if ((el.tag || '').toLowerCase() !== 'img') return null;
|
|
6097
|
+
const html = el.html || '';
|
|
6098
|
+
// Once an edit is baked, the element's LIVE src is a `data:` URL (canvas-lib's
|
|
6099
|
+
// PhotoPreviewBridge swaps it in directly) — it never matches PHOTO_ASSET_RE,
|
|
6100
|
+
// so a REPLAYED/serialized selection (a WS resync, or the persisted
|
|
6101
|
+
// `_active.json` restored on boot) that arrives without the dedicated
|
|
6102
|
+
// `photoAsset` field would otherwise permanently lose the Photo tab for any
|
|
6103
|
+
// already-edited photo. The element still carries the `data-photo-asset` tag
|
|
6104
|
+
// the bridge stamped, and that DOES survive into an outerHTML snapshot — check
|
|
6105
|
+
// it before falling back to the plain assets/<sha8> src scan.
|
|
6106
|
+
const tagged = /data-photo-asset="([^"]+)"/.exec(html);
|
|
6107
|
+
if (tagged) return tagged[1];
|
|
6108
|
+
const m = PHOTO_ASSET_RE.exec(`${el.attrs?.src || ''} ${html}`);
|
|
6109
|
+
return m ? m[0] : null;
|
|
6110
|
+
}
|
|
6111
|
+
|
|
6097
6112
|
function InspectorPanel({
|
|
6098
6113
|
selected,
|
|
6099
6114
|
onClose,
|
|
@@ -6114,6 +6129,16 @@ function InspectorPanel({
|
|
|
6114
6129
|
onTabChange,
|
|
6115
6130
|
width,
|
|
6116
6131
|
resizing,
|
|
6132
|
+
// feature-photo-editor (Task 13/15) — the Photo tab + its live-preview /
|
|
6133
|
+
// bg-removal / undo channels. `photoSel` is the annotation-image target
|
|
6134
|
+
// (threaded up separately since the annotation model has no DOM selection);
|
|
6135
|
+
// an artboard `<img>` target is derived from `selected` instead.
|
|
6136
|
+
photoSel,
|
|
6137
|
+
photoRev,
|
|
6138
|
+
onPhotoEdit,
|
|
6139
|
+
onPhotoRemoveBackground,
|
|
6140
|
+
onPhotoRecordEdit,
|
|
6141
|
+
onPhotoUndoRedo,
|
|
6117
6142
|
}) {
|
|
6118
6143
|
// Tab is controllable from the parent (the guided tour drives it to 'css' /
|
|
6119
6144
|
// 'layers' so a spotlight step lands on a real row) but falls back to local
|
|
@@ -6412,10 +6437,41 @@ function InspectorPanel({
|
|
|
6412
6437
|
};
|
|
6413
6438
|
// `selected` may be a single element, an array (multi-select), or null.
|
|
6414
6439
|
const el = Array.isArray(selected) ? selected[0] : selected;
|
|
6440
|
+
// feature-photo-editor (Task 13) — resolve the Photo-tab target. Priority: an
|
|
6441
|
+
// annotation-image threaded up (`photoSel`, no DOM selection) → a Photo-ONLY
|
|
6442
|
+
// panel; else a content-addressed artboard `<img>` selection → Photo alongside
|
|
6443
|
+
// the normal tabs.
|
|
6444
|
+
const photoTarget = photoSel
|
|
6445
|
+
? { asset: photoSel.asset, kind: 'annotation-image' }
|
|
6446
|
+
: (() => {
|
|
6447
|
+
const a = photoAssetOfSelection(el);
|
|
6448
|
+
return a ? { asset: a, kind: 'artboard-img' } : null;
|
|
6449
|
+
})();
|
|
6450
|
+
const photoOnly = photoTarget?.kind === 'annotation-image';
|
|
6451
|
+
// Cmd+Z / Cmd+Shift+Z (or Cmd+Y) while focus is still inside a Photo-tab
|
|
6452
|
+
// slider — mirrors `onKnobKeyDown` above for the same reason: the window-
|
|
6453
|
+
// level shortcut listener bails out whenever `document.activeElement` is an
|
|
6454
|
+
// `<input>` (native text-undo would otherwise win), so a commit-then-Cmd+Z
|
|
6455
|
+
// with the slider still focused needs its own forwarder here.
|
|
6456
|
+
const onPhotoKnobKeyDown = (e) => {
|
|
6457
|
+
if (!(e.metaKey || e.ctrlKey) || e.altKey) return;
|
|
6458
|
+
const k = e.key.toLowerCase();
|
|
6459
|
+
if (k === 'z') {
|
|
6460
|
+
e.preventDefault();
|
|
6461
|
+
onPhotoUndoRedo?.(e.shiftKey ? 'redo' : 'undo');
|
|
6462
|
+
} else if (k === 'y') {
|
|
6463
|
+
e.preventDefault();
|
|
6464
|
+
onPhotoUndoRedo?.('redo');
|
|
6465
|
+
}
|
|
6466
|
+
};
|
|
6467
|
+
// An annotation-image has no element tabs; force Photo. Otherwise honor the
|
|
6468
|
+
// requested tab, but drop off a stale 'photo' tab when the new selection isn't
|
|
6469
|
+
// photo-eligible (so switching from a photo to a normal element lands sanely).
|
|
6470
|
+
const effTab = photoOnly ? 'photo' : tab === 'photo' && !photoTarget ? 'inspect' : tab;
|
|
6415
6471
|
const tabBtn = (id, label, icon) => (
|
|
6416
6472
|
<button
|
|
6417
6473
|
type="button"
|
|
6418
|
-
className={'st-rp-tab' + (
|
|
6474
|
+
className={'st-rp-tab' + (effTab === id ? ' is-active' : '')}
|
|
6419
6475
|
onClick={() => setTab(id)}
|
|
6420
6476
|
>
|
|
6421
6477
|
<StIcon name={icon} size={14} />
|
|
@@ -6431,9 +6487,16 @@ function InspectorPanel({
|
|
|
6431
6487
|
data-tour="inspector"
|
|
6432
6488
|
>
|
|
6433
6489
|
<div className="st-rp-tabs" data-tour="inspector-tabs">
|
|
6434
|
-
{
|
|
6435
|
-
|
|
6436
|
-
|
|
6490
|
+
{photoOnly ? (
|
|
6491
|
+
tabBtn('photo', 'Photo', 'image')
|
|
6492
|
+
) : (
|
|
6493
|
+
<>
|
|
6494
|
+
{tabBtn('inspect', 'Inspect', 'sliders')}
|
|
6495
|
+
{tabBtn('layers', 'Layers', 'layers')}
|
|
6496
|
+
{tabBtn('css', 'CSS', 'code')}
|
|
6497
|
+
{photoTarget ? tabBtn('photo', 'Photo', 'image') : null}
|
|
6498
|
+
</>
|
|
6499
|
+
)}
|
|
6437
6500
|
<button
|
|
6438
6501
|
type="button"
|
|
6439
6502
|
className="st-iconbtn"
|
|
@@ -6469,7 +6532,18 @@ function InspectorPanel({
|
|
|
6469
6532
|
</div>
|
|
6470
6533
|
) : null}
|
|
6471
6534
|
<div className="st-rp-body">
|
|
6472
|
-
{
|
|
6535
|
+
{effTab === 'photo' && photoTarget ? (
|
|
6536
|
+
<div onKeyDown={onPhotoKnobKeyDown}>
|
|
6537
|
+
<PhotoKnobs
|
|
6538
|
+
key={`${photoTarget.asset}:${photoRev ?? 0}`}
|
|
6539
|
+
asset={photoTarget.asset}
|
|
6540
|
+
ColorPicker={ColorPicker}
|
|
6541
|
+
onEdit={(edit) => onPhotoEdit?.(photoTarget.asset, edit)}
|
|
6542
|
+
onRemoveBackground={onPhotoRemoveBackground}
|
|
6543
|
+
onRecordEdit={(before, after) => onPhotoRecordEdit?.(photoTarget.asset, before, after)}
|
|
6544
|
+
/>
|
|
6545
|
+
</div>
|
|
6546
|
+
) : !el ? (
|
|
6473
6547
|
<div className="st-rp-empty">
|
|
6474
6548
|
{/* <p> wrapper — st-rp-empty is a flex column, bare text nodes +
|
|
6475
6549
|
kbd would stack as stretched flex items. */}
|
|
@@ -6477,7 +6551,7 @@ function InspectorPanel({
|
|
|
6477
6551
|
Hold <Kbd>⌘</Kbd> inside the canvas and click an element to inspect it.
|
|
6478
6552
|
</p>
|
|
6479
6553
|
</div>
|
|
6480
|
-
) :
|
|
6554
|
+
) : effTab === 'inspect' ? (
|
|
6481
6555
|
<>
|
|
6482
6556
|
<div className="st-rp-hd">{el.selector || el.tag || 'element'}</div>
|
|
6483
6557
|
<div className="st-insp-row">
|
|
@@ -6526,7 +6600,7 @@ function InspectorPanel({
|
|
|
6526
6600
|
) : null}
|
|
6527
6601
|
<InspectComputed el={el} />
|
|
6528
6602
|
</>
|
|
6529
|
-
) :
|
|
6603
|
+
) : effTab === 'layers' ? (
|
|
6530
6604
|
<>
|
|
6531
6605
|
<div className="st-rp-hd">Layers{layersTree?.nodes?.length ? '' : ' · ancestry'}</div>
|
|
6532
6606
|
{layersTree?.nodes?.length ? (
|
|
@@ -6877,7 +6951,9 @@ function App() {
|
|
|
6877
6951
|
const [activeComps, setActiveComps] = useState([]);
|
|
6878
6952
|
const [timelineFrame, setTimelineFrame] = useState(0);
|
|
6879
6953
|
const [timelinePlaying, setTimelinePlaying] = useState(false);
|
|
6880
|
-
|
|
6954
|
+
// Default off — looping is an explicit opt-in per session, not a surprise
|
|
6955
|
+
// once the user presses Play (rca/issue-video-artboard-loop-defaults-on).
|
|
6956
|
+
const [timelineLoop, setTimelineLoop] = useState(false);
|
|
6881
6957
|
const [timelineMuted, setTimelineMuted] = useState(false);
|
|
6882
6958
|
const [timelineVolume, setTimelineVolume] = useState(1);
|
|
6883
6959
|
const [timelineHeight, setTimelineHeight] = useState(216);
|
|
@@ -6900,6 +6976,15 @@ function App() {
|
|
|
6900
6976
|
useEffect(() => {
|
|
6901
6977
|
timelineCompIdRef.current = timelineCompId;
|
|
6902
6978
|
}, [timelineCompId]);
|
|
6979
|
+
// rca/issue-video-artboard-frame-reset-on-edit — last known playhead, read
|
|
6980
|
+
// from the `timeline-comps` handler below (which fires on every comp
|
|
6981
|
+
// (re)mount, incl. a ⌘R hard iframe reload) to re-seed the fresh Player
|
|
6982
|
+
// instance. A ref, not state, so the handler always sees the latest value
|
|
6983
|
+
// without pulling `timelineFrame` into that big message-listener's deps.
|
|
6984
|
+
const timelineFrameRef = useRef(0);
|
|
6985
|
+
useEffect(() => {
|
|
6986
|
+
timelineFrameRef.current = timelineFrame;
|
|
6987
|
+
}, [timelineFrame]);
|
|
6903
6988
|
// The DCArtboard id the timeline scoped to (from parseCompTimeline). Used for
|
|
6904
6989
|
// /_api/comp-clips + every clip op so the enumerator targets the SAME comp the
|
|
6905
6990
|
// rows came from — `timelineCompId` is the Player's `videocomp-N` id, which
|
|
@@ -6947,6 +7032,17 @@ function App() {
|
|
|
6947
7032
|
// Inspector tab is lifted so View ▸ Layers can open the panel ON the Layers
|
|
6948
7033
|
// tab (the menu item sat disabled as "Phase 12" long after the tab shipped).
|
|
6949
7034
|
const [inspectorTab, setInspectorTab] = useState('inspect');
|
|
7035
|
+
// feature-photo-editor (Task 13) — the annotation-image Photo-tab target,
|
|
7036
|
+
// threaded up from the annotation layer's "Edit Photo…" (the annotation model
|
|
7037
|
+
// has no DOM selection to ride, unlike an artboard `<img>`). `{ asset, strokeId }`
|
|
7038
|
+
// or null. Cleared whenever a normal DOM selection arrives so the two never
|
|
7039
|
+
// fight over the panel. The shell-side photo undo stack lives alongside it.
|
|
7040
|
+
const [photoSel, setPhotoSel] = useState(null);
|
|
7041
|
+
const photoUndoRef = useRef({ undo: [], redo: [] });
|
|
7042
|
+
// Bumped by the Cmd+Z/Cmd+Shift+Z photo-undo handler below so the mounted
|
|
7043
|
+
// PhotoKnobs (keyed on `asset:photoRev`) remounts and re-fetches the sidecar
|
|
7044
|
+
// it just PUT, instead of drifting from its own stale local `edit` state.
|
|
7045
|
+
const [photoRev, setPhotoRev] = useState(0);
|
|
6950
7046
|
// feature-element-editing-robustness Stage C — auto-open the Inspector on the
|
|
6951
7047
|
// CSS tab when a fresh single selection arrives AND no right panel is already
|
|
6952
7048
|
// open. Preference-backed (default ON); disable it in the View menu. Refs keep
|
|
@@ -7049,6 +7145,7 @@ function App() {
|
|
|
7049
7145
|
// toggles on its own and coexists with Inspector/Changes/Comments/Chat.
|
|
7050
7146
|
const toggleTimeline = useCallback(() => setTimelineOpen((v) => !v), []);
|
|
7051
7147
|
const whatsNew = useWhatsNew(MDCC_VERSION);
|
|
7148
|
+
const exportCenter = useExportCenter();
|
|
7052
7149
|
// Phase 29 (E4) — first-run onboarding wizard. The native shell boots a minimal
|
|
7053
7150
|
// "welcome" project on first launch; we ask it whether this is a first run and, if
|
|
7054
7151
|
// so, show the wizard OVER the (empty) canvas browser. Completing any door switches
|
|
@@ -7134,8 +7231,8 @@ function App() {
|
|
|
7134
7231
|
// persistent prefs broadcast to every open canvas iframe; presentMode is a
|
|
7135
7232
|
// non-destructive "hide ALL chrome + shell, artboards only" overlay with an
|
|
7136
7233
|
// Esc / floating-pill escape hatch back to the chrome.
|
|
7137
|
-
const [minimapVisible, setMinimapVisible] = useState(
|
|
7138
|
-
const [zoomCtlVisible, setZoomCtlVisible] = useState(
|
|
7234
|
+
const [minimapVisible, setMinimapVisible] = useState(() => readBoolStore(MINIMAP_STORE, false));
|
|
7235
|
+
const [zoomCtlVisible, setZoomCtlVisible] = useState(() => readBoolStore(ZOOMCTL_STORE, false));
|
|
7139
7236
|
const [presentMode, setPresentMode] = useState(false);
|
|
7140
7237
|
// P2/P3 (Plan C) — top-bar live state. (Zoom lives in the canvas toolbar pill,
|
|
7141
7238
|
// so the top bar no longer mirrors it.)
|
|
@@ -7166,6 +7263,96 @@ function App() {
|
|
|
7166
7263
|
[activePath]
|
|
7167
7264
|
);
|
|
7168
7265
|
|
|
7266
|
+
// ── feature-photo-editor — the Photo tab's three channels ─────────────────
|
|
7267
|
+
// (1) live preview: broadcast the edit DOWN to the active canvas iframe, whose
|
|
7268
|
+
// canvas-lib `PhotoPreviewBridge` bakes the composite and swaps it directly
|
|
7269
|
+
// into the matching `<img>`/`<image>` element's src/href (iteration 2 — see
|
|
7270
|
+
// the bridge's own header comment for why it's a direct swap, not an
|
|
7271
|
+
// overlay). (2) undo: a shell-side stack (photo edits are sidecar writes,
|
|
7272
|
+
// not the canvas-side source-edit stack). (3) background removal: the
|
|
7273
|
+
// client-side @imgly ML flow (Task 12).
|
|
7274
|
+
const onPhotoEdit = useCallback(
|
|
7275
|
+
(asset, edit) => postToActiveCanvas({ dgn: 'photo-preview', asset, edit }),
|
|
7276
|
+
[postToActiveCanvas]
|
|
7277
|
+
);
|
|
7278
|
+
const onPhotoRecordEdit = useCallback((asset, before, after) => {
|
|
7279
|
+
photoUndoRef.current.undo.push({ asset, before, after });
|
|
7280
|
+
photoUndoRef.current.redo.length = 0;
|
|
7281
|
+
}, []);
|
|
7282
|
+
// Pops the shell-side photo-undo stack, re-persists the reverted edit through
|
|
7283
|
+
// the same PUT route PhotoKnobs itself uses, re-broadcasts it to the live
|
|
7284
|
+
// preview, and bumps `photoRev` so the mounted PhotoKnobs (keyed on
|
|
7285
|
+
// `asset:photoRev`) remounts and re-fetches instead of drifting from its own
|
|
7286
|
+
// stale local state. Returns false (does nothing) when the requested stack is
|
|
7287
|
+
// empty, so callers can fall through to the canvas's own undo stack.
|
|
7288
|
+
const performPhotoUndo = useCallback(
|
|
7289
|
+
(redo) => {
|
|
7290
|
+
const stack = redo ? photoUndoRef.current.redo : photoUndoRef.current.undo;
|
|
7291
|
+
if (!stack.length) return false;
|
|
7292
|
+
const entry = stack.pop();
|
|
7293
|
+
(redo ? photoUndoRef.current.undo : photoUndoRef.current.redo).push(entry);
|
|
7294
|
+
const edit = (redo ? entry.after : entry.before) || {};
|
|
7295
|
+
onPhotoEdit(entry.asset, edit);
|
|
7296
|
+
fetch(`/_api/photo-edit?asset=${encodeURIComponent(entry.asset)}`, {
|
|
7297
|
+
method: 'PUT',
|
|
7298
|
+
headers: { 'Content-Type': 'application/json' },
|
|
7299
|
+
body: JSON.stringify(edit),
|
|
7300
|
+
}).catch(() => {});
|
|
7301
|
+
setPhotoRev((v) => v + 1);
|
|
7302
|
+
return true;
|
|
7303
|
+
},
|
|
7304
|
+
[onPhotoEdit]
|
|
7305
|
+
);
|
|
7306
|
+
// Task 12 — magic background removal, entirely client-side (WASM/WebGPU via
|
|
7307
|
+
// @imgly/background-removal — NEVER the -node native-addon variant, DDR-070).
|
|
7308
|
+
// The lib is DYNAMICALLY imported so a session that never removes a background
|
|
7309
|
+
// pays zero bundle cost (the lazy-bundle guarantee). Fetches the source bytes,
|
|
7310
|
+
// runs the matte, uploads it content-addressed through the SAME /_api/asset lane
|
|
7311
|
+
// drag-drop uses, and returns the new `assets/<sha8>.png` for PhotoEdit.
|
|
7312
|
+
const onPhotoRemoveBackground = useCallback(
|
|
7313
|
+
async (asset) => {
|
|
7314
|
+
// Scan/shimmer reveal on the live photo while the ML pass runs, the same
|
|
7315
|
+
// "something is actively happening here" language as the agent-edit
|
|
7316
|
+
// artboard rim (artboard-activity-overlay.tsx) — but applied directly to
|
|
7317
|
+
// the photo element itself (a `data-photo-busy` attribute + injected CSS
|
|
7318
|
+
// mask sweep, inspect.ts) rather than a floating tracked overlay. A
|
|
7319
|
+
// floating decoy is exactly the architecture DDR-161's addendum tore out
|
|
7320
|
+
// for the live-edit preview (z-index/resize/hit-testing bugs); an
|
|
7321
|
+
// attribute toggle on the real element sidesteps that whole class.
|
|
7322
|
+
postToActiveCanvas({ dgn: 'photo-busy', asset, busy: true });
|
|
7323
|
+
try {
|
|
7324
|
+
const srcRes = await fetch(`/${asset.replace(/^\/+/, '')}`);
|
|
7325
|
+
if (!srcRes.ok) return null;
|
|
7326
|
+
const srcBlob = await srcRes.blob();
|
|
7327
|
+
const { removeBackground } = await import('@imgly/background-removal');
|
|
7328
|
+
// Inference is 100% CLIENT-SIDE (WASM/WebGPU) — the user's pixels NEVER
|
|
7329
|
+
// leave the browser. Only the public model weights (~40 MB, identical for
|
|
7330
|
+
// everyone) are fetched from IMG.LY's default host on first use; they're
|
|
7331
|
+
// too large to bundle in the npm tarball (`resources.json` ships empty).
|
|
7332
|
+
// Self-hosting those weights off the dev server for offline / air-gapped
|
|
7333
|
+
// parity is the flagged Task-11 follow-up (a one-time download step) — until
|
|
7334
|
+
// then the default host provides them, with no pixel-privacy exposure.
|
|
7335
|
+
const matte = await removeBackground(srcBlob);
|
|
7336
|
+
const up = await fetch('/_api/asset', {
|
|
7337
|
+
method: 'POST',
|
|
7338
|
+
headers: { 'content-type': matte.type || 'image/png' },
|
|
7339
|
+
body: matte,
|
|
7340
|
+
});
|
|
7341
|
+
const j = await up.json().catch(() => ({}));
|
|
7342
|
+
if (up.ok && j.path) return { maskAsset: j.path };
|
|
7343
|
+
return null;
|
|
7344
|
+
} catch (err) {
|
|
7345
|
+
console.error('[photo] background removal failed', err);
|
|
7346
|
+
return null;
|
|
7347
|
+
} finally {
|
|
7348
|
+
// Always clears, success or failure — an errored pass must not leave
|
|
7349
|
+
// the shimmer stuck on the photo forever.
|
|
7350
|
+
postToActiveCanvas({ dgn: 'photo-busy', asset, busy: false });
|
|
7351
|
+
}
|
|
7352
|
+
},
|
|
7353
|
+
[postToActiveCanvas]
|
|
7354
|
+
);
|
|
7355
|
+
|
|
7169
7356
|
// DDR-150 dogfood #7 — a file dragged from Finder and dropped on a shell area
|
|
7170
7357
|
// with no drop target used to make the browser NAVIGATE AWAY to the file
|
|
7171
7358
|
// (file:// replaces the app). Document-level guard: always cancel the default
|
|
@@ -7554,6 +7741,16 @@ function App() {
|
|
|
7554
7741
|
localStorage.setItem(SECTIONS_STORE, JSON.stringify(sectionsExpanded));
|
|
7555
7742
|
} catch {}
|
|
7556
7743
|
}, [sectionsExpanded]);
|
|
7744
|
+
useEffect(() => {
|
|
7745
|
+
try {
|
|
7746
|
+
localStorage.setItem(MINIMAP_STORE, minimapVisible ? '1' : '0');
|
|
7747
|
+
} catch {}
|
|
7748
|
+
}, [minimapVisible]);
|
|
7749
|
+
useEffect(() => {
|
|
7750
|
+
try {
|
|
7751
|
+
localStorage.setItem(ZOOMCTL_STORE, zoomCtlVisible ? '1' : '0');
|
|
7752
|
+
} catch {}
|
|
7753
|
+
}, [zoomCtlVisible]);
|
|
7557
7754
|
|
|
7558
7755
|
const toggleSection = useCallback((label, defaultOpen) => {
|
|
7559
7756
|
setSectionsExpanded((prev) => {
|
|
@@ -7696,6 +7893,10 @@ function App() {
|
|
|
7696
7893
|
);
|
|
7697
7894
|
} catch {}
|
|
7698
7895
|
}
|
|
7896
|
+
} else if (m.type === 'export:job' && m.payload) {
|
|
7897
|
+
// feature-background-export-notification-center — full-snapshot
|
|
7898
|
+
// job state on every queued/running/progress/done/failed change.
|
|
7899
|
+
exportCenter.upsert(m.payload);
|
|
7699
7900
|
} else if (m.type === 'sync:status' && m.payload) {
|
|
7700
7901
|
// Phase 9 Task 8 — hub connection state for the offline banner.
|
|
7701
7902
|
setSyncStatus(m.payload);
|
|
@@ -7880,8 +8081,11 @@ function App() {
|
|
|
7880
8081
|
useEffect(() => {
|
|
7881
8082
|
if (gitStatus?.repo === false) return; // solo / non-git project — no remote
|
|
7882
8083
|
refreshRemoteSync();
|
|
7883
|
-
|
|
7884
|
-
|
|
8084
|
+
// Poll in the background too — not only while Changes is open — so the dock's
|
|
8085
|
+
// "Get latest" nudge surfaces a teammate's publish proactively. Slower cadence
|
|
8086
|
+
// when the panel is closed to stay network-polite (the ahead/behind probe is
|
|
8087
|
+
// TTL-cached server-side, so a missed tick is cheap to re-issue).
|
|
8088
|
+
const id = setInterval(refreshRemoteSync, changesOpen ? 60000 : 120000);
|
|
7885
8089
|
return () => clearInterval(id);
|
|
7886
8090
|
}, [gitStatus?.repo, changesOpen, refreshRemoteSync]);
|
|
7887
8091
|
|
|
@@ -8262,7 +8466,22 @@ function App() {
|
|
|
8262
8466
|
// canvas must not be able to plant a selection with no user gesture. The
|
|
8263
8467
|
// user can only click the visible active canvas, so a non-active source is
|
|
8264
8468
|
// never a legitimate selection.
|
|
8265
|
-
|
|
8469
|
+
//
|
|
8470
|
+
// The SAME gate now covers the privileged SOURCE-WRITE relays — `edit-text`
|
|
8471
|
+
// (inline text commit) and `apply-edit` (undo/redo re-application). Both
|
|
8472
|
+
// ride into a main-origin `.tsx` rewrite; a write path is strictly more
|
|
8473
|
+
// dangerous than a selection, yet was previously ungated (ethical-hacker
|
|
8474
|
+
// F-B, DDR-160 follow-up). A background/synced untrusted canvas (DDR-054)
|
|
8475
|
+
// must not drive a gestureless write into another canvas — and inline edits
|
|
8476
|
+
// + Cmd+Z always originate in the ACTIVE canvas the user is looking at, so
|
|
8477
|
+
// a non-active source is never legitimate here either.
|
|
8478
|
+
if (
|
|
8479
|
+
m.dgn === 'select' ||
|
|
8480
|
+
m.dgn === 'select-set' ||
|
|
8481
|
+
m.dgn === 'clear-select' ||
|
|
8482
|
+
m.dgn === 'edit-text' ||
|
|
8483
|
+
m.dgn === 'apply-edit'
|
|
8484
|
+
) {
|
|
8266
8485
|
const activeWin =
|
|
8267
8486
|
activePath && activePath !== SYSTEM_TAB
|
|
8268
8487
|
? iframesRef.current.get(activePath)?.contentWindow
|
|
@@ -8270,10 +8489,12 @@ function App() {
|
|
|
8270
8489
|
if (e.source !== activeWin) return;
|
|
8271
8490
|
}
|
|
8272
8491
|
if (m.dgn === 'select' && m.selection) {
|
|
8492
|
+
setPhotoSel(null); // a DOM selection supersedes an annotation-image Photo target
|
|
8273
8493
|
wsSend({ type: 'select', selection: m.selection });
|
|
8274
8494
|
setSelected(m.selection);
|
|
8275
8495
|
maybeAutoOpenInspectorOnSelect(m.selection); // Stage C
|
|
8276
8496
|
} else if (m.dgn === 'select-set') {
|
|
8497
|
+
setPhotoSel(null);
|
|
8277
8498
|
// Canvas multi-select. Payload shape:
|
|
8278
8499
|
// null → empty selection
|
|
8279
8500
|
// Selection → length-1 (back-compat with legacy single-element shape)
|
|
@@ -8297,6 +8518,7 @@ function App() {
|
|
|
8297
8518
|
maybeAutoOpenInspectorOnSelect(payload); // Stage C
|
|
8298
8519
|
}
|
|
8299
8520
|
} else if (m.dgn === 'clear-select') {
|
|
8521
|
+
setPhotoSel(null);
|
|
8300
8522
|
wsSend({ type: 'clear-select' });
|
|
8301
8523
|
setSelected(null);
|
|
8302
8524
|
} else if (m.dgn === 'edit-text' && m.id) {
|
|
@@ -8318,7 +8540,15 @@ function App() {
|
|
|
8318
8540
|
fetch('/_api/edit-text', {
|
|
8319
8541
|
method: 'POST',
|
|
8320
8542
|
headers: { 'content-type': 'application/json' },
|
|
8321
|
-
body: JSON.stringify({
|
|
8543
|
+
body: JSON.stringify({
|
|
8544
|
+
canvas: m.file,
|
|
8545
|
+
id: m.id,
|
|
8546
|
+
text: m.text ?? '',
|
|
8547
|
+
// Context for editing `{variable}` text — which rendered instance +
|
|
8548
|
+
// its pre-edit text, so the engine targets the right source string.
|
|
8549
|
+
...(typeof m.occurrence === 'number' ? { occurrence: m.occurrence } : {}),
|
|
8550
|
+
...(typeof m.before === 'string' ? { before: m.before } : {}),
|
|
8551
|
+
}),
|
|
8322
8552
|
})
|
|
8323
8553
|
.then((r) => r.json().catch(() => ({})))
|
|
8324
8554
|
.then((j) => {
|
|
@@ -8351,6 +8581,11 @@ function App() {
|
|
|
8351
8581
|
} else {
|
|
8352
8582
|
url = '/_api/edit-text';
|
|
8353
8583
|
body = { canvas: m.canvas, id: m.id, text: value ?? '' };
|
|
8584
|
+
// Undo/redo of a `{variable}` text edit needs to re-target the source
|
|
8585
|
+
// string: the occurrence + the value currently on disk (`from` — the
|
|
8586
|
+
// side we're replacing FROM). Harmless for literal text.
|
|
8587
|
+
if (typeof m.occurrence === 'number') body.occurrence = m.occurrence;
|
|
8588
|
+
if (typeof m.from === 'string') body.before = m.from;
|
|
8354
8589
|
}
|
|
8355
8590
|
editApplyChainRef.current = editApplyChainRef.current
|
|
8356
8591
|
.catch(() => {})
|
|
@@ -8527,38 +8762,54 @@ function App() {
|
|
|
8527
8762
|
const activeWin = activePath ? iframesRef.current.get(activePath)?.contentWindow : null;
|
|
8528
8763
|
if (e.source === activeWin && typeof m.id === 'string') pasteStyleRef.current?.(m.id);
|
|
8529
8764
|
} else if (m.dgn === 'insert-request') {
|
|
8530
|
-
// Stage I3 — insert a synthesized div/text/image relative to `refId
|
|
8765
|
+
// Stage I3 — insert a synthesized div/text/image relative to `refId`,
|
|
8766
|
+
// OR — empty-artboard fallback (tool-palette "+ Element" on a fresh
|
|
8767
|
+
// artboard with no elements yet) — as a child of `artboardId`. Exactly
|
|
8768
|
+
// one of the two must be present; the artboardId variant only makes
|
|
8769
|
+
// sense inside-start/inside-end (no sibling to be before/after).
|
|
8531
8770
|
const activeWin = activePath ? iframesRef.current.get(activePath)?.contentWindow : null;
|
|
8771
|
+
const hasRefId = typeof m.refId === 'string';
|
|
8772
|
+
const hasArtboardId = typeof m.artboardId === 'string';
|
|
8773
|
+
const okPosition =
|
|
8774
|
+
m.position === 'before' ||
|
|
8775
|
+
m.position === 'after' ||
|
|
8776
|
+
m.position === 'inside-start' ||
|
|
8777
|
+
m.position === 'inside-end';
|
|
8532
8778
|
const okShape =
|
|
8533
|
-
|
|
8779
|
+
hasRefId !== hasArtboardId &&
|
|
8534
8780
|
(m.kind === 'div' || m.kind === 'text' || m.kind === 'image') &&
|
|
8535
|
-
|
|
8536
|
-
|
|
8537
|
-
m.position === 'inside-start' ||
|
|
8538
|
-
m.position === 'inside-end');
|
|
8781
|
+
okPosition &&
|
|
8782
|
+
(!hasArtboardId || m.position === 'inside-start' || m.position === 'inside-end');
|
|
8539
8783
|
if (e.source === activeWin && okShape) {
|
|
8540
8784
|
insertElementShellRef.current?.(m.refId, m.position, m.kind, {
|
|
8785
|
+
artboardId: hasArtboardId ? m.artboardId : undefined,
|
|
8541
8786
|
src: typeof m.src === 'string' ? m.src : undefined,
|
|
8542
8787
|
refIndex: Number.isInteger(m.refIndex) ? m.refIndex : undefined,
|
|
8543
8788
|
});
|
|
8544
8789
|
}
|
|
8545
8790
|
} else if (m.dgn === 'insert-image-request') {
|
|
8546
|
-
// Stage F/I3 — "Insert ▸ Image" from the canvas context menu
|
|
8547
|
-
//
|
|
8548
|
-
//
|
|
8549
|
-
//
|
|
8791
|
+
// Stage F/I3 — "Insert ▸ Image" from the canvas context menu, or the
|
|
8792
|
+
// tool-palette's empty-artboard fallback (`artboardId` in place of
|
|
8793
|
+
// `refId`). An image needs a contained asset src, so the shell opens
|
|
8794
|
+
// the AssetPicker; the pick then drives insertElementShell(kind:'image').
|
|
8795
|
+
// Confused-deputy gated + pinned to the active canvas like the other
|
|
8796
|
+
// request verbs.
|
|
8550
8797
|
const activeWin = activePath ? iframesRef.current.get(activePath)?.contentWindow : null;
|
|
8798
|
+
const hasRefId = typeof m.refId === 'string';
|
|
8799
|
+
const hasArtboardId = typeof m.artboardId === 'string';
|
|
8551
8800
|
const okShape =
|
|
8552
|
-
|
|
8801
|
+
hasRefId !== hasArtboardId &&
|
|
8553
8802
|
(m.position === 'before' ||
|
|
8554
8803
|
m.position === 'after' ||
|
|
8555
8804
|
m.position === 'inside-start' ||
|
|
8556
|
-
m.position === 'inside-end')
|
|
8805
|
+
m.position === 'inside-end') &&
|
|
8806
|
+
(!hasArtboardId || m.position === 'inside-start' || m.position === 'inside-end');
|
|
8557
8807
|
if (e.source === activeWin && okShape) {
|
|
8558
8808
|
openAssetPickerRef.current?.({
|
|
8559
8809
|
purpose: 'insert-image',
|
|
8560
8810
|
canvas: activePath,
|
|
8561
8811
|
refId: m.refId,
|
|
8812
|
+
artboardId: hasArtboardId ? m.artboardId : undefined,
|
|
8562
8813
|
position: m.position,
|
|
8563
8814
|
refIndex: Number.isInteger(m.refIndex) ? m.refIndex : undefined,
|
|
8564
8815
|
});
|
|
@@ -8590,6 +8841,27 @@ function App() {
|
|
|
8590
8841
|
before: typeof m.before === 'string' ? m.before : null,
|
|
8591
8842
|
});
|
|
8592
8843
|
}
|
|
8844
|
+
} else if (m.dgn === 'edit-annotation-photo-request') {
|
|
8845
|
+
// feature-photo-editor (Task 17) — "Edit Photo…" on an annotation
|
|
8846
|
+
// ImageStroke. The annotation model has no data-cd-id / DOM selection, so
|
|
8847
|
+
// it can't ride the normal select path; instead the canvas posts its
|
|
8848
|
+
// `assets/<sha8>.<ext>` href up and the shell opens the Photo-only tab on
|
|
8849
|
+
// it. Same confused-deputy gate (DDR-054) + active-canvas pin as F3.
|
|
8850
|
+
const activeWin = activePath ? iframesRef.current.get(activePath)?.contentWindow : null;
|
|
8851
|
+
const asset = PHOTO_ASSET_RE.exec(typeof m.asset === 'string' ? m.asset : '')?.[0] || null;
|
|
8852
|
+
if (e.source === activeWin && typeof m.id === 'string' && asset) {
|
|
8853
|
+
setPhotoSel({ asset, strokeId: m.id });
|
|
8854
|
+
openRightPanel('inspector');
|
|
8855
|
+
setInspectorTab('photo');
|
|
8856
|
+
}
|
|
8857
|
+
} else if (m.dgn === 'open-sticker-picker') {
|
|
8858
|
+
// Phase 4 (whiteboard-improvements) — the toolbar's Stickers button.
|
|
8859
|
+
// Confused-deputy gated + pinned to the active canvas like the other
|
|
8860
|
+
// request verbs above.
|
|
8861
|
+
const activeWin = activePath ? iframesRef.current.get(activePath)?.contentWindow : null;
|
|
8862
|
+
if (e.source === activeWin) {
|
|
8863
|
+
openStickerPickerRef.current?.({ canvas: activePath });
|
|
8864
|
+
}
|
|
8593
8865
|
} else if (m.dgn === 'insert-artboard-request') {
|
|
8594
8866
|
// Stage I4 — insert a new empty artboard from a screen-size preset.
|
|
8595
8867
|
const activeWin = activePath ? iframesRef.current.get(activePath)?.contentWindow : null;
|
|
@@ -8626,7 +8898,12 @@ function App() {
|
|
|
8626
8898
|
}
|
|
8627
8899
|
} else if (m.dgn === 'open-inspector') {
|
|
8628
8900
|
// Phase 12 — context-menu "Inspect" / tool-palette Inspect opens the right panel.
|
|
8901
|
+
// feature-photo-editor (Task 16) — an optional `tab` lands the panel on a
|
|
8902
|
+
// specific tab (the element "Edit Photo…" entry passes `tab: 'photo'`; the
|
|
8903
|
+
// img is already selected via select-set, so the Photo tab derives its
|
|
8904
|
+
// target from that selection's `photoAsset`).
|
|
8629
8905
|
openRightPanel('inspector');
|
|
8906
|
+
if (typeof m.tab === 'string') setInspectorTab(m.tab);
|
|
8630
8907
|
} else if (m.dgn === 'present-enter') {
|
|
8631
8908
|
// Canvas tool-palette "Presentation mode" button — Present Mode is a
|
|
8632
8909
|
// shell-level state (hides the menubar / sidebar / panels), so the
|
|
@@ -8720,6 +8997,23 @@ function App() {
|
|
|
8720
8997
|
height: Math.max(1, Math.round(Number(c.height) || 0)),
|
|
8721
8998
|
}));
|
|
8722
8999
|
setActiveComps(safe);
|
|
9000
|
+
// rca/issue-video-artboard-frame-reset-on-edit — the SAME comp
|
|
9001
|
+
// re-announcing means its Player just remounted (an edit-triggered
|
|
9002
|
+
// HMR remount, or a ⌘R hard iframe reload that wiped video-comp.tsx's
|
|
9003
|
+
// own module-scope frame mirror too). The shell's `timelineFrame`
|
|
9004
|
+
// survives either way (this component doesn't remount) — push it back
|
|
9005
|
+
// down so the fresh Player opens where the Timeline left off, instead
|
|
9006
|
+
// of the poster-frame default. Skip on a genuinely new comp/canvas
|
|
9007
|
+
// (no prior id, or frame still at its post-switch 0).
|
|
9008
|
+
const prevCompId = timelineCompIdRef.current;
|
|
9009
|
+
const stillPresent = prevCompId && safe.some((c) => c.id === prevCompId);
|
|
9010
|
+
if (stillPresent && timelineFrameRef.current > 0) {
|
|
9011
|
+
postToActiveCanvas({
|
|
9012
|
+
dgn: 'timeline-seek',
|
|
9013
|
+
frame: timelineFrameRef.current,
|
|
9014
|
+
id: prevCompId,
|
|
9015
|
+
});
|
|
9016
|
+
}
|
|
8723
9017
|
} else if (m.dgn === 'timeline-frame' && typeof m.frame === 'number') {
|
|
8724
9018
|
// Live playhead mirror from the Player (preview scrub/playback).
|
|
8725
9019
|
const activeWin =
|
|
@@ -8731,6 +9025,17 @@ function App() {
|
|
|
8731
9025
|
// artboard's Player must not fight it for the readout (multi-comp canvas).
|
|
8732
9026
|
if (m.id && timelineCompIdRef.current && m.id !== timelineCompIdRef.current) return;
|
|
8733
9027
|
if (Number.isFinite(m.frame)) setTimelineFrame(Math.max(0, Math.round(m.frame)));
|
|
9028
|
+
} else if (m.dgn === 'timeline-ended') {
|
|
9029
|
+
// The Player stopped itself at the last frame (loop off) — resync the
|
|
9030
|
+
// shell's Play/Pause button, which otherwise has no way to learn
|
|
9031
|
+
// playback ended on its own (rca/issue-video-artboard-loop-defaults-on).
|
|
9032
|
+
const activeWin =
|
|
9033
|
+
activePath && activePath !== SYSTEM_TAB
|
|
9034
|
+
? iframesRef.current.get(activePath)?.contentWindow
|
|
9035
|
+
: null;
|
|
9036
|
+
if (e.source !== activeWin) return;
|
|
9037
|
+
if (m.id && timelineCompIdRef.current && m.id !== timelineCompIdRef.current) return;
|
|
9038
|
+
setTimelinePlaying(false);
|
|
8734
9039
|
} else if (m.dgn === 'toggle-palette') {
|
|
8735
9040
|
// ⌘K pressed while focus was inside the canvas iframe — the injected
|
|
8736
9041
|
// inspector forwards the chord here since the iframe's keydown never
|
|
@@ -8743,6 +9048,8 @@ function App() {
|
|
|
8743
9048
|
else if (m.id === 'inspector') toggleRightPanel('inspector');
|
|
8744
9049
|
else if (m.id === 'assistant' && isNativeApp()) toggleRightPanel('assistant');
|
|
8745
9050
|
else if (m.id === 'comments') toggleRightPanel('comments');
|
|
9051
|
+
else if (m.id === 'changes') toggleRightPanel('changes');
|
|
9052
|
+
else if (m.id === 'timeline') toggleTimeline();
|
|
8746
9053
|
else if (m.id === 'export') setExportDialog({ mode: 'export' });
|
|
8747
9054
|
else if (m.id === 'handoff') setExportDialog({ mode: 'handoff' });
|
|
8748
9055
|
} else if (m.dgn === 'open-export') {
|
|
@@ -8753,6 +9060,11 @@ function App() {
|
|
|
8753
9060
|
mode: 'export',
|
|
8754
9061
|
scope: m.detail && typeof m.detail.scope === 'string' ? m.detail.scope : undefined,
|
|
8755
9062
|
});
|
|
9063
|
+
} else if (m.dgn === 'open-timeline-request') {
|
|
9064
|
+
// Artboard-chrome context menu's "Open Timeline" (video-comp artboards
|
|
9065
|
+
// only). Scope the Timeline to the right-clicked artboard, then open it.
|
|
9066
|
+
if (typeof m.artboardId === 'string') setCanvasActiveArtboard(m.artboardId.slice(0, 120));
|
|
9067
|
+
setTimelineOpen(true);
|
|
8756
9068
|
} else if (m.dgn === 'loaded' && m.file) {
|
|
8757
9069
|
// iframe finished loading — drop the compile skeleton, push current
|
|
8758
9070
|
// comments + carry over focused pin if any
|
|
@@ -8854,7 +9166,12 @@ function App() {
|
|
|
8854
9166
|
} catch {}
|
|
8855
9167
|
};
|
|
8856
9168
|
try {
|
|
8857
|
-
|
|
9169
|
+
// feature-background-export-notification-center — enqueue and reply
|
|
9170
|
+
// with the job id immediately; the notification center (which
|
|
9171
|
+
// already owns the WS connection) is the single place status/
|
|
9172
|
+
// progress/completion live from here, regardless of which dialog
|
|
9173
|
+
// created the job.
|
|
9174
|
+
const r = await fetch('/_api/export-jobs', {
|
|
8858
9175
|
method: 'POST',
|
|
8859
9176
|
headers: { 'content-type': 'application/json' },
|
|
8860
9177
|
body: JSON.stringify(payload),
|
|
@@ -8863,28 +9180,8 @@ function App() {
|
|
|
8863
9180
|
reply({ ok: false, error: (await r.text()) || String(r.status) });
|
|
8864
9181
|
return;
|
|
8865
9182
|
}
|
|
8866
|
-
const
|
|
8867
|
-
|
|
8868
|
-
const filename = (fn && fn[1]) || 'export';
|
|
8869
|
-
const blob = await r.blob();
|
|
8870
|
-
if (isNativeApp()) {
|
|
8871
|
-
// Native: route the bridged in-canvas export through the OS save dialog
|
|
8872
|
-
// too (WKWebView swallows the `<a download>` blob). Mirror the shell
|
|
8873
|
-
// ExportDialog's doExport native branch.
|
|
8874
|
-
const bytes = Array.from(new Uint8Array(await blob.arrayBuffer()));
|
|
8875
|
-
const savedPath = await saveExport(filename, bytes);
|
|
8876
|
-
reply(savedPath ? { ok: true, filename } : { ok: false, error: 'Save cancelled' });
|
|
8877
|
-
return;
|
|
8878
|
-
}
|
|
8879
|
-
const url = URL.createObjectURL(blob);
|
|
8880
|
-
const a = document.createElement('a');
|
|
8881
|
-
a.href = url;
|
|
8882
|
-
a.download = filename;
|
|
8883
|
-
document.body.appendChild(a);
|
|
8884
|
-
a.click();
|
|
8885
|
-
a.remove();
|
|
8886
|
-
URL.revokeObjectURL(url);
|
|
8887
|
-
reply({ ok: true, filename });
|
|
9183
|
+
const { jobId } = await r.json();
|
|
9184
|
+
reply({ ok: true, jobId });
|
|
8888
9185
|
} catch (err) {
|
|
8889
9186
|
reply({ ok: false, error: err && err.message ? err.message : String(err) });
|
|
8890
9187
|
}
|
|
@@ -8917,6 +9214,7 @@ function App() {
|
|
|
8917
9214
|
zoomCtlVisible,
|
|
8918
9215
|
broadcastChrome,
|
|
8919
9216
|
activePath,
|
|
9217
|
+
postToActiveCanvas,
|
|
8920
9218
|
]);
|
|
8921
9219
|
|
|
8922
9220
|
// Tell the active canvas iframe to drop any persistent selection (canvas
|
|
@@ -9301,6 +9599,7 @@ function App() {
|
|
|
9301
9599
|
'/_api/insert-element',
|
|
9302
9600
|
{
|
|
9303
9601
|
refId,
|
|
9602
|
+
artboardId: typeof opts.artboardId === 'string' ? opts.artboardId : undefined,
|
|
9304
9603
|
position,
|
|
9305
9604
|
kind,
|
|
9306
9605
|
src: typeof opts.src === 'string' ? opts.src : undefined,
|
|
@@ -9506,8 +9805,11 @@ function App() {
|
|
|
9506
9805
|
t.tagName === 'SELECT' ||
|
|
9507
9806
|
t.isContentEditable);
|
|
9508
9807
|
if (editable) return;
|
|
9509
|
-
|
|
9808
|
+
// Suppress the WKWebView back-nav unconditionally once focus isn't editable —
|
|
9809
|
+
// the activePath/SYSTEM_TAB check below is app logic, not default-action gating,
|
|
9810
|
+
// so it must not gate preventDefault (that gap caused the desktop "Starting…" hang).
|
|
9510
9811
|
e.preventDefault();
|
|
9812
|
+
if (!activePath || activePath === SYSTEM_TAB) return;
|
|
9511
9813
|
const one = Array.isArray(selected) ? (selected.length === 1 ? selected[0] : null) : selected;
|
|
9512
9814
|
if (one?.artboardId && !one.id) deleteArtboardShell(one.artboardId);
|
|
9513
9815
|
};
|
|
@@ -9523,6 +9825,16 @@ function App() {
|
|
|
9523
9825
|
useEffect(() => {
|
|
9524
9826
|
openAssetPickerRef.current = (req) => setAssetPickerReq(req);
|
|
9525
9827
|
}, []);
|
|
9828
|
+
|
|
9829
|
+
// Phase 4 (whiteboard-improvements) — { canvas: activePath } when open, null
|
|
9830
|
+
// when closed. Mirrors assetPickerReq's ref-indirection (the message
|
|
9831
|
+
// listener below is set up with a minimal dep array, so it reaches a FRESH
|
|
9832
|
+
// opener via a ref rather than needing setStickerPickerReq in its own deps).
|
|
9833
|
+
const [stickerPickerReq, setStickerPickerReq] = useState(null);
|
|
9834
|
+
const openStickerPickerRef = useRef(null);
|
|
9835
|
+
useEffect(() => {
|
|
9836
|
+
openStickerPickerRef.current = (req) => setStickerPickerReq(req);
|
|
9837
|
+
}, []);
|
|
9526
9838
|
const onAssetPicked = useCallback(
|
|
9527
9839
|
(pickedPath) => {
|
|
9528
9840
|
const req = assetPickerReq;
|
|
@@ -9538,6 +9850,7 @@ function App() {
|
|
|
9538
9850
|
}
|
|
9539
9851
|
if (req.purpose === 'insert-image') {
|
|
9540
9852
|
insertElementShell(req.refId, req.position || 'after', 'image', {
|
|
9853
|
+
artboardId: req.artboardId,
|
|
9541
9854
|
src: pickedPath,
|
|
9542
9855
|
refIndex: req.refIndex,
|
|
9543
9856
|
});
|
|
@@ -9583,6 +9896,47 @@ function App() {
|
|
|
9583
9896
|
},
|
|
9584
9897
|
[assetPickerReq, insertElementShell, activePath, recordSourceEdit, postToActiveCanvas]
|
|
9585
9898
|
);
|
|
9899
|
+
|
|
9900
|
+
// Phase 4 (whiteboard-improvements) — a bundled sticker has no project asset
|
|
9901
|
+
// path yet (it lives in MAUDE's own STICKERS_DIR, main-origin-only per
|
|
9902
|
+
// DDR-054), so re-upload its bytes through the SAME /_api/asset lane every
|
|
9903
|
+
// other image source uses (content-addressed, canvas-origin-allowlisted) —
|
|
9904
|
+
// then relay the resulting project-relative path down to the canvas exactly
|
|
9905
|
+
// like replace-annotation-media above (the annotation model owns its own
|
|
9906
|
+
// strokes; the shell performs no stroke write of its own).
|
|
9907
|
+
const onStickerPicked = useCallback(
|
|
9908
|
+
async (sticker) => {
|
|
9909
|
+
const req = stickerPickerReq;
|
|
9910
|
+
setStickerPickerReq(null);
|
|
9911
|
+
if (!req || !sticker?.url) return;
|
|
9912
|
+
// G3 security (DDR-152) — same re-check as onAssetPicked: the request
|
|
9913
|
+
// captured the active canvas when the picker opened; abort rather than
|
|
9914
|
+
// insert into whatever canvas happens to be active now.
|
|
9915
|
+
if (req.canvas && req.canvas !== activePath) {
|
|
9916
|
+
console.warn('[sticker-picker] active canvas changed since request — aborting');
|
|
9917
|
+
return;
|
|
9918
|
+
}
|
|
9919
|
+
try {
|
|
9920
|
+
const blob = await fetch(sticker.url).then((r) =>
|
|
9921
|
+
r.ok ? r.blob() : Promise.reject(new Error(`HTTP ${r.status}`))
|
|
9922
|
+
);
|
|
9923
|
+
const res = await fetch('/_api/asset', {
|
|
9924
|
+
method: 'POST',
|
|
9925
|
+
headers: { 'content-type': blob.type || 'image/png' },
|
|
9926
|
+
body: blob,
|
|
9927
|
+
});
|
|
9928
|
+
const j = await res.json().catch(() => ({}));
|
|
9929
|
+
if (!res.ok || !j.path) {
|
|
9930
|
+
console.warn('[sticker-picker]', j.error || `upload failed (HTTP ${res.status})`);
|
|
9931
|
+
return;
|
|
9932
|
+
}
|
|
9933
|
+
postToActiveCanvas({ dgn: 'insert-sticker', path: j.path });
|
|
9934
|
+
} catch {
|
|
9935
|
+
console.warn('[sticker-picker] could not load or upload that sticker');
|
|
9936
|
+
}
|
|
9937
|
+
},
|
|
9938
|
+
[stickerPickerReq, activePath, postToActiveCanvas]
|
|
9939
|
+
);
|
|
9586
9940
|
// Media-section "Replace…" (CssKnobs) → open the picker in replace mode with
|
|
9587
9941
|
// the element's current src as the undo before-value (captured from the
|
|
9588
9942
|
// Selection's `attrs.src`, not the resolved URL).
|
|
@@ -9686,8 +10040,19 @@ function App() {
|
|
|
9686
10040
|
) {
|
|
9687
10041
|
return; // the timeline shortcuts effect claims it
|
|
9688
10042
|
}
|
|
9689
|
-
e.preventDefault();
|
|
9690
10043
|
const redo = e.key === 'y' || e.key === 'Y' || e.shiftKey;
|
|
10044
|
+
// feature-photo-editor — photo edits are sidecar writes (`/_api/photo-edit`),
|
|
10045
|
+
// invisible to the canvas's own source-edit undo stack, so they need
|
|
10046
|
+
// their own owner here. Claims the keypress only while the Photo tab
|
|
10047
|
+
// is the one on screen AND it actually has something to undo/redo —
|
|
10048
|
+
// otherwise falls through to the canvas stack as before. (Focus still
|
|
10049
|
+
// inside a Photo-tab slider is handled separately by
|
|
10050
|
+
// `onPhotoKnobKeyDown`, mirroring `onKnobKeyDown` for CSS knobs.)
|
|
10051
|
+
if (inspectorTab === 'photo' && performPhotoUndo(redo)) {
|
|
10052
|
+
e.preventDefault();
|
|
10053
|
+
return;
|
|
10054
|
+
}
|
|
10055
|
+
e.preventDefault();
|
|
9691
10056
|
postToActiveCanvas({ dgn: redo ? 'redo' : 'undo' });
|
|
9692
10057
|
return;
|
|
9693
10058
|
}
|
|
@@ -9745,6 +10110,12 @@ function App() {
|
|
|
9745
10110
|
setExportDialog({ mode: 'handoff' });
|
|
9746
10111
|
return;
|
|
9747
10112
|
}
|
|
10113
|
+
// Cmd+Shift+T — toggle the Timeline (video-comp scrub) dock.
|
|
10114
|
+
if (meta && e.shiftKey && (e.key === 't' || e.key === 'T')) {
|
|
10115
|
+
e.preventDefault();
|
|
10116
|
+
toggleTimeline();
|
|
10117
|
+
return;
|
|
10118
|
+
}
|
|
9748
10119
|
// Cmd+C / Ctrl+C — Phase 4.1 removed the shell-side comment-drop chord.
|
|
9749
10120
|
// Canvas comment-drop is the `C` tool letter (press C in the canvas,
|
|
9750
10121
|
// then click the element) or right-click "Add comment". Cmd+C now
|
|
@@ -9860,6 +10231,9 @@ function App() {
|
|
|
9860
10231
|
clearActiveCanvasSelection,
|
|
9861
10232
|
presentMode,
|
|
9862
10233
|
exitPresent,
|
|
10234
|
+
toggleTimeline,
|
|
10235
|
+
inspectorTab,
|
|
10236
|
+
performPhotoUndo,
|
|
9863
10237
|
]);
|
|
9864
10238
|
|
|
9865
10239
|
const registerIframe = useCallback((path, el) => {
|
|
@@ -10011,6 +10385,7 @@ function App() {
|
|
|
10011
10385
|
<UpdateBanner update={updateReady} onDismiss={() => setUpdateReady(null)} />
|
|
10012
10386
|
<SyncBanner status={syncStatus} />
|
|
10013
10387
|
{!usageNudge && !tourSteps && <WhatsNewToast wn={whatsNew} />}
|
|
10388
|
+
{!usageNudge && !tourSteps && <ExportToast center={exportCenter} />}
|
|
10014
10389
|
{gitLifecycle && (
|
|
10015
10390
|
<div role="status" aria-live="polite" className="st-banner st-banner--info">
|
|
10016
10391
|
<span className="st-banner-dot" aria-hidden="true" />
|
|
@@ -10064,6 +10439,7 @@ function App() {
|
|
|
10064
10439
|
onOpenReadiness={() => setReadinessOpen(true)}
|
|
10065
10440
|
onOpenWhatsNew={whatsNew.openPanel}
|
|
10066
10441
|
whatsNewCount={whatsNew.unseen.length}
|
|
10442
|
+
exportCenter={exportCenter}
|
|
10067
10443
|
artboardCount={activeArtboards}
|
|
10068
10444
|
inspectorOpen={inspectorOpen}
|
|
10069
10445
|
inspectorTab={inspectorTab}
|
|
@@ -10150,6 +10526,8 @@ function App() {
|
|
|
10150
10526
|
dirtyByPath={dirtyByPath}
|
|
10151
10527
|
project={project}
|
|
10152
10528
|
gitBranch={gitStatus?.branch}
|
|
10529
|
+
remoteSync={remoteSync}
|
|
10530
|
+
onGetLatest={gitGetLatest}
|
|
10153
10531
|
/>
|
|
10154
10532
|
{sidebarOpen && (
|
|
10155
10533
|
<PanelGrip
|
|
@@ -10232,6 +10610,12 @@ function App() {
|
|
|
10232
10610
|
onResizeArtboard={resizeArtboardShell}
|
|
10233
10611
|
editScope={editScope}
|
|
10234
10612
|
onUndoRedo={(dir) => postToActiveCanvas({ dgn: dir })}
|
|
10613
|
+
photoSel={photoSel}
|
|
10614
|
+
photoRev={photoRev}
|
|
10615
|
+
onPhotoEdit={onPhotoEdit}
|
|
10616
|
+
onPhotoRemoveBackground={onPhotoRemoveBackground}
|
|
10617
|
+
onPhotoRecordEdit={onPhotoRecordEdit}
|
|
10618
|
+
onPhotoUndoRedo={(dir) => performPhotoUndo(dir === 'redo')}
|
|
10235
10619
|
layersTree={layersTree}
|
|
10236
10620
|
canvasFile={activePath}
|
|
10237
10621
|
onSelectLayer={(n) =>
|
|
@@ -10695,6 +11079,9 @@ function App() {
|
|
|
10695
11079
|
onClose={() => setAssetPickerReq(null)}
|
|
10696
11080
|
/>
|
|
10697
11081
|
)}
|
|
11082
|
+
{stickerPickerReq && (
|
|
11083
|
+
<StickerPicker onPick={onStickerPicked} onClose={() => setStickerPickerReq(null)} />
|
|
11084
|
+
)}
|
|
10698
11085
|
{diffTarget && (
|
|
10699
11086
|
<DiffView
|
|
10700
11087
|
target={diffTarget}
|
|
@@ -10730,6 +11117,7 @@ function App() {
|
|
|
10730
11117
|
}}
|
|
10731
11118
|
/>
|
|
10732
11119
|
<WhatsNewPanel wn={whatsNew} onStartTour={startTour} />
|
|
11120
|
+
<ExportPanel center={exportCenter} />
|
|
10733
11121
|
<ReadinessDialog open={readinessOpen} onClose={() => setReadinessOpen(false)} />
|
|
10734
11122
|
{usageNudge && !tourSteps && !collabNudge && (
|
|
10735
11123
|
<div className="mdcc-tour-nudge" role="status" aria-live="polite">
|