@1agh/maude 0.42.0 → 0.44.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/acp/bridge.ts +44 -10
- package/apps/studio/acp/env.ts +23 -0
- package/apps/studio/acp/login-state.ts +263 -0
- package/apps/studio/acp/plugin-bootstrap.ts +31 -25
- package/apps/studio/acp/probe.ts +152 -1
- package/apps/studio/annotations-layer.tsx +1451 -324
- package/apps/studio/annotations-model.ts +58 -0
- package/apps/studio/api.ts +568 -24
- package/apps/studio/bin/_audio-search.mjs +140 -0
- package/apps/studio/bin/_import-asset-pdf-worker.mjs +34 -0
- package/apps/studio/bin/_import-asset.mjs +815 -0
- package/apps/studio/bin/_import-brand.mjs +635 -0
- package/apps/studio/bin/_import-tokens-alias-resolver.mjs +95 -0
- package/apps/studio/bin/_import-tokens.mjs +1201 -0
- package/apps/studio/bin/_ingest-footage.mjs +386 -0
- package/apps/studio/bin/_probe-footage-playwright.mjs +269 -0
- package/apps/studio/bin/_transcribe.mjs +419 -0
- package/apps/studio/bin/_transcribe.test.mjs +80 -0
- package/apps/studio/bin/_video-playwright.mjs +141 -71
- package/apps/studio/bin/audio-search.sh +28 -0
- package/apps/studio/bin/generate.sh +154 -0
- package/apps/studio/bin/import-asset.sh +34 -0
- package/apps/studio/bin/import-brand.sh +29 -0
- package/apps/studio/bin/import-tokens.sh +33 -0
- 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 +258 -0
- package/apps/studio/bin/probe-footage.sh +28 -0
- package/apps/studio/bin/read-annotations.mjs +125 -2
- package/apps/studio/bin/transcribe.sh +39 -0
- package/apps/studio/canvas-edit.ts +656 -12
- package/apps/studio/canvas-lib.tsx +637 -10
- package/apps/studio/canvas-pipeline.ts +65 -7
- package/apps/studio/canvas-shell.tsx +156 -16
- package/apps/studio/client/app.jsx +2385 -699
- package/apps/studio/client/export-center.jsx +35 -1
- package/apps/studio/client/generate-dialog.jsx +352 -0
- package/apps/studio/client/github.js +18 -0
- package/apps/studio/client/inspector-controls.jsx +781 -0
- package/apps/studio/client/panels/BrandUploadPanel.jsx +233 -0
- package/apps/studio/client/panels/ChatPanel.jsx +15 -33
- package/apps/studio/client/panels/GitPanel.jsx +8 -0
- package/apps/studio/client/panels/IntroVideoDialog.jsx +44 -0
- package/apps/studio/client/panels/OnboardingWizard.jsx +12 -0
- package/apps/studio/client/panels/ReadinessList.jsx +227 -7
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +133 -21
- package/apps/studio/client/panels/SettingsPanel.jsx +828 -0
- package/apps/studio/client/panels/SetupChecklist.jsx +223 -0
- 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 +432 -0
- package/apps/studio/client/styles/3-shell-maude.css +376 -28
- package/apps/studio/client/styles/4-components.css +223 -0
- package/apps/studio/client/styles/6-acp-chat.css +86 -0
- package/apps/studio/client/tour/quick-setup-tour.js +39 -0
- package/apps/studio/commands/annotation-strokes-command.ts +13 -3
- package/apps/studio/commands/edit-source-command.ts +31 -4
- package/apps/studio/config.schema.json +70 -0
- package/apps/studio/context-menu.tsx +42 -8
- package/apps/studio/context.ts +16 -0
- package/apps/studio/design-setup-readiness.ts +115 -0
- package/apps/studio/dist/client.bundle.js +5572 -21
- 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/draw/palette.ts +34 -0
- package/apps/studio/exporters/jobs.ts +47 -5
- package/apps/studio/exporters/video.ts +24 -4
- package/apps/studio/footage/schema.test.ts +287 -0
- package/apps/studio/footage/schema.ts +662 -0
- package/apps/studio/footage-store.ts +235 -0
- package/apps/studio/fs-watch.ts +10 -0
- package/apps/studio/generation/adapters/elevenlabs.ts +370 -0
- package/apps/studio/generation/adapters/gemini.ts +542 -0
- package/apps/studio/generation/adapters/groq.test.ts +80 -0
- package/apps/studio/generation/adapters/groq.ts +247 -0
- package/apps/studio/generation/audio-library.test.ts +95 -0
- package/apps/studio/generation/audio-library.ts +156 -0
- package/apps/studio/generation/captions.test.ts +78 -0
- package/apps/studio/generation/captions.ts +132 -0
- package/apps/studio/generation/download.ts +76 -0
- package/apps/studio/generation/elevenlabs.test.ts +223 -0
- package/apps/studio/generation/gemini.test.ts +350 -0
- package/apps/studio/generation/jobs.test.ts +128 -0
- package/apps/studio/generation/jobs.ts +243 -0
- package/apps/studio/generation/keys.test.ts +75 -0
- package/apps/studio/generation/keys.ts +162 -0
- package/apps/studio/generation/prefs.test.ts +95 -0
- package/apps/studio/generation/prefs.ts +82 -0
- package/apps/studio/generation/registry.ts +88 -0
- package/apps/studio/generation/types.test.ts +123 -0
- package/apps/studio/generation/types.ts +266 -0
- package/apps/studio/generation/whisper-models.test.ts +152 -0
- package/apps/studio/generation/whisper-models.ts +257 -0
- package/apps/studio/generation/whisper-spike-results.md +61 -0
- package/apps/studio/git/endpoints.ts +68 -7
- package/apps/studio/git/service.ts +199 -21
- package/apps/studio/github/service.ts +67 -0
- package/apps/studio/handoff.ts +3 -2
- package/apps/studio/http.ts +1039 -22
- package/apps/studio/input-router.tsx +10 -0
- package/apps/studio/inspect.ts +53 -1
- package/apps/studio/media/intro.mp4 +0 -0
- package/apps/studio/media-commit-chain.ts +119 -0
- package/apps/studio/paths.ts +20 -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/readiness.ts +108 -20
- package/apps/studio/runtime-bundle.ts +29 -6
- package/apps/studio/scaffold-design.ts +1145 -0
- package/apps/studio/server.ts +18 -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/_helpers.ts +15 -2
- package/apps/studio/test/acp-bootstrap-brief.test.ts +11 -0
- package/apps/studio/test/acp-bridge.test.ts +34 -1
- package/apps/studio/test/acp-commands.test.ts +2 -1
- package/apps/studio/test/acp-env.test.ts +30 -0
- package/apps/studio/test/acp-plugin-bootstrap.test.ts +23 -57
- package/apps/studio/test/acp-session-plugins.test.ts +72 -1
- package/apps/studio/test/annotation-strokes-command.test.ts +17 -0
- package/apps/studio/test/annotations-layer.test.ts +174 -0
- 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 +50 -0
- package/apps/studio/test/csp-canvas-shell.test.ts +11 -0
- package/apps/studio/test/csrf-write-guard.test.ts +23 -1
- package/apps/studio/test/design-setup-readiness.test.ts +155 -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 +154 -0
- package/apps/studio/test/fixtures/fake-claude-auth.mjs +13 -0
- package/apps/studio/test/footage-store.test.ts +100 -0
- package/apps/studio/test/generate-route.test.ts +106 -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/import-asset-browser.test.ts +64 -0
- package/apps/studio/test/import-asset-route.test.ts +71 -0
- package/apps/studio/test/import-asset.test.ts +399 -0
- package/apps/studio/test/import-brand-browser.test.ts +88 -0
- package/apps/studio/test/import-brand-route.test.ts +120 -0
- package/apps/studio/test/import-brand.test.ts +206 -0
- package/apps/studio/test/import-tokens.test.ts +722 -0
- 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/media-commit-chain.test.ts +210 -0
- package/apps/studio/test/photo-bg-remove-validation.test.ts +71 -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/readiness.test.ts +13 -11
- package/apps/studio/test/scaffold-design.test.ts +122 -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/tour-overlay.test.tsx +16 -0
- 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 +49 -21
- package/apps/studio/ui-prefs.ts +130 -0
- 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 +40 -1
- package/apps/studio/use-chrome-visibility.tsx +8 -2
- package/apps/studio/use-selection-set.tsx +21 -0
- package/apps/studio/video-comp.tsx +48 -7
- package/apps/studio/whats-new.json +149 -0
- package/apps/studio/ws.ts +5 -0
- package/cli/bin/maude.mjs +6 -6
- package/cli/commands/design.mjs +67 -3
- package/cli/lib/gitignore-block.mjs +2 -0
- package/cli/lib/pkg-root.mjs +107 -0
- package/cli/lib/pkg-root.test.mjs +79 -0
- package/cli/lib/reconstruct-toolset.test.mjs +194 -0
- package/cli/lib/update-check.mjs +8 -0
- package/package.json +13 -11
- package/plugins/design/dependencies.json +18 -0
- package/plugins/design/templates/_shell.html +1 -0
|
@@ -87,6 +87,7 @@ import {
|
|
|
87
87
|
createContext,
|
|
88
88
|
Fragment,
|
|
89
89
|
isValidElement,
|
|
90
|
+
type MouseEvent as ReactMouseEvent,
|
|
90
91
|
type ReactNode,
|
|
91
92
|
type PointerEvent as ReactPointerEvent,
|
|
92
93
|
type RefObject,
|
|
@@ -106,6 +107,11 @@ import {
|
|
|
106
107
|
diffLayoutPositions,
|
|
107
108
|
} from './commands/move-artboards-command.ts';
|
|
108
109
|
import { scopedCdSelector, selectorIndex, shortText } from './dom-selection.ts';
|
|
110
|
+
// Photo editor (feature-photo-editor) — schema.ts is DEPENDENCY-FREE (no pixi),
|
|
111
|
+
// so this static import is safe and adds zero bundle cost. The WebGL compositor
|
|
112
|
+
// (photo/pipeline.ts, which imports pixi.js) is loaded via a DYNAMIC import
|
|
113
|
+
// inside <PhotoLayer> only when an edited photo actually mounts — see there.
|
|
114
|
+
import { isDefaultEdit, type PhotoEdit } from './photo/schema.ts';
|
|
109
115
|
import { AgentPresenceProvider, useAgentPresence } from './use-agent-presence.tsx';
|
|
110
116
|
import { type DragState, useArtboardDrag } from './use-artboard-drag.tsx';
|
|
111
117
|
import {
|
|
@@ -118,13 +124,14 @@ import { CollabProvider, canvasSlugFromPath } from './use-collab.tsx';
|
|
|
118
124
|
import { useSelectionSetOptional } from './use-selection-set.tsx';
|
|
119
125
|
import { MaybeToolProvider, useToolModeOptional } from './use-tool-mode.tsx';
|
|
120
126
|
import { UndoStackProvider, useUndoSinks, useUndoStackOptional } from './use-undo-stack.tsx';
|
|
127
|
+
// DDR-148 — video-comp canvas kind. Imported (not just re-exported below) so
|
|
128
|
+
// DCArtboard can identity-match <VideoComp> for the header badge. Its
|
|
129
|
+
// `remotion`/`@remotion/player` imports resolve through the canvas importmap
|
|
130
|
+
// (RUNTIME_PACKAGES), same as react/motion.
|
|
131
|
+
import { VideoComp } from './video-comp.tsx';
|
|
121
132
|
|
|
122
133
|
export type { CompSnapshot, VideoCompMeta, VideoCompProps } from './video-comp.tsx';
|
|
123
|
-
|
|
124
|
-
// <VideoComp> (Remotion composition mounted in <Player> + the deterministic
|
|
125
|
-
// seek bridge). Its `remotion`/`@remotion/player` imports resolve through the
|
|
126
|
-
// canvas importmap (RUNTIME_PACKAGES), same as react/motion.
|
|
127
|
-
export { VideoComp } from './video-comp.tsx';
|
|
134
|
+
export { VideoComp };
|
|
128
135
|
|
|
129
136
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
130
137
|
// Module constants
|
|
@@ -320,6 +327,28 @@ button.dc-artboard-label {
|
|
|
320
327
|
width: 100%;
|
|
321
328
|
}
|
|
322
329
|
button.dc-artboard-label:focus-visible { outline: 2px solid var(--maude-hud-accent, #d63b1f); outline-offset: -2px; }
|
|
330
|
+
/* DDR-148 — video-artboard badge, overlaid top-right of the header. Opens
|
|
331
|
+
the timeline panel (same postMessage the context-menu entry sends). */
|
|
332
|
+
.dc-canvas .dc-artboard-video-badge {
|
|
333
|
+
position: absolute;
|
|
334
|
+
top: 4px;
|
|
335
|
+
right: 6px;
|
|
336
|
+
z-index: 2;
|
|
337
|
+
display: inline-flex;
|
|
338
|
+
align-items: center;
|
|
339
|
+
justify-content: center;
|
|
340
|
+
width: 18px;
|
|
341
|
+
height: 18px;
|
|
342
|
+
padding: 0;
|
|
343
|
+
appearance: none;
|
|
344
|
+
border: none;
|
|
345
|
+
border-radius: 3px;
|
|
346
|
+
background: var(--bg-2, #e8e3d8);
|
|
347
|
+
color: var(--fg-1, #4a3f30);
|
|
348
|
+
cursor: pointer;
|
|
349
|
+
}
|
|
350
|
+
.dc-canvas .dc-artboard-video-badge:hover { background: color-mix(in oklab, var(--fg-0, #2a2520) 12%, var(--bg-2, #e8e3d8)); }
|
|
351
|
+
.dc-canvas .dc-artboard-video-badge:focus-visible { outline: 2px solid var(--maude-hud-accent, #d63b1f); outline-offset: 1px; }
|
|
323
352
|
/* Active-artboard ring is in canvas-shell HALO_CSS (subtle 1 px tint). */
|
|
324
353
|
/* Phase 4.2 — drag chrome. */
|
|
325
354
|
.dc-canvas[data-active-tool="move"] .dc-artboard-label { cursor: grab; }
|
|
@@ -369,6 +398,11 @@ export interface WorldContextValue {
|
|
|
369
398
|
rectFor: (id: string) => ArtboardRect | null;
|
|
370
399
|
/** All artboards in render order, with resolved world-coord positions. */
|
|
371
400
|
artboards: ArtboardRect[];
|
|
401
|
+
/** Hug-mode artboards report their measured DOM height here so the shared
|
|
402
|
+
* ArtboardRect model (drag hit-test, marquee, fit-to-screen, culling) stays
|
|
403
|
+
* in sync with content that grows/shrinks. Runtime-only — never persisted
|
|
404
|
+
* to meta; a fixed-mode board never calls this. */
|
|
405
|
+
reportMeasuredHeight: (id: string, h: number) => void;
|
|
372
406
|
/** Current pan/zoom state. `null` until the first useLayoutEffect runs. */
|
|
373
407
|
viewport: ViewportState | null;
|
|
374
408
|
/** id of the artboard closest to the viewport center (recomputed on settle). */
|
|
@@ -451,6 +485,29 @@ function harvestArtboards(children: ReactNode): ArtboardSeed[] {
|
|
|
451
485
|
return out;
|
|
452
486
|
}
|
|
453
487
|
|
|
488
|
+
/**
|
|
489
|
+
* Does this artboard's subtree contain a <VideoComp>? Same identity-match +
|
|
490
|
+
* displayName-fallback walk as harvestArtboards, short-circuiting on the
|
|
491
|
+
* first hit — used to badge video artboards in DCArtboard's header.
|
|
492
|
+
*/
|
|
493
|
+
function subtreeHasVideoComp(children: ReactNode): boolean {
|
|
494
|
+
function visit(node: ReactNode): boolean {
|
|
495
|
+
if (node == null || typeof node === 'boolean') return false;
|
|
496
|
+
if (Array.isArray(node)) return node.some(visit);
|
|
497
|
+
if (!isValidElement(node)) return false;
|
|
498
|
+
const type = node.type;
|
|
499
|
+
if (
|
|
500
|
+
type === VideoComp ||
|
|
501
|
+
(typeof type === 'function' && (type as { displayName?: string }).displayName === 'VideoComp')
|
|
502
|
+
) {
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
const childProp = (node.props as { children?: ReactNode } | null | undefined)?.children;
|
|
506
|
+
return childProp != null ? visit(childProp) : false;
|
|
507
|
+
}
|
|
508
|
+
return visit(children);
|
|
509
|
+
}
|
|
510
|
+
|
|
454
511
|
function synthDefaultGrid(seeds: ArtboardSeed[]): ArtboardRect[] {
|
|
455
512
|
// Render order (the order DCArtboards appear in JSX), not alphabetical —
|
|
456
513
|
// authors label artboards DS-01 / DS-02 / CV-01 etc. and expect that
|
|
@@ -1583,16 +1640,33 @@ function DesignCanvasInner({ children, controls }: DesignCanvasProps) {
|
|
|
1583
1640
|
// useLayoutEffect writes the initial transform before first paint.
|
|
1584
1641
|
const worldStyle: CSSProperties = { visibility: 'hidden' };
|
|
1585
1642
|
|
|
1643
|
+
// Hug-mode height feedback (artboard "hug height" default). A hug board's
|
|
1644
|
+
// CSS height is `auto` (content-driven) — the DOM is authoritative, and this
|
|
1645
|
+
// just mirrors the measured height into the shared ArtboardRect model so
|
|
1646
|
+
// OTHER consumers (marquee, distribute, fit-to-screen, off-screen culling)
|
|
1647
|
+
// see the current box. Never touches meta/JSX — purely runtime state, unlike
|
|
1648
|
+
// commitArtboardPositions/applyArtboardLayout which persist. A no-op guard
|
|
1649
|
+
// on <1px delta avoids a ResizeObserver → setState → render → (stable, since
|
|
1650
|
+
// CSS height never reads back rect.h in hug mode) loop.
|
|
1651
|
+
const reportMeasuredHeight = useCallback((id: string, h: number) => {
|
|
1652
|
+
setArtboards((prev) => {
|
|
1653
|
+
const cur = prev.find((r) => r.id === id);
|
|
1654
|
+
if (!cur || Math.abs(cur.h - h) < 1) return prev;
|
|
1655
|
+
return prev.map((r) => (r.id === id ? { ...r, h } : r));
|
|
1656
|
+
});
|
|
1657
|
+
}, []);
|
|
1658
|
+
|
|
1586
1659
|
const ctxValue = useMemo<WorldContextValue>(
|
|
1587
1660
|
() => ({
|
|
1588
1661
|
rectFor,
|
|
1589
1662
|
artboards,
|
|
1663
|
+
reportMeasuredHeight,
|
|
1590
1664
|
viewport: controller.viewport,
|
|
1591
1665
|
activeArtboardId,
|
|
1592
1666
|
hostRef,
|
|
1593
1667
|
worldRef,
|
|
1594
1668
|
}),
|
|
1595
|
-
[rectFor, artboards, controller.viewport, activeArtboardId]
|
|
1669
|
+
[rectFor, artboards, reportMeasuredHeight, controller.viewport, activeArtboardId]
|
|
1596
1670
|
);
|
|
1597
1671
|
|
|
1598
1672
|
const showMiniMap = controls?.minimap !== false;
|
|
@@ -1741,12 +1815,30 @@ export function DCArtboard({
|
|
|
1741
1815
|
label,
|
|
1742
1816
|
width,
|
|
1743
1817
|
height,
|
|
1818
|
+
fixed,
|
|
1819
|
+
background,
|
|
1820
|
+
padding,
|
|
1821
|
+
layout,
|
|
1822
|
+
gap,
|
|
1744
1823
|
children,
|
|
1745
1824
|
}: {
|
|
1746
1825
|
id: string;
|
|
1747
1826
|
label: string;
|
|
1748
1827
|
width: number;
|
|
1749
1828
|
height: number;
|
|
1829
|
+
/** Height sizing mode. Default (omitted/false) = HUG — the board grows to
|
|
1830
|
+
* fit content; `height` becomes a min-height floor rather than an exact
|
|
1831
|
+
* size. `true` = FIXED — today's behavior, `height` is exact and overflow
|
|
1832
|
+
* clips. Width is always exact regardless of this flag. */
|
|
1833
|
+
fixed?: boolean;
|
|
1834
|
+
/** `.dc-artboard-body` background (CSS color/token, e.g. "var(--bg-1)"). */
|
|
1835
|
+
background?: string;
|
|
1836
|
+
/** `.dc-artboard-body` padding, px, all sides. */
|
|
1837
|
+
padding?: number;
|
|
1838
|
+
/** `.dc-artboard-body` layout mode. Default = the engine's plain block flow. */
|
|
1839
|
+
layout?: 'block' | 'flex-col' | 'flex-row' | 'grid';
|
|
1840
|
+
/** `.dc-artboard-body` gap, px — only visible under flex-col/flex-row/grid. */
|
|
1841
|
+
gap?: number;
|
|
1750
1842
|
children: ReactNode;
|
|
1751
1843
|
}) {
|
|
1752
1844
|
const ctx = useWorldContext();
|
|
@@ -1761,6 +1853,21 @@ export function DCArtboard({
|
|
|
1761
1853
|
// its presence color + show its funny name instead of the generic file label.
|
|
1762
1854
|
const agent = useAgentPresence();
|
|
1763
1855
|
const rect = ctx ? ctx.rectFor(id) : null;
|
|
1856
|
+
// DDR-148 — badge video artboards in the header; clicking the badge opens
|
|
1857
|
+
// the timeline panel via the same postMessage the context-menu "Open
|
|
1858
|
+
// Timeline" entry already sends (canvas-shell.tsx artboardHasVideo/handler).
|
|
1859
|
+
const hasVideo = useMemo(() => subtreeHasVideoComp(children), [children]);
|
|
1860
|
+
const openTimeline = useCallback(
|
|
1861
|
+
(e: ReactMouseEvent<HTMLButtonElement>) => {
|
|
1862
|
+
e.stopPropagation();
|
|
1863
|
+
try {
|
|
1864
|
+
window.parent.postMessage({ dgn: 'open-timeline-request', artboardId: id }, '*');
|
|
1865
|
+
} catch {
|
|
1866
|
+
/* detached / cross-origin */
|
|
1867
|
+
}
|
|
1868
|
+
},
|
|
1869
|
+
[id]
|
|
1870
|
+
);
|
|
1764
1871
|
|
|
1765
1872
|
// Drag hook — always called (hook rules). Inert outside DesignCanvas
|
|
1766
1873
|
// (allRects empty, enabled=false), so specimens / legacy uses get a plain
|
|
@@ -1793,11 +1900,67 @@ export function DCArtboard({
|
|
|
1793
1900
|
}
|
|
1794
1901
|
}, [dragHook.dragState, dragBus]);
|
|
1795
1902
|
|
|
1903
|
+
// Hug default (artboard "hug height") — the authored `height` prop is a
|
|
1904
|
+
// FLOOR (min-height), not an exact size, unless `fixed` pins it. The floor
|
|
1905
|
+
// stays stable across renders (it only changes when the JSX prop itself is
|
|
1906
|
+
// edited), so it never ratchets up from a previously-larger measured value
|
|
1907
|
+
// the way feeding `rect.h` back as the floor would.
|
|
1908
|
+
const heightFloor = typeof height === 'number' ? height : VP_GRID.h;
|
|
1909
|
+
const articleRef = useRef<HTMLElement | null>(null);
|
|
1910
|
+
const measured = useArtboardBounds(articleRef as RefObject<HTMLElement | null>);
|
|
1911
|
+
useEffect(() => {
|
|
1912
|
+
if (!ctx || fixed) return;
|
|
1913
|
+
if (dragHook.dragState.kind !== 'idle') return;
|
|
1914
|
+
if (measured.height <= 0) return;
|
|
1915
|
+
ctx.reportMeasuredHeight(id, Math.round(measured.height));
|
|
1916
|
+
}, [ctx, fixed, dragHook.dragState.kind, measured.height, id]);
|
|
1917
|
+
|
|
1918
|
+
// Phase 2 — background/padding/layout/gap apply to the BODY (content box),
|
|
1919
|
+
// not the frame (label header stays chrome-styled). Undefined keys are
|
|
1920
|
+
// simply absent from the style object — no engine-CSS default is clobbered.
|
|
1921
|
+
const bodyStyle = useMemo<CSSProperties>(() => {
|
|
1922
|
+
const st: CSSProperties = {};
|
|
1923
|
+
if (background) st.background = background;
|
|
1924
|
+
if (typeof padding === 'number') st.padding = padding;
|
|
1925
|
+
if (typeof gap === 'number') st.gap = gap;
|
|
1926
|
+
if (layout === 'flex-col') {
|
|
1927
|
+
st.display = 'flex';
|
|
1928
|
+
st.flexDirection = 'column';
|
|
1929
|
+
} else if (layout === 'flex-row') {
|
|
1930
|
+
st.display = 'flex';
|
|
1931
|
+
st.flexDirection = 'row';
|
|
1932
|
+
} else if (layout === 'grid') {
|
|
1933
|
+
st.display = 'grid';
|
|
1934
|
+
}
|
|
1935
|
+
return st;
|
|
1936
|
+
}, [background, padding, gap, layout]);
|
|
1937
|
+
|
|
1938
|
+
// Read-back surface for the Inspector's ArtboardKnobs panel — the SAME
|
|
1939
|
+
// generic "custom HTML attributes" escape hatch dom-selection.ts already
|
|
1940
|
+
// scrapes off the selected element (styleMapsFor → Selection.attrs), so the
|
|
1941
|
+
// panel pre-fills current state with no new plumbing. React omits an
|
|
1942
|
+
// attribute entirely when its value is `undefined`.
|
|
1943
|
+
const readBackAttrs = {
|
|
1944
|
+
'data-dc-fixed': fixed ? 'true' : undefined,
|
|
1945
|
+
'data-dc-bg': background,
|
|
1946
|
+
'data-dc-padding': typeof padding === 'number' ? String(padding) : undefined,
|
|
1947
|
+
'data-dc-layout': layout,
|
|
1948
|
+
'data-dc-gap': typeof gap === 'number' ? String(gap) : undefined,
|
|
1949
|
+
};
|
|
1950
|
+
|
|
1796
1951
|
if (!ctx || !rect) {
|
|
1797
1952
|
return (
|
|
1798
|
-
<article
|
|
1953
|
+
<article
|
|
1954
|
+
className="dc-artboard"
|
|
1955
|
+
data-dc-screen={id}
|
|
1956
|
+
ref={articleRef}
|
|
1957
|
+
style={fixed ? { width, height } : { width, height: 'auto', minHeight: heightFloor }}
|
|
1958
|
+
{...readBackAttrs}
|
|
1959
|
+
>
|
|
1799
1960
|
<header className="dc-artboard-label sku">{label}</header>
|
|
1800
|
-
<div className="dc-artboard-body"
|
|
1961
|
+
<div className="dc-artboard-body" style={bodyStyle}>
|
|
1962
|
+
{children}
|
|
1963
|
+
</div>
|
|
1801
1964
|
</article>
|
|
1802
1965
|
);
|
|
1803
1966
|
}
|
|
@@ -1850,11 +2013,16 @@ export function DCArtboard({
|
|
|
1850
2013
|
className={`dc-artboard dc-positioned${isInDrag ? ' dc-dragging' : ''}`}
|
|
1851
2014
|
data-dc-screen={id}
|
|
1852
2015
|
aria-current={isActive ? 'true' : undefined}
|
|
2016
|
+
ref={articleRef}
|
|
2017
|
+
{...readBackAttrs}
|
|
1853
2018
|
style={{
|
|
1854
2019
|
left: liveX,
|
|
1855
2020
|
top: liveY,
|
|
1856
2021
|
width: rect.w,
|
|
1857
|
-
height:
|
|
2022
|
+
// Hug default: height:auto + minHeight floor, content dictates the
|
|
2023
|
+
// box; the ResizeObserver above mirrors the settled size back into
|
|
2024
|
+
// rect.h for other consumers. Fixed: today's exact-height behavior.
|
|
2025
|
+
...(fixed ? { height: rect.h } : { height: 'auto', minHeight: heightFloor }),
|
|
1858
2026
|
// Off-screen artboards skip layout+paint. On a large multi-board
|
|
1859
2027
|
// canvas (e.g. an 18-board moodboard) every board otherwise paints
|
|
1860
2028
|
// onto one huge .dc-world plane whose device-pixel size exceeds
|
|
@@ -1872,7 +2040,36 @@ export function DCArtboard({
|
|
|
1872
2040
|
<button type="button" className="dc-artboard-label sku" aria-label={`Artboard ${label}`}>
|
|
1873
2041
|
{label}
|
|
1874
2042
|
</button>
|
|
1875
|
-
|
|
2043
|
+
{hasVideo ? (
|
|
2044
|
+
<button
|
|
2045
|
+
type="button"
|
|
2046
|
+
className="dc-artboard-video-badge"
|
|
2047
|
+
aria-label={`Open timeline — ${label}`}
|
|
2048
|
+
title="Video artboard — open timeline"
|
|
2049
|
+
onClick={openTimeline}
|
|
2050
|
+
>
|
|
2051
|
+
<svg viewBox="0 0 16 16" width="12" height="12" fill="none" aria-hidden="true">
|
|
2052
|
+
<rect
|
|
2053
|
+
x="1.5"
|
|
2054
|
+
y="4.5"
|
|
2055
|
+
width="8"
|
|
2056
|
+
height="7"
|
|
2057
|
+
rx="1.2"
|
|
2058
|
+
stroke="currentColor"
|
|
2059
|
+
strokeWidth="1.3"
|
|
2060
|
+
/>
|
|
2061
|
+
<path
|
|
2062
|
+
d="M9.5 7l4-2.3v6.6l-4-2.3"
|
|
2063
|
+
stroke="currentColor"
|
|
2064
|
+
strokeWidth="1.3"
|
|
2065
|
+
strokeLinejoin="round"
|
|
2066
|
+
/>
|
|
2067
|
+
</svg>
|
|
2068
|
+
</button>
|
|
2069
|
+
) : null}
|
|
2070
|
+
<div className="dc-artboard-body" style={bodyStyle}>
|
|
2071
|
+
{children}
|
|
2072
|
+
</div>
|
|
1876
2073
|
</article>
|
|
1877
2074
|
{showActivity ? (
|
|
1878
2075
|
<ArtboardActivityOverlay
|
|
@@ -2136,6 +2333,436 @@ export function DrawProof({
|
|
|
2136
2333
|
}
|
|
2137
2334
|
DrawProof.displayName = 'DrawProof';
|
|
2138
2335
|
|
|
2336
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
2337
|
+
// PhotoLayer (feature-photo-editor, Task 6) — the non-destructive WebGL photo
|
|
2338
|
+
// compositor surface. Renders a source photo (artboard `<img>` or annotation
|
|
2339
|
+
// `ImageStroke`) with a live `PhotoEdit` applied through pixi.js.
|
|
2340
|
+
//
|
|
2341
|
+
// LAZY-BUNDLE GUARANTEE (the plan's load-bearing acceptance criterion + BUILDER's
|
|
2342
|
+
// flagged top risk): an UNEDITED photo (`isDefaultEdit(edit)`) renders as the
|
|
2343
|
+
// plain `<img>` and NEVER touches pixi. The compositor module (photo/pipeline.ts)
|
|
2344
|
+
// is only reached through a DYNAMIC `import()` inside the effect, so a canvas
|
|
2345
|
+
// with zero edited photos pays zero pixi.js/bg-removal cost. Verified empirically
|
|
2346
|
+
// against `buildCanvasModule` (no eager `pixi.js` import in the default-edit
|
|
2347
|
+
// bundle) — see test/photo-canvas-bundle.test.ts.
|
|
2348
|
+
//
|
|
2349
|
+
// A11y: the pixi output is a `<canvas>` (a black box to AT), so it carries
|
|
2350
|
+
// `role="img"` + `aria-label` from `alt` (validation step 7 requirement).
|
|
2351
|
+
// Reduced-motion: the compositor renders statically (autoStart:false, no ticker).
|
|
2352
|
+
|
|
2353
|
+
export interface PhotoLayerProps {
|
|
2354
|
+
/** Relative `assets/<sha8>.<ext>` source (validated upstream). */
|
|
2355
|
+
source: string;
|
|
2356
|
+
/** Live non-destructive edit. Absent / neutral ⇒ plain `<img>`, no pixi. */
|
|
2357
|
+
edit?: PhotoEdit | null;
|
|
2358
|
+
width: number;
|
|
2359
|
+
height: number;
|
|
2360
|
+
alt?: string;
|
|
2361
|
+
className?: string;
|
|
2362
|
+
style?: CSSProperties;
|
|
2363
|
+
/** Resolve a relative asset path to a fetchable URL (defaults to identity —
|
|
2364
|
+
* relative `assets/…` already resolves against the canvas iframe origin). */
|
|
2365
|
+
resolveUrl?: (rel: string) => string;
|
|
2366
|
+
/** Fired once the pixi compositor has mounted + drawn its first frame. The
|
|
2367
|
+
* preview bridge hides the original element only AFTER this, so a background
|
|
2368
|
+
* cutout never flashes the untouched original underneath (and no flicker). */
|
|
2369
|
+
onReady?: () => void;
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
export function PhotoLayer({
|
|
2373
|
+
source,
|
|
2374
|
+
edit,
|
|
2375
|
+
width,
|
|
2376
|
+
height,
|
|
2377
|
+
alt = '',
|
|
2378
|
+
className,
|
|
2379
|
+
style,
|
|
2380
|
+
resolveUrl,
|
|
2381
|
+
onReady,
|
|
2382
|
+
}: PhotoLayerProps) {
|
|
2383
|
+
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
2384
|
+
const rendererRef = useRef<{ destroy(): void; update(e: PhotoEdit): void } | null>(null);
|
|
2385
|
+
const active = !isDefaultEdit(edit);
|
|
2386
|
+
|
|
2387
|
+
// Mount / tear down the pixi compositor only while an edit is active. The
|
|
2388
|
+
// compositor is DYNAMICALLY imported (lazy-bundle guarantee — see header).
|
|
2389
|
+
// Re-created only when the source/box identity changes; edit-param changes are
|
|
2390
|
+
// pushed via the second effect below (no teardown → smooth live scrub).
|
|
2391
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: `edit` is deliberately excluded — re-creating the pixi Application on every scrub would thrash; edit updates flow through the second effect's `update()` (mount seeds from the current edit).
|
|
2392
|
+
useEffect(() => {
|
|
2393
|
+
if (!active) return;
|
|
2394
|
+
const canvas = canvasRef.current;
|
|
2395
|
+
if (!canvas) return;
|
|
2396
|
+
let disposed = false;
|
|
2397
|
+
import('./photo/pipeline.ts')
|
|
2398
|
+
.then(({ PhotoRenderer }) =>
|
|
2399
|
+
PhotoRenderer.create({
|
|
2400
|
+
canvas,
|
|
2401
|
+
source,
|
|
2402
|
+
edit: (edit ?? {}) as PhotoEdit,
|
|
2403
|
+
width,
|
|
2404
|
+
height,
|
|
2405
|
+
resolveUrl,
|
|
2406
|
+
})
|
|
2407
|
+
)
|
|
2408
|
+
.then((r) => {
|
|
2409
|
+
if (disposed) {
|
|
2410
|
+
r.destroy();
|
|
2411
|
+
return;
|
|
2412
|
+
}
|
|
2413
|
+
rendererRef.current = r;
|
|
2414
|
+
onReady?.();
|
|
2415
|
+
})
|
|
2416
|
+
.catch((err) => {
|
|
2417
|
+
console.error('[PhotoLayer] compositor failed to mount', err);
|
|
2418
|
+
});
|
|
2419
|
+
return () => {
|
|
2420
|
+
disposed = true;
|
|
2421
|
+
rendererRef.current?.destroy();
|
|
2422
|
+
rendererRef.current = null;
|
|
2423
|
+
};
|
|
2424
|
+
}, [active, source, width, height, resolveUrl, onReady]);
|
|
2425
|
+
|
|
2426
|
+
// Live-update the mounted compositor on edit-param change (no re-create).
|
|
2427
|
+
useEffect(() => {
|
|
2428
|
+
if (active && edit && rendererRef.current) rendererRef.current.update(edit);
|
|
2429
|
+
}, [edit, active]);
|
|
2430
|
+
|
|
2431
|
+
if (!active) {
|
|
2432
|
+
const src = resolveUrl ? resolveUrl(source) : source;
|
|
2433
|
+
return (
|
|
2434
|
+
<img src={src} width={width} height={height} alt={alt} className={className} style={style} />
|
|
2435
|
+
);
|
|
2436
|
+
}
|
|
2437
|
+
return (
|
|
2438
|
+
<canvas
|
|
2439
|
+
ref={canvasRef}
|
|
2440
|
+
width={width}
|
|
2441
|
+
height={height}
|
|
2442
|
+
role="img"
|
|
2443
|
+
aria-label={alt || 'Edited photo'}
|
|
2444
|
+
className={className}
|
|
2445
|
+
style={{ width, height, display: 'block', ...style }}
|
|
2446
|
+
/>
|
|
2447
|
+
);
|
|
2448
|
+
}
|
|
2449
|
+
PhotoLayer.displayName = 'PhotoLayer';
|
|
2450
|
+
|
|
2451
|
+
// PhotoPreviewBridge (feature-photo-editor) — applies a live/persisted
|
|
2452
|
+
// PhotoEdit DIRECTLY to the real photo element (artboard `<img>` or
|
|
2453
|
+
// annotation `<image>`) by swapping its `src`/`href` to a baked data URL,
|
|
2454
|
+
// instead of floating a separate WebGL-rendered decoy on top of it.
|
|
2455
|
+
//
|
|
2456
|
+
// Iteration 1 of this bridge did the "decoy" version: a `position:fixed` div
|
|
2457
|
+
// tracked the real element's screen rect via a per-frame rAF loop and hid the
|
|
2458
|
+
// original underneath (`visibility:hidden`) while a live `<PhotoLayer>` pixi
|
|
2459
|
+
// canvas rendered on top. That broke every bit of free native DOM behavior
|
|
2460
|
+
// the real element used to have:
|
|
2461
|
+
// - cmd+click / right-click hit-testing landed on whatever was BEHIND the
|
|
2462
|
+
// now-invisible original (a hidden element isn't hit-testable), while the
|
|
2463
|
+
// decoy was `pointer-events:none` so it couldn't take the click either —
|
|
2464
|
+
// net result, nothing was clickable.
|
|
2465
|
+
// - the decoy rendered at a STABLE "world" pixel size and was CSS-stretched
|
|
2466
|
+
// to the live screen box on zoom; the stretch didn't reliably track the
|
|
2467
|
+
// real box, so the visible photo grew/shrank relative to its own frame.
|
|
2468
|
+
// - it needed its own z-index (originally 30 — drew over the context menu)
|
|
2469
|
+
// instead of just sitting at the element's normal stacking position.
|
|
2470
|
+
// - it only knew about an edit via the transient postMessage below, so any
|
|
2471
|
+
// iframe remount (Cmd+R, HMR) reset it to nothing until a human reopened
|
|
2472
|
+
// the Inspector and nudged a knob.
|
|
2473
|
+
// Swapping the REAL element's `src`/`href` sidesteps all of it: resize, zoom,
|
|
2474
|
+
// hit-testing, and stacking become the browser's native `<img>`/`<image>`
|
|
2475
|
+
// behavior, not a hand-rolled tracker. Non-destructive still holds — only the
|
|
2476
|
+
// LIVE DOM attribute is mutated, never the authored TSX/SVG source; the
|
|
2477
|
+
// on-disk `PhotoEdit` sidecar (`/_api/photo-edit`) stays the persisted source
|
|
2478
|
+
// of truth, re-applied by the hydration scan below on every canvas (re)mount.
|
|
2479
|
+
// The bake is at the source's NATIVE resolution (`renderPhotoDataUrl`), so
|
|
2480
|
+
// the result stays sharp across any later resize/zoom with no re-bake.
|
|
2481
|
+
|
|
2482
|
+
/** `assets/<sha8>.<ext>` substring inside a `src`/`href`/`xlink:href`. */
|
|
2483
|
+
const ASSET_REF_RE = /assets\/[0-9a-f]{8}\.[a-z0-9]+/i;
|
|
2484
|
+
|
|
2485
|
+
/** Matches a photo element by the `data-photo-asset` tag `apply()` stamps on
|
|
2486
|
+
* first touch, falling back to a literal src/href substring match for an
|
|
2487
|
+
* element this bridge hasn't touched yet. The tag is load-bearing: once
|
|
2488
|
+
* baked, the element's `src`/`href` is a `data:` URL that no longer contains
|
|
2489
|
+
* the original asset path, so the substring match alone would lose track of
|
|
2490
|
+
* it on the very next edit. */
|
|
2491
|
+
function findPhotoEl(asset: string): Element | null {
|
|
2492
|
+
if (typeof document === 'undefined') return null;
|
|
2493
|
+
// Scoped to img/image (not a bare `[data-photo-asset]` attribute selector) —
|
|
2494
|
+
// the canvas iframe is untrusted content (DDR-054); an authored canvas could
|
|
2495
|
+
// otherwise stamp the tag on an arbitrary element to redirect a bake.
|
|
2496
|
+
for (const n of document.querySelectorAll('img[data-photo-asset], image[data-photo-asset]')) {
|
|
2497
|
+
if (n.getAttribute('data-photo-asset') === asset) return n;
|
|
2498
|
+
}
|
|
2499
|
+
for (const n of document.querySelectorAll('img, image')) {
|
|
2500
|
+
const src =
|
|
2501
|
+
n.getAttribute('src') || n.getAttribute('href') || n.getAttribute('xlink:href') || '';
|
|
2502
|
+
if (src.includes(asset)) return n;
|
|
2503
|
+
}
|
|
2504
|
+
return null;
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
function extractAssetRef(el: Element): string | null {
|
|
2508
|
+
// Only trust `data-photo-asset` when it actually has the `assets/<sha8>.<ext>`
|
|
2509
|
+
// shape — the tag is attacker-controllable (untrusted canvas content,
|
|
2510
|
+
// DDR-054), and an unshaped value would otherwise ride unbounded into
|
|
2511
|
+
// `_active.json`/the WS broadcast via inspect.ts's `enrich()`.
|
|
2512
|
+
const tagged = el.getAttribute('data-photo-asset');
|
|
2513
|
+
if (tagged && ASSET_REF_RE.test(tagged)) return tagged;
|
|
2514
|
+
const ref =
|
|
2515
|
+
el.getAttribute('src') || el.getAttribute('href') || el.getAttribute('xlink:href') || '';
|
|
2516
|
+
return ref.match(ASSET_REF_RE)?.[0] ?? null;
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
function setPhotoElSrc(el: Element, url: string): void {
|
|
2520
|
+
if (el.tagName.toLowerCase() === 'image') {
|
|
2521
|
+
el.setAttribute(el.hasAttribute('xlink:href') ? 'xlink:href' : 'href', url);
|
|
2522
|
+
} else {
|
|
2523
|
+
el.setAttribute('src', url);
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
const BAKE_DEBOUNCE_MS = 80;
|
|
2528
|
+
|
|
2529
|
+
export function PhotoPreviewBridge() {
|
|
2530
|
+
// The ORIGINAL (unedited) src/href per asset, captured the first time this
|
|
2531
|
+
// bridge touches that element — so turning an edit off restores exactly
|
|
2532
|
+
// what the element pointed to, not a guess.
|
|
2533
|
+
const originalRef = useRef<Map<string, string>>(new Map());
|
|
2534
|
+
// Per-asset bake generation — guards a slow (or out-of-order) render from
|
|
2535
|
+
// clobbering a NEWER edit that already resolved first.
|
|
2536
|
+
const tokenRef = useRef<Map<string, number>>(new Map());
|
|
2537
|
+
const bakeTimers = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());
|
|
2538
|
+
|
|
2539
|
+
const bake = useCallback((asset: string, original: string, edit: PhotoEdit) => {
|
|
2540
|
+
const token = (tokenRef.current.get(asset) ?? 0) + 1;
|
|
2541
|
+
tokenRef.current.set(asset, token);
|
|
2542
|
+
import('./photo/pipeline.ts')
|
|
2543
|
+
.then(({ renderPhotoDataUrl }) => renderPhotoDataUrl({ source: original, edit }))
|
|
2544
|
+
.then((dataUrl) => {
|
|
2545
|
+
if (tokenRef.current.get(asset) !== token) return; // superseded by a newer edit
|
|
2546
|
+
const live = findPhotoEl(asset);
|
|
2547
|
+
if (live) setPhotoElSrc(live, dataUrl);
|
|
2548
|
+
})
|
|
2549
|
+
.catch((err) => {
|
|
2550
|
+
console.error('[PhotoPreviewBridge] bake failed', err);
|
|
2551
|
+
});
|
|
2552
|
+
}, []);
|
|
2553
|
+
|
|
2554
|
+
const apply = useCallback(
|
|
2555
|
+
(asset: string, edit: PhotoEdit | null) => {
|
|
2556
|
+
const el = findPhotoEl(asset);
|
|
2557
|
+
if (!el) return;
|
|
2558
|
+
if (!el.hasAttribute('data-photo-asset')) el.setAttribute('data-photo-asset', asset);
|
|
2559
|
+
if (!originalRef.current.has(asset)) {
|
|
2560
|
+
const orig =
|
|
2561
|
+
el.getAttribute('src') || el.getAttribute('href') || el.getAttribute('xlink:href') || '';
|
|
2562
|
+
originalRef.current.set(asset, orig);
|
|
2563
|
+
}
|
|
2564
|
+
const original = originalRef.current.get(asset) ?? '';
|
|
2565
|
+
const timers = bakeTimers.current;
|
|
2566
|
+
clearTimeout(timers.get(asset));
|
|
2567
|
+
if (!edit || isDefaultEdit(edit)) {
|
|
2568
|
+
timers.delete(asset);
|
|
2569
|
+
setPhotoElSrc(el, original);
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
timers.set(
|
|
2573
|
+
asset,
|
|
2574
|
+
setTimeout(() => bake(asset, original, edit), BAKE_DEBOUNCE_MS)
|
|
2575
|
+
);
|
|
2576
|
+
},
|
|
2577
|
+
[bake]
|
|
2578
|
+
);
|
|
2579
|
+
|
|
2580
|
+
useEffect(() => {
|
|
2581
|
+
const timers = bakeTimers.current;
|
|
2582
|
+
return () => {
|
|
2583
|
+
for (const t of timers.values()) clearTimeout(t);
|
|
2584
|
+
};
|
|
2585
|
+
}, []);
|
|
2586
|
+
|
|
2587
|
+
useEffect(() => {
|
|
2588
|
+
const onMsg = (e: MessageEvent) => {
|
|
2589
|
+
const m = e.data as { dgn?: string; asset?: unknown; edit?: unknown; busy?: unknown } | null;
|
|
2590
|
+
if (!m) return;
|
|
2591
|
+
// Background-removal busy shimmer (Task 12 reveal) — a `data-photo-busy`
|
|
2592
|
+
// attribute toggle on the real element, styled by inspect.ts's single CSS
|
|
2593
|
+
// injection point (mirrors `.dc-activity-scan`'s sweep language). No
|
|
2594
|
+
// separate tracked overlay — see the header comment above `apply()`.
|
|
2595
|
+
// `m.asset` must pass the same shape check `extractAssetRef` applies
|
|
2596
|
+
// elsewhere in this file (fix-photo-editor-followup-debt, Task 8) —
|
|
2597
|
+
// an empty string previously matched EVERY photo element via
|
|
2598
|
+
// `findPhotoEl`'s substring-match fallback (`src.includes('')` is always
|
|
2599
|
+
// true), so an empty/malformed `asset` is now a no-op instead of toggling
|
|
2600
|
+
// the busy shimmer on every photo on the canvas.
|
|
2601
|
+
if (m.dgn === 'photo-busy' && typeof m.asset === 'string' && ASSET_REF_RE.test(m.asset)) {
|
|
2602
|
+
const el = findPhotoEl(m.asset);
|
|
2603
|
+
el?.toggleAttribute('data-photo-busy', !!m.busy);
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
if (m.dgn !== 'photo-preview' || typeof m.asset !== 'string') return;
|
|
2607
|
+
const asset = m.asset;
|
|
2608
|
+
const edit = (m.edit ?? null) as PhotoEdit | null;
|
|
2609
|
+
apply(asset, edit);
|
|
2610
|
+
};
|
|
2611
|
+
window.addEventListener('message', onMsg);
|
|
2612
|
+
return () => window.removeEventListener('message', onMsg);
|
|
2613
|
+
}, [apply]);
|
|
2614
|
+
// Boot-time (+ ongoing) hydration from the PERSISTED sidecar, not just the
|
|
2615
|
+
// live `photo-preview` message above. Without this, a saved PhotoEdit is
|
|
2616
|
+
// invisible after anything that re-mounts the canvas doc (Cmd+R, an
|
|
2617
|
+
// HMR remount, or a resize that recreates the photo's DOM node) — the
|
|
2618
|
+
// message-only bridge starts every fresh mount with an empty `edits` map,
|
|
2619
|
+
// and nothing re-sends the already-saved edit until a human happens to
|
|
2620
|
+
// reopen the Inspector Photo tab and touch a knob. A MutationObserver
|
|
2621
|
+
// re-scan (not just an initial one-shot) is what makes this self-heal after
|
|
2622
|
+
// those remounts, since the photo element's DOM node is often a NEW node
|
|
2623
|
+
// post-remount, not the one the original message targeted.
|
|
2624
|
+
useEffect(() => {
|
|
2625
|
+
if (typeof document === 'undefined') return;
|
|
2626
|
+
let cancelled = false;
|
|
2627
|
+
let raf = 0;
|
|
2628
|
+
// Every asset gets AT MOST one fetch attempt for this bridge's lifetime —
|
|
2629
|
+
// without this, a canvas whose DOM keeps mutating (an animation, a
|
|
2630
|
+
// re-rendering component) reissues the full unfetched set on every
|
|
2631
|
+
// mutation frame, forever (security review finding: unbounded fetch
|
|
2632
|
+
// amplification against the dev server from a zero-gesture background
|
|
2633
|
+
// scan).
|
|
2634
|
+
const attempted = new Set<string>();
|
|
2635
|
+
// Safety ceiling per pass — a pathological/hostile canvas DOM (thousands
|
|
2636
|
+
// of img/image elements) shouldn't be able to fan out unbounded fetches
|
|
2637
|
+
// in one scan; the next mutation-triggered pass picks up where this left
|
|
2638
|
+
// off since `attempted` persists across passes.
|
|
2639
|
+
const MAX_SCAN_PER_PASS = 500;
|
|
2640
|
+
const scan = () => {
|
|
2641
|
+
let scanned = 0;
|
|
2642
|
+
for (const n of document.querySelectorAll('img, image')) {
|
|
2643
|
+
const asset = extractAssetRef(n);
|
|
2644
|
+
if (!asset || attempted.has(asset)) continue;
|
|
2645
|
+
if (++scanned > MAX_SCAN_PER_PASS) break;
|
|
2646
|
+
attempted.add(asset);
|
|
2647
|
+
fetch(`/_api/photo-edit?asset=${encodeURIComponent(asset)}`)
|
|
2648
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
2649
|
+
.then((edit) => {
|
|
2650
|
+
if (cancelled || !edit || isDefaultEdit(edit)) return;
|
|
2651
|
+
apply(asset, edit);
|
|
2652
|
+
})
|
|
2653
|
+
.catch(() => {});
|
|
2654
|
+
}
|
|
2655
|
+
};
|
|
2656
|
+
scan();
|
|
2657
|
+
const observer = new MutationObserver(() => {
|
|
2658
|
+
cancelAnimationFrame(raf);
|
|
2659
|
+
raf = requestAnimationFrame(scan);
|
|
2660
|
+
});
|
|
2661
|
+
observer.observe(document.body, {
|
|
2662
|
+
childList: true,
|
|
2663
|
+
subtree: true,
|
|
2664
|
+
attributes: true,
|
|
2665
|
+
attributeFilter: ['src', 'href'],
|
|
2666
|
+
});
|
|
2667
|
+
return () => {
|
|
2668
|
+
cancelled = true;
|
|
2669
|
+
cancelAnimationFrame(raf);
|
|
2670
|
+
observer.disconnect();
|
|
2671
|
+
};
|
|
2672
|
+
}, [apply]);
|
|
2673
|
+
return null; // mutates the real elements directly — no visible DOM of its own.
|
|
2674
|
+
}
|
|
2675
|
+
PhotoPreviewBridge.displayName = 'PhotoPreviewBridge';
|
|
2676
|
+
|
|
2677
|
+
// PhotoBgRemoveHarness (feature-photo-editor, Task 18) — the headless CLI proof
|
|
2678
|
+
// harness `photo-bg-remove.sh` mounts inside a throwaway `_photo/<slug>.bgremove.tsx`
|
|
2679
|
+
// canvas (mirrors DrawProof's role for the draw engine). Runs the EXACT SAME
|
|
2680
|
+
// client-side ML flow the interactive "Remove Background" button uses (Task 12,
|
|
2681
|
+
// app.jsx `onPhotoRemoveBackground`) — @imgly/background-removal, WASM/WebGPU,
|
|
2682
|
+
// pixels never leave the browser — then persists the result and reports back to
|
|
2683
|
+
// the driving CLI script via DOM attributes it polls (no return value crosses
|
|
2684
|
+
// the process boundary; agent-browser reads attributes off the DOM instead).
|
|
2685
|
+
//
|
|
2686
|
+
// `/_api/asset` and `/_api/photo-edit` are BOTH canvas-safe routes (see their
|
|
2687
|
+
// CANVAS_SAFE_API comments in http.ts), so this harness posts directly from the
|
|
2688
|
+
// canvas origin — no main-origin relay / cross-origin workaround needed, despite
|
|
2689
|
+
// the split-origin (DDR-054) boundary the rest of the canvas runs inside.
|
|
2690
|
+
|
|
2691
|
+
export interface PhotoBgRemoveHarnessProps {
|
|
2692
|
+
/** Relative `assets/<sha8>.<ext>` source to remove the background from. */
|
|
2693
|
+
source: string;
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
export function PhotoBgRemoveHarness({ source }: PhotoBgRemoveHarnessProps) {
|
|
2697
|
+
const [status, setStatus] = useState<'pending' | 'done' | 'error'>('pending');
|
|
2698
|
+
const [result, setResult] = useState<string | null>(null);
|
|
2699
|
+
const [error, setError] = useState<string | null>(null);
|
|
2700
|
+
const ranRef = useRef(false);
|
|
2701
|
+
|
|
2702
|
+
useEffect(() => {
|
|
2703
|
+
if (ranRef.current) return; // one ML pass per mount — a dev-mode double-effect must not double-run it
|
|
2704
|
+
ranRef.current = true;
|
|
2705
|
+
let cancelled = false;
|
|
2706
|
+
(async () => {
|
|
2707
|
+
try {
|
|
2708
|
+
const srcRes = await fetch(`/${source.replace(/^\/+/, '')}`);
|
|
2709
|
+
if (!srcRes.ok) throw new Error(`source fetch failed: ${srcRes.status}`);
|
|
2710
|
+
const srcBlob = await srcRes.blob();
|
|
2711
|
+
const { removeBackground } = await import('@imgly/background-removal');
|
|
2712
|
+
const matte = await removeBackground(srcBlob);
|
|
2713
|
+
const up = await fetch('/_api/asset', {
|
|
2714
|
+
method: 'POST',
|
|
2715
|
+
headers: { 'content-type': matte.type || 'image/png' },
|
|
2716
|
+
body: matte,
|
|
2717
|
+
});
|
|
2718
|
+
const upJson = (await up.json().catch(() => ({}))) as { path?: string };
|
|
2719
|
+
if (!up.ok || !upJson.path) throw new Error(`asset upload failed: ${up.status}`);
|
|
2720
|
+
const maskAsset = upJson.path;
|
|
2721
|
+
|
|
2722
|
+
// Merge onto whatever's already in the sidecar (mirrors photo-adjust.sh's
|
|
2723
|
+
// merge-by-default behavior) instead of clobbering unrelated fields.
|
|
2724
|
+
const base: unknown = await fetch(`/_api/photo-edit?asset=${encodeURIComponent(source)}`)
|
|
2725
|
+
.then((r) => (r.ok ? r.json() : {}))
|
|
2726
|
+
.catch(() => ({}));
|
|
2727
|
+
const nextEdit = {
|
|
2728
|
+
...(base && typeof base === 'object' ? base : {}),
|
|
2729
|
+
backgroundRemoved: { enabled: true, maskAsset },
|
|
2730
|
+
};
|
|
2731
|
+
const put = await fetch(`/_api/photo-edit?asset=${encodeURIComponent(source)}`, {
|
|
2732
|
+
method: 'PUT',
|
|
2733
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2734
|
+
body: JSON.stringify(nextEdit),
|
|
2735
|
+
});
|
|
2736
|
+
if (!put.ok) throw new Error(`photo-edit save failed: ${put.status}`);
|
|
2737
|
+
if (cancelled) return;
|
|
2738
|
+
setResult(maskAsset);
|
|
2739
|
+
setStatus('done');
|
|
2740
|
+
} catch (err) {
|
|
2741
|
+
if (cancelled) return;
|
|
2742
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
2743
|
+
setStatus('error');
|
|
2744
|
+
}
|
|
2745
|
+
})();
|
|
2746
|
+
return () => {
|
|
2747
|
+
cancelled = true;
|
|
2748
|
+
};
|
|
2749
|
+
}, [source]);
|
|
2750
|
+
|
|
2751
|
+
return (
|
|
2752
|
+
<div
|
|
2753
|
+
data-photo-bgremove-status={status}
|
|
2754
|
+
data-photo-bgremove-result={result ?? ''}
|
|
2755
|
+
data-photo-bgremove-error={error ?? ''}
|
|
2756
|
+
style={{ padding: 24, font: '13px monospace' }}
|
|
2757
|
+
>
|
|
2758
|
+
photo-bg-remove: {status}
|
|
2759
|
+
{result ? ` → ${result}` : ''}
|
|
2760
|
+
{error ? ` (${error})` : ''}
|
|
2761
|
+
</div>
|
|
2762
|
+
);
|
|
2763
|
+
}
|
|
2764
|
+
PhotoBgRemoveHarness.displayName = 'PhotoBgRemoveHarness';
|
|
2765
|
+
|
|
2139
2766
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
2140
2767
|
// SnapGuideOverlay (Phase 4.2) — renders 1 px guide lines while a drag is in
|
|
2141
2768
|
// flight. Mounted by canvas-shell as a chrome layer outside `.dc-world`, so
|