@agent-native/core 0.80.11 → 0.81.1
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +53 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/design-connect.ts +419 -19
- package/corpus/core/src/client/AgentPanel.tsx +33 -1
- package/corpus/core/src/client/ConnectBuilderCard.tsx +1 -0
- package/corpus/core/src/client/chat/index.ts +1 -0
- package/corpus/core/src/client/chat/run-recovery.tsx +9 -7
- package/corpus/core/src/client/chat-view-transition.ts +33 -2
- package/corpus/core/src/client/composer/PromptComposer.tsx +21 -2
- package/corpus/core/src/client/index.ts +1 -0
- package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -4
- package/corpus/core/src/client/sharing/ShareButton.tsx +3 -3
- package/corpus/core/src/client/use-agent-chat-home-handoff.ts +31 -3
- package/corpus/core/src/db/client.ts +29 -2
- package/corpus/core/src/provider-api/index.ts +29 -0
- package/corpus/core/src/server/core-routes-plugin.ts +58 -20
- package/corpus/core/src/styles/agent-native.css +54 -0
- package/corpus/core/src/usage/store.ts +2 -2
- package/corpus/templates/analytics/app/components/layout/Layout.tsx +118 -10
- package/corpus/templates/analytics/app/components/layout/layout-route-policy.ts +7 -0
- package/corpus/templates/analytics/app/global.css +8 -0
- package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +7 -1
- package/corpus/templates/analytics/app/lib/chat-handoff.ts +34 -0
- package/corpus/templates/analytics/app/pages/Ask.tsx +9 -18
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +2 -2
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +109 -21
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +4 -6
- package/corpus/templates/analytics/changelog/2026-06-30-ask-opens-a-fresh-chat-unless-you-were-just-chatting-and-das.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-30-dashboard-email-reports-are-more-reliable-and-use-cleaner.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-30-dashboard-toolbar-controls-use-a-quieter-share-button-and-ve.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-30-session-replay-rows-show-whole-minute-durations-in-the-playl.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-30-session-replays-now-render-captured-styling-and-keep-the-eve.md +6 -0
- package/corpus/templates/analytics/server/jobs/dashboard-report.ts +16 -3
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +31 -14
- package/corpus/templates/assets/actions/open-asset-picker.ts +9 -0
- package/corpus/templates/assets/app/global.css +15 -0
- package/corpus/templates/assets/app/hooks/use-navigation-state.ts +4 -0
- package/corpus/templates/assets/app/routes/library.tsx +90 -16
- package/corpus/templates/assets/changelog/2026-06-30-the-assets-picker-now-supports-a-vertical-embedded-layout-fo.md +6 -0
- package/corpus/templates/clips/actions/finalize-recording.ts +1 -12
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +10 -5
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -8
- package/corpus/templates/clips/changelog/2026-06-30-extension-save-recovery-avoids-false-upload-failures.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-30-longer-chrome-extension-recordings-now-keep-saving-reliably-.md +6 -0
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
- package/corpus/templates/clips/chrome-extension/src/background.ts +19 -10
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +38 -12
- package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +224 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +119 -16
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +16 -0
- package/corpus/templates/clips/server/lib/recording-upload-state.ts +48 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +9 -1
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +63 -13
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +74 -0
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +32 -0
- package/corpus/templates/design/AGENTS.md +27 -0
- package/corpus/templates/design/CODE_NATIVE_DESIGN_STUDIO_IMPLEMENTATION_PLAN.md +1 -1
- package/corpus/templates/design/DESIGN-STUDIO-PLAN.md +5 -5
- package/corpus/templates/design/actions/apply-component-prop-edit.ts +63 -20
- package/corpus/templates/design/actions/apply-motion-edit.ts +67 -69
- package/corpus/templates/design/actions/apply-shader-fill.ts +148 -56
- package/corpus/templates/design/actions/capture-design-state.ts +27 -4
- package/corpus/templates/design/actions/connect-localhost.ts +59 -22
- package/corpus/templates/design/actions/export-zip.ts +23 -8
- package/corpus/templates/design/actions/generate-design.ts +53 -1
- package/corpus/templates/design/actions/get-component-details.ts +5 -19
- package/corpus/templates/design/actions/get-motion-timeline.ts +135 -10
- package/corpus/templates/design/actions/grant-localhost-write-consent.ts +116 -0
- package/corpus/templates/design/actions/import-design-tokens.ts +521 -0
- package/corpus/templates/design/actions/index-design-tokens.ts +10 -1
- package/corpus/templates/design/actions/insert-design-native-asset.ts +268 -0
- package/corpus/templates/design/actions/insert-figma-library-asset.ts +233 -0
- package/corpus/templates/design/actions/list-design-extensions.ts +32 -10
- package/corpus/templates/design/actions/list-design-native-assets.ts +105 -0
- package/corpus/templates/design/actions/list-figma-library-assets.ts +292 -0
- package/corpus/templates/design/actions/migrate-board-objects-to-file.ts +238 -0
- package/corpus/templates/design/actions/navigate.ts +18 -3
- package/corpus/templates/design/actions/provider-api-catalog.ts +2 -2
- package/corpus/templates/design/actions/provider-api-docs.ts +1 -1
- package/corpus/templates/design/actions/provider-api-request.ts +3 -3
- package/corpus/templates/design/actions/revoke-localhost-write-consent.ts +48 -0
- package/corpus/templates/design/actions/run-design-audit.ts +53 -4
- package/corpus/templates/design/actions/run-design-extension-action.ts +1 -1
- package/corpus/templates/design/actions/view-screen.ts +1 -1
- package/corpus/templates/design/actions/write-local-file.ts +213 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +230 -3308
- package/corpus/templates/design/app/components/design/DesignEditorSkeleton.tsx +30 -20
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +776 -287
- package/corpus/templates/design/app/components/design/EditPanel.tsx +212 -208
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +14 -2
- package/corpus/templates/design/app/components/design/LocalhostWriteConsentDialog.tsx +161 -0
- package/corpus/templates/design/app/components/design/MotionDock.tsx +235 -266
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +490 -55
- package/corpus/templates/design/app/components/design/ReviewPanel.tsx +43 -62
- package/corpus/templates/design/app/components/design/StatesPanel.tsx +34 -61
- package/corpus/templates/design/app/components/design/TokensPanel.tsx +251 -0
- package/corpus/templates/design/app/components/design/bridge/codegen.ts +125 -0
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +4037 -0
- package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +62 -0
- package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +272 -0
- package/corpus/templates/design/app/components/design/bridge/nav.bridge.ts +116 -0
- package/corpus/templates/design/app/components/design/bridge/sample.bridge.ts +24 -0
- package/corpus/templates/design/app/components/design/bridge/shader-fill-preview.bridge.ts +78 -0
- package/corpus/templates/design/app/components/design/bridge/tsconfig.json +17 -0
- package/corpus/templates/design/app/components/design/bridge/tweak.bridge.ts +28 -0
- package/corpus/templates/design/app/components/design/bridge/zoom.bridge.ts +43 -0
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +45 -16
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +88 -9
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +94 -9
- package/corpus/templates/design/app/components/layout/Layout.tsx +19 -3
- package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -1
- package/corpus/templates/design/app/global.css +7 -0
- package/corpus/templates/design/app/hooks/use-navigation-state.ts +23 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +21 -0
- package/corpus/templates/design/app/i18n-data.ts +358 -0
- package/corpus/templates/design/app/lib/pending-generation.ts +1 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +2402 -499
- package/corpus/templates/design/app/pages/Index.tsx +5 -1
- package/corpus/templates/design/changelog/2026-06-30-applying-the-one-click-fix-for-a-small-tap-target-accessibil.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-canvas-creation-screen-dragging-undo-redo-and-image-fill-edi.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-component-prop-edits-and-shader-fills-now-preserve-rapid-sam.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-can-browse-figma-library-components-and-insert-them-i.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-now-has-a-figma-style-left-rail-for-file-agent-assets.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-now-has-native-components-embedded-media-and-figma-im.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-preview-now-includes-a-publish-app-waitlist-option-fr.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-s-left-rail-now-matches-the-sidebar-chrome-and-uses-t.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-s-tools-create-menu-now-stays-compact-instead-of-rend.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-starter-prompts-now-begin-generating-immediately-and-.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-design-tools-now-use-a-quieter-figma-style-list-layout.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-drawing-rectangles-on-the-all-screens-canvas-no-longer-flash.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-fixed-code-native-design-studio-edits-so-component-props-mot.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-fixed-visual-editing-hover-overlays-and-spacing-handles-so-s.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-motion-edits-now-save-automatically-without-a-separate-css-a.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-motion-timelines-now-reopen-as-editable-tracks-after-writing.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-new-design-prompts-with-uploaded-screenshots-work-reliably-o.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-screen-previews-in-all-screens-view-now-keep-rounded-outline.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-selection-outlines-now-sit-flush-on-rectangles-drawn-directl.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-share-options-use-a-tighter-design-editor-popover-with-compa.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-the-collapsed-motion-panel-now-stays-tucked-under-the-left-s.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-the-design-inspector-now-shows-states-and-review-in-a-tighte.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-the-sidebar-keeps-the-organization-picker-space-stable-while.md +6 -0
- package/corpus/templates/design/changelog/2026-06-30-tokens-can-now-be-imported-from-pasted-notes-code-files-fold.md +6 -0
- package/corpus/templates/design/e2e/helpers.ts +168 -65
- package/corpus/templates/design/package.json +3 -1
- package/corpus/templates/design/server/db/schema.ts +23 -0
- package/corpus/templates/design/server/handlers/uploads.ts +22 -1
- package/corpus/templates/design/server/lib/provider-api.ts +7 -4
- package/corpus/templates/design/server/lib/verify-write-grant.ts +108 -0
- package/corpus/templates/design/server/plugins/db.ts +39 -0
- package/corpus/templates/design/server/register-secrets.ts +38 -0
- package/corpus/templates/design/shared/board-file.ts +228 -0
- package/corpus/templates/design/shared/board-objects.ts +288 -0
- package/corpus/templates/design/shared/code-layer.ts +52 -0
- package/corpus/templates/design/shared/motion-compiler.ts +58 -7
- package/corpus/templates/design/shared/motion-timeline.ts +2 -2
- package/corpus/templates/design/tsconfig.json +2 -1
- package/corpus/templates/slides/AGENTS.md +9 -0
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +4 -0
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +4 -24
- package/corpus/templates/slides/app/lib/export-pdf-client.ts +29 -20
- package/corpus/templates/slides/app/lib/export-pptx-client.ts +365 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +12 -9
- package/corpus/templates/slides/app/vite-env.d.ts +18 -0
- package/corpus/templates/slides/changelog/2026-06-30-powerpoint-downloads-now-preserve-the-rendered-slide-layout-.md +6 -0
- package/corpus/templates/slides/changelog/2026-06-30-unavailable-decks-open-in-a-cleaner-full-height-centered-sta.md +6 -0
- package/corpus/templates/slides/package.json +1 -0
- package/corpus/templates/slides/vite.config.ts +1 -0
- package/dist/cli/design-connect.d.ts +25 -0
- package/dist/cli/design-connect.d.ts.map +1 -1
- package/dist/cli/design-connect.js +357 -18
- package/dist/cli/design-connect.js.map +1 -1
- package/dist/client/AgentPanel.d.ts +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +18 -3
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
- package/dist/client/ConnectBuilderCard.js +1 -0
- package/dist/client/ConnectBuilderCard.js.map +1 -1
- package/dist/client/chat/index.d.ts +1 -1
- package/dist/client/chat/index.d.ts.map +1 -1
- package/dist/client/chat/index.js +1 -1
- package/dist/client/chat/index.js.map +1 -1
- package/dist/client/chat/run-recovery.d.ts.map +1 -1
- package/dist/client/chat/run-recovery.js +5 -5
- package/dist/client/chat/run-recovery.js.map +1 -1
- package/dist/client/chat-view-transition.d.ts +6 -0
- package/dist/client/chat-view-transition.d.ts.map +1 -1
- package/dist/client/chat-view-transition.js +23 -2
- package/dist/client/chat-view-transition.js.map +1 -1
- package/dist/client/composer/PromptComposer.d.ts.map +1 -1
- package/dist/client/composer/PromptComposer.js +3 -2
- package/dist/client/composer/PromptComposer.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
- package/dist/client/org/OrgSwitcher.js +11 -4
- package/dist/client/org/OrgSwitcher.js.map +1 -1
- package/dist/client/sharing/ShareButton.js +3 -3
- package/dist/client/sharing/ShareButton.js.map +1 -1
- package/dist/client/use-agent-chat-home-handoff.d.ts +8 -2
- package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
- package/dist/client/use-agent-chat-home-handoff.js +20 -6
- package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/db/client.d.ts +1 -0
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +28 -2
- package/dist/db/client.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +8 -8
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/index.d.ts +5 -4
- package/dist/provider-api/index.d.ts.map +1 -1
- package/dist/provider-api/index.js +28 -0
- package/dist/provider-api/index.js.map +1 -1
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/core-routes-plugin.d.ts +25 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +43 -18
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/styles/agent-native.css +54 -0
- package/dist/usage/store.js +2 -2
- package/dist/usage/store.js.map +1 -1
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
emailToColor,
|
|
11
11
|
emailToName,
|
|
12
12
|
PresenceBar,
|
|
13
|
-
|
|
13
|
+
AgentChatSurface,
|
|
14
14
|
ShareButton,
|
|
15
15
|
agentNativePath,
|
|
16
16
|
appBasePath,
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
type PromptComposerSubmitOptions,
|
|
34
34
|
} from "@agent-native/core/client";
|
|
35
35
|
import type { TweakDefinition } from "@shared/api";
|
|
36
|
+
import { isBoardFile } from "@shared/board-file";
|
|
36
37
|
import {
|
|
37
38
|
parseCanvasFrameGeometryById,
|
|
38
39
|
type CanvasFrameGeometry,
|
|
@@ -57,6 +58,7 @@ import {
|
|
|
57
58
|
hasCapability,
|
|
58
59
|
} from "@shared/design-source-capabilities";
|
|
59
60
|
import { shouldUseLiveFileContent } from "@shared/html-content";
|
|
61
|
+
import type { MotionTrack } from "@shared/motion-timeline";
|
|
60
62
|
import {
|
|
61
63
|
resolveTweaksToCssVars,
|
|
62
64
|
type TweakSelections,
|
|
@@ -81,11 +83,14 @@ import {
|
|
|
81
83
|
IconFrame,
|
|
82
84
|
IconX,
|
|
83
85
|
IconPin,
|
|
86
|
+
IconAssembly,
|
|
84
87
|
IconCode,
|
|
85
88
|
IconArchive,
|
|
89
|
+
IconFile,
|
|
86
90
|
IconPhoto,
|
|
87
91
|
IconRefresh,
|
|
88
92
|
IconChevronDown,
|
|
93
|
+
IconChevronRight,
|
|
89
94
|
IconCheck,
|
|
90
95
|
IconPointer,
|
|
91
96
|
IconTypography,
|
|
@@ -139,7 +144,11 @@ import {
|
|
|
139
144
|
type MotionTrackWire,
|
|
140
145
|
} from "@/components/design/DesignCanvas";
|
|
141
146
|
import { DesignEditorSkeleton } from "@/components/design/DesignEditorSkeleton";
|
|
142
|
-
import
|
|
147
|
+
import {
|
|
148
|
+
AssetLibraryPanel,
|
|
149
|
+
DesignExtensionsPanel,
|
|
150
|
+
type DesignExtensionSlotContext,
|
|
151
|
+
} from "@/components/design/DesignExtensionsPanel";
|
|
143
152
|
import {
|
|
144
153
|
EditPanel,
|
|
145
154
|
type InspectCodeData,
|
|
@@ -152,6 +161,10 @@ import {
|
|
|
152
161
|
type LayersPanelMoveIntent,
|
|
153
162
|
type LayersPanelNode,
|
|
154
163
|
} from "@/components/design/LayersPanel";
|
|
164
|
+
import {
|
|
165
|
+
LocalhostWriteConsentDialog,
|
|
166
|
+
type LocalhostWriteConsentPayload,
|
|
167
|
+
} from "@/components/design/LocalhostWriteConsentDialog";
|
|
155
168
|
import { LocalSourceEditBanner } from "@/components/design/LocalSourceEditBanner";
|
|
156
169
|
import {
|
|
157
170
|
MotionDock,
|
|
@@ -161,9 +174,11 @@ import {
|
|
|
161
174
|
MultiScreenCanvas,
|
|
162
175
|
OVERVIEW_FRAME_WIDTH,
|
|
163
176
|
type CanvasPrimitiveInsert,
|
|
177
|
+
type FrameGeometry,
|
|
164
178
|
} from "@/components/design/MultiScreenCanvas";
|
|
165
179
|
import { QuestionFlow } from "@/components/design/QuestionFlow";
|
|
166
180
|
import type { ReviewPanelProps } from "@/components/design/ReviewPanel";
|
|
181
|
+
import { TokensPanel } from "@/components/design/TokensPanel";
|
|
167
182
|
import type { ElementInfo, DeviceFrameType } from "@/components/design/types";
|
|
168
183
|
import {
|
|
169
184
|
DEVICE_FRAME_VIEWPORTS,
|
|
@@ -197,6 +212,11 @@ import {
|
|
|
197
212
|
DropdownMenuTrigger,
|
|
198
213
|
} from "@/components/ui/dropdown-menu";
|
|
199
214
|
import { Input } from "@/components/ui/input";
|
|
215
|
+
import {
|
|
216
|
+
Popover,
|
|
217
|
+
PopoverContent,
|
|
218
|
+
PopoverTrigger,
|
|
219
|
+
} from "@/components/ui/popover";
|
|
200
220
|
import { Spinner } from "@/components/ui/spinner";
|
|
201
221
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
202
222
|
import { Textarea } from "@/components/ui/textarea";
|
|
@@ -244,6 +264,12 @@ const AUTO_RETRY_DELAY_MS = 1200;
|
|
|
244
264
|
const STORED_RUN_LIVENESS_GRACE_MS = 20_000;
|
|
245
265
|
const MAX_DESIGN_UNDO_STACK = 50;
|
|
246
266
|
const OVERVIEW_ZOOM_THRESHOLD = 60;
|
|
267
|
+
const MOTION_DOCK_TRANSITION_MS = 200;
|
|
268
|
+
const MOTION_AUTOSAVE_DELAY_MS = 500;
|
|
269
|
+
/** Extensions that the localhost bridge allows to be written back to source. */
|
|
270
|
+
const LOCALHOST_WRITE_EXTENSIONS = new Set([".html", ".htm", ".css"]);
|
|
271
|
+
const NO_LOCALHOST_WRITE_CONTENT_MESSAGE =
|
|
272
|
+
"No content to write. Open the screen first." /* i18n-ignore */;
|
|
247
273
|
const FOCUSED_SCREEN_ZOOM = 100;
|
|
248
274
|
const KEEPALIVE_FILE_SAVE_MAX_BYTES = 60_000;
|
|
249
275
|
const UNSUPPORTED_HTML2CANVAS_COLOR_RE =
|
|
@@ -341,13 +367,16 @@ export function getOverviewEnterTarget(args: {
|
|
|
341
367
|
|
|
342
368
|
export function getSidebarCodeLayerSelectionState(args: {
|
|
343
369
|
currentViewMode: "single" | "overview";
|
|
370
|
+
ownerFileId?: string | null;
|
|
344
371
|
overviewSelectedScreenIds: string[];
|
|
345
372
|
}) {
|
|
346
|
-
const { currentViewMode, overviewSelectedScreenIds } = args;
|
|
373
|
+
const { currentViewMode, ownerFileId, overviewSelectedScreenIds } = args;
|
|
347
374
|
return {
|
|
348
375
|
viewMode: currentViewMode,
|
|
349
376
|
overviewSelectedScreenIds:
|
|
350
|
-
currentViewMode === "overview"
|
|
377
|
+
currentViewMode === "overview" && ownerFileId
|
|
378
|
+
? [ownerFileId]
|
|
379
|
+
: overviewSelectedScreenIds,
|
|
351
380
|
};
|
|
352
381
|
}
|
|
353
382
|
|
|
@@ -459,6 +488,21 @@ export function getLayerMoveIterationOrder<T>(
|
|
|
459
488
|
return placement === "after" ? [...orderedIds].reverse() : [...orderedIds];
|
|
460
489
|
}
|
|
461
490
|
|
|
491
|
+
export function removeUndoRedoOrderKind<T extends string>(
|
|
492
|
+
order: readonly T[],
|
|
493
|
+
kind: T,
|
|
494
|
+
): T[] {
|
|
495
|
+
return order.filter((entry) => entry !== kind);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
type UndoRedoOrderKind = "content" | "file-content" | "geometry";
|
|
499
|
+
|
|
500
|
+
function isContentUndoRedoOrderKind(
|
|
501
|
+
kind: UndoRedoOrderKind | undefined,
|
|
502
|
+
): kind is "content" | "file-content" {
|
|
503
|
+
return kind === "content" || kind === "file-content";
|
|
504
|
+
}
|
|
505
|
+
|
|
462
506
|
function resolveZoomUpdate(update: SetStateAction<number>, current: number) {
|
|
463
507
|
return typeof update === "function" ? update(current) : update;
|
|
464
508
|
}
|
|
@@ -734,6 +778,83 @@ interface CodingHandoffResult {
|
|
|
734
778
|
fileCount?: number;
|
|
735
779
|
}
|
|
736
780
|
|
|
781
|
+
export interface MotionTimelineRow {
|
|
782
|
+
id: string | null;
|
|
783
|
+
designId: string;
|
|
784
|
+
sourceRef: string | null;
|
|
785
|
+
filePath: string | null;
|
|
786
|
+
tracks: unknown;
|
|
787
|
+
durationMs: number;
|
|
788
|
+
defaultEase: string;
|
|
789
|
+
compiledHash: string | null;
|
|
790
|
+
cssHash?: string | null;
|
|
791
|
+
source?: "stored" | "recovered-css" | "stored-css-drift";
|
|
792
|
+
createdAt: string | null;
|
|
793
|
+
updatedAt: string | null;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
interface MotionTimelineQueryResult {
|
|
797
|
+
designId: string;
|
|
798
|
+
timelines: MotionTimelineRow[];
|
|
799
|
+
count: number;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function isMotionTrack(value: unknown): value is MotionTrack {
|
|
803
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
804
|
+
const candidate = value as {
|
|
805
|
+
targetNodeId?: unknown;
|
|
806
|
+
property?: unknown;
|
|
807
|
+
keyframes?: unknown;
|
|
808
|
+
};
|
|
809
|
+
return (
|
|
810
|
+
typeof candidate.targetNodeId === "string" &&
|
|
811
|
+
typeof candidate.property === "string" &&
|
|
812
|
+
Array.isArray(candidate.keyframes)
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function normalizeMotionTracks(value: unknown): MotionTrack[] {
|
|
817
|
+
return Array.isArray(value) ? value.filter(isMotionTrack) : [];
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function labelForMotionTrack(
|
|
821
|
+
track: MotionTrack,
|
|
822
|
+
projection: CodeLayerProjection,
|
|
823
|
+
): string {
|
|
824
|
+
const node = projection.nodes.find(
|
|
825
|
+
(candidate) =>
|
|
826
|
+
candidate.dataAttributes["data-agent-native-node-id"] ===
|
|
827
|
+
track.targetNodeId || candidate.id === track.targetNodeId,
|
|
828
|
+
);
|
|
829
|
+
return node?.layerName || node?.tag || track.targetNodeId;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
export function hydrateMotionDockTracks(
|
|
833
|
+
tracks: unknown,
|
|
834
|
+
projection: CodeLayerProjection,
|
|
835
|
+
): MotionDockTrack[] {
|
|
836
|
+
return normalizeMotionTracks(tracks).map((track) => ({
|
|
837
|
+
...track,
|
|
838
|
+
label: labelForMotionTrack(track, projection),
|
|
839
|
+
}));
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
export function motionTimelineFingerprint(
|
|
843
|
+
fileId: string,
|
|
844
|
+
timeline: MotionTimelineRow | null | undefined,
|
|
845
|
+
): string {
|
|
846
|
+
if (!timeline) return `${fileId}:empty`;
|
|
847
|
+
return [
|
|
848
|
+
fileId,
|
|
849
|
+
timeline.id ?? "css",
|
|
850
|
+
timeline.updatedAt ?? "no-updated-at",
|
|
851
|
+
timeline.compiledHash ?? "no-compiled-hash",
|
|
852
|
+
timeline.cssHash ?? "no-css-hash",
|
|
853
|
+
timeline.source ?? "stored",
|
|
854
|
+
JSON.stringify(timeline.tracks),
|
|
855
|
+
].join(":");
|
|
856
|
+
}
|
|
857
|
+
|
|
737
858
|
function buildSignInHrefForDesignIntent(intent: PostAuthDesignIntent): string {
|
|
738
859
|
const base = agentNativePath("/_agent-native/sign-in");
|
|
739
860
|
if (typeof window === "undefined") return base;
|
|
@@ -894,6 +1015,7 @@ function designEditorCommandFromSearchParams(
|
|
|
894
1015
|
): DesignEditorCommand | null {
|
|
895
1016
|
const editorView = searchParams.get("view");
|
|
896
1017
|
const inspector = searchParams.get("inspector");
|
|
1018
|
+
const leftPanel = normalizeDesignLeftPanel(searchParams.get("panel"));
|
|
897
1019
|
const screen =
|
|
898
1020
|
searchParams.get("screen") ??
|
|
899
1021
|
searchParams.get("fileId") ??
|
|
@@ -907,6 +1029,7 @@ function designEditorCommandFromSearchParams(
|
|
|
907
1029
|
inspector !== "design" &&
|
|
908
1030
|
inspector !== "tweaks" &&
|
|
909
1031
|
inspector !== "extensions" &&
|
|
1032
|
+
!leftPanel &&
|
|
910
1033
|
!screen &&
|
|
911
1034
|
!tool
|
|
912
1035
|
) {
|
|
@@ -926,6 +1049,7 @@ function designEditorCommandFromSearchParams(
|
|
|
926
1049
|
) {
|
|
927
1050
|
command.inspectorTab = inspector;
|
|
928
1051
|
}
|
|
1052
|
+
if (leftPanel) command.leftPanel = leftPanel;
|
|
929
1053
|
if (screen) command.screen = screen;
|
|
930
1054
|
if (Number.isFinite(zoom)) {
|
|
931
1055
|
command.zoom = zoom;
|
|
@@ -1433,6 +1557,10 @@ function appendCanvasPrimitiveToHtml(
|
|
|
1433
1557
|
const element = doc.createElement("div");
|
|
1434
1558
|
element.setAttribute("data-agent-native-node-id", nodeId);
|
|
1435
1559
|
element.setAttribute("data-agent-native-layer-name", layerName);
|
|
1560
|
+
// Kind marker so the layers panel shows a shape/text/frame icon for this
|
|
1561
|
+
// primitive (rectangle/ellipse/text/frame) instead of the generic code
|
|
1562
|
+
// glyph. Read by treeTypeForNode in shared/code-layer.ts.
|
|
1563
|
+
element.setAttribute("data-an-primitive", primitive.kind);
|
|
1436
1564
|
element.style.position = "absolute";
|
|
1437
1565
|
element.style.left = `${left}px`;
|
|
1438
1566
|
element.style.top = `${top}px`;
|
|
@@ -2054,6 +2182,36 @@ function elementInfoFromCodeLayerNode(node: CodeLayerNode): ElementInfo {
|
|
|
2054
2182
|
};
|
|
2055
2183
|
}
|
|
2056
2184
|
|
|
2185
|
+
function camelCaseCssProperty(property: string): string {
|
|
2186
|
+
return property.replace(/-([a-z])/g, (_, letter: string) =>
|
|
2187
|
+
letter.toUpperCase(),
|
|
2188
|
+
);
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
function cssStyleAliases(
|
|
2192
|
+
styles: Record<string, string>,
|
|
2193
|
+
): Record<string, string> {
|
|
2194
|
+
const result: Record<string, string> = {};
|
|
2195
|
+
for (const [property, value] of Object.entries(styles)) {
|
|
2196
|
+
result[property] = value;
|
|
2197
|
+
if (property.includes("-")) {
|
|
2198
|
+
result[camelCaseCssProperty(property)] = value;
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
return result;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
function refreshedComputedStyles(
|
|
2205
|
+
info: ElementInfo,
|
|
2206
|
+
sourceStyles: Record<string, string>,
|
|
2207
|
+
sourceClasses: readonly string[],
|
|
2208
|
+
): Record<string, string> {
|
|
2209
|
+
const sourceWithAliases = cssStyleAliases(sourceStyles);
|
|
2210
|
+
return sourceClasses.length > 0
|
|
2211
|
+
? { ...info.computedStyles, ...sourceWithAliases }
|
|
2212
|
+
: sourceWithAliases;
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2057
2215
|
function codeLayerNodeMatchesBridgeTarget(
|
|
2058
2216
|
node: CodeLayerNode,
|
|
2059
2217
|
selector?: string,
|
|
@@ -2143,7 +2301,7 @@ function canonicalElementInfoForCodeLayerNode(
|
|
|
2143
2301
|
...info,
|
|
2144
2302
|
sourceId: bridgeSourceIdForCodeLayerNode(node),
|
|
2145
2303
|
selector: preferredCodeLayerSelector(node),
|
|
2146
|
-
classes: node.classes
|
|
2304
|
+
classes: node.classes,
|
|
2147
2305
|
confidence: node.confidence,
|
|
2148
2306
|
editCapabilities: info.editCapabilities?.some((capability) =>
|
|
2149
2307
|
capability.kind.startsWith("deterministic"),
|
|
@@ -2188,27 +2346,52 @@ function codeLayerPatchMessage(
|
|
|
2188
2346
|
: message;
|
|
2189
2347
|
}
|
|
2190
2348
|
|
|
2191
|
-
function
|
|
2349
|
+
export function refreshElementInfoFromContent(
|
|
2192
2350
|
content: string,
|
|
2193
2351
|
info: ElementInfo | null,
|
|
2194
|
-
):
|
|
2195
|
-
if (!info) return
|
|
2352
|
+
): ElementInfo | null {
|
|
2353
|
+
if (!info) return null;
|
|
2196
2354
|
const projection = buildCodeLayerProjection(content);
|
|
2197
|
-
|
|
2355
|
+
const node =
|
|
2356
|
+
resolveCodeLayerNodeFromElementInfo(projection, info) ??
|
|
2198
2357
|
resolveCodeLayerNodeFromBridge(
|
|
2199
2358
|
projection,
|
|
2200
2359
|
info.selector,
|
|
2201
2360
|
info.sourceId ?? info.id,
|
|
2202
|
-
)
|
|
2203
|
-
) {
|
|
2204
|
-
|
|
2361
|
+
);
|
|
2362
|
+
if (node) {
|
|
2363
|
+
const sourceInfo = elementInfoFromCodeLayerNode(node);
|
|
2364
|
+
return {
|
|
2365
|
+
...canonicalElementInfoForCodeLayerNode(info, node),
|
|
2366
|
+
computedStyles: refreshedComputedStyles(
|
|
2367
|
+
info,
|
|
2368
|
+
sourceInfo.computedStyles,
|
|
2369
|
+
sourceInfo.classes,
|
|
2370
|
+
),
|
|
2371
|
+
textContent: sourceInfo.textContent,
|
|
2372
|
+
isFlexChild: sourceInfo.isFlexChild,
|
|
2373
|
+
isFlexContainer: sourceInfo.isFlexContainer,
|
|
2374
|
+
parentDisplay: sourceInfo.parentDisplay,
|
|
2375
|
+
};
|
|
2205
2376
|
}
|
|
2206
|
-
if (!info.selector || typeof window === "undefined") return
|
|
2377
|
+
if (!info.selector || typeof window === "undefined") return null;
|
|
2207
2378
|
try {
|
|
2208
2379
|
const doc = new DOMParser().parseFromString(content, "text/html");
|
|
2209
|
-
|
|
2380
|
+
const element = queryUniqueSelector(doc, info.selector);
|
|
2381
|
+
if (!element) return null;
|
|
2382
|
+
const classes = Array.from(element.classList);
|
|
2383
|
+
return {
|
|
2384
|
+
...info,
|
|
2385
|
+
classes,
|
|
2386
|
+
computedStyles: refreshedComputedStyles(
|
|
2387
|
+
info,
|
|
2388
|
+
parseInlineStyleAttribute(element.getAttribute("style")),
|
|
2389
|
+
classes,
|
|
2390
|
+
),
|
|
2391
|
+
textContent: element.textContent?.slice(0, 200) ?? info.textContent,
|
|
2392
|
+
};
|
|
2210
2393
|
} catch {
|
|
2211
|
-
return
|
|
2394
|
+
return null;
|
|
2212
2395
|
}
|
|
2213
2396
|
}
|
|
2214
2397
|
|
|
@@ -2300,6 +2483,41 @@ function findCodeLayerNodeInProjection(
|
|
|
2300
2483
|
return fallbackMatches.length === 1 ? (fallbackMatches[0] ?? null) : null;
|
|
2301
2484
|
}
|
|
2302
2485
|
|
|
2486
|
+
export function findMovedCodeLayerNodeInProjection(
|
|
2487
|
+
projection: CodeLayerProjection,
|
|
2488
|
+
previousNode: CodeLayerNode,
|
|
2489
|
+
movedNodeId?: string | null,
|
|
2490
|
+
): CodeLayerNode | null {
|
|
2491
|
+
if (movedNodeId) {
|
|
2492
|
+
const movedMatch = projection.nodes.find(
|
|
2493
|
+
(node) =>
|
|
2494
|
+
node.id === movedNodeId ||
|
|
2495
|
+
node.dataAttributes["data-agent-native-node-id"] === movedNodeId ||
|
|
2496
|
+
node.dataAttributes["data-code-layer-id"] === movedNodeId ||
|
|
2497
|
+
node.dataAttributes["data-layer-id"] === movedNodeId ||
|
|
2498
|
+
node.dataAttributes["data-builder-id"] === movedNodeId ||
|
|
2499
|
+
node.dataAttributes["data-loc"] === movedNodeId ||
|
|
2500
|
+
node.attributes.id === movedNodeId,
|
|
2501
|
+
);
|
|
2502
|
+
if (movedMatch) return movedMatch;
|
|
2503
|
+
}
|
|
2504
|
+
return findCodeLayerNodeInProjection(projection, previousNode);
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
export function parseInlineStyleAttribute(
|
|
2508
|
+
style: string | null | undefined,
|
|
2509
|
+
): Record<string, string> {
|
|
2510
|
+
const result: Record<string, string> = {};
|
|
2511
|
+
for (const declaration of (style ?? "").split(";")) {
|
|
2512
|
+
const separator = declaration.indexOf(":");
|
|
2513
|
+
if (separator <= 0) continue;
|
|
2514
|
+
const property = declaration.slice(0, separator).trim();
|
|
2515
|
+
const value = declaration.slice(separator + 1).trim();
|
|
2516
|
+
if (property && value) result[property] = value;
|
|
2517
|
+
}
|
|
2518
|
+
return result;
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2303
2521
|
function AgentNativeMenuMark({ className }: { className?: string }) {
|
|
2304
2522
|
return (
|
|
2305
2523
|
<svg
|
|
@@ -2326,6 +2544,110 @@ function AgentNativeMenuMark({ className }: { className?: string }) {
|
|
|
2326
2544
|
);
|
|
2327
2545
|
}
|
|
2328
2546
|
|
|
2547
|
+
type DesignLeftPanel = "file" | "agent" | "assets" | "tools" | "tokens";
|
|
2548
|
+
|
|
2549
|
+
function normalizeDesignLeftPanel(value: unknown): DesignLeftPanel | undefined {
|
|
2550
|
+
if (value === "extensions") return "tools";
|
|
2551
|
+
return value === "file" ||
|
|
2552
|
+
value === "agent" ||
|
|
2553
|
+
value === "assets" ||
|
|
2554
|
+
value === "tools" ||
|
|
2555
|
+
value === "tokens"
|
|
2556
|
+
? value
|
|
2557
|
+
: undefined;
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
function DesignWorkspaceRail({
|
|
2561
|
+
activePanel,
|
|
2562
|
+
projectMenu,
|
|
2563
|
+
onPanelChange,
|
|
2564
|
+
}: {
|
|
2565
|
+
activePanel: DesignLeftPanel;
|
|
2566
|
+
projectMenu: ReactNode;
|
|
2567
|
+
onPanelChange: (panel: DesignLeftPanel) => void;
|
|
2568
|
+
}) {
|
|
2569
|
+
const t = useT();
|
|
2570
|
+
const items: Array<{
|
|
2571
|
+
panel: DesignLeftPanel;
|
|
2572
|
+
label: string;
|
|
2573
|
+
icon: ReactNode;
|
|
2574
|
+
}> = [
|
|
2575
|
+
{
|
|
2576
|
+
panel: "file",
|
|
2577
|
+
label: t("designEditor.leftRail.file"),
|
|
2578
|
+
icon: <IconFile className="size-[15px]" />,
|
|
2579
|
+
},
|
|
2580
|
+
{
|
|
2581
|
+
panel: "agent",
|
|
2582
|
+
label: t("designEditor.leftRail.agent"),
|
|
2583
|
+
icon: <IconMessage className="size-[15px]" />,
|
|
2584
|
+
},
|
|
2585
|
+
{
|
|
2586
|
+
panel: "assets",
|
|
2587
|
+
label: t("designEditor.leftRail.assets"),
|
|
2588
|
+
icon: <IconPhoto className="size-[15px]" />,
|
|
2589
|
+
},
|
|
2590
|
+
{
|
|
2591
|
+
panel: "tools",
|
|
2592
|
+
label: t("designEditor.leftRail.tools"),
|
|
2593
|
+
icon: <IconTerminal2 className="size-[15px]" />,
|
|
2594
|
+
},
|
|
2595
|
+
{
|
|
2596
|
+
panel: "tokens",
|
|
2597
|
+
label: t("designEditor.leftRail.tokens"),
|
|
2598
|
+
icon: <IconAssembly className="size-[15px]" />,
|
|
2599
|
+
},
|
|
2600
|
+
];
|
|
2601
|
+
|
|
2602
|
+
return (
|
|
2603
|
+
<nav
|
|
2604
|
+
aria-label={t("designEditor.leftRail.label")}
|
|
2605
|
+
className="flex w-[52px] shrink-0 flex-col items-center border-r border-[var(--design-editor-panel-divider-color)] bg-[var(--design-editor-panel-bg)] py-3"
|
|
2606
|
+
>
|
|
2607
|
+
<div className="mb-3 flex h-8 items-center justify-center">
|
|
2608
|
+
{projectMenu}
|
|
2609
|
+
</div>
|
|
2610
|
+
<div className="mb-5 h-px w-8 bg-border/70" />
|
|
2611
|
+
<div className="flex min-h-0 flex-1 flex-col items-center gap-4">
|
|
2612
|
+
{items.map((item) => {
|
|
2613
|
+
const active = item.panel === activePanel;
|
|
2614
|
+
return (
|
|
2615
|
+
<Tooltip key={item.panel}>
|
|
2616
|
+
<TooltipTrigger asChild>
|
|
2617
|
+
<button
|
|
2618
|
+
type="button"
|
|
2619
|
+
aria-label={item.label}
|
|
2620
|
+
aria-current={active ? "page" : undefined}
|
|
2621
|
+
onClick={() => onPanelChange(item.panel)}
|
|
2622
|
+
className={cn(
|
|
2623
|
+
"group flex w-12 cursor-pointer flex-col items-center justify-start gap-1.5 rounded-none text-[10px] font-medium leading-none text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)]",
|
|
2624
|
+
active && "text-foreground",
|
|
2625
|
+
)}
|
|
2626
|
+
>
|
|
2627
|
+
<span
|
|
2628
|
+
className={cn(
|
|
2629
|
+
"flex size-8 items-center justify-center rounded-lg transition-colors",
|
|
2630
|
+
active
|
|
2631
|
+
? "bg-[var(--design-editor-selection-color)] text-[var(--design-editor-accent-color)]"
|
|
2632
|
+
: "text-muted-foreground group-hover:bg-[var(--design-editor-layer-hover-color)] group-hover:text-foreground",
|
|
2633
|
+
)}
|
|
2634
|
+
>
|
|
2635
|
+
{item.icon}
|
|
2636
|
+
</span>
|
|
2637
|
+
<span className="max-w-full truncate leading-none">
|
|
2638
|
+
{item.label}
|
|
2639
|
+
</span>
|
|
2640
|
+
</button>
|
|
2641
|
+
</TooltipTrigger>
|
|
2642
|
+
<TooltipContent side="right">{item.label}</TooltipContent>
|
|
2643
|
+
</Tooltip>
|
|
2644
|
+
);
|
|
2645
|
+
})}
|
|
2646
|
+
</div>
|
|
2647
|
+
</nav>
|
|
2648
|
+
);
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2329
2651
|
interface DesignCollaborator {
|
|
2330
2652
|
user: CollabUser;
|
|
2331
2653
|
image?: string;
|
|
@@ -2524,7 +2846,13 @@ function DesignToolbarTool({
|
|
|
2524
2846
|
? "bg-[var(--design-editor-accent-color)] text-white"
|
|
2525
2847
|
: "hover:bg-white/10 hover:text-white",
|
|
2526
2848
|
)}
|
|
2527
|
-
onClick={
|
|
2849
|
+
onClick={(event) => {
|
|
2850
|
+
if (event.detail === 0) onPrimary();
|
|
2851
|
+
}}
|
|
2852
|
+
onPointerDown={(event) => {
|
|
2853
|
+
if (event.button !== 0) return;
|
|
2854
|
+
onPrimary();
|
|
2855
|
+
}}
|
|
2528
2856
|
aria-label={label}
|
|
2529
2857
|
aria-pressed={active}
|
|
2530
2858
|
>
|
|
@@ -3141,6 +3469,10 @@ export default function DesignEditor() {
|
|
|
3141
3469
|
const appStateVersion = useChangeVersion("app-state");
|
|
3142
3470
|
const browserTabId = getBrowserTabId();
|
|
3143
3471
|
const embedded = isEmbedAuthActive();
|
|
3472
|
+
const designChatScope = useMemo(
|
|
3473
|
+
() => (id ? ({ type: "design" as const, id } as const) : null),
|
|
3474
|
+
[id],
|
|
3475
|
+
);
|
|
3144
3476
|
|
|
3145
3477
|
const isBuilderDesignEmbed = useMemo(() => {
|
|
3146
3478
|
if (typeof window === "undefined") return false;
|
|
@@ -3185,6 +3517,8 @@ export default function DesignEditor() {
|
|
|
3185
3517
|
const [contentRenderRevision, setContentRenderRevision] = useState(0);
|
|
3186
3518
|
const [activeInspectorTab, setActiveInspectorTab] =
|
|
3187
3519
|
useState<InspectorTab>("design");
|
|
3520
|
+
const [activeLeftPanel, setActiveLeftPanel] =
|
|
3521
|
+
useState<DesignLeftPanel>("file");
|
|
3188
3522
|
const [leftSidebarWidth, setLeftSidebarWidth] = useState(256);
|
|
3189
3523
|
const [rightSidebarWidth, setRightSidebarWidth] = useState(256);
|
|
3190
3524
|
const [layersSearchQuery, setLayersSearchQuery] = useState("");
|
|
@@ -3195,7 +3529,79 @@ export default function DesignEditor() {
|
|
|
3195
3529
|
const [overviewSelectedScreenIds, setOverviewSelectedScreenIds] = useState<
|
|
3196
3530
|
string[]
|
|
3197
3531
|
>([]);
|
|
3532
|
+
const [createdOverviewLayerSelection, setCreatedOverviewLayerSelection] =
|
|
3533
|
+
useState<{ screenId: string; layerId: string } | null>(null);
|
|
3198
3534
|
const pendingOverviewScreenSelectionRef = useRef<string | null>(null);
|
|
3535
|
+
const pendingOverviewLayerSelectionRef = useRef<string | null>(null);
|
|
3536
|
+
// Tracks the nodeId of the most recently created TEXT primitive across one
|
|
3537
|
+
// handleCreatePrimitive → handlePrimitiveCreated round-trip. Cleared after
|
|
3538
|
+
// use. Lets handlePrimitiveCreated trigger begin-text-edit without needing
|
|
3539
|
+
// the primitive kind in its signature.
|
|
3540
|
+
const pendingTextEditNodeIdRef = useRef<string | null>(null);
|
|
3541
|
+
const pendingOverviewLayerSelectionClearTimerRef = useRef<number | null>(
|
|
3542
|
+
null,
|
|
3543
|
+
);
|
|
3544
|
+
|
|
3545
|
+
useEffect(() => {
|
|
3546
|
+
const focusAgentComposer = () => {
|
|
3547
|
+
requestAnimationFrame(() => {
|
|
3548
|
+
const panel = document.querySelector("[data-design-agent-panel]");
|
|
3549
|
+
const prosemirror = panel?.querySelector(
|
|
3550
|
+
".ProseMirror",
|
|
3551
|
+
) as HTMLElement | null;
|
|
3552
|
+
if (prosemirror) {
|
|
3553
|
+
prosemirror.focus();
|
|
3554
|
+
return;
|
|
3555
|
+
}
|
|
3556
|
+
const textarea = panel?.querySelector("textarea") as HTMLElement | null;
|
|
3557
|
+
textarea?.focus();
|
|
3558
|
+
});
|
|
3559
|
+
};
|
|
3560
|
+
const openAgentPanel = () => {
|
|
3561
|
+
setActiveLeftPanel("agent");
|
|
3562
|
+
focusAgentComposer();
|
|
3563
|
+
};
|
|
3564
|
+
const toggleAgentPanel = () =>
|
|
3565
|
+
setActiveLeftPanel((current) => {
|
|
3566
|
+
const next = current === "agent" ? "file" : "agent";
|
|
3567
|
+
if (next === "agent") focusAgentComposer();
|
|
3568
|
+
return next;
|
|
3569
|
+
});
|
|
3570
|
+
window.addEventListener("agent-panel:open", openAgentPanel);
|
|
3571
|
+
window.addEventListener("agent-panel:toggle", toggleAgentPanel);
|
|
3572
|
+
return () => {
|
|
3573
|
+
window.removeEventListener("agent-panel:open", openAgentPanel);
|
|
3574
|
+
window.removeEventListener("agent-panel:toggle", toggleAgentPanel);
|
|
3575
|
+
};
|
|
3576
|
+
}, []);
|
|
3577
|
+
|
|
3578
|
+
const clearPendingOverviewLayerSelectionTimer = useCallback(() => {
|
|
3579
|
+
if (pendingOverviewLayerSelectionClearTimerRef.current === null) return;
|
|
3580
|
+
window.clearTimeout(pendingOverviewLayerSelectionClearTimerRef.current);
|
|
3581
|
+
pendingOverviewLayerSelectionClearTimerRef.current = null;
|
|
3582
|
+
}, []);
|
|
3583
|
+
const schedulePendingOverviewLayerSelectionClear = useCallback(
|
|
3584
|
+
(layerId: string) => {
|
|
3585
|
+
clearPendingOverviewLayerSelectionTimer();
|
|
3586
|
+
pendingOverviewLayerSelectionClearTimerRef.current = window.setTimeout(
|
|
3587
|
+
() => {
|
|
3588
|
+
if (pendingOverviewLayerSelectionRef.current === layerId) {
|
|
3589
|
+
pendingOverviewLayerSelectionRef.current = null;
|
|
3590
|
+
}
|
|
3591
|
+
setCreatedOverviewLayerSelection((current) =>
|
|
3592
|
+
current?.layerId === layerId ? null : current,
|
|
3593
|
+
);
|
|
3594
|
+
pendingOverviewLayerSelectionClearTimerRef.current = null;
|
|
3595
|
+
},
|
|
3596
|
+
1800,
|
|
3597
|
+
);
|
|
3598
|
+
},
|
|
3599
|
+
[clearPendingOverviewLayerSelectionTimer],
|
|
3600
|
+
);
|
|
3601
|
+
useEffect(
|
|
3602
|
+
() => clearPendingOverviewLayerSelectionTimer,
|
|
3603
|
+
[clearPendingOverviewLayerSelectionTimer],
|
|
3604
|
+
);
|
|
3199
3605
|
const [lockedLayerIds, setLockedLayerIds] = useState<Set<string>>(
|
|
3200
3606
|
() => new Set(),
|
|
3201
3607
|
);
|
|
@@ -3220,10 +3626,63 @@ export default function DesignEditor() {
|
|
|
3220
3626
|
|
|
3221
3627
|
// ── Motion dock state (§6.3) ────────────────────────────────────────────────
|
|
3222
3628
|
// The MotionDock is mounted below the canvas and shown when motionDockOpen.
|
|
3223
|
-
// Tracks and durationMs are local state;
|
|
3629
|
+
// Tracks and durationMs are local state; edits autosave via applyMotionEdit.
|
|
3224
3630
|
const [motionDockOpen, setMotionDockOpen] = useState(false);
|
|
3631
|
+
const [motionDockMounted, setMotionDockMounted] = useState(false);
|
|
3632
|
+
const motionDockUnmountTimerRef = useRef<number | null>(null);
|
|
3633
|
+
const [motionTimelineId, setMotionTimelineId] = useState<string | null>(null);
|
|
3225
3634
|
const [motionTracks, setMotionTracks] = useState<MotionDockTrack[]>([]);
|
|
3226
3635
|
const [motionDurationMs, setMotionDurationMs] = useState(1000);
|
|
3636
|
+
const [motionTracksDirty, setMotionTracksDirty] = useState(false);
|
|
3637
|
+
const [motionAutosaveRevision, setMotionAutosaveRevision] = useState(0);
|
|
3638
|
+
const [motionHydrationFingerprint, setMotionHydrationFingerprint] = useState<
|
|
3639
|
+
string | null
|
|
3640
|
+
>(null);
|
|
3641
|
+
const motionAutosaveRevisionRef = useRef(0);
|
|
3642
|
+
const motionAutosaveFailedRevisionRef = useRef<number | null>(null);
|
|
3643
|
+
const motionAutosaveTimerRef = useRef<number | null>(null);
|
|
3644
|
+
const lastScheduledMotionAutosaveRevisionRef = useRef(0);
|
|
3645
|
+
const previousMotionFileIdRef = useRef<string | null>(null);
|
|
3646
|
+
const clearMotionDockUnmountTimer = useCallback(() => {
|
|
3647
|
+
if (motionDockUnmountTimerRef.current === null) return;
|
|
3648
|
+
window.clearTimeout(motionDockUnmountTimerRef.current);
|
|
3649
|
+
motionDockUnmountTimerRef.current = null;
|
|
3650
|
+
}, []);
|
|
3651
|
+
const clearMotionAutosaveTimer = useCallback(() => {
|
|
3652
|
+
if (motionAutosaveTimerRef.current === null) return;
|
|
3653
|
+
window.clearTimeout(motionAutosaveTimerRef.current);
|
|
3654
|
+
motionAutosaveTimerRef.current = null;
|
|
3655
|
+
}, []);
|
|
3656
|
+
const setMotionDockOpenAnimated = useCallback(
|
|
3657
|
+
(open: boolean) => {
|
|
3658
|
+
clearMotionDockUnmountTimer();
|
|
3659
|
+
if (open) {
|
|
3660
|
+
setMotionDockMounted(true);
|
|
3661
|
+
if (typeof window === "undefined") {
|
|
3662
|
+
setMotionDockOpen(true);
|
|
3663
|
+
return;
|
|
3664
|
+
}
|
|
3665
|
+
window.requestAnimationFrame(() => setMotionDockOpen(true));
|
|
3666
|
+
return;
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
setMotionDockOpen(false);
|
|
3670
|
+
if (typeof window === "undefined") {
|
|
3671
|
+
setMotionDockMounted(false);
|
|
3672
|
+
return;
|
|
3673
|
+
}
|
|
3674
|
+
motionDockUnmountTimerRef.current = window.setTimeout(() => {
|
|
3675
|
+
setMotionDockMounted(false);
|
|
3676
|
+
motionDockUnmountTimerRef.current = null;
|
|
3677
|
+
}, MOTION_DOCK_TRANSITION_MS);
|
|
3678
|
+
},
|
|
3679
|
+
[clearMotionDockUnmountTimer],
|
|
3680
|
+
);
|
|
3681
|
+
useEffect(
|
|
3682
|
+
() => () => clearMotionDockUnmountTimer(),
|
|
3683
|
+
[clearMotionDockUnmountTimer],
|
|
3684
|
+
);
|
|
3685
|
+
useEffect(() => () => clearMotionAutosaveTimer(), [clearMotionAutosaveTimer]);
|
|
3227
3686
|
const [shaderFillPreview, setShaderFillPreview] = useState<{
|
|
3228
3687
|
selector?: string;
|
|
3229
3688
|
nodeId?: string;
|
|
@@ -3302,9 +3761,7 @@ export default function DesignEditor() {
|
|
|
3302
3761
|
}, [isBuilderDesignEmbed]);
|
|
3303
3762
|
|
|
3304
3763
|
const focusDesignInspectorForSelection = useCallback(() => {
|
|
3305
|
-
setActiveInspectorTab(
|
|
3306
|
-
current === "extensions" ? current : "design",
|
|
3307
|
-
);
|
|
3764
|
+
setActiveInspectorTab("design");
|
|
3308
3765
|
}, []);
|
|
3309
3766
|
|
|
3310
3767
|
useEffect(() => {
|
|
@@ -3313,9 +3770,7 @@ export default function DesignEditor() {
|
|
|
3313
3770
|
|
|
3314
3771
|
useEffect(() => {
|
|
3315
3772
|
if (hasSelectedElement) return;
|
|
3316
|
-
setActiveInspectorTab(
|
|
3317
|
-
current === "extensions" ? current : "tweaks",
|
|
3318
|
-
);
|
|
3773
|
+
setActiveInspectorTab("tweaks");
|
|
3319
3774
|
}, [hasSelectedElement]);
|
|
3320
3775
|
|
|
3321
3776
|
const startSidebarResize = useCallback(
|
|
@@ -3378,8 +3833,8 @@ export default function DesignEditor() {
|
|
|
3378
3833
|
const suppressContentHistoryRef = useRef(false);
|
|
3379
3834
|
const geometryUndoStackRef = useRef<GeometryHistoryEntry[]>([]);
|
|
3380
3835
|
const geometryRedoStackRef = useRef<GeometryHistoryEntry[]>([]);
|
|
3381
|
-
const historyOrderRef = useRef<
|
|
3382
|
-
const redoOrderRef = useRef<
|
|
3836
|
+
const historyOrderRef = useRef<UndoRedoOrderKind[]>([]);
|
|
3837
|
+
const redoOrderRef = useRef<UndoRedoOrderKind[]>([]);
|
|
3383
3838
|
const syncUndoRedoState = useCallback(() => {
|
|
3384
3839
|
const undoManager = undoManagerRef.current;
|
|
3385
3840
|
setCanUndo(
|
|
@@ -3393,6 +3848,23 @@ export default function DesignEditor() {
|
|
|
3393
3848
|
geometryRedoStackRef.current.length > 0,
|
|
3394
3849
|
);
|
|
3395
3850
|
}, []);
|
|
3851
|
+
const recordContentHistoryEntry = useCallback(
|
|
3852
|
+
(entry: ContentHistoryEntry) => {
|
|
3853
|
+
if (entry.before === entry.after) return;
|
|
3854
|
+
contentUndoStackRef.current = [
|
|
3855
|
+
...contentUndoStackRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
|
|
3856
|
+
entry,
|
|
3857
|
+
];
|
|
3858
|
+
contentRedoStackRef.current = [];
|
|
3859
|
+
historyOrderRef.current = [
|
|
3860
|
+
...historyOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
|
|
3861
|
+
"file-content",
|
|
3862
|
+
];
|
|
3863
|
+
redoOrderRef.current = [];
|
|
3864
|
+
syncUndoRedoState();
|
|
3865
|
+
},
|
|
3866
|
+
[syncUndoRedoState],
|
|
3867
|
+
);
|
|
3396
3868
|
const clearLocalUndoRedoStacks = useCallback(() => {
|
|
3397
3869
|
contentUndoStackRef.current = [];
|
|
3398
3870
|
contentRedoStackRef.current = [];
|
|
@@ -3408,6 +3880,18 @@ export default function DesignEditor() {
|
|
|
3408
3880
|
// Dismissible localhost-source banner (reset per session).
|
|
3409
3881
|
const [localSourceBannerDismissed, setLocalSourceBannerDismissed] =
|
|
3410
3882
|
useState(false);
|
|
3883
|
+
// Localhost write-consent dialog state. When the agent wants to write a local
|
|
3884
|
+
// file and no valid grant exists for the active connection, we show the dialog
|
|
3885
|
+
// with a pending payload; the user clicks "Allow writes" to mint a grant.
|
|
3886
|
+
const [localhostWriteConsentOpen, setLocalhostWriteConsentOpen] =
|
|
3887
|
+
useState(false);
|
|
3888
|
+
const [localhostWriteConsentPayload, setLocalhostWriteConsentPayload] =
|
|
3889
|
+
useState<LocalhostWriteConsentPayload | null>(null);
|
|
3890
|
+
// Active localhost connection id for the consent dialog.
|
|
3891
|
+
const [localhostConsentConnectionId, setLocalhostConsentConnectionId] =
|
|
3892
|
+
useState<string>("");
|
|
3893
|
+
// Tracks whether an "Apply to source" write is in progress.
|
|
3894
|
+
const [applyToSourcePending, setApplyToSourcePending] = useState(false);
|
|
3411
3895
|
// Shared visual-editor annotate overlays. drawMode owns the send toolbar,
|
|
3412
3896
|
// while pinMode temporarily routes canvas clicks to comment pins that queue
|
|
3413
3897
|
// into the same agent submission.
|
|
@@ -3819,6 +4303,8 @@ export default function DesignEditor() {
|
|
|
3819
4303
|
const exportHtmlMutation = useActionMutation("export-html");
|
|
3820
4304
|
const exportZipMutation = useActionMutation("export-zip");
|
|
3821
4305
|
const applyMotionEditMutation = useActionMutation("apply-motion-edit");
|
|
4306
|
+
const applyMotionEdit = applyMotionEditMutation.mutate;
|
|
4307
|
+
const motionAutosavePending = applyMotionEditMutation.isPending;
|
|
3822
4308
|
// §6.4 breakpoint mutations — wired to MultiScreenCanvas + affordance
|
|
3823
4309
|
const addBreakpointMutation = useActionMutation("add-breakpoint");
|
|
3824
4310
|
const setActiveBreakpointMutation = useActionMutation(
|
|
@@ -3832,12 +4318,28 @@ export default function DesignEditor() {
|
|
|
3832
4318
|
"open-component-source",
|
|
3833
4319
|
);
|
|
3834
4320
|
|
|
4321
|
+
// Board file migration — lazy, idempotent, triggers on design open when
|
|
4322
|
+
// designs.data.boardFileId is absent.
|
|
4323
|
+
const migrateBoardObjectsMutation = useActionMutation(
|
|
4324
|
+
"migrate-board-objects-to-file",
|
|
4325
|
+
);
|
|
4326
|
+
|
|
3835
4327
|
// §6.6 — "Make it real" migration flow (migrate-inline-design-to-app).
|
|
3836
4328
|
// The mutation stays unconditional; the dialog gates on isSignedIn.
|
|
3837
4329
|
const migrateMutation = useActionMutation("migrate-inline-design-to-app");
|
|
3838
4330
|
|
|
3839
4331
|
// Dialog open/close state for the "Make this a real app" flow.
|
|
3840
4332
|
const [makeRealDialogOpen, setMakeRealDialogOpen] = useState(false);
|
|
4333
|
+
const [publishWaitlistPopoverOpen, setPublishWaitlistPopoverOpen] =
|
|
4334
|
+
useState(false);
|
|
4335
|
+
const [publishWaitlistPopoverView, setPublishWaitlistPopoverView] = useState<
|
|
4336
|
+
"actions" | "waitlist"
|
|
4337
|
+
>("actions");
|
|
4338
|
+
const [publishWaitlistJoined, setPublishWaitlistJoined] = useState(false);
|
|
4339
|
+
const [joiningPublishWaitlist, setJoiningPublishWaitlist] = useState(false);
|
|
4340
|
+
const [publishWaitlistError, setPublishWaitlistError] = useState<
|
|
4341
|
+
string | null
|
|
4342
|
+
>(null);
|
|
3841
4343
|
|
|
3842
4344
|
// Result payload returned by migrate-inline-design-to-app on success.
|
|
3843
4345
|
// `null` = not yet migrated; populated once the Builder agent accepts the job.
|
|
@@ -3878,6 +4380,16 @@ export default function DesignEditor() {
|
|
|
3878
4380
|
const fileSaveTimersRef = useRef<Record<string, number>>({});
|
|
3879
4381
|
const postAuthSaveRef = useRef<string | null>(null);
|
|
3880
4382
|
|
|
4383
|
+
const cancelQueuedFileContentSave = useCallback((fileId: string) => {
|
|
4384
|
+
const timer = fileSaveTimersRef.current[fileId];
|
|
4385
|
+
if (timer) {
|
|
4386
|
+
window.clearTimeout(timer);
|
|
4387
|
+
delete fileSaveTimersRef.current[fileId];
|
|
4388
|
+
}
|
|
4389
|
+
delete pendingFileSavesRef.current[fileId];
|
|
4390
|
+
delete latestFileSaveForUnloadRef.current[fileId];
|
|
4391
|
+
}, []);
|
|
4392
|
+
|
|
3881
4393
|
const saveFileContent = useCallback(
|
|
3882
4394
|
(pending: FileContentSaveRequest) => {
|
|
3883
4395
|
if (!canEditDesignRef.current) return;
|
|
@@ -4258,6 +4770,39 @@ export default function DesignEditor() {
|
|
|
4258
4770
|
() => getCanvasFrameGeometry(designDataJson),
|
|
4259
4771
|
[designDataJson],
|
|
4260
4772
|
);
|
|
4773
|
+
|
|
4774
|
+
// ── Board file ─────────────────────────────────────────────────────────────
|
|
4775
|
+
// The board is a reserved design_file (filename "__board__.html") whose id is
|
|
4776
|
+
// stored in designs.data.boardFileId. On design open, if boardFileId is absent,
|
|
4777
|
+
// we trigger migrate-board-objects-to-file (lazy + idempotent) which creates
|
|
4778
|
+
// the board file and migrates any legacy boardObjects.
|
|
4779
|
+
const boardFileId = useMemo(() => {
|
|
4780
|
+
const raw = (designDataJson as Record<string, unknown>).boardFileId;
|
|
4781
|
+
return typeof raw === "string" && raw.length > 0 ? raw : undefined;
|
|
4782
|
+
}, [designDataJson]);
|
|
4783
|
+
|
|
4784
|
+
// Trigger migration on design open when boardFileId is absent.
|
|
4785
|
+
const migrateBoardTriggeredRef = useRef<string | null>(null);
|
|
4786
|
+
useEffect(() => {
|
|
4787
|
+
if (!id || !canEditDesign) return;
|
|
4788
|
+
if (boardFileId) return; // already migrated
|
|
4789
|
+
if (migrateBoardTriggeredRef.current === id) return;
|
|
4790
|
+
migrateBoardTriggeredRef.current = id;
|
|
4791
|
+
migrateBoardObjectsMutation.mutate({ designId: id } as any, {
|
|
4792
|
+
onSuccess: () => {
|
|
4793
|
+
void queryClient.invalidateQueries({
|
|
4794
|
+
queryKey: ["action", "get-design", { id }],
|
|
4795
|
+
});
|
|
4796
|
+
},
|
|
4797
|
+
});
|
|
4798
|
+
}, [
|
|
4799
|
+
boardFileId,
|
|
4800
|
+
canEditDesign,
|
|
4801
|
+
id,
|
|
4802
|
+
migrateBoardObjectsMutation,
|
|
4803
|
+
queryClient,
|
|
4804
|
+
]);
|
|
4805
|
+
|
|
4261
4806
|
const overviewScreens = useMemo(() => {
|
|
4262
4807
|
const metadataByFileId = getDesignDataRecord(
|
|
4263
4808
|
designDataJson,
|
|
@@ -4295,45 +4840,82 @@ export default function DesignEditor() {
|
|
|
4295
4840
|
? breakpointSet.breakpoints.map((bp) => bp.widthPx)
|
|
4296
4841
|
: undefined;
|
|
4297
4842
|
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4843
|
+
// Exclude the board file — it is rendered by its own DesignCanvas instance
|
|
4844
|
+
// in MultiScreenCanvas and must not appear as a screen frame.
|
|
4845
|
+
return files
|
|
4846
|
+
.filter((file) => !isBoardFile(file.filename))
|
|
4847
|
+
.map((file) => {
|
|
4848
|
+
const metadata = getDesignDataRecord(metadataByFileId, file.id);
|
|
4849
|
+
const stringValue = (key: string) =>
|
|
4850
|
+
typeof metadata[key] === "string"
|
|
4851
|
+
? (metadata[key] as string)
|
|
4852
|
+
: undefined;
|
|
4853
|
+
const numberValue = (key: string) =>
|
|
4854
|
+
typeof metadata[key] === "number" && Number.isFinite(metadata[key])
|
|
4855
|
+
? (metadata[key] as number)
|
|
4856
|
+
: undefined;
|
|
4857
|
+
return {
|
|
4858
|
+
id: file.id,
|
|
4859
|
+
filename: file.filename,
|
|
4860
|
+
content: file.content,
|
|
4861
|
+
updatedAt: file.updatedAt,
|
|
4862
|
+
sourceType: stringValue("sourceType"),
|
|
4863
|
+
source: stringValue("source"),
|
|
4864
|
+
sourceFile: stringValue("sourceFile"),
|
|
4865
|
+
connectionId: stringValue("connectionId"),
|
|
4866
|
+
lod: stringValue("lod"),
|
|
4867
|
+
previewState: stringValue("previewState"),
|
|
4868
|
+
status: stringValue("status"),
|
|
4869
|
+
title: stringValue("title"),
|
|
4870
|
+
width: numberValue("width"),
|
|
4871
|
+
height: numberValue("height"),
|
|
4872
|
+
url: stringValue("url"),
|
|
4873
|
+
previewUrl: stringValue("previewUrl"),
|
|
4874
|
+
// Breakpoint preview widths (§6.4). When non-empty, MultiScreenCanvas
|
|
4875
|
+
// renders one iframe per width to the right of the primary frame.
|
|
4876
|
+
breakpointWidths: bpWidths,
|
|
4877
|
+
// Active breakpoint width tracked in component state; shared across all
|
|
4878
|
+
// screens (a design has one active breakpoint set at a time in v1).
|
|
4879
|
+
activeBreakpointWidth: bpWidths?.includes(
|
|
4880
|
+
activeBreakpointWidthState ?? -1,
|
|
4881
|
+
)
|
|
4882
|
+
? activeBreakpointWidthState
|
|
4883
|
+
: undefined,
|
|
4884
|
+
};
|
|
4885
|
+
});
|
|
4886
|
+
}, [designDataJson, files, activeBreakpointWidthState, boardFileId]);
|
|
4887
|
+
|
|
4888
|
+
// The board file's current HTML content — sourced from the files array (which
|
|
4889
|
+
// includes pending local writes). undefined when boardFileId is not yet set.
|
|
4890
|
+
const boardFileContent = useMemo(() => {
|
|
4891
|
+
if (!boardFileId) return undefined;
|
|
4892
|
+
const boardFile = files.find((file) => file.id === boardFileId);
|
|
4893
|
+
return typeof boardFile?.content === "string" ? boardFile.content : "";
|
|
4894
|
+
}, [boardFileId, files]);
|
|
4895
|
+
|
|
4896
|
+
// Logical canvas-space bounding box of the board iframe. The board covers the
|
|
4897
|
+
// full canvas surface, so we compute the union of all screen frame geometries.
|
|
4898
|
+
// x:0/y:0 since screen geometries use canvas-space coords that start at origin.
|
|
4899
|
+
const boardFrameGeometry = useMemo((): FrameGeometry | undefined => {
|
|
4900
|
+
if (!boardFileId) return undefined;
|
|
4901
|
+
const entries = Object.values(canvasFrameGeometryById);
|
|
4902
|
+
if (entries.length === 0) return { x: 0, y: 0, width: 1920, height: 1080 };
|
|
4903
|
+
let maxRight = 0;
|
|
4904
|
+
let maxBottom = 0;
|
|
4905
|
+
for (const geo of entries) {
|
|
4906
|
+
const right = (geo.x ?? 0) + (geo.width ?? 0);
|
|
4907
|
+
const bottom = (geo.y ?? 0) + (geo.height ?? 0);
|
|
4908
|
+
if (right > maxRight) maxRight = right;
|
|
4909
|
+
if (bottom > maxBottom) maxBottom = bottom;
|
|
4910
|
+
}
|
|
4911
|
+
return {
|
|
4912
|
+
x: 0,
|
|
4913
|
+
y: 0,
|
|
4914
|
+
width: Math.min(Math.max(maxRight, 1920), 16384),
|
|
4915
|
+
height: Math.min(Math.max(maxBottom, 1080), 16384),
|
|
4916
|
+
};
|
|
4917
|
+
}, [boardFileId, canvasFrameGeometryById]);
|
|
4918
|
+
|
|
4337
4919
|
const queueFrameGeometrySave = useCallback(
|
|
4338
4920
|
(geometryById: CanvasFrameGeometryById) => {
|
|
4339
4921
|
if (!id || !canEditDesignRef.current) return;
|
|
@@ -4426,6 +5008,8 @@ export default function DesignEditor() {
|
|
|
4426
5008
|
},
|
|
4427
5009
|
];
|
|
4428
5010
|
geometryRedoStackRef.current = [];
|
|
5011
|
+
contentRedoStackRef.current = [];
|
|
5012
|
+
undoManagerRef.current?.clear(false, true);
|
|
4429
5013
|
historyOrderRef.current = [
|
|
4430
5014
|
...historyOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
|
|
4431
5015
|
"geometry",
|
|
@@ -4435,11 +5019,12 @@ export default function DesignEditor() {
|
|
|
4435
5019
|
beforeSnapshot,
|
|
4436
5020
|
afterSnapshot,
|
|
4437
5021
|
);
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
5022
|
+
writeFrameGeometrySnapshot(
|
|
5023
|
+
afterSnapshot,
|
|
5024
|
+
resizedFrameIds.length > 0
|
|
5025
|
+
? { syncViewportFrameIds: resizedFrameIds }
|
|
5026
|
+
: undefined,
|
|
5027
|
+
);
|
|
4443
5028
|
syncUndoRedoState();
|
|
4444
5029
|
},
|
|
4445
5030
|
[syncUndoRedoState, writeFrameGeometrySnapshot],
|
|
@@ -4553,6 +5138,7 @@ export default function DesignEditor() {
|
|
|
4553
5138
|
return;
|
|
4554
5139
|
}
|
|
4555
5140
|
|
|
5141
|
+
const shouldSkipQuestions = pending.skipQuestions === true;
|
|
4556
5142
|
const context = [
|
|
4557
5143
|
sourceContext,
|
|
4558
5144
|
`Design id: "${id}"`,
|
|
@@ -4563,18 +5149,26 @@ export default function DesignEditor() {
|
|
|
4563
5149
|
: "",
|
|
4564
5150
|
fileContext,
|
|
4565
5151
|
"",
|
|
4566
|
-
...
|
|
5152
|
+
...(shouldSkipQuestions
|
|
5153
|
+
? designGenerationDirectives(id, pendingDesignSystemId)
|
|
5154
|
+
: designIntakeQuestionDirectives(id, pendingDesignSystemId)),
|
|
4567
5155
|
].join("\n");
|
|
4568
5156
|
|
|
4569
5157
|
clearGenerationCompleteTimer();
|
|
4570
5158
|
setGenerationIssue(null);
|
|
4571
|
-
const runTabId = agentSubmit(
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
5159
|
+
const runTabId = agentSubmit(
|
|
5160
|
+
shouldSkipQuestions
|
|
5161
|
+
? `Generate design for "${design.title}": ${prompt}`
|
|
5162
|
+
: `Create design: ${prompt}`,
|
|
5163
|
+
context,
|
|
5164
|
+
{
|
|
5165
|
+
model: pending.model,
|
|
5166
|
+
engine: pending.engine,
|
|
5167
|
+
effort: pending.effort,
|
|
5168
|
+
newTab: true,
|
|
5169
|
+
images,
|
|
5170
|
+
},
|
|
5171
|
+
);
|
|
4578
5172
|
setGenerationChatTabId(runTabId);
|
|
4579
5173
|
patchPendingGeneration(id, {
|
|
4580
5174
|
runTabId,
|
|
@@ -4609,6 +5203,25 @@ export default function DesignEditor() {
|
|
|
4609
5203
|
}, [files, activeFileId]);
|
|
4610
5204
|
|
|
4611
5205
|
const activeFile = files.find((f) => f.id === activeFileId) ?? files[0];
|
|
5206
|
+
const motionTimelineQueryParams =
|
|
5207
|
+
id && activeFile?.id
|
|
5208
|
+
? { designId: id, sourceRef: activeFile.id }
|
|
5209
|
+
: { designId: "", sourceRef: "" };
|
|
5210
|
+
const { data: motionTimelineResult } =
|
|
5211
|
+
useActionQuery<MotionTimelineQueryResult>(
|
|
5212
|
+
"get-motion-timeline",
|
|
5213
|
+
motionTimelineQueryParams,
|
|
5214
|
+
{
|
|
5215
|
+
enabled: Boolean(id && activeFile?.id),
|
|
5216
|
+
refetchOnMount: "always",
|
|
5217
|
+
},
|
|
5218
|
+
);
|
|
5219
|
+
useEffect(() => {
|
|
5220
|
+
if (activeFile && !embedded) return;
|
|
5221
|
+
clearMotionDockUnmountTimer();
|
|
5222
|
+
setMotionDockOpen(false);
|
|
5223
|
+
setMotionDockMounted(false);
|
|
5224
|
+
}, [activeFile, clearMotionDockUnmountTimer, embedded]);
|
|
4612
5225
|
useEffect(() => {
|
|
4613
5226
|
if (!reviewFileId || reviewFileId === activeFile?.id) return;
|
|
4614
5227
|
setReviewFileId(null);
|
|
@@ -4722,16 +5335,18 @@ export default function DesignEditor() {
|
|
|
4722
5335
|
if (target && !targetFile) return false;
|
|
4723
5336
|
|
|
4724
5337
|
const inspectorTab =
|
|
4725
|
-
command.inspectorTab === "design" ||
|
|
4726
|
-
command.inspectorTab === "tweaks" ||
|
|
4727
|
-
command.inspectorTab === "extensions"
|
|
5338
|
+
command.inspectorTab === "design" || command.inspectorTab === "tweaks"
|
|
4728
5339
|
? command.inspectorTab
|
|
4729
|
-
: command.inspector === "design" ||
|
|
4730
|
-
command.inspector === "tweaks" ||
|
|
4731
|
-
command.inspector === "extensions"
|
|
5340
|
+
: command.inspector === "design" || command.inspector === "tweaks"
|
|
4732
5341
|
? command.inspector
|
|
4733
5342
|
: undefined;
|
|
4734
5343
|
if (inspectorTab) setActiveInspectorTab(inspectorTab);
|
|
5344
|
+
const leftPanel =
|
|
5345
|
+
normalizeDesignLeftPanel(command.leftPanel) ??
|
|
5346
|
+
normalizeDesignLeftPanel(command.panel) ??
|
|
5347
|
+
normalizeDesignLeftPanel(command.inspectorTab) ??
|
|
5348
|
+
normalizeDesignLeftPanel(command.inspector);
|
|
5349
|
+
if (leftPanel) setActiveLeftPanel(leftPanel);
|
|
4735
5350
|
|
|
4736
5351
|
const commandTool = normalizeDesignTool(command.tool);
|
|
4737
5352
|
const effectiveCommandTool =
|
|
@@ -4888,21 +5503,59 @@ export default function DesignEditor() {
|
|
|
4888
5503
|
const handleAddScreen = useCallback(() => {
|
|
4889
5504
|
if (!id || !canEditDesign) return;
|
|
4890
5505
|
const filename = nextBlankScreenFilename(files);
|
|
5506
|
+
const content = blankScreenHtml(prettyScreenName(filename));
|
|
4891
5507
|
createFileMutation.mutate(
|
|
4892
5508
|
{
|
|
4893
5509
|
designId: id,
|
|
4894
5510
|
filename,
|
|
4895
|
-
content
|
|
5511
|
+
content,
|
|
4896
5512
|
fileType: "html",
|
|
4897
5513
|
} as any,
|
|
4898
5514
|
{
|
|
4899
5515
|
onSuccess: (result: any) => {
|
|
4900
5516
|
const nextId = typeof result?.id === "string" ? result.id : null;
|
|
4901
|
-
queryClient.invalidateQueries({
|
|
4902
|
-
queryKey: ["action", "get-design"],
|
|
4903
|
-
});
|
|
4904
5517
|
if (nextId) {
|
|
5518
|
+
const now = new Date().toISOString();
|
|
5519
|
+
queryClient.setQueryData(
|
|
5520
|
+
["action", "get-design", { id }],
|
|
5521
|
+
(old: any) => {
|
|
5522
|
+
if (
|
|
5523
|
+
!old ||
|
|
5524
|
+
typeof old !== "object" ||
|
|
5525
|
+
!Array.isArray(old.files)
|
|
5526
|
+
) {
|
|
5527
|
+
return old;
|
|
5528
|
+
}
|
|
5529
|
+
const optimisticFile: DesignFile = {
|
|
5530
|
+
id: nextId,
|
|
5531
|
+
filename,
|
|
5532
|
+
fileType: "html",
|
|
5533
|
+
content,
|
|
5534
|
+
createdAt:
|
|
5535
|
+
typeof result?.createdAt === "string"
|
|
5536
|
+
? result.createdAt
|
|
5537
|
+
: now,
|
|
5538
|
+
updatedAt:
|
|
5539
|
+
typeof result?.updatedAt === "string"
|
|
5540
|
+
? result.updatedAt
|
|
5541
|
+
: now,
|
|
5542
|
+
};
|
|
5543
|
+
return {
|
|
5544
|
+
...old,
|
|
5545
|
+
files: old.files.some(
|
|
5546
|
+
(file: DesignFile) => file.id === nextId,
|
|
5547
|
+
)
|
|
5548
|
+
? old.files.map((file: DesignFile) =>
|
|
5549
|
+
file.id === nextId ? optimisticFile : file,
|
|
5550
|
+
)
|
|
5551
|
+
: [...old.files, optimisticFile],
|
|
5552
|
+
};
|
|
5553
|
+
},
|
|
5554
|
+
);
|
|
4905
5555
|
pendingOverviewScreenSelectionRef.current = nextId;
|
|
5556
|
+
pendingOverviewLayerSelectionRef.current = null;
|
|
5557
|
+
clearPendingOverviewLayerSelectionTimer();
|
|
5558
|
+
setCreatedOverviewLayerSelection(null);
|
|
4906
5559
|
setActiveFileId(nextId);
|
|
4907
5560
|
setSelectedElement(null);
|
|
4908
5561
|
setSelectedLayerIdsState([nextId]);
|
|
@@ -4912,6 +5565,9 @@ export default function DesignEditor() {
|
|
|
4912
5565
|
viewModeRef.current = "overview";
|
|
4913
5566
|
setViewMode("overview");
|
|
4914
5567
|
}
|
|
5568
|
+
queryClient.invalidateQueries({
|
|
5569
|
+
queryKey: ["action", "get-design"],
|
|
5570
|
+
});
|
|
4915
5571
|
},
|
|
4916
5572
|
onError: (error) => {
|
|
4917
5573
|
toast.error(
|
|
@@ -4922,12 +5578,21 @@ export default function DesignEditor() {
|
|
|
4922
5578
|
},
|
|
4923
5579
|
},
|
|
4924
5580
|
);
|
|
4925
|
-
}, [
|
|
5581
|
+
}, [
|
|
5582
|
+
canEditDesign,
|
|
5583
|
+
clearPendingOverviewLayerSelectionTimer,
|
|
5584
|
+
createFileMutation,
|
|
5585
|
+
files,
|
|
5586
|
+
id,
|
|
5587
|
+
queryClient,
|
|
5588
|
+
t,
|
|
5589
|
+
]);
|
|
4926
5590
|
|
|
4927
5591
|
const handleCreateScreenFrame = useCallback(
|
|
4928
5592
|
(geometry: { x: number; y: number; width: number; height: number }) => {
|
|
4929
5593
|
if (!id || !canEditDesign) return;
|
|
4930
5594
|
const filename = nextBlankScreenFilename(files);
|
|
5595
|
+
const content = blankScreenHtml(prettyScreenName(filename));
|
|
4931
5596
|
const nextGeometry = {
|
|
4932
5597
|
x: Math.round(geometry.x),
|
|
4933
5598
|
y: Math.round(geometry.y),
|
|
@@ -4938,17 +5603,54 @@ export default function DesignEditor() {
|
|
|
4938
5603
|
{
|
|
4939
5604
|
designId: id,
|
|
4940
5605
|
filename,
|
|
4941
|
-
content
|
|
5606
|
+
content,
|
|
4942
5607
|
fileType: "html",
|
|
4943
5608
|
} as any,
|
|
4944
5609
|
{
|
|
4945
5610
|
onSuccess: (result: any) => {
|
|
4946
5611
|
const nextId = typeof result?.id === "string" ? result.id : null;
|
|
4947
|
-
queryClient.invalidateQueries({
|
|
4948
|
-
queryKey: ["action", "get-design"],
|
|
4949
|
-
});
|
|
4950
5612
|
if (nextId) {
|
|
5613
|
+
const now = new Date().toISOString();
|
|
5614
|
+
queryClient.setQueryData(
|
|
5615
|
+
["action", "get-design", { id }],
|
|
5616
|
+
(old: any) => {
|
|
5617
|
+
if (
|
|
5618
|
+
!old ||
|
|
5619
|
+
typeof old !== "object" ||
|
|
5620
|
+
!Array.isArray(old.files)
|
|
5621
|
+
) {
|
|
5622
|
+
return old;
|
|
5623
|
+
}
|
|
5624
|
+
const optimisticFile: DesignFile = {
|
|
5625
|
+
id: nextId,
|
|
5626
|
+
filename,
|
|
5627
|
+
fileType: "html",
|
|
5628
|
+
content,
|
|
5629
|
+
createdAt:
|
|
5630
|
+
typeof result?.createdAt === "string"
|
|
5631
|
+
? result.createdAt
|
|
5632
|
+
: now,
|
|
5633
|
+
updatedAt:
|
|
5634
|
+
typeof result?.updatedAt === "string"
|
|
5635
|
+
? result.updatedAt
|
|
5636
|
+
: now,
|
|
5637
|
+
};
|
|
5638
|
+
return {
|
|
5639
|
+
...old,
|
|
5640
|
+
files: old.files.some(
|
|
5641
|
+
(file: DesignFile) => file.id === nextId,
|
|
5642
|
+
)
|
|
5643
|
+
? old.files.map((file: DesignFile) =>
|
|
5644
|
+
file.id === nextId ? optimisticFile : file,
|
|
5645
|
+
)
|
|
5646
|
+
: [...old.files, optimisticFile],
|
|
5647
|
+
};
|
|
5648
|
+
},
|
|
5649
|
+
);
|
|
4951
5650
|
pendingOverviewScreenSelectionRef.current = nextId;
|
|
5651
|
+
pendingOverviewLayerSelectionRef.current = null;
|
|
5652
|
+
clearPendingOverviewLayerSelectionTimer();
|
|
5653
|
+
setCreatedOverviewLayerSelection(null);
|
|
4952
5654
|
setActiveFileId(nextId);
|
|
4953
5655
|
setSelectedElement(null);
|
|
4954
5656
|
setSelectedLayerIdsState([nextId]);
|
|
@@ -4962,6 +5664,9 @@ export default function DesignEditor() {
|
|
|
4962
5664
|
[nextId]: nextGeometry,
|
|
4963
5665
|
});
|
|
4964
5666
|
}
|
|
5667
|
+
queryClient.invalidateQueries({
|
|
5668
|
+
queryKey: ["action", "get-design"],
|
|
5669
|
+
});
|
|
4965
5670
|
},
|
|
4966
5671
|
onError: (error) => {
|
|
4967
5672
|
toast.error(
|
|
@@ -4976,6 +5681,7 @@ export default function DesignEditor() {
|
|
|
4976
5681
|
[
|
|
4977
5682
|
canEditDesign,
|
|
4978
5683
|
canvasFrameGeometryById,
|
|
5684
|
+
clearPendingOverviewLayerSelectionTimer,
|
|
4979
5685
|
createFileMutation,
|
|
4980
5686
|
files,
|
|
4981
5687
|
id,
|
|
@@ -5203,11 +5909,11 @@ export default function DesignEditor() {
|
|
|
5203
5909
|
if (!isLocalEdit) {
|
|
5204
5910
|
setSelectedElement((prev) => {
|
|
5205
5911
|
if (!prev) return prev;
|
|
5206
|
-
return
|
|
5912
|
+
return refreshElementInfoFromContent(next, prev);
|
|
5207
5913
|
});
|
|
5208
5914
|
setHoveredElement((prev) => {
|
|
5209
5915
|
if (!prev) return prev;
|
|
5210
|
-
return
|
|
5916
|
+
return refreshElementInfoFromContent(next, prev);
|
|
5211
5917
|
});
|
|
5212
5918
|
}
|
|
5213
5919
|
};
|
|
@@ -5225,6 +5931,14 @@ export default function DesignEditor() {
|
|
|
5225
5931
|
if (!ydoc || !isSynced) {
|
|
5226
5932
|
undoManagerRef.current?.destroy();
|
|
5227
5933
|
undoManagerRef.current = null;
|
|
5934
|
+
historyOrderRef.current = removeUndoRedoOrderKind(
|
|
5935
|
+
historyOrderRef.current,
|
|
5936
|
+
"content",
|
|
5937
|
+
);
|
|
5938
|
+
redoOrderRef.current = removeUndoRedoOrderKind(
|
|
5939
|
+
redoOrderRef.current,
|
|
5940
|
+
"content",
|
|
5941
|
+
);
|
|
5228
5942
|
syncUndoRedoState();
|
|
5229
5943
|
return;
|
|
5230
5944
|
}
|
|
@@ -5258,7 +5972,15 @@ export default function DesignEditor() {
|
|
|
5258
5972
|
um.off("stack-cleared", syncState);
|
|
5259
5973
|
um.destroy();
|
|
5260
5974
|
undoManagerRef.current = null;
|
|
5261
|
-
|
|
5975
|
+
historyOrderRef.current = removeUndoRedoOrderKind(
|
|
5976
|
+
historyOrderRef.current,
|
|
5977
|
+
"content",
|
|
5978
|
+
);
|
|
5979
|
+
redoOrderRef.current = removeUndoRedoOrderKind(
|
|
5980
|
+
redoOrderRef.current,
|
|
5981
|
+
"content",
|
|
5982
|
+
);
|
|
5983
|
+
syncUndoRedoState();
|
|
5262
5984
|
};
|
|
5263
5985
|
}, [ydoc, isSynced, syncUndoRedoState]);
|
|
5264
5986
|
|
|
@@ -5642,6 +6364,56 @@ export default function DesignEditor() {
|
|
|
5642
6364
|
() => buildCodeLayerProjection(activeContent),
|
|
5643
6365
|
[activeContent],
|
|
5644
6366
|
);
|
|
6367
|
+
const activeMotionTimeline = motionTimelineResult?.timelines?.[0] ?? null;
|
|
6368
|
+
const activeMotionHydrationFingerprint = activeFile?.id
|
|
6369
|
+
? motionTimelineFingerprint(activeFile.id, activeMotionTimeline)
|
|
6370
|
+
: null;
|
|
6371
|
+
const activeMotionProjectionFingerprint = activeMotionHydrationFingerprint
|
|
6372
|
+
? activeMotionHydrationFingerprint
|
|
6373
|
+
: null;
|
|
6374
|
+
|
|
6375
|
+
useEffect(() => {
|
|
6376
|
+
const fileId = activeFile?.id ?? null;
|
|
6377
|
+
if (previousMotionFileIdRef.current === fileId) return;
|
|
6378
|
+
previousMotionFileIdRef.current = fileId;
|
|
6379
|
+
clearMotionAutosaveTimer();
|
|
6380
|
+
motionAutosaveRevisionRef.current = 0;
|
|
6381
|
+
motionAutosaveFailedRevisionRef.current = null;
|
|
6382
|
+
lastScheduledMotionAutosaveRevisionRef.current = 0;
|
|
6383
|
+
setMotionTimelineId(null);
|
|
6384
|
+
setMotionTracks([]);
|
|
6385
|
+
setMotionDurationMs(1000);
|
|
6386
|
+
setMotionTracksDirty(false);
|
|
6387
|
+
setMotionAutosaveRevision(0);
|
|
6388
|
+
setMotionHydrationFingerprint(null);
|
|
6389
|
+
}, [activeFile?.id, clearMotionAutosaveTimer]);
|
|
6390
|
+
|
|
6391
|
+
useEffect(() => {
|
|
6392
|
+
if (!activeFile?.id || !activeMotionProjectionFingerprint) return;
|
|
6393
|
+
if (motionTracksDirty) return;
|
|
6394
|
+
if (motionHydrationFingerprint === activeMotionProjectionFingerprint)
|
|
6395
|
+
return;
|
|
6396
|
+
|
|
6397
|
+
const hydratedTracks = activeMotionTimeline
|
|
6398
|
+
? hydrateMotionDockTracks(
|
|
6399
|
+
activeMotionTimeline.tracks,
|
|
6400
|
+
activeCodeLayerProjection,
|
|
6401
|
+
)
|
|
6402
|
+
: [];
|
|
6403
|
+
|
|
6404
|
+
setMotionTimelineId(activeMotionTimeline?.id ?? null);
|
|
6405
|
+
setMotionTracks(hydratedTracks);
|
|
6406
|
+
setMotionDurationMs(activeMotionTimeline?.durationMs ?? 1000);
|
|
6407
|
+
setMotionHydrationFingerprint(activeMotionProjectionFingerprint);
|
|
6408
|
+
}, [
|
|
6409
|
+
activeCodeLayerProjection,
|
|
6410
|
+
activeFile?.id,
|
|
6411
|
+
activeMotionProjectionFingerprint,
|
|
6412
|
+
activeMotionTimeline,
|
|
6413
|
+
motionHydrationFingerprint,
|
|
6414
|
+
motionTracksDirty,
|
|
6415
|
+
]);
|
|
6416
|
+
|
|
5645
6417
|
const selectedCodeLayerNode = useMemo(() => {
|
|
5646
6418
|
if (!selectedElement) return null;
|
|
5647
6419
|
return resolveCodeLayerNodeFromElementInfo(
|
|
@@ -5789,11 +6561,37 @@ export default function DesignEditor() {
|
|
|
5789
6561
|
return { nodeId, label };
|
|
5790
6562
|
}, [selectedCodeLayerNode, selectedElement?.tagName]);
|
|
5791
6563
|
|
|
6564
|
+
const markMotionTracksDirty = useCallback(() => {
|
|
6565
|
+
setMotionTracksDirty(true);
|
|
6566
|
+
setMotionAutosaveRevision((revision) => {
|
|
6567
|
+
const next = revision + 1;
|
|
6568
|
+
motionAutosaveRevisionRef.current = next;
|
|
6569
|
+
motionAutosaveFailedRevisionRef.current = null;
|
|
6570
|
+
return next;
|
|
6571
|
+
});
|
|
6572
|
+
}, []);
|
|
6573
|
+
|
|
6574
|
+
const handleMotionTracksChange = useCallback(
|
|
6575
|
+
(tracks: MotionDockTrack[]) => {
|
|
6576
|
+
setMotionTracks(tracks);
|
|
6577
|
+
markMotionTracksDirty();
|
|
6578
|
+
},
|
|
6579
|
+
[markMotionTracksDirty],
|
|
6580
|
+
);
|
|
6581
|
+
|
|
6582
|
+
const handleMotionDurationChange = useCallback(
|
|
6583
|
+
(durationMs: number) => {
|
|
6584
|
+
setMotionDurationMs(durationMs);
|
|
6585
|
+
markMotionTracksDirty();
|
|
6586
|
+
},
|
|
6587
|
+
[markMotionTracksDirty],
|
|
6588
|
+
);
|
|
6589
|
+
|
|
5792
6590
|
// Serialisable subset of the dock's tracks for the DesignCanvas motion-preview
|
|
5793
6591
|
// bridge. Strips the UI-only `label` field. Only populated while the dock is
|
|
5794
6592
|
// open so a closed dock never leaves preview overrides on the canvas; an empty
|
|
5795
6593
|
// array makes DesignCanvas send `motion-preview-clear`. Scrubbing previews
|
|
5796
|
-
// these tracks live in the iframe
|
|
6594
|
+
// these tracks live in the iframe; autosave only runs for track/duration edits.
|
|
5797
6595
|
const motionTracksWire = useMemo<MotionTrackWire[]>(() => {
|
|
5798
6596
|
if (!motionDockOpen || motionTracks.length === 0) return [];
|
|
5799
6597
|
return motionTracks.map(({ label: _label, ...track }) => track);
|
|
@@ -5934,6 +6732,8 @@ export default function DesignEditor() {
|
|
|
5934
6732
|
refreshPreview?: boolean;
|
|
5935
6733
|
skipPreview?: boolean;
|
|
5936
6734
|
immediateSave?: boolean;
|
|
6735
|
+
persist?: boolean;
|
|
6736
|
+
updatedAt?: string;
|
|
5937
6737
|
} = {},
|
|
5938
6738
|
) => {
|
|
5939
6739
|
if (!activeFile || !canEditDesignRef.current) return;
|
|
@@ -5950,27 +6750,21 @@ export default function DesignEditor() {
|
|
|
5950
6750
|
!yjsHistoryAvailable &&
|
|
5951
6751
|
previousContent !== nextContent
|
|
5952
6752
|
) {
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
syncUndoRedoState();
|
|
6753
|
+
recordContentHistoryEntry({
|
|
6754
|
+
fileId: activeFile.id,
|
|
6755
|
+
before: previousContent,
|
|
6756
|
+
after: nextContent,
|
|
6757
|
+
});
|
|
6758
|
+
}
|
|
6759
|
+
if (options.updatedAt) {
|
|
6760
|
+
clearPendingLocalFileContent(activeFile.id);
|
|
6761
|
+
} else {
|
|
6762
|
+
markPendingLocalFileContent(
|
|
6763
|
+
activeFile.id,
|
|
6764
|
+
nextContent,
|
|
6765
|
+
activeFile.updatedAt,
|
|
6766
|
+
);
|
|
5968
6767
|
}
|
|
5969
|
-
markPendingLocalFileContent(
|
|
5970
|
-
activeFile.id,
|
|
5971
|
-
nextContent,
|
|
5972
|
-
activeFile.updatedAt,
|
|
5973
|
-
);
|
|
5974
6768
|
setCollabContent(nextContent);
|
|
5975
6769
|
setCollabContentFileId(activeFile.id);
|
|
5976
6770
|
lastLocalContentRef.current = nextContent;
|
|
@@ -5991,7 +6785,13 @@ export default function DesignEditor() {
|
|
|
5991
6785
|
// from a client-clock timestamp can, under clock skew, make a
|
|
5992
6786
|
// later server-authored agent edit look "older" and get
|
|
5993
6787
|
// dropped by the watermark gate (agent edit silently lost).
|
|
5994
|
-
{
|
|
6788
|
+
{
|
|
6789
|
+
...file,
|
|
6790
|
+
content: nextContent,
|
|
6791
|
+
...(options.updatedAt
|
|
6792
|
+
? { updatedAt: options.updatedAt }
|
|
6793
|
+
: {}),
|
|
6794
|
+
}
|
|
5995
6795
|
: file,
|
|
5996
6796
|
),
|
|
5997
6797
|
};
|
|
@@ -6016,19 +6816,26 @@ export default function DesignEditor() {
|
|
|
6016
6816
|
}, LOCAL_EDIT_ORIGIN);
|
|
6017
6817
|
}
|
|
6018
6818
|
}
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6819
|
+
if (options.persist === false) {
|
|
6820
|
+
cancelQueuedFileContentSave(activeFile.id);
|
|
6821
|
+
} else {
|
|
6822
|
+
queueFileContentSave(activeFile.id, nextContent, {
|
|
6823
|
+
syncCollab: !(ydoc && isSynced),
|
|
6824
|
+
immediate: options.immediateSave,
|
|
6825
|
+
});
|
|
6826
|
+
}
|
|
6023
6827
|
},
|
|
6024
6828
|
[
|
|
6025
6829
|
activeFile,
|
|
6830
|
+
cancelQueuedFileContentSave,
|
|
6831
|
+
clearPendingLocalFileContent,
|
|
6026
6832
|
id,
|
|
6027
6833
|
isSynced,
|
|
6028
6834
|
markPendingLocalFileContent,
|
|
6029
6835
|
queryClient,
|
|
6030
6836
|
queueFileContentSave,
|
|
6031
6837
|
replacePreviewContent,
|
|
6838
|
+
recordContentHistoryEntry,
|
|
6032
6839
|
syncUndoRedoState,
|
|
6033
6840
|
ydoc,
|
|
6034
6841
|
],
|
|
@@ -6038,7 +6845,12 @@ export default function DesignEditor() {
|
|
|
6038
6845
|
(
|
|
6039
6846
|
fileId: string,
|
|
6040
6847
|
nextContent: string,
|
|
6041
|
-
options: {
|
|
6848
|
+
options: {
|
|
6849
|
+
refreshPreview?: boolean;
|
|
6850
|
+
skipPreview?: boolean;
|
|
6851
|
+
persist?: boolean;
|
|
6852
|
+
updatedAt?: string;
|
|
6853
|
+
} = {},
|
|
6042
6854
|
) => {
|
|
6043
6855
|
if (!canEditDesignRef.current) return;
|
|
6044
6856
|
if (fileId === activeFile?.id) {
|
|
@@ -6046,7 +6858,15 @@ export default function DesignEditor() {
|
|
|
6046
6858
|
return;
|
|
6047
6859
|
}
|
|
6048
6860
|
const previousFile = files.find((file) => file.id === fileId);
|
|
6049
|
-
|
|
6861
|
+
if (options.updatedAt) {
|
|
6862
|
+
clearPendingLocalFileContent(fileId);
|
|
6863
|
+
} else {
|
|
6864
|
+
markPendingLocalFileContent(
|
|
6865
|
+
fileId,
|
|
6866
|
+
nextContent,
|
|
6867
|
+
previousFile?.updatedAt,
|
|
6868
|
+
);
|
|
6869
|
+
}
|
|
6050
6870
|
queryClient.setQueryData(["action", "get-design", { id }], (old: any) => {
|
|
6051
6871
|
if (!old || typeof old !== "object" || !Array.isArray(old.files)) {
|
|
6052
6872
|
return old;
|
|
@@ -6054,19 +6874,33 @@ export default function DesignEditor() {
|
|
|
6054
6874
|
return {
|
|
6055
6875
|
...old,
|
|
6056
6876
|
files: old.files.map((file: DesignFile) =>
|
|
6057
|
-
file.id === fileId
|
|
6877
|
+
file.id === fileId
|
|
6878
|
+
? {
|
|
6879
|
+
...file,
|
|
6880
|
+
content: nextContent,
|
|
6881
|
+
...(options.updatedAt
|
|
6882
|
+
? { updatedAt: options.updatedAt }
|
|
6883
|
+
: {}),
|
|
6884
|
+
}
|
|
6885
|
+
: file,
|
|
6058
6886
|
),
|
|
6059
6887
|
};
|
|
6060
6888
|
});
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6889
|
+
if (options.persist === false) {
|
|
6890
|
+
cancelQueuedFileContentSave(fileId);
|
|
6891
|
+
} else {
|
|
6892
|
+
saveFileContent({
|
|
6893
|
+
id: fileId,
|
|
6894
|
+
content: nextContent,
|
|
6895
|
+
syncCollab: true,
|
|
6896
|
+
});
|
|
6897
|
+
}
|
|
6066
6898
|
},
|
|
6067
6899
|
[
|
|
6068
6900
|
activeFile?.id,
|
|
6069
6901
|
applyLocalContentUpdate,
|
|
6902
|
+
cancelQueuedFileContentSave,
|
|
6903
|
+
clearPendingLocalFileContent,
|
|
6070
6904
|
files,
|
|
6071
6905
|
id,
|
|
6072
6906
|
markPendingLocalFileContent,
|
|
@@ -6075,10 +6909,105 @@ export default function DesignEditor() {
|
|
|
6075
6909
|
],
|
|
6076
6910
|
);
|
|
6077
6911
|
|
|
6912
|
+
useEffect(() => {
|
|
6913
|
+
if (!id || !activeFile?.id || !motionTracksDirty) return;
|
|
6914
|
+
if (motionTracks.length === 0) return;
|
|
6915
|
+
if (motionAutosavePending) return;
|
|
6916
|
+
if (motionAutosaveFailedRevisionRef.current === motionAutosaveRevision)
|
|
6917
|
+
return;
|
|
6918
|
+
if (
|
|
6919
|
+
lastScheduledMotionAutosaveRevisionRef.current ===
|
|
6920
|
+
motionAutosaveRevision &&
|
|
6921
|
+
motionAutosaveTimerRef.current !== null
|
|
6922
|
+
) {
|
|
6923
|
+
return;
|
|
6924
|
+
}
|
|
6925
|
+
|
|
6926
|
+
const revisionAtSchedule = motionAutosaveRevision;
|
|
6927
|
+
lastScheduledMotionAutosaveRevisionRef.current = revisionAtSchedule;
|
|
6928
|
+
clearMotionAutosaveTimer();
|
|
6929
|
+
motionAutosaveTimerRef.current = window.setTimeout(() => {
|
|
6930
|
+
motionAutosaveTimerRef.current = null;
|
|
6931
|
+
if (motionAutosaveRevisionRef.current !== revisionAtSchedule) return;
|
|
6932
|
+
applyMotionEdit(
|
|
6933
|
+
{
|
|
6934
|
+
designId: id,
|
|
6935
|
+
fileId: activeFile.id,
|
|
6936
|
+
timelineId: motionTimelineId ?? undefined,
|
|
6937
|
+
sourceRef: activeFile.id,
|
|
6938
|
+
tracks: motionTracks.map(({ label: _label, ...track }) => track),
|
|
6939
|
+
durationMs: motionDurationMs,
|
|
6940
|
+
currentContent: getFreshActiveFileContent({
|
|
6941
|
+
activeContent,
|
|
6942
|
+
latestContent: latestActiveContentRef.current,
|
|
6943
|
+
lastLocalContent: lastLocalContentRef.current,
|
|
6944
|
+
}),
|
|
6945
|
+
includeContent: true,
|
|
6946
|
+
},
|
|
6947
|
+
{
|
|
6948
|
+
onSuccess: (result) => {
|
|
6949
|
+
const response = result as {
|
|
6950
|
+
fileId?: unknown;
|
|
6951
|
+
timelineId?: unknown;
|
|
6952
|
+
patchedContent?: unknown;
|
|
6953
|
+
};
|
|
6954
|
+
if (typeof response.timelineId === "string") {
|
|
6955
|
+
setMotionTimelineId(response.timelineId);
|
|
6956
|
+
}
|
|
6957
|
+
if (motionAutosaveRevisionRef.current === revisionAtSchedule) {
|
|
6958
|
+
setMotionTracksDirty(false);
|
|
6959
|
+
setMotionHydrationFingerprint(null);
|
|
6960
|
+
lastScheduledMotionAutosaveRevisionRef.current = 0;
|
|
6961
|
+
}
|
|
6962
|
+
if (
|
|
6963
|
+
typeof response.fileId === "string" &&
|
|
6964
|
+
typeof response.patchedContent === "string"
|
|
6965
|
+
) {
|
|
6966
|
+
applyFileContentUpdate(response.fileId, response.patchedContent, {
|
|
6967
|
+
refreshPreview: response.fileId === activeFile.id,
|
|
6968
|
+
});
|
|
6969
|
+
}
|
|
6970
|
+
void queryClient.invalidateQueries({
|
|
6971
|
+
queryKey: ["action", "get-motion-timeline"],
|
|
6972
|
+
});
|
|
6973
|
+
},
|
|
6974
|
+
onError: (error) => {
|
|
6975
|
+
if (motionAutosaveRevisionRef.current === revisionAtSchedule) {
|
|
6976
|
+
motionAutosaveFailedRevisionRef.current = revisionAtSchedule;
|
|
6977
|
+
lastScheduledMotionAutosaveRevisionRef.current = 0;
|
|
6978
|
+
}
|
|
6979
|
+
toast.error(
|
|
6980
|
+
error instanceof Error
|
|
6981
|
+
? error.message
|
|
6982
|
+
: // i18n-ignore: fallback toast for motion autosave failure.
|
|
6983
|
+
"Motion changes could not be saved.",
|
|
6984
|
+
);
|
|
6985
|
+
},
|
|
6986
|
+
},
|
|
6987
|
+
);
|
|
6988
|
+
}, MOTION_AUTOSAVE_DELAY_MS);
|
|
6989
|
+
}, [
|
|
6990
|
+
activeFile?.id,
|
|
6991
|
+
activeContent,
|
|
6992
|
+
applyFileContentUpdate,
|
|
6993
|
+
applyMotionEdit,
|
|
6994
|
+
clearMotionAutosaveTimer,
|
|
6995
|
+
id,
|
|
6996
|
+
motionAutosaveRevision,
|
|
6997
|
+
motionAutosavePending,
|
|
6998
|
+
motionDurationMs,
|
|
6999
|
+
motionTimelineId,
|
|
7000
|
+
motionTracks,
|
|
7001
|
+
motionTracksDirty,
|
|
7002
|
+
queryClient,
|
|
7003
|
+
]);
|
|
7004
|
+
|
|
6078
7005
|
const handleComponentPropApplied = useCallback(
|
|
6079
|
-
(fileId: string, nextContent: string) => {
|
|
7006
|
+
(fileId: string, nextContent: string, updatedAt?: string) => {
|
|
6080
7007
|
applyFileContentUpdate(fileId, nextContent, {
|
|
6081
7008
|
refreshPreview: fileId === activeFile?.id,
|
|
7009
|
+
persist: false,
|
|
7010
|
+
updatedAt,
|
|
6082
7011
|
});
|
|
6083
7012
|
},
|
|
6084
7013
|
[activeFile?.id, applyFileContentUpdate],
|
|
@@ -6098,6 +7027,7 @@ export default function DesignEditor() {
|
|
|
6098
7027
|
) {
|
|
6099
7028
|
applyFileContentUpdate(result.fileId, result.patchedContent, {
|
|
6100
7029
|
refreshPreview: result.fileId === activeFile?.id,
|
|
7030
|
+
persist: false,
|
|
6101
7031
|
});
|
|
6102
7032
|
}
|
|
6103
7033
|
void handleRunDesignAudit();
|
|
@@ -6142,14 +7072,29 @@ export default function DesignEditor() {
|
|
|
6142
7072
|
if (!canEditDesign) return false;
|
|
6143
7073
|
const targetFile = files.find((file) => file.id === screenId);
|
|
6144
7074
|
if (!targetFile) return false;
|
|
7075
|
+
const pendingContent = pendingLocalFileContentsRef.current.get(
|
|
7076
|
+
targetFile.id,
|
|
7077
|
+
)?.content;
|
|
7078
|
+
const storedContent = targetFile.content ?? "";
|
|
6145
7079
|
const baseContent =
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
7080
|
+
pendingContent ??
|
|
7081
|
+
(targetFile.id === activeFile?.id
|
|
7082
|
+
? (() => {
|
|
7083
|
+
const liveContent =
|
|
7084
|
+
ydoc && isSynced
|
|
7085
|
+
? ydoc.getText("content").toString()
|
|
7086
|
+
: ((collabContentFileIdRef.current === activeFile.id
|
|
7087
|
+
? collabContentRef.current
|
|
7088
|
+
: null) ?? activeContent);
|
|
7089
|
+
return shouldUseLiveFileContent({
|
|
7090
|
+
liveContent,
|
|
7091
|
+
storedContent,
|
|
7092
|
+
fileType: targetFile.fileType,
|
|
7093
|
+
})
|
|
7094
|
+
? liveContent
|
|
7095
|
+
: storedContent;
|
|
7096
|
+
})()
|
|
7097
|
+
: storedContent);
|
|
6153
7098
|
const nextContent = appendCanvasPrimitiveToHtml(baseContent, primitive);
|
|
6154
7099
|
if (!nextContent) {
|
|
6155
7100
|
toast.error(t("designEditor.toasts.primitiveInsertFailed"));
|
|
@@ -6166,6 +7111,11 @@ export default function DesignEditor() {
|
|
|
6166
7111
|
if (targetFile.id === activeFile?.id) {
|
|
6167
7112
|
applyLocalContentUpdate(nextContent, { immediateSave: true });
|
|
6168
7113
|
} else {
|
|
7114
|
+
recordContentHistoryEntry({
|
|
7115
|
+
fileId: targetFile.id,
|
|
7116
|
+
before: baseContent,
|
|
7117
|
+
after: nextContent,
|
|
7118
|
+
});
|
|
6169
7119
|
queryClient.setQueryData(
|
|
6170
7120
|
["action", "get-design", { id }],
|
|
6171
7121
|
(old: any) => {
|
|
@@ -6189,7 +7139,21 @@ export default function DesignEditor() {
|
|
|
6189
7139
|
});
|
|
6190
7140
|
}
|
|
6191
7141
|
|
|
6192
|
-
|
|
7142
|
+
const result = projectedNodeId ?? primitive.nodeId ?? true;
|
|
7143
|
+
|
|
7144
|
+
// Record the nodeId when a TEXT primitive is created so the next
|
|
7145
|
+
// handlePrimitiveCreated (or handleBoardDrawPrimitive) can immediately
|
|
7146
|
+
// enter text-edit mode — fixing the "click to add text should let me
|
|
7147
|
+
// type immediately" bug. The ref is read once and cleared.
|
|
7148
|
+
if (primitive.kind === "text") {
|
|
7149
|
+
const textNodeId =
|
|
7150
|
+
typeof result === "string" ? result : (primitive.nodeId ?? null);
|
|
7151
|
+
pendingTextEditNodeIdRef.current = textNodeId;
|
|
7152
|
+
} else {
|
|
7153
|
+
pendingTextEditNodeIdRef.current = null;
|
|
7154
|
+
}
|
|
7155
|
+
|
|
7156
|
+
return result;
|
|
6193
7157
|
},
|
|
6194
7158
|
[
|
|
6195
7159
|
activeContent,
|
|
@@ -6200,6 +7164,7 @@ export default function DesignEditor() {
|
|
|
6200
7164
|
id,
|
|
6201
7165
|
isSynced,
|
|
6202
7166
|
queryClient,
|
|
7167
|
+
recordContentHistoryEntry,
|
|
6203
7168
|
saveFileContent,
|
|
6204
7169
|
t,
|
|
6205
7170
|
ydoc,
|
|
@@ -6213,25 +7178,112 @@ export default function DesignEditor() {
|
|
|
6213
7178
|
// new primitive selected, matching Figma behaviour. We activate the
|
|
6214
7179
|
// target screen (so the layers panel shows its content) and select the
|
|
6215
7180
|
// new node, but do NOT switch to single/full view.
|
|
6216
|
-
pendingOverviewScreenSelectionRef.current =
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
7181
|
+
pendingOverviewScreenSelectionRef.current = screenId;
|
|
7182
|
+
pendingOverviewLayerSelectionRef.current = nodeId;
|
|
7183
|
+
clearPendingOverviewLayerSelectionTimer();
|
|
7184
|
+
flushSync(() => {
|
|
7185
|
+
setCreatedOverviewLayerSelection({ screenId, layerId: nodeId });
|
|
7186
|
+
setActiveFileId(screenId);
|
|
7187
|
+
setSelectedElement(null);
|
|
7188
|
+
setHoveredElement(null);
|
|
7189
|
+
setSelectedLayerIdsState([nodeId]);
|
|
7190
|
+
setOverviewSelectedScreenIds([screenId]);
|
|
7191
|
+
setActiveTool("move");
|
|
7192
|
+
setMode("edit");
|
|
7193
|
+
});
|
|
6224
7194
|
// viewMode stays at "overview" — no setViewMode("single") call here.
|
|
7195
|
+
|
|
7196
|
+
// Immediately enter text-editing for newly created TEXT primitives so the
|
|
7197
|
+
// user can start typing without a second click (fixes auto-size flow too).
|
|
7198
|
+
// We read-and-clear the ref set by handleCreatePrimitive just above. The
|
|
7199
|
+
// 50 ms delay gives React a tick to commit the new content into the active
|
|
7200
|
+
// screen's iframe before the bridge tries to find the element by nodeId.
|
|
7201
|
+
const textNodeId = pendingTextEditNodeIdRef.current;
|
|
7202
|
+
pendingTextEditNodeIdRef.current = null;
|
|
7203
|
+
if (textNodeId) {
|
|
7204
|
+
setTimeout(() => {
|
|
7205
|
+
(window as any).__designCanvasBeginTextEdit?.(textNodeId);
|
|
7206
|
+
}, 50);
|
|
7207
|
+
}
|
|
6225
7208
|
},
|
|
6226
|
-
[],
|
|
7209
|
+
[clearPendingOverviewLayerSelectionTimer],
|
|
6227
7210
|
);
|
|
6228
7211
|
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
7212
|
+
/**
|
|
7213
|
+
* Called by MultiScreenCanvas when a draft primitive is committed in empty
|
|
7214
|
+
* canvas space (outside all screen frames). Appends the primitive to the
|
|
7215
|
+
* board file content via the shared handleCreatePrimitive path so the bridge
|
|
7216
|
+
* engine handles persistence identically to in-screen elements.
|
|
7217
|
+
*/
|
|
7218
|
+
const handleBoardDrawPrimitive = useCallback(
|
|
7219
|
+
(primitive: CanvasPrimitiveInsert) => {
|
|
7220
|
+
if (!boardFileId || !canEditDesign) return false;
|
|
7221
|
+
const result = handleCreatePrimitive(boardFileId, primitive);
|
|
7222
|
+
if (!result) return false;
|
|
7223
|
+
|
|
7224
|
+
// For TEXT primitives drawn on the board surface, immediately enter
|
|
7225
|
+
// text-editing mode via begin-text-edit. The board DesignCanvas uses
|
|
7226
|
+
// registerRuntimeBridge=false so window.__designCanvasBeginTextEdit is
|
|
7227
|
+
// not set; instead we broadcast the message to all preview iframes — the
|
|
7228
|
+
// one that contains the new nodeId will handle it, others ignore it.
|
|
7229
|
+
// The 50 ms delay lets the board iframe receive its content update first.
|
|
7230
|
+
if (primitive.kind === "text") {
|
|
7231
|
+
const textNodeId =
|
|
7232
|
+
pendingTextEditNodeIdRef.current ?? primitive.nodeId ?? null;
|
|
7233
|
+
pendingTextEditNodeIdRef.current = null;
|
|
7234
|
+
if (textNodeId) {
|
|
7235
|
+
setTimeout(() => {
|
|
7236
|
+
document
|
|
7237
|
+
.querySelectorAll<HTMLIFrameElement>(
|
|
7238
|
+
"iframe[data-design-preview-iframe]",
|
|
7239
|
+
)
|
|
7240
|
+
.forEach((iframe) => {
|
|
7241
|
+
iframe.contentWindow?.postMessage(
|
|
7242
|
+
{ type: "begin-text-edit", nodeId: textNodeId },
|
|
7243
|
+
"*",
|
|
7244
|
+
);
|
|
7245
|
+
});
|
|
7246
|
+
}, 50);
|
|
7247
|
+
}
|
|
7248
|
+
}
|
|
7249
|
+
|
|
7250
|
+
return result;
|
|
7251
|
+
},
|
|
7252
|
+
[boardFileId, canEditDesign, handleCreatePrimitive],
|
|
7253
|
+
);
|
|
7254
|
+
|
|
7255
|
+
const handleOverviewScreenSelectionChange = useCallback(
|
|
7256
|
+
(ids: string[]) => {
|
|
7257
|
+
const pendingId = pendingOverviewScreenSelectionRef.current;
|
|
7258
|
+
const fileIds = new Set(files.map((file) => file.id));
|
|
7259
|
+
if (pendingId && ids.length === 0) return;
|
|
7260
|
+
if (pendingId && ids.includes(pendingId)) {
|
|
7261
|
+
setOverviewSelectedScreenIds(
|
|
7262
|
+
ids.filter(
|
|
7263
|
+
(layerId) => fileIds.has(layerId) || layerId === pendingId,
|
|
7264
|
+
),
|
|
7265
|
+
);
|
|
7266
|
+
if (fileIds.has(pendingId)) {
|
|
7267
|
+
pendingOverviewScreenSelectionRef.current = null;
|
|
7268
|
+
}
|
|
7269
|
+
return;
|
|
7270
|
+
}
|
|
7271
|
+
if (pendingId) {
|
|
7272
|
+
pendingOverviewScreenSelectionRef.current = null;
|
|
7273
|
+
pendingOverviewLayerSelectionRef.current = null;
|
|
7274
|
+
clearPendingOverviewLayerSelectionTimer();
|
|
7275
|
+
setCreatedOverviewLayerSelection(null);
|
|
7276
|
+
}
|
|
7277
|
+
setOverviewSelectedScreenIds(
|
|
7278
|
+
ids.filter((layerId) => fileIds.has(layerId)),
|
|
7279
|
+
);
|
|
7280
|
+
},
|
|
7281
|
+
[clearPendingOverviewLayerSelectionTimer, files],
|
|
7282
|
+
);
|
|
7283
|
+
|
|
7284
|
+
const shouldPreserveBlockedOverviewLayerSelectionRef = useRef<
|
|
7285
|
+
(screenId: string) => boolean
|
|
7286
|
+
>(() => false);
|
|
6235
7287
|
|
|
6236
7288
|
const handleMoveTool = useCallback(() => {
|
|
6237
7289
|
if (!canEditDesign) return;
|
|
@@ -6243,36 +7295,42 @@ export default function DesignEditor() {
|
|
|
6243
7295
|
|
|
6244
7296
|
const handleFrameTool = useCallback(() => {
|
|
6245
7297
|
if (!canEditDesign) return;
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
7298
|
+
flushSync(() => {
|
|
7299
|
+
setActiveTool("frame");
|
|
7300
|
+
setMode("edit");
|
|
7301
|
+
setDrawMode(false);
|
|
7302
|
+
setPinMode(false);
|
|
7303
|
+
setSelectedElement(null);
|
|
7304
|
+
viewModeRef.current = "overview";
|
|
7305
|
+
setViewMode("overview");
|
|
7306
|
+
});
|
|
6253
7307
|
}, [canEditDesign]);
|
|
6254
7308
|
|
|
6255
7309
|
const handleTextTool = useCallback(() => {
|
|
6256
7310
|
if (!canEditDesign) return;
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
setViewMode("overview");
|
|
6260
|
-
setMode("edit");
|
|
6261
|
-
setDrawMode(false);
|
|
6262
|
-
setPinMode(false);
|
|
6263
|
-
setSelectedElement(null);
|
|
6264
|
-
}, [canEditDesign]);
|
|
6265
|
-
|
|
6266
|
-
const handleShapeTool = useCallback(
|
|
6267
|
-
(tool: ShapeTool) => {
|
|
6268
|
-
if (!canEditDesign) return;
|
|
6269
|
-
setActiveTool(tool);
|
|
7311
|
+
flushSync(() => {
|
|
7312
|
+
setActiveTool("text");
|
|
6270
7313
|
viewModeRef.current = "overview";
|
|
6271
7314
|
setViewMode("overview");
|
|
6272
7315
|
setMode("edit");
|
|
6273
7316
|
setDrawMode(false);
|
|
6274
7317
|
setPinMode(false);
|
|
6275
7318
|
setSelectedElement(null);
|
|
7319
|
+
});
|
|
7320
|
+
}, [canEditDesign]);
|
|
7321
|
+
|
|
7322
|
+
const handleShapeTool = useCallback(
|
|
7323
|
+
(tool: ShapeTool) => {
|
|
7324
|
+
if (!canEditDesign) return;
|
|
7325
|
+
flushSync(() => {
|
|
7326
|
+
setActiveTool(tool);
|
|
7327
|
+
viewModeRef.current = "overview";
|
|
7328
|
+
setViewMode("overview");
|
|
7329
|
+
setMode("edit");
|
|
7330
|
+
setDrawMode(false);
|
|
7331
|
+
setPinMode(false);
|
|
7332
|
+
setSelectedElement(null);
|
|
7333
|
+
});
|
|
6276
7334
|
},
|
|
6277
7335
|
[canEditDesign],
|
|
6278
7336
|
);
|
|
@@ -6283,13 +7341,15 @@ export default function DesignEditor() {
|
|
|
6283
7341
|
|
|
6284
7342
|
const handlePenTool = useCallback(() => {
|
|
6285
7343
|
if (!canEditDesign) return;
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
7344
|
+
flushSync(() => {
|
|
7345
|
+
setActiveTool("pen");
|
|
7346
|
+
viewModeRef.current = "overview";
|
|
7347
|
+
setViewMode("overview");
|
|
7348
|
+
setMode("edit");
|
|
7349
|
+
setDrawMode(false);
|
|
7350
|
+
setPinMode(false);
|
|
7351
|
+
setSelectedElement(null);
|
|
7352
|
+
});
|
|
6293
7353
|
}, [canEditDesign]);
|
|
6294
7354
|
|
|
6295
7355
|
const handleHandTool = useCallback(() => {
|
|
@@ -6456,6 +7516,7 @@ export default function DesignEditor() {
|
|
|
6456
7516
|
mode,
|
|
6457
7517
|
activeTool,
|
|
6458
7518
|
inspectorTab: activeInspectorTab,
|
|
7519
|
+
leftPanel: activeLeftPanel,
|
|
6459
7520
|
// §8 DesignNavigationState additions — dock + breakpoint context
|
|
6460
7521
|
dock: { kind: "motion" as const, open: motionDockOpen },
|
|
6461
7522
|
motion: {
|
|
@@ -6523,6 +7584,7 @@ export default function DesignEditor() {
|
|
|
6523
7584
|
mode: selection.mode,
|
|
6524
7585
|
activeTool: selection.activeTool,
|
|
6525
7586
|
inspectorTab: selection.inspectorTab,
|
|
7587
|
+
leftPanel: selection.leftPanel,
|
|
6526
7588
|
dock: selection.dock,
|
|
6527
7589
|
motion: selection.motion,
|
|
6528
7590
|
breakpoint: selection.breakpoint,
|
|
@@ -6563,6 +7625,7 @@ export default function DesignEditor() {
|
|
|
6563
7625
|
mode,
|
|
6564
7626
|
activeTool,
|
|
6565
7627
|
activeInspectorTab,
|
|
7628
|
+
activeLeftPanel,
|
|
6566
7629
|
overviewSelectedScreenIds,
|
|
6567
7630
|
viewMode,
|
|
6568
7631
|
zoom,
|
|
@@ -6617,6 +7680,8 @@ export default function DesignEditor() {
|
|
|
6617
7680
|
designTitle: design?.title ?? null,
|
|
6618
7681
|
activeFileId: activeFile?.id ?? null,
|
|
6619
7682
|
activeFilename: activeFile?.filename ?? null,
|
|
7683
|
+
activeFileUpdatedAt: activeFile?.updatedAt ?? null,
|
|
7684
|
+
activeContent,
|
|
6620
7685
|
viewMode,
|
|
6621
7686
|
zoom,
|
|
6622
7687
|
screens: files.map((file) => ({
|
|
@@ -6638,12 +7703,22 @@ export default function DesignEditor() {
|
|
|
6638
7703
|
});
|
|
6639
7704
|
},
|
|
6640
7705
|
onShaderFillPreviewClear: () => setShaderFillPreview(null),
|
|
7706
|
+
onShaderFillApplied: (fileId, content, updatedAt) => {
|
|
7707
|
+
applyFileContentUpdate(fileId, content, {
|
|
7708
|
+
refreshPreview: fileId === activeFile?.id,
|
|
7709
|
+
persist: false,
|
|
7710
|
+
updatedAt,
|
|
7711
|
+
});
|
|
7712
|
+
},
|
|
6641
7713
|
}),
|
|
6642
7714
|
[
|
|
7715
|
+
activeContent,
|
|
6643
7716
|
activeFile?.filename,
|
|
6644
7717
|
activeFile?.fileType,
|
|
6645
7718
|
activeFile?.id,
|
|
7719
|
+
activeFile?.updatedAt,
|
|
6646
7720
|
activeTool,
|
|
7721
|
+
applyFileContentUpdate,
|
|
6647
7722
|
design?.title,
|
|
6648
7723
|
files,
|
|
6649
7724
|
id,
|
|
@@ -6660,6 +7735,23 @@ export default function DesignEditor() {
|
|
|
6660
7735
|
|
|
6661
7736
|
const handleScreenElementSelect = useCallback(
|
|
6662
7737
|
(screenId: string, info: ElementInfo) => {
|
|
7738
|
+
const pendingLayerId = pendingOverviewLayerSelectionRef.current;
|
|
7739
|
+
const pendingScreenId = pendingOverviewScreenSelectionRef.current;
|
|
7740
|
+
const createdSelection = createdOverviewLayerSelection;
|
|
7741
|
+
if (createdSelection?.screenId === screenId) {
|
|
7742
|
+
return;
|
|
7743
|
+
}
|
|
7744
|
+
if (
|
|
7745
|
+
(pendingLayerId || createdSelection?.layerId) &&
|
|
7746
|
+
(!pendingScreenId || pendingScreenId === screenId) &&
|
|
7747
|
+
isScreenRootElementInfo(info)
|
|
7748
|
+
) {
|
|
7749
|
+
return;
|
|
7750
|
+
}
|
|
7751
|
+
pendingOverviewScreenSelectionRef.current = null;
|
|
7752
|
+
pendingOverviewLayerSelectionRef.current = null;
|
|
7753
|
+
clearPendingOverviewLayerSelectionTimer();
|
|
7754
|
+
setCreatedOverviewLayerSelection(null);
|
|
6663
7755
|
const projection = getCodeLayerProjectionForScreen(screenId);
|
|
6664
7756
|
const canonical = projection
|
|
6665
7757
|
? canonicalizeElementInfoFromProjection(projection, info)
|
|
@@ -6667,6 +7759,14 @@ export default function DesignEditor() {
|
|
|
6667
7759
|
const node = projection
|
|
6668
7760
|
? resolveCodeLayerNodeFromElementInfo(projection, canonical)
|
|
6669
7761
|
: null;
|
|
7762
|
+
if (
|
|
7763
|
+
shouldPreserveBlockedOverviewLayerSelectionRef.current(screenId) &&
|
|
7764
|
+
(isScreenRootElementInfo(canonical) ||
|
|
7765
|
+
!node ||
|
|
7766
|
+
selectedLayerIdsState.includes(node.id))
|
|
7767
|
+
) {
|
|
7768
|
+
return;
|
|
7769
|
+
}
|
|
6670
7770
|
setActiveFileId(screenId);
|
|
6671
7771
|
setSelectedElement(canonical);
|
|
6672
7772
|
setHoveredElement(null);
|
|
@@ -6679,21 +7779,49 @@ export default function DesignEditor() {
|
|
|
6679
7779
|
setMode("edit");
|
|
6680
7780
|
focusDesignInspectorForSelection();
|
|
6681
7781
|
},
|
|
6682
|
-
[
|
|
7782
|
+
[
|
|
7783
|
+
clearPendingOverviewLayerSelectionTimer,
|
|
7784
|
+
createdOverviewLayerSelection,
|
|
7785
|
+
focusDesignInspectorForSelection,
|
|
7786
|
+
getCodeLayerProjectionForScreen,
|
|
7787
|
+
selectedLayerIdsState,
|
|
7788
|
+
],
|
|
6683
7789
|
);
|
|
6684
7790
|
|
|
6685
|
-
const handleScreenElementClear = useCallback(
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
7791
|
+
const handleScreenElementClear = useCallback(
|
|
7792
|
+
(screenId: string) => {
|
|
7793
|
+
const pendingLayerId = pendingOverviewLayerSelectionRef.current;
|
|
7794
|
+
const pendingScreenId = pendingOverviewScreenSelectionRef.current;
|
|
7795
|
+
const createdSelection = createdOverviewLayerSelection;
|
|
7796
|
+
if (createdSelection?.screenId === screenId) {
|
|
7797
|
+
return;
|
|
7798
|
+
}
|
|
7799
|
+
if (
|
|
7800
|
+
(pendingLayerId || createdSelection?.layerId) &&
|
|
7801
|
+
(!pendingScreenId || pendingScreenId === screenId)
|
|
7802
|
+
) {
|
|
7803
|
+
return;
|
|
7804
|
+
}
|
|
7805
|
+
if (shouldPreserveBlockedOverviewLayerSelectionRef.current(screenId)) {
|
|
7806
|
+
return;
|
|
7807
|
+
}
|
|
7808
|
+
pendingOverviewScreenSelectionRef.current = null;
|
|
7809
|
+
pendingOverviewLayerSelectionRef.current = null;
|
|
7810
|
+
clearPendingOverviewLayerSelectionTimer();
|
|
7811
|
+
setCreatedOverviewLayerSelection(null);
|
|
7812
|
+
setActiveFileId(screenId);
|
|
7813
|
+
setSelectedElement(null);
|
|
7814
|
+
setHoveredElement(null);
|
|
7815
|
+
setHoveredElementScreenId(null);
|
|
7816
|
+
setSelectedLayerIdsState([]);
|
|
7817
|
+
if (viewModeRef.current === "overview") {
|
|
7818
|
+
setOverviewSelectedScreenIds([]);
|
|
7819
|
+
}
|
|
7820
|
+
setActiveTool("move");
|
|
7821
|
+
setMode("edit");
|
|
7822
|
+
},
|
|
7823
|
+
[clearPendingOverviewLayerSelectionTimer, createdOverviewLayerSelection],
|
|
7824
|
+
);
|
|
6697
7825
|
|
|
6698
7826
|
const handleElementSelect = useCallback(
|
|
6699
7827
|
(info: ElementInfo) => {
|
|
@@ -6721,6 +7849,10 @@ export default function DesignEditor() {
|
|
|
6721
7849
|
|
|
6722
7850
|
const handleScreenElementDblClickText = useCallback(
|
|
6723
7851
|
(screenId: string, info: ElementInfo) => {
|
|
7852
|
+
pendingOverviewScreenSelectionRef.current = null;
|
|
7853
|
+
pendingOverviewLayerSelectionRef.current = null;
|
|
7854
|
+
clearPendingOverviewLayerSelectionTimer();
|
|
7855
|
+
setCreatedOverviewLayerSelection(null);
|
|
6724
7856
|
const projection = getCodeLayerProjectionForScreen(screenId);
|
|
6725
7857
|
const canonical = projection
|
|
6726
7858
|
? canonicalizeElementInfoFromProjection(projection, info)
|
|
@@ -6739,7 +7871,12 @@ export default function DesignEditor() {
|
|
|
6739
7871
|
setMode("edit");
|
|
6740
7872
|
focusDesignInspectorForSelection();
|
|
6741
7873
|
},
|
|
6742
|
-
[
|
|
7874
|
+
[
|
|
7875
|
+
clearPendingOverviewLayerSelectionTimer,
|
|
7876
|
+
createdOverviewLayerSelection,
|
|
7877
|
+
focusDesignInspectorForSelection,
|
|
7878
|
+
getCodeLayerProjectionForScreen,
|
|
7879
|
+
],
|
|
6743
7880
|
);
|
|
6744
7881
|
|
|
6745
7882
|
const handleElementDblClickText = useCallback(
|
|
@@ -6944,7 +8081,9 @@ export default function DesignEditor() {
|
|
|
6944
8081
|
createdAt: Date.now(),
|
|
6945
8082
|
});
|
|
6946
8083
|
const sendStyleChange = (window as any).__designCanvasSendStyle;
|
|
6947
|
-
|
|
8084
|
+
const runtimeStyleApplied =
|
|
8085
|
+
!options.runtimeApplied && typeof sendStyleChange === "function";
|
|
8086
|
+
if (runtimeStyleApplied) {
|
|
6948
8087
|
const selectorCandidates = targetNode
|
|
6949
8088
|
? codeLayerSelectorAliases(targetNode)
|
|
6950
8089
|
: selector
|
|
@@ -7095,6 +8234,12 @@ export default function DesignEditor() {
|
|
|
7095
8234
|
queueFileContentSave(activeFile.id, resolvedNextContent, {
|
|
7096
8235
|
syncCollab: !(ydoc && isSynced),
|
|
7097
8236
|
});
|
|
8237
|
+
if (
|
|
8238
|
+
!runtimeStyleApplied &&
|
|
8239
|
+
!replacePreviewContent(resolvedNextContent, selector)
|
|
8240
|
+
) {
|
|
8241
|
+
setContentRenderRevision((revision) => revision + 1);
|
|
8242
|
+
}
|
|
7098
8243
|
if (resolvedNode) setSelectedLayerIdsState([resolvedNode.id]);
|
|
7099
8244
|
setSelectedElement((prev) => {
|
|
7100
8245
|
if (options.elementInfo) return options.elementInfo;
|
|
@@ -7122,6 +8267,7 @@ export default function DesignEditor() {
|
|
|
7122
8267
|
activeBreakpointWidthState,
|
|
7123
8268
|
canEditDesign,
|
|
7124
8269
|
queueFileContentSave,
|
|
8270
|
+
replacePreviewContent,
|
|
7125
8271
|
selectedElement,
|
|
7126
8272
|
t,
|
|
7127
8273
|
ydoc,
|
|
@@ -7585,6 +8731,28 @@ export default function DesignEditor() {
|
|
|
7585
8731
|
return false;
|
|
7586
8732
|
}
|
|
7587
8733
|
applyFileContentUpdate(screenId, patch.content, { skipPreview: true });
|
|
8734
|
+
const movedNode =
|
|
8735
|
+
(patch.result.after?.nodeId
|
|
8736
|
+
? patch.projection.nodes.find(
|
|
8737
|
+
(node) => node.id === patch.result.after?.nodeId,
|
|
8738
|
+
)
|
|
8739
|
+
: null) ??
|
|
8740
|
+
resolveCodeLayerNodeFromBridge(
|
|
8741
|
+
patch.projection,
|
|
8742
|
+
selector,
|
|
8743
|
+
details?.sourceId ??
|
|
8744
|
+
elementInfo?.sourceId ??
|
|
8745
|
+
(targetNode
|
|
8746
|
+
? bridgeSourceIdForCodeLayerNode(targetNode)
|
|
8747
|
+
: undefined),
|
|
8748
|
+
);
|
|
8749
|
+
if (movedNode) {
|
|
8750
|
+
setActiveFileId(screenId);
|
|
8751
|
+
setSelectedLayerIdsState([movedNode.id]);
|
|
8752
|
+
setSelectedElement(elementInfoFromCodeLayerNode(movedNode));
|
|
8753
|
+
} else {
|
|
8754
|
+
setSelectedElement(null);
|
|
8755
|
+
}
|
|
7588
8756
|
return true;
|
|
7589
8757
|
},
|
|
7590
8758
|
[
|
|
@@ -8319,12 +9487,14 @@ export default function DesignEditor() {
|
|
|
8319
9487
|
*
|
|
8320
9488
|
* The bridge in the source screen's iframe posts phase:"end" with the
|
|
8321
9489
|
* selector / sourceNodeId of the dragged element. MultiScreenCanvas maps
|
|
8322
|
-
* the board point to a target screen
|
|
8323
|
-
*
|
|
8324
|
-
*
|
|
8325
|
-
*
|
|
8326
|
-
*
|
|
8327
|
-
*
|
|
9490
|
+
* the board point to a target screen, optionally runs a hit-test in the
|
|
9491
|
+
* target iframe to resolve an anchorNodeId and placement, then calls this
|
|
9492
|
+
* handler. We resolve both screens' content, identify the node by its
|
|
9493
|
+
* data-agent-native-node-id (falling back to a projection lookup by selector
|
|
9494
|
+
* when only the selector is available), call moveNodeBetweenDocuments with
|
|
9495
|
+
* the anchor from the hit-test (or top-level "inside" fallback), persist
|
|
9496
|
+
* both files, switch the active screen to the target, and select the moved
|
|
9497
|
+
* node — keeping viewMode "overview" throughout.
|
|
8328
9498
|
*/
|
|
8329
9499
|
const handleCrossScreenElementDrop = useCallback(
|
|
8330
9500
|
({
|
|
@@ -8332,11 +9502,15 @@ export default function DesignEditor() {
|
|
|
8332
9502
|
sourceNodeId,
|
|
8333
9503
|
sourceScreenId,
|
|
8334
9504
|
targetScreenId,
|
|
9505
|
+
targetAnchorNodeId,
|
|
9506
|
+
targetAnchorPlacement,
|
|
8335
9507
|
}: {
|
|
8336
9508
|
sourceSelector: string;
|
|
8337
9509
|
sourceNodeId?: string;
|
|
8338
9510
|
sourceScreenId: string;
|
|
8339
9511
|
targetScreenId: string;
|
|
9512
|
+
targetAnchorNodeId?: string;
|
|
9513
|
+
targetAnchorPlacement?: "before" | "after" | "inside";
|
|
8340
9514
|
}) => {
|
|
8341
9515
|
if (!canEditDesign) return;
|
|
8342
9516
|
if (sourceScreenId === targetScreenId) return;
|
|
@@ -8366,9 +9540,16 @@ export default function DesignEditor() {
|
|
|
8366
9540
|
sourceNodeId ??
|
|
8367
9541
|
sourceSelector;
|
|
8368
9542
|
|
|
9543
|
+
// Use hit-test anchor when the canvas supplied one; fall back to
|
|
9544
|
+
// top-level body append ("inside" with no anchor = existing behaviour).
|
|
8369
9545
|
const result = moveNodeBetweenDocuments(sourceContent, destContent, {
|
|
8370
9546
|
nodeId: nodeAttrId,
|
|
8371
|
-
|
|
9547
|
+
...(targetAnchorNodeId
|
|
9548
|
+
? {
|
|
9549
|
+
anchorNodeId: targetAnchorNodeId,
|
|
9550
|
+
placement: targetAnchorPlacement ?? "inside",
|
|
9551
|
+
}
|
|
9552
|
+
: { placement: "inside" }),
|
|
8372
9553
|
});
|
|
8373
9554
|
if (result.status !== "applied") {
|
|
8374
9555
|
toast.error(
|
|
@@ -8602,11 +9783,11 @@ export default function DesignEditor() {
|
|
|
8602
9783
|
// Clear stale selection if the undo removed the selected element.
|
|
8603
9784
|
setSelectedElement((prev) => {
|
|
8604
9785
|
if (!prev) return prev;
|
|
8605
|
-
return
|
|
9786
|
+
return refreshElementInfoFromContent(next, prev);
|
|
8606
9787
|
});
|
|
8607
9788
|
setHoveredElement((prev) => {
|
|
8608
9789
|
if (!prev) return prev;
|
|
8609
|
-
return
|
|
9790
|
+
return refreshElementInfoFromContent(next, prev);
|
|
8610
9791
|
});
|
|
8611
9792
|
}
|
|
8612
9793
|
redoOrderRef.current = [
|
|
@@ -8616,34 +9797,49 @@ export default function DesignEditor() {
|
|
|
8616
9797
|
return true;
|
|
8617
9798
|
}
|
|
8618
9799
|
|
|
8619
|
-
|
|
8620
|
-
|
|
8621
|
-
if (!entry
|
|
9800
|
+
const entry =
|
|
9801
|
+
contentUndoStackRef.current[contentUndoStackRef.current.length - 1];
|
|
9802
|
+
if (!entry) return false;
|
|
9803
|
+
if (
|
|
9804
|
+
entry.fileId !== activeFile?.id &&
|
|
9805
|
+
!files.some((file) => file.id === entry.fileId)
|
|
9806
|
+
) {
|
|
9807
|
+
return false;
|
|
9808
|
+
}
|
|
9809
|
+
contentUndoStackRef.current.pop();
|
|
8622
9810
|
contentRedoStackRef.current = [
|
|
8623
9811
|
...contentRedoStackRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
|
|
8624
9812
|
entry,
|
|
8625
9813
|
];
|
|
8626
9814
|
redoOrderRef.current = [
|
|
8627
9815
|
...redoOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
|
|
8628
|
-
"content",
|
|
9816
|
+
"file-content",
|
|
8629
9817
|
];
|
|
8630
9818
|
suppressContentHistoryRef.current = true;
|
|
8631
9819
|
try {
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
|
|
9820
|
+
if (entry.fileId === activeFile?.id) {
|
|
9821
|
+
applyLocalContentUpdate(entry.before, {
|
|
9822
|
+
refreshPreview: false,
|
|
9823
|
+
immediateSave: true,
|
|
9824
|
+
});
|
|
9825
|
+
} else {
|
|
9826
|
+
applyFileContentUpdate(entry.fileId, entry.before, {
|
|
9827
|
+
refreshPreview: false,
|
|
9828
|
+
});
|
|
9829
|
+
}
|
|
8636
9830
|
} finally {
|
|
8637
9831
|
suppressContentHistoryRef.current = false;
|
|
8638
9832
|
}
|
|
8639
|
-
|
|
8640
|
-
|
|
8641
|
-
|
|
8642
|
-
|
|
8643
|
-
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
|
|
9833
|
+
if (entry.fileId === activeFile?.id) {
|
|
9834
|
+
setSelectedElement((prev) => {
|
|
9835
|
+
if (!prev) return prev;
|
|
9836
|
+
return refreshElementInfoFromContent(entry.before, prev);
|
|
9837
|
+
});
|
|
9838
|
+
setHoveredElement((prev) => {
|
|
9839
|
+
if (!prev) return prev;
|
|
9840
|
+
return refreshElementInfoFromContent(entry.before, prev);
|
|
9841
|
+
});
|
|
9842
|
+
}
|
|
8647
9843
|
return true;
|
|
8648
9844
|
};
|
|
8649
9845
|
const undoGeometry = () => {
|
|
@@ -8670,7 +9866,7 @@ export default function DesignEditor() {
|
|
|
8670
9866
|
const didUndo =
|
|
8671
9867
|
preferred === "geometry"
|
|
8672
9868
|
? undoGeometry() || undoContent()
|
|
8673
|
-
: preferred
|
|
9869
|
+
: isContentUndoRedoOrderKind(preferred)
|
|
8674
9870
|
? undoContent() || undoGeometry()
|
|
8675
9871
|
: undoContent() || undoGeometry();
|
|
8676
9872
|
if (didUndo) {
|
|
@@ -8679,8 +9875,10 @@ export default function DesignEditor() {
|
|
|
8679
9875
|
}, [
|
|
8680
9876
|
ydoc,
|
|
8681
9877
|
activeFile,
|
|
9878
|
+
applyFileContentUpdate,
|
|
8682
9879
|
applyLocalContentUpdate,
|
|
8683
9880
|
canEditDesign,
|
|
9881
|
+
files,
|
|
8684
9882
|
isSynced,
|
|
8685
9883
|
markPendingLocalFileContent,
|
|
8686
9884
|
queueFileContentSave,
|
|
@@ -8712,11 +9910,11 @@ export default function DesignEditor() {
|
|
|
8712
9910
|
// Clear stale selection if the redo removed the selected element.
|
|
8713
9911
|
setSelectedElement((prev) => {
|
|
8714
9912
|
if (!prev) return prev;
|
|
8715
|
-
return
|
|
9913
|
+
return refreshElementInfoFromContent(next, prev);
|
|
8716
9914
|
});
|
|
8717
9915
|
setHoveredElement((prev) => {
|
|
8718
9916
|
if (!prev) return prev;
|
|
8719
|
-
return
|
|
9917
|
+
return refreshElementInfoFromContent(next, prev);
|
|
8720
9918
|
});
|
|
8721
9919
|
}
|
|
8722
9920
|
historyOrderRef.current = [
|
|
@@ -8726,34 +9924,49 @@ export default function DesignEditor() {
|
|
|
8726
9924
|
return true;
|
|
8727
9925
|
}
|
|
8728
9926
|
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
if (!entry
|
|
9927
|
+
const entry =
|
|
9928
|
+
contentRedoStackRef.current[contentRedoStackRef.current.length - 1];
|
|
9929
|
+
if (!entry) return false;
|
|
9930
|
+
if (
|
|
9931
|
+
entry.fileId !== activeFile?.id &&
|
|
9932
|
+
!files.some((file) => file.id === entry.fileId)
|
|
9933
|
+
) {
|
|
9934
|
+
return false;
|
|
9935
|
+
}
|
|
9936
|
+
contentRedoStackRef.current.pop();
|
|
8732
9937
|
contentUndoStackRef.current = [
|
|
8733
9938
|
...contentUndoStackRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
|
|
8734
9939
|
entry,
|
|
8735
9940
|
];
|
|
8736
9941
|
historyOrderRef.current = [
|
|
8737
9942
|
...historyOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
|
|
8738
|
-
"content",
|
|
9943
|
+
"file-content",
|
|
8739
9944
|
];
|
|
8740
9945
|
suppressContentHistoryRef.current = true;
|
|
8741
9946
|
try {
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
9947
|
+
if (entry.fileId === activeFile?.id) {
|
|
9948
|
+
applyLocalContentUpdate(entry.after, {
|
|
9949
|
+
refreshPreview: false,
|
|
9950
|
+
immediateSave: true,
|
|
9951
|
+
});
|
|
9952
|
+
} else {
|
|
9953
|
+
applyFileContentUpdate(entry.fileId, entry.after, {
|
|
9954
|
+
refreshPreview: false,
|
|
9955
|
+
});
|
|
9956
|
+
}
|
|
8746
9957
|
} finally {
|
|
8747
9958
|
suppressContentHistoryRef.current = false;
|
|
8748
9959
|
}
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
|
|
8756
|
-
|
|
9960
|
+
if (entry.fileId === activeFile?.id) {
|
|
9961
|
+
setSelectedElement((prev) => {
|
|
9962
|
+
if (!prev) return prev;
|
|
9963
|
+
return refreshElementInfoFromContent(entry.after, prev);
|
|
9964
|
+
});
|
|
9965
|
+
setHoveredElement((prev) => {
|
|
9966
|
+
if (!prev) return prev;
|
|
9967
|
+
return refreshElementInfoFromContent(entry.after, prev);
|
|
9968
|
+
});
|
|
9969
|
+
}
|
|
8757
9970
|
return true;
|
|
8758
9971
|
};
|
|
8759
9972
|
const redoGeometry = () => {
|
|
@@ -8780,7 +9993,7 @@ export default function DesignEditor() {
|
|
|
8780
9993
|
const didRedo =
|
|
8781
9994
|
preferred === "geometry"
|
|
8782
9995
|
? redoGeometry() || redoContent()
|
|
8783
|
-
: preferred
|
|
9996
|
+
: isContentUndoRedoOrderKind(preferred)
|
|
8784
9997
|
? redoContent() || redoGeometry()
|
|
8785
9998
|
: redoContent() || redoGeometry();
|
|
8786
9999
|
if (didRedo) {
|
|
@@ -8789,8 +10002,10 @@ export default function DesignEditor() {
|
|
|
8789
10002
|
}, [
|
|
8790
10003
|
ydoc,
|
|
8791
10004
|
activeFile,
|
|
10005
|
+
applyFileContentUpdate,
|
|
8792
10006
|
applyLocalContentUpdate,
|
|
8793
10007
|
canEditDesign,
|
|
10008
|
+
files,
|
|
8794
10009
|
isSynced,
|
|
8795
10010
|
markPendingLocalFileContent,
|
|
8796
10011
|
queueFileContentSave,
|
|
@@ -8865,6 +10080,10 @@ export default function DesignEditor() {
|
|
|
8865
10080
|
const enterOverviewFromZoom = useCallback(() => {
|
|
8866
10081
|
if (viewModeRef.current === "overview") return;
|
|
8867
10082
|
viewModeRef.current = "overview";
|
|
10083
|
+
pendingOverviewScreenSelectionRef.current = null;
|
|
10084
|
+
pendingOverviewLayerSelectionRef.current = null;
|
|
10085
|
+
clearPendingOverviewLayerSelectionTimer();
|
|
10086
|
+
setCreatedOverviewLayerSelection(null);
|
|
8868
10087
|
runEditorViewTransition(() => {
|
|
8869
10088
|
setDrawMode(false);
|
|
8870
10089
|
setPinMode(false);
|
|
@@ -8874,7 +10093,7 @@ export default function DesignEditor() {
|
|
|
8874
10093
|
setActiveTool("move");
|
|
8875
10094
|
setViewMode("overview");
|
|
8876
10095
|
});
|
|
8877
|
-
}, [runEditorViewTransition]);
|
|
10096
|
+
}, [clearPendingOverviewLayerSelectionTimer, runEditorViewTransition]);
|
|
8878
10097
|
|
|
8879
10098
|
const enterSingleScreen = useCallback(
|
|
8880
10099
|
(fileId?: string | null) => {
|
|
@@ -8888,6 +10107,10 @@ export default function DesignEditor() {
|
|
|
8888
10107
|
return;
|
|
8889
10108
|
}
|
|
8890
10109
|
viewModeRef.current = "single";
|
|
10110
|
+
pendingOverviewScreenSelectionRef.current = null;
|
|
10111
|
+
pendingOverviewLayerSelectionRef.current = null;
|
|
10112
|
+
clearPendingOverviewLayerSelectionTimer();
|
|
10113
|
+
setCreatedOverviewLayerSelection(null);
|
|
8891
10114
|
runEditorViewTransition(() => {
|
|
8892
10115
|
if (fileId) setActiveFileId(fileId);
|
|
8893
10116
|
setDrawMode(false);
|
|
@@ -8900,7 +10123,11 @@ export default function DesignEditor() {
|
|
|
8900
10123
|
setViewMode("single");
|
|
8901
10124
|
});
|
|
8902
10125
|
},
|
|
8903
|
-
[
|
|
10126
|
+
[
|
|
10127
|
+
activeFileId,
|
|
10128
|
+
clearPendingOverviewLayerSelectionTimer,
|
|
10129
|
+
runEditorViewTransition,
|
|
10130
|
+
],
|
|
8904
10131
|
);
|
|
8905
10132
|
|
|
8906
10133
|
useEffect(() => {
|
|
@@ -8971,14 +10198,22 @@ export default function DesignEditor() {
|
|
|
8971
10198
|
|
|
8972
10199
|
const handleSidebarScreenSelect = useCallback(
|
|
8973
10200
|
(screenId: string) => {
|
|
10201
|
+
pendingOverviewScreenSelectionRef.current = null;
|
|
10202
|
+
pendingOverviewLayerSelectionRef.current = null;
|
|
10203
|
+
clearPendingOverviewLayerSelectionTimer();
|
|
10204
|
+
setCreatedOverviewLayerSelection(null);
|
|
8974
10205
|
setOverviewSelectedScreenIds([]);
|
|
8975
10206
|
setSelectedLayerIdsState([]);
|
|
8976
10207
|
enterSingleScreen(screenId);
|
|
8977
10208
|
},
|
|
8978
|
-
[enterSingleScreen],
|
|
10209
|
+
[clearPendingOverviewLayerSelectionTimer, enterSingleScreen],
|
|
8979
10210
|
);
|
|
8980
10211
|
|
|
8981
10212
|
const handleSidebarScreenOverview = useCallback(() => {
|
|
10213
|
+
pendingOverviewScreenSelectionRef.current = null;
|
|
10214
|
+
pendingOverviewLayerSelectionRef.current = null;
|
|
10215
|
+
clearPendingOverviewLayerSelectionTimer();
|
|
10216
|
+
setCreatedOverviewLayerSelection(null);
|
|
8982
10217
|
setOverviewSelectedScreenIds([]);
|
|
8983
10218
|
setSelectedLayerIdsState([]);
|
|
8984
10219
|
if (viewModeRef.current === "overview") {
|
|
@@ -8991,7 +10226,7 @@ export default function DesignEditor() {
|
|
|
8991
10226
|
return;
|
|
8992
10227
|
}
|
|
8993
10228
|
enterOverviewFromZoom();
|
|
8994
|
-
}, [enterOverviewFromZoom]);
|
|
10229
|
+
}, [clearPendingOverviewLayerSelectionTimer, enterOverviewFromZoom]);
|
|
8995
10230
|
|
|
8996
10231
|
const handlePinToolToggle = useCallback(() => {
|
|
8997
10232
|
if (!activeFile || !canEditDesign) return;
|
|
@@ -9806,11 +11041,11 @@ ${serializedHtml}
|
|
|
9806
11041
|
? "Preparing agent prompt..."
|
|
9807
11042
|
: codingHandoffPreviewFallback);
|
|
9808
11043
|
const shareExportTab = (
|
|
9809
|
-
<div className="space-y-
|
|
9810
|
-
<div className="text-
|
|
11044
|
+
<div className="space-y-3">
|
|
11045
|
+
<div className="text-[11px] font-semibold uppercase text-muted-foreground">
|
|
9811
11046
|
{"Format" /* i18n-ignore share export section label */}
|
|
9812
11047
|
</div>
|
|
9813
|
-
<div className="grid grid-cols-1 gap-
|
|
11048
|
+
<div className="grid grid-cols-1 gap-1.5 sm:grid-cols-2">
|
|
9814
11049
|
{shareExportOptions.map((option) => {
|
|
9815
11050
|
const selected = option.value === shareExportFormat;
|
|
9816
11051
|
const ExportIcon = option.Icon;
|
|
@@ -9820,45 +11055,47 @@ ${serializedHtml}
|
|
|
9820
11055
|
type="button"
|
|
9821
11056
|
onClick={() => setShareExportFormat(option.value)}
|
|
9822
11057
|
className={cn(
|
|
9823
|
-
"relative min-h-
|
|
11058
|
+
"relative flex min-h-[76px] items-start gap-2.5 rounded-md border border-[var(--design-editor-control-border)] bg-[var(--design-editor-control-bg)] p-2.5 text-left transition-colors hover:bg-[var(--design-editor-panel-raised-bg)]",
|
|
9824
11059
|
selected
|
|
9825
|
-
? "
|
|
9826
|
-
: "
|
|
11060
|
+
? "bg-[var(--design-editor-panel-raised-bg)] ring-1 ring-[var(--design-editor-accent-color)]"
|
|
11061
|
+
: "",
|
|
9827
11062
|
)}
|
|
9828
11063
|
>
|
|
9829
|
-
<span className="
|
|
9830
|
-
<ExportIcon className="size-5" strokeWidth={1.75} />
|
|
11064
|
+
<span className="inline-flex size-7 shrink-0 items-center justify-center rounded-md bg-[var(--design-editor-panel-raised-bg)] text-muted-foreground">
|
|
11065
|
+
<ExportIcon className="size-3.5" strokeWidth={1.75} />
|
|
9831
11066
|
</span>
|
|
9832
|
-
<span className="
|
|
9833
|
-
|
|
9834
|
-
|
|
9835
|
-
|
|
11067
|
+
<span className="min-w-0 flex-1">
|
|
11068
|
+
<span className="block truncate text-[12px] font-semibold text-foreground">
|
|
11069
|
+
{option.title}{" "}
|
|
11070
|
+
<span className="text-[11px] font-medium text-muted-foreground">
|
|
11071
|
+
{option.extension}
|
|
11072
|
+
</span>
|
|
11073
|
+
</span>
|
|
11074
|
+
<span className="mt-0.5 block text-[11px] leading-4 text-muted-foreground">
|
|
11075
|
+
{option.description}
|
|
9836
11076
|
</span>
|
|
9837
|
-
</span>
|
|
9838
|
-
<span className="mt-1 block text-sm leading-5 text-muted-foreground">
|
|
9839
|
-
{option.description}
|
|
9840
11077
|
</span>
|
|
9841
11078
|
<span
|
|
9842
11079
|
aria-hidden
|
|
9843
11080
|
className={cn(
|
|
9844
|
-
"absolute right-
|
|
11081
|
+
"absolute right-2.5 top-2.5 inline-flex size-4 items-center justify-center rounded-full border",
|
|
9845
11082
|
selected
|
|
9846
|
-
? "border-
|
|
9847
|
-
: "border-border bg-
|
|
11083
|
+
? "border-[var(--design-editor-accent-color)] bg-[var(--design-editor-accent-color)] text-[var(--design-editor-accent-contrast-color)]"
|
|
11084
|
+
: "border-[var(--design-editor-control-border)] bg-[var(--design-editor-panel-bg)]",
|
|
9848
11085
|
)}
|
|
9849
11086
|
>
|
|
9850
|
-
{selected ? <IconCheck className="size-
|
|
11087
|
+
{selected ? <IconCheck className="size-3" /> : null}
|
|
9851
11088
|
</span>
|
|
9852
11089
|
</button>
|
|
9853
11090
|
);
|
|
9854
11091
|
})}
|
|
9855
11092
|
</div>
|
|
9856
|
-
<div className="flex flex-wrap items-center justify-between gap-
|
|
11093
|
+
<div className="flex flex-wrap items-center justify-between gap-2 border-t border-[var(--design-editor-panel-divider-color)] pt-3">
|
|
9857
11094
|
<div className="min-w-0">
|
|
9858
|
-
<div className="text-
|
|
11095
|
+
<div className="text-[12px] font-medium text-foreground">
|
|
9859
11096
|
{selectedShareExportOption.title}
|
|
9860
11097
|
</div>
|
|
9861
|
-
<div className="text-
|
|
11098
|
+
<div className="text-[11px] text-muted-foreground">
|
|
9862
11099
|
{selectedShareExportOption.description}
|
|
9863
11100
|
</div>
|
|
9864
11101
|
</div>
|
|
@@ -9866,34 +11103,34 @@ ${serializedHtml}
|
|
|
9866
11103
|
type="button"
|
|
9867
11104
|
onClick={selectedShareExportOption.onDownload}
|
|
9868
11105
|
disabled={selectedShareExportOption.disabled}
|
|
9869
|
-
className="h-
|
|
11106
|
+
className="h-8 gap-1.5 rounded-md px-3 text-[12px]"
|
|
9870
11107
|
>
|
|
9871
|
-
<IconDownload className="size-
|
|
11108
|
+
<IconDownload className="size-3.5" />
|
|
9872
11109
|
{"Download" /* i18n-ignore share export action */}
|
|
9873
11110
|
</Button>
|
|
9874
11111
|
</div>
|
|
9875
11112
|
</div>
|
|
9876
11113
|
);
|
|
9877
11114
|
const shareSendToTab = (
|
|
9878
|
-
<div className="space-y-
|
|
9879
|
-
<div className="overflow-hidden rounded-
|
|
9880
|
-
<div className="flex h-
|
|
11115
|
+
<div className="space-y-3">
|
|
11116
|
+
<div className="overflow-hidden rounded-md border border-neutral-800 bg-neutral-950 shadow-sm">
|
|
11117
|
+
<div className="flex h-8 items-center border-b border-neutral-800 px-3">
|
|
9881
11118
|
<div className="flex items-center gap-2">
|
|
9882
|
-
<span className="size-
|
|
9883
|
-
<span className="size-
|
|
9884
|
-
<span className="size-
|
|
11119
|
+
<span className="size-2.5 rounded-full bg-red-500" />
|
|
11120
|
+
<span className="size-2.5 rounded-full bg-yellow-400" />
|
|
11121
|
+
<span className="size-2.5 rounded-full bg-green-500" />
|
|
9885
11122
|
</div>
|
|
9886
|
-
<div className="min-w-0 flex-1 truncate text-center text-
|
|
11123
|
+
<div className="min-w-0 flex-1 truncate text-center text-[12px] font-medium text-neutral-400">
|
|
9887
11124
|
{"Your agent" /* i18n-ignore terminal title */}
|
|
9888
11125
|
</div>
|
|
9889
|
-
<IconTerminal2 className="size-
|
|
11126
|
+
<IconTerminal2 className="size-3.5 text-neutral-500" />
|
|
9890
11127
|
</div>
|
|
9891
|
-
<pre className="max-h-
|
|
11128
|
+
<pre className="max-h-44 overflow-auto whitespace-pre-wrap break-words px-3 py-3 font-mono text-[12px] leading-5 text-neutral-100">
|
|
9892
11129
|
{`> ${codingHandoffPreviewText}`}
|
|
9893
11130
|
</pre>
|
|
9894
11131
|
</div>
|
|
9895
11132
|
|
|
9896
|
-
<div className="flex flex-wrap items-center gap-
|
|
11133
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
9897
11134
|
<Button
|
|
9898
11135
|
type="button"
|
|
9899
11136
|
onClick={handleSendToPrimaryAction}
|
|
@@ -9902,12 +11139,12 @@ ${serializedHtml}
|
|
|
9902
11139
|
? !activeFile || codingHandoffLoading
|
|
9903
11140
|
: codingHandoffLoading
|
|
9904
11141
|
}
|
|
9905
|
-
className="h-
|
|
11142
|
+
className="h-8 gap-1.5 rounded-md px-3 text-[12px]"
|
|
9906
11143
|
>
|
|
9907
11144
|
{downloadZipInstead ? (
|
|
9908
|
-
<IconArchive className="size-
|
|
11145
|
+
<IconArchive className="size-3.5" />
|
|
9909
11146
|
) : (
|
|
9910
|
-
<IconClipboard className="size-
|
|
11147
|
+
<IconClipboard className="size-3.5" />
|
|
9911
11148
|
)}
|
|
9912
11149
|
{
|
|
9913
11150
|
downloadZipInstead
|
|
@@ -9917,20 +11154,20 @@ ${serializedHtml}
|
|
|
9917
11154
|
</Button>
|
|
9918
11155
|
</div>
|
|
9919
11156
|
|
|
9920
|
-
<div className="space-y-
|
|
9921
|
-
<div className="flex items-start gap-
|
|
11157
|
+
<div className="space-y-3">
|
|
11158
|
+
<div className="flex items-start gap-2.5">
|
|
9922
11159
|
<Checkbox
|
|
9923
11160
|
checked={downloadZipInstead}
|
|
9924
11161
|
onCheckedChange={(checked) =>
|
|
9925
11162
|
setDownloadZipInstead(checked === true)
|
|
9926
11163
|
}
|
|
9927
|
-
className="mt-
|
|
11164
|
+
className="mt-0.5"
|
|
9928
11165
|
/>
|
|
9929
11166
|
<div className="min-w-0">
|
|
9930
|
-
<div className="text-
|
|
11167
|
+
<div className="text-[12px] font-medium text-foreground">
|
|
9931
11168
|
{"Download zip instead" /* i18n-ignore share send option */}
|
|
9932
11169
|
</div>
|
|
9933
|
-
<div className="mt-0.5 text-
|
|
11170
|
+
<div className="mt-0.5 text-[11px] leading-4 text-muted-foreground">
|
|
9934
11171
|
{
|
|
9935
11172
|
"For agents without the Design connector, drop the bundle into your agent's chat manually." /* i18n-ignore share send option description */
|
|
9936
11173
|
}
|
|
@@ -9938,8 +11175,8 @@ ${serializedHtml}
|
|
|
9938
11175
|
</div>
|
|
9939
11176
|
</div>
|
|
9940
11177
|
|
|
9941
|
-
<div className="space-y-
|
|
9942
|
-
<label className="text-
|
|
11178
|
+
<div className="space-y-1.5">
|
|
11179
|
+
<label className="text-[12px] font-medium text-foreground">
|
|
9943
11180
|
{
|
|
9944
11181
|
"Give the agent more detail on what to implement" /* i18n-ignore share send detail label */
|
|
9945
11182
|
}{" "}
|
|
@@ -9951,12 +11188,17 @@ ${serializedHtml}
|
|
|
9951
11188
|
value={codingHandoffDetail}
|
|
9952
11189
|
onChange={(event) => setCodingHandoffDetail(event.target.value)}
|
|
9953
11190
|
placeholder={activeFile?.filename ?? "Add implementation notes..."}
|
|
9954
|
-
className="min-h-
|
|
11191
|
+
className="min-h-20 resize-none rounded-md bg-[var(--design-editor-control-bg)] text-[12px]"
|
|
9955
11192
|
/>
|
|
9956
11193
|
</div>
|
|
9957
11194
|
</div>
|
|
9958
11195
|
</div>
|
|
9959
11196
|
);
|
|
11197
|
+
const designSharePopoverClassName =
|
|
11198
|
+
"z-[100010] !w-[min(620px,calc(100vw-32px))] !p-3 " +
|
|
11199
|
+
"[&_[role=tablist]]:!inline-flex [&_[role=tablist]]:!w-fit [&_[role=tablist]]:!self-start [&_[role=tablist]]:justify-start [&_[role=tablist]]:gap-0.5 [&_[role=tablist]]:rounded-none [&_[role=tablist]]:bg-transparent [&_[role=tablist]]:p-0 " +
|
|
11200
|
+
"[&_[role=tab]]:!h-6 [&_[role=tab]]:!flex-none [&_[role=tab]]:rounded-md [&_[role=tab]]:px-2 [&_[role=tab]]:text-[11px] [&_[role=tab]]:font-semibold [&_[role=tab]]:shadow-none [&_[role=tab]]:ring-0 " +
|
|
11201
|
+
"[&_[role=tab]:hover]:bg-[var(--design-editor-panel-raised-bg)] [&_[role=tab]:hover]:text-foreground [&_[role=tab][aria-selected=true]]:bg-[var(--design-editor-panel-raised-bg)] [&_[role=tab][aria-selected=true]]:text-foreground [&_[role=tab][aria-selected=true]]:ring-0";
|
|
9960
11202
|
const designShareTabs = {
|
|
9961
11203
|
shareLabel: "Share link" /* i18n-ignore share tab label */,
|
|
9962
11204
|
defaultValue: "share",
|
|
@@ -10066,6 +11308,23 @@ ${serializedHtml}
|
|
|
10066
11308
|
});
|
|
10067
11309
|
return state;
|
|
10068
11310
|
}, [codeLayerModelsByFile, hiddenLayerIds, lockedLayerIds]);
|
|
11311
|
+
useEffect(() => {
|
|
11312
|
+
shouldPreserveBlockedOverviewLayerSelectionRef.current = (
|
|
11313
|
+
screenId: string,
|
|
11314
|
+
) => {
|
|
11315
|
+
if (viewModeRef.current !== "overview") return false;
|
|
11316
|
+
return selectedLayerIdsState.some((layerId) => {
|
|
11317
|
+
const owner = codeLayerOwnerByNodeId.get(layerId);
|
|
11318
|
+
if (!owner || owner.fileId !== screenId) return false;
|
|
11319
|
+
return (
|
|
11320
|
+
effectiveCodeLayerState.lockedIds.has(screenId) ||
|
|
11321
|
+
effectiveCodeLayerState.hiddenIds.has(screenId) ||
|
|
11322
|
+
effectiveCodeLayerState.lockedIds.has(layerId) ||
|
|
11323
|
+
effectiveCodeLayerState.hiddenIds.has(layerId)
|
|
11324
|
+
);
|
|
11325
|
+
});
|
|
11326
|
+
};
|
|
11327
|
+
}, [codeLayerOwnerByNodeId, effectiveCodeLayerState, selectedLayerIdsState]);
|
|
10069
11328
|
useEffect(() => {
|
|
10070
11329
|
const fileIds = new Set(files.map((file) => file.id));
|
|
10071
11330
|
const allCodeLayerNodes = codeLayerModelsByFile.flatMap(
|
|
@@ -10171,25 +11430,9 @@ ${serializedHtml}
|
|
|
10171
11430
|
|
|
10172
11431
|
const layerPanelFiles = useMemo<LayersPanelFile[]>(
|
|
10173
11432
|
() =>
|
|
10174
|
-
files
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
filename: file.filename,
|
|
10178
|
-
fileType: file.fileType,
|
|
10179
|
-
detail: file.filename,
|
|
10180
|
-
locked: lockedLayerIds.has(file.id),
|
|
10181
|
-
hidden: hiddenLayerIds.has(file.id),
|
|
10182
|
-
lockable: true,
|
|
10183
|
-
hideable: true,
|
|
10184
|
-
renamable: true,
|
|
10185
|
-
})),
|
|
10186
|
-
[files, hiddenLayerIds, lockedLayerIds],
|
|
10187
|
-
);
|
|
10188
|
-
const overviewLayerPanelFiles = useMemo<LayersPanelFile[]>(
|
|
10189
|
-
() =>
|
|
10190
|
-
files.map((file) => {
|
|
10191
|
-
const model = codeLayerModelByFileId.get(file.id);
|
|
10192
|
-
return {
|
|
11433
|
+
files
|
|
11434
|
+
.filter((file) => !isBoardFile(file.filename))
|
|
11435
|
+
.map((file) => ({
|
|
10193
11436
|
id: file.id,
|
|
10194
11437
|
name: prettyScreenName(file.filename),
|
|
10195
11438
|
filename: file.filename,
|
|
@@ -10200,16 +11443,54 @@ ${serializedHtml}
|
|
|
10200
11443
|
lockable: true,
|
|
10201
11444
|
hideable: true,
|
|
10202
11445
|
renamable: true,
|
|
10203
|
-
|
|
10204
|
-
|
|
10205
|
-
|
|
10206
|
-
|
|
10207
|
-
|
|
10208
|
-
|
|
10209
|
-
|
|
11446
|
+
})),
|
|
11447
|
+
[files, hiddenLayerIds, lockedLayerIds],
|
|
11448
|
+
);
|
|
11449
|
+
const overviewLayerPanelFiles = useMemo<LayersPanelFile[]>(
|
|
11450
|
+
() =>
|
|
11451
|
+
files
|
|
11452
|
+
.filter((file) => !isBoardFile(file.filename))
|
|
11453
|
+
.map((file) => {
|
|
11454
|
+
const model = codeLayerModelByFileId.get(file.id);
|
|
11455
|
+
return {
|
|
11456
|
+
id: file.id,
|
|
11457
|
+
name: prettyScreenName(file.filename),
|
|
11458
|
+
filename: file.filename,
|
|
11459
|
+
fileType: file.fileType,
|
|
11460
|
+
detail: file.filename,
|
|
11461
|
+
locked: lockedLayerIds.has(file.id),
|
|
11462
|
+
hidden: hiddenLayerIds.has(file.id),
|
|
11463
|
+
lockable: true,
|
|
11464
|
+
hideable: true,
|
|
11465
|
+
renamable: true,
|
|
11466
|
+
layers: codeLayerTreeToPanelNodes(
|
|
11467
|
+
model?.tree ?? [],
|
|
11468
|
+
lockedLayerIds,
|
|
11469
|
+
hiddenLayerIds,
|
|
11470
|
+
),
|
|
11471
|
+
};
|
|
11472
|
+
}),
|
|
10210
11473
|
[codeLayerModelByFileId, files, hiddenLayerIds, lockedLayerIds],
|
|
10211
11474
|
);
|
|
10212
11475
|
|
|
11476
|
+
// Board objects shown as top-level peer rows in the layers panel, right
|
|
11477
|
+
// alongside the screen frames. Derived from the same code-layer model that
|
|
11478
|
+
// feeds codeLayerOwnerByNodeId so a layer-row click resolves to the board
|
|
11479
|
+
// file (sets it active + selects the element). buildCodeLayerProjection was
|
|
11480
|
+
// the wrong source here: it produced different node ids that the owner map
|
|
11481
|
+
// could not route, and returned no roots for the migrated board fragments.
|
|
11482
|
+
const boardElements = useMemo<LayersPanelNode[] | undefined>(() => {
|
|
11483
|
+
if (!boardFileId) return undefined;
|
|
11484
|
+
const model = codeLayerModelByFileId.get(boardFileId);
|
|
11485
|
+
if (!model?.tree?.length) return undefined;
|
|
11486
|
+
const nodes = codeLayerTreeToPanelNodes(
|
|
11487
|
+
model.tree,
|
|
11488
|
+
lockedLayerIds,
|
|
11489
|
+
hiddenLayerIds,
|
|
11490
|
+
);
|
|
11491
|
+
return nodes.length > 0 ? nodes : undefined;
|
|
11492
|
+
}, [boardFileId, codeLayerModelByFileId, lockedLayerIds, hiddenLayerIds]);
|
|
11493
|
+
|
|
10213
11494
|
const activeLayerPanelNodes = useMemo<LayersPanelNode[]>(
|
|
10214
11495
|
() => activeCodeLayerPanelNodes,
|
|
10215
11496
|
[activeCodeLayerPanelNodes],
|
|
@@ -10223,6 +11504,18 @@ ${serializedHtml}
|
|
|
10223
11504
|
).map((node) => node.id),
|
|
10224
11505
|
);
|
|
10225
11506
|
const fileIds = new Set(files.map((file) => file.id));
|
|
11507
|
+
const pendingOverviewScreenId = pendingOverviewScreenSelectionRef.current;
|
|
11508
|
+
const pendingOverviewLayerId = pendingOverviewLayerSelectionRef.current;
|
|
11509
|
+
if (pendingOverviewScreenId) {
|
|
11510
|
+
validIds.add(pendingOverviewScreenId);
|
|
11511
|
+
fileIds.add(pendingOverviewScreenId);
|
|
11512
|
+
}
|
|
11513
|
+
if (pendingOverviewLayerId) {
|
|
11514
|
+
validIds.add(pendingOverviewLayerId);
|
|
11515
|
+
}
|
|
11516
|
+
if (createdOverviewLayerSelection) {
|
|
11517
|
+
validIds.add(createdOverviewLayerSelection.layerId);
|
|
11518
|
+
}
|
|
10226
11519
|
if (selectedElementLayerId) validIds.add(selectedElementLayerId);
|
|
10227
11520
|
files.forEach((file) => validIds.add(file.id));
|
|
10228
11521
|
const selectedStateIds = selectedLayerIdsState.filter((layerId) =>
|
|
@@ -10235,11 +11528,13 @@ ${serializedHtml}
|
|
|
10235
11528
|
viewMode === "overview" &&
|
|
10236
11529
|
selectedStateIds.some((layerId) => fileIds.has(layerId));
|
|
10237
11530
|
const baseSelection =
|
|
10238
|
-
viewMode === "overview" &&
|
|
10239
|
-
?
|
|
10240
|
-
|
|
10241
|
-
|
|
10242
|
-
|
|
11531
|
+
viewMode === "overview" && createdOverviewLayerSelection
|
|
11532
|
+
? [createdOverviewLayerSelection.layerId]
|
|
11533
|
+
: viewMode === "overview" && !hasOverviewCodeLayerSelection
|
|
11534
|
+
? overviewSelectedScreenIds.length > 0 || !hasOverviewFileSelection
|
|
11535
|
+
? overviewSelectedScreenIds
|
|
11536
|
+
: selectedLayerIdsState
|
|
11537
|
+
: selectedLayerIdsState;
|
|
10243
11538
|
const filtered = baseSelection.filter((layerId) => validIds.has(layerId));
|
|
10244
11539
|
if (selectedElementLayerId && !filtered.includes(selectedElementLayerId)) {
|
|
10245
11540
|
if (filtered.length > 1) return [...filtered, selectedElementLayerId];
|
|
@@ -10249,6 +11544,7 @@ ${serializedHtml}
|
|
|
10249
11544
|
}, [
|
|
10250
11545
|
activeCodeLayerProjection.nodes,
|
|
10251
11546
|
codeLayerModelsByFile,
|
|
11547
|
+
createdOverviewLayerSelection,
|
|
10252
11548
|
files,
|
|
10253
11549
|
overviewSelectedScreenIds,
|
|
10254
11550
|
selectedElementLayerId,
|
|
@@ -10261,6 +11557,59 @@ ${serializedHtml}
|
|
|
10261
11557
|
selectedLayerIdsRef.current = selectedLayerIds;
|
|
10262
11558
|
}, [selectedLayerIds]);
|
|
10263
11559
|
|
|
11560
|
+
const layerPanelSelectedIds = useMemo(
|
|
11561
|
+
() =>
|
|
11562
|
+
viewMode === "overview" && createdOverviewLayerSelection
|
|
11563
|
+
? [createdOverviewLayerSelection.layerId]
|
|
11564
|
+
: selectedLayerIds,
|
|
11565
|
+
[createdOverviewLayerSelection, selectedLayerIds, viewMode],
|
|
11566
|
+
);
|
|
11567
|
+
|
|
11568
|
+
const layerPanelExpandedIds = useMemo(() => {
|
|
11569
|
+
if (viewMode !== "overview" || !createdOverviewLayerSelection) {
|
|
11570
|
+
return expandedLayerIds;
|
|
11571
|
+
}
|
|
11572
|
+
const next = new Set(expandedLayerIds);
|
|
11573
|
+
next.add(createdOverviewLayerSelection.screenId);
|
|
11574
|
+
return Array.from(next);
|
|
11575
|
+
}, [createdOverviewLayerSelection, expandedLayerIds, viewMode]);
|
|
11576
|
+
|
|
11577
|
+
useEffect(() => {
|
|
11578
|
+
const pendingLayerId = pendingOverviewLayerSelectionRef.current;
|
|
11579
|
+
if (!pendingLayerId) return;
|
|
11580
|
+
if (!selectedLayerIdsState.includes(pendingLayerId)) {
|
|
11581
|
+
pendingOverviewLayerSelectionRef.current = null;
|
|
11582
|
+
clearPendingOverviewLayerSelectionTimer();
|
|
11583
|
+
return;
|
|
11584
|
+
}
|
|
11585
|
+
const owner = codeLayerOwnerByNodeId.get(pendingLayerId);
|
|
11586
|
+
if (!owner) return;
|
|
11587
|
+
schedulePendingOverviewLayerSelectionClear(pendingLayerId);
|
|
11588
|
+
setActiveFileId(owner.fileId);
|
|
11589
|
+
setSelectedElement(elementInfoFromCodeLayerNode(owner.node));
|
|
11590
|
+
setExpandedLayerIds((current) => {
|
|
11591
|
+
const next = new Set(current);
|
|
11592
|
+
next.add(owner.fileId);
|
|
11593
|
+
collectCodeLayerAncestors(owner.tree, pendingLayerId).forEach((id) =>
|
|
11594
|
+
next.add(id),
|
|
11595
|
+
);
|
|
11596
|
+
return next.size === current.length ? current : Array.from(next);
|
|
11597
|
+
});
|
|
11598
|
+
}, [
|
|
11599
|
+
clearPendingOverviewLayerSelectionTimer,
|
|
11600
|
+
codeLayerOwnerByNodeId,
|
|
11601
|
+
schedulePendingOverviewLayerSelectionClear,
|
|
11602
|
+
selectedLayerIdsState,
|
|
11603
|
+
]);
|
|
11604
|
+
|
|
11605
|
+
useEffect(() => {
|
|
11606
|
+
const pendingScreenId = pendingOverviewScreenSelectionRef.current;
|
|
11607
|
+
if (!pendingScreenId) return;
|
|
11608
|
+
if (files.some((file) => file.id === pendingScreenId)) {
|
|
11609
|
+
pendingOverviewScreenSelectionRef.current = null;
|
|
11610
|
+
}
|
|
11611
|
+
}, [files]);
|
|
11612
|
+
|
|
10264
11613
|
useEffect(() => {
|
|
10265
11614
|
setSelectedLayerIdsState((current) => {
|
|
10266
11615
|
if (!selectedElementLayerId) {
|
|
@@ -10286,7 +11635,26 @@ ${serializedHtml}
|
|
|
10286
11635
|
ancestorIds.forEach((ancestorId) => next.add(ancestorId));
|
|
10287
11636
|
return next.size === current.length ? current : Array.from(next);
|
|
10288
11637
|
});
|
|
10289
|
-
}, [activeCodeLayerTree, codeLayerOwnerByNodeId, selectedElementLayerId]);
|
|
11638
|
+
}, [activeCodeLayerTree, codeLayerOwnerByNodeId, selectedElementLayerId]);
|
|
11639
|
+
|
|
11640
|
+
useEffect(() => {
|
|
11641
|
+
const selectedCodeLayerIds = selectedLayerIds.filter((layerId) =>
|
|
11642
|
+
codeLayerOwnerByNodeId.has(layerId),
|
|
11643
|
+
);
|
|
11644
|
+
if (selectedCodeLayerIds.length === 0) return;
|
|
11645
|
+
setExpandedLayerIds((current) => {
|
|
11646
|
+
const next = new Set(current);
|
|
11647
|
+
selectedCodeLayerIds.forEach((layerId) => {
|
|
11648
|
+
const owner = codeLayerOwnerByNodeId.get(layerId);
|
|
11649
|
+
if (!owner) return;
|
|
11650
|
+
next.add(owner.fileId);
|
|
11651
|
+
collectCodeLayerAncestors(owner.tree, layerId).forEach((ancestorId) =>
|
|
11652
|
+
next.add(ancestorId),
|
|
11653
|
+
);
|
|
11654
|
+
});
|
|
11655
|
+
return next.size === current.length ? current : Array.from(next);
|
|
11656
|
+
});
|
|
11657
|
+
}, [codeLayerOwnerByNodeId, selectedLayerIds]);
|
|
10290
11658
|
|
|
10291
11659
|
useEffect(() => {
|
|
10292
11660
|
if (!selectedElementLayerId) return;
|
|
@@ -10381,6 +11749,8 @@ ${serializedHtml}
|
|
|
10381
11749
|
return defaultMatch?.id ?? "auto";
|
|
10382
11750
|
}, [activeBreakpointWidthState, statesPanelBreakpoints]);
|
|
10383
11751
|
|
|
11752
|
+
const publishDesignTitle = design?.title?.trim() || "Untitled design";
|
|
11753
|
+
|
|
10384
11754
|
const handleOpenDesignPreview = useCallback(() => {
|
|
10385
11755
|
if (activeScreenPreviewUrl) {
|
|
10386
11756
|
window.open(activeScreenPreviewUrl, "_blank", "noopener,noreferrer");
|
|
@@ -10397,6 +11767,53 @@ ${serializedHtml}
|
|
|
10397
11767
|
window.setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000);
|
|
10398
11768
|
}, [activeContent, activeScreenPreviewUrl]);
|
|
10399
11769
|
|
|
11770
|
+
const handleJoinPublishWaitlist = useCallback(async () => {
|
|
11771
|
+
if (!isSignedIn) {
|
|
11772
|
+
handleSignInToSave();
|
|
11773
|
+
return;
|
|
11774
|
+
}
|
|
11775
|
+
|
|
11776
|
+
setJoiningPublishWaitlist(true);
|
|
11777
|
+
setPublishWaitlistError(null);
|
|
11778
|
+
|
|
11779
|
+
try {
|
|
11780
|
+
const res = await fetch(
|
|
11781
|
+
new URL(
|
|
11782
|
+
agentNativePath("/_agent-native/builder/branch-waitlist"),
|
|
11783
|
+
window.location.origin,
|
|
11784
|
+
).href,
|
|
11785
|
+
{
|
|
11786
|
+
method: "POST",
|
|
11787
|
+
headers: { "Content-Type": "application/json" },
|
|
11788
|
+
body: JSON.stringify({
|
|
11789
|
+
pageUrl: window.location.href,
|
|
11790
|
+
prompt: `Publish design "${publishDesignTitle}" as an app.`,
|
|
11791
|
+
source: "design_editor_publish_app_menu",
|
|
11792
|
+
useCase: "design_publish_app",
|
|
11793
|
+
}),
|
|
11794
|
+
},
|
|
11795
|
+
);
|
|
11796
|
+
const data = await res.json().catch(() => ({}));
|
|
11797
|
+
if (!res.ok) {
|
|
11798
|
+
throw new Error(
|
|
11799
|
+
typeof data?.error === "string"
|
|
11800
|
+
? data.error
|
|
11801
|
+
: `Request failed (${res.status})`,
|
|
11802
|
+
);
|
|
11803
|
+
}
|
|
11804
|
+
|
|
11805
|
+
setPublishWaitlistJoined(true);
|
|
11806
|
+
} catch (err) {
|
|
11807
|
+
setPublishWaitlistError(
|
|
11808
|
+
err instanceof Error
|
|
11809
|
+
? err.message
|
|
11810
|
+
: "Couldn't join the waitlist. Please try again.",
|
|
11811
|
+
);
|
|
11812
|
+
} finally {
|
|
11813
|
+
setJoiningPublishWaitlist(false);
|
|
11814
|
+
}
|
|
11815
|
+
}, [handleSignInToSave, isSignedIn, publishDesignTitle]);
|
|
11816
|
+
|
|
10400
11817
|
const activeLayerId =
|
|
10401
11818
|
selectedLayerIds[selectedLayerIds.length - 1] ??
|
|
10402
11819
|
selectedElementLayerId ??
|
|
@@ -10430,6 +11847,198 @@ ${serializedHtml}
|
|
|
10430
11847
|
source: activeOverviewScreen?.source,
|
|
10431
11848
|
})
|
|
10432
11849
|
: undefined;
|
|
11850
|
+
// Connection id for the active localhost screen — needed to mint write grants.
|
|
11851
|
+
const activeLocalhostConnectionId = activeScreenIsLocalSource
|
|
11852
|
+
? ((activeOverviewScreen as { connectionId?: string } | undefined)
|
|
11853
|
+
?.connectionId ?? "")
|
|
11854
|
+
: "";
|
|
11855
|
+
|
|
11856
|
+
/**
|
|
11857
|
+
* Request consent to write a local file for the active localhost screen.
|
|
11858
|
+
* If no valid grant exists, opens the consent dialog; once granted the
|
|
11859
|
+
* caller should proceed to call write-local-file via the action surface.
|
|
11860
|
+
*
|
|
11861
|
+
* Only works when the active screen is localhost-backed and the current user
|
|
11862
|
+
* has editor access. For non-localhost screens use the normal Ask-AI path.
|
|
11863
|
+
*
|
|
11864
|
+
* The files parameter is for display in the consent dialog only; the actual
|
|
11865
|
+
* write must be performed by the caller via the write-local-file action.
|
|
11866
|
+
*/
|
|
11867
|
+
const requestLocalhostWrite = useCallback(
|
|
11868
|
+
(opts: {
|
|
11869
|
+
files: string[];
|
|
11870
|
+
onGranted: LocalhostWriteConsentPayload["onGranted"];
|
|
11871
|
+
onCancel?: () => void;
|
|
11872
|
+
}) => {
|
|
11873
|
+
if (!id || !canEditDesign || !activeLocalhostConnectionId) return;
|
|
11874
|
+
|
|
11875
|
+
const rootPath =
|
|
11876
|
+
activeScreenRouteSourceFile ?? activeLocalhostConnectionId;
|
|
11877
|
+
|
|
11878
|
+
setLocalhostConsentConnectionId(activeLocalhostConnectionId);
|
|
11879
|
+
setLocalhostWriteConsentPayload({
|
|
11880
|
+
rootPath,
|
|
11881
|
+
files: opts.files,
|
|
11882
|
+
onGranted: opts.onGranted,
|
|
11883
|
+
onCancel: opts.onCancel ?? (() => {}),
|
|
11884
|
+
});
|
|
11885
|
+
setLocalhostWriteConsentOpen(true);
|
|
11886
|
+
},
|
|
11887
|
+
[
|
|
11888
|
+
activeLocalhostConnectionId,
|
|
11889
|
+
activeScreenRouteSourceFile,
|
|
11890
|
+
canEditDesign,
|
|
11891
|
+
id,
|
|
11892
|
+
],
|
|
11893
|
+
);
|
|
11894
|
+
// requestLocalhostWrite is consumed via the component instance or by
|
|
11895
|
+
// connected inspector components; not all render paths call it directly.
|
|
11896
|
+
void requestLocalhostWrite;
|
|
11897
|
+
|
|
11898
|
+
/**
|
|
11899
|
+
* Derive a relative file path from the active localhost screen.
|
|
11900
|
+
* Prefers `sourceFile` (the build-output relative path recorded at connect
|
|
11901
|
+
* time) over the URL pathname so the path maps to the actual file on disk.
|
|
11902
|
+
* Returns undefined when no usable path can be determined.
|
|
11903
|
+
*/
|
|
11904
|
+
const activeLocalhostRelPath = useMemo<string | undefined>(() => {
|
|
11905
|
+
if (!activeScreenIsLocalSource) return undefined;
|
|
11906
|
+
// Prefer the explicit sourceFile (e.g. "src/index.html").
|
|
11907
|
+
const sf = activeScreenRouteSourceFile;
|
|
11908
|
+
if (sf?.trim()) return sf.trim();
|
|
11909
|
+
// Fall back to URL pathname (e.g. "/page.html" → "page.html").
|
|
11910
|
+
const url = activeOverviewScreen?.url;
|
|
11911
|
+
if (!url) return undefined;
|
|
11912
|
+
try {
|
|
11913
|
+
const pathname = new URL(url).pathname.replace(/^\//, "");
|
|
11914
|
+
return pathname || undefined;
|
|
11915
|
+
} catch {
|
|
11916
|
+
return undefined;
|
|
11917
|
+
}
|
|
11918
|
+
}, [
|
|
11919
|
+
activeScreenIsLocalSource,
|
|
11920
|
+
activeScreenRouteSourceFile,
|
|
11921
|
+
activeOverviewScreen?.url,
|
|
11922
|
+
]);
|
|
11923
|
+
|
|
11924
|
+
/** True when the active localhost screen maps to an HTML/CSS file we can write. */
|
|
11925
|
+
const activeLocalhostRouteIsWritable =
|
|
11926
|
+
activeScreenIsLocalSource &&
|
|
11927
|
+
Boolean(activeLocalhostRelPath) &&
|
|
11928
|
+
LOCALHOST_WRITE_EXTENSIONS.has(
|
|
11929
|
+
(activeLocalhostRelPath?.match(/\.[^.]+$/) ?? [])[0]?.toLowerCase() ?? "",
|
|
11930
|
+
);
|
|
11931
|
+
|
|
11932
|
+
/**
|
|
11933
|
+
* Strip editor-only node-id attributes from HTML source so they are not
|
|
11934
|
+
* written back to the user's local file.
|
|
11935
|
+
*
|
|
11936
|
+
* Kept attributes (intentional user content):
|
|
11937
|
+
* - data-agent-native-layer-name (human-readable display name)
|
|
11938
|
+
* - data-screen="…" (prototype navigation)
|
|
11939
|
+
* - any other data-* not listed below
|
|
11940
|
+
*
|
|
11941
|
+
* Stripped attributes (editor plumbing only):
|
|
11942
|
+
* - data-agent-native-node-id (stable selection id stamped by the editor)
|
|
11943
|
+
* - data-code-layer-id (alternative layer id for localhost components)
|
|
11944
|
+
*/
|
|
11945
|
+
function stripEditorOnlyAttributes(html: string): string {
|
|
11946
|
+
if (typeof window === "undefined") return html;
|
|
11947
|
+
try {
|
|
11948
|
+
const doc = new DOMParser().parseFromString(html, "text/html");
|
|
11949
|
+
const STRIP_ATTRS = [
|
|
11950
|
+
"data-agent-native-node-id",
|
|
11951
|
+
"data-code-layer-id",
|
|
11952
|
+
] as const;
|
|
11953
|
+
for (const attr of STRIP_ATTRS) {
|
|
11954
|
+
doc.querySelectorAll(`[${attr}]`).forEach((el) => {
|
|
11955
|
+
el.removeAttribute(attr);
|
|
11956
|
+
});
|
|
11957
|
+
}
|
|
11958
|
+
// Serialise back. Use outerHTML of <html> to preserve doctype-less
|
|
11959
|
+
// fragments; for full documents prefer innerHTML wrapping.
|
|
11960
|
+
const doctype = doc.doctype
|
|
11961
|
+
? new XMLSerializer().serializeToString(doc.doctype) + "\n"
|
|
11962
|
+
: "";
|
|
11963
|
+
const htmlEl = doc.documentElement;
|
|
11964
|
+
return doctype + htmlEl.outerHTML;
|
|
11965
|
+
} catch {
|
|
11966
|
+
// If DOMParser fails (e.g. malformed HTML) fall back to the raw content.
|
|
11967
|
+
return html;
|
|
11968
|
+
}
|
|
11969
|
+
}
|
|
11970
|
+
|
|
11971
|
+
/**
|
|
11972
|
+
* "Apply to source" — write the current editor content back to the local
|
|
11973
|
+
* file via the bridge. Opens the consent dialog if no grant exists yet, then
|
|
11974
|
+
* calls write-local-file with a clean version of the editor content (editor-
|
|
11975
|
+
* only attribute stamps stripped).
|
|
11976
|
+
*
|
|
11977
|
+
* Only operates on HTML/CSS routes (gated by activeLocalhostRouteIsWritable).
|
|
11978
|
+
* For non-HTML routes (React/JSX/TS), keep routing to the agent chat instead.
|
|
11979
|
+
*/
|
|
11980
|
+
const handleApplyToSource = useCallback(() => {
|
|
11981
|
+
if (
|
|
11982
|
+
!id ||
|
|
11983
|
+
!canEditDesign ||
|
|
11984
|
+
!activeLocalhostConnectionId ||
|
|
11985
|
+
!activeLocalhostRelPath
|
|
11986
|
+
)
|
|
11987
|
+
return;
|
|
11988
|
+
const relPath = activeLocalhostRelPath;
|
|
11989
|
+
const connectionId = activeLocalhostConnectionId;
|
|
11990
|
+
// Snapshot current editor content at call time.
|
|
11991
|
+
const rawContent = latestActiveContentRef.current;
|
|
11992
|
+
if (!rawContent) {
|
|
11993
|
+
toast.error(NO_LOCALHOST_WRITE_CONTENT_MESSAGE);
|
|
11994
|
+
return;
|
|
11995
|
+
}
|
|
11996
|
+
// Strip editor-only attributes before writing so the on-disk file stays
|
|
11997
|
+
// clean. Only strip for HTML routes; CSS files have no DOM attributes.
|
|
11998
|
+
const ext = (relPath.match(/\.[^.]+$/) ?? [])[0]?.toLowerCase() ?? "";
|
|
11999
|
+
const content =
|
|
12000
|
+
ext === ".html" || ext === ".htm"
|
|
12001
|
+
? stripEditorOnlyAttributes(rawContent)
|
|
12002
|
+
: rawContent;
|
|
12003
|
+
|
|
12004
|
+
requestLocalhostWrite({
|
|
12005
|
+
files: [relPath],
|
|
12006
|
+
onGranted: ({ bridgeToken, rootPath: grantedRootPath, grantId }) => {
|
|
12007
|
+
void (async () => {
|
|
12008
|
+
setApplyToSourcePending(true);
|
|
12009
|
+
try {
|
|
12010
|
+
await callAction("write-local-file", {
|
|
12011
|
+
designId: id,
|
|
12012
|
+
connectionId,
|
|
12013
|
+
relPath,
|
|
12014
|
+
content,
|
|
12015
|
+
});
|
|
12016
|
+
toast.success(
|
|
12017
|
+
`Written to ${relPath} (grant ${grantId.slice(0, 6)}…, root ${grantedRootPath})`,
|
|
12018
|
+
);
|
|
12019
|
+
} catch (err) {
|
|
12020
|
+
toast.error(
|
|
12021
|
+
`Write failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
12022
|
+
);
|
|
12023
|
+
} finally {
|
|
12024
|
+
setApplyToSourcePending(false);
|
|
12025
|
+
}
|
|
12026
|
+
// Suppress unused-var warning; bridgeToken is checked by the bridge
|
|
12027
|
+
// internally via the X-Bridge-Token header (set in write-local-file).
|
|
12028
|
+
void bridgeToken;
|
|
12029
|
+
})();
|
|
12030
|
+
},
|
|
12031
|
+
onCancel: () => {
|
|
12032
|
+
setApplyToSourcePending(false);
|
|
12033
|
+
},
|
|
12034
|
+
});
|
|
12035
|
+
}, [
|
|
12036
|
+
id,
|
|
12037
|
+
canEditDesign,
|
|
12038
|
+
activeLocalhostConnectionId,
|
|
12039
|
+
activeLocalhostRelPath,
|
|
12040
|
+
requestLocalhostWrite,
|
|
12041
|
+
]);
|
|
10433
12042
|
|
|
10434
12043
|
// canGroup: 2+ DOM-node layers selected in the active screen (not file rows).
|
|
10435
12044
|
const fileIdSet = new Set(files.map((f) => f.id));
|
|
@@ -10574,7 +12183,11 @@ ${serializedHtml}
|
|
|
10574
12183
|
// Group by source file so multiple nodes from the same source are
|
|
10575
12184
|
// applied sequentially against the running source content.
|
|
10576
12185
|
const sourceContentMap = new Map<string, string>();
|
|
10577
|
-
|
|
12186
|
+
const movedNodeIdByDraggedId = new Map<string, string>();
|
|
12187
|
+
for (const { draggedId, sourceFileId } of getLayerMoveIterationOrder(
|
|
12188
|
+
crossFileDrags,
|
|
12189
|
+
intent.placement,
|
|
12190
|
+
)) {
|
|
10578
12191
|
const srcFile = files.find((f) => f.id === sourceFileId);
|
|
10579
12192
|
if (!srcFile) continue;
|
|
10580
12193
|
const currentSourceContent = getLayerMoveSourceContent({
|
|
@@ -10617,6 +12230,7 @@ ${serializedHtml}
|
|
|
10617
12230
|
}
|
|
10618
12231
|
sourceContentMap.set(sourceFileId, result.sourceHtml);
|
|
10619
12232
|
nextDestContent = result.destHtml;
|
|
12233
|
+
movedNodeIdByDraggedId.set(draggedId, result.movedNodeId ?? nodeAttrId);
|
|
10620
12234
|
moved = true;
|
|
10621
12235
|
}
|
|
10622
12236
|
|
|
@@ -10630,12 +12244,16 @@ ${serializedHtml}
|
|
|
10630
12244
|
? buildCodeLayerTree(finalDestProjection)
|
|
10631
12245
|
: [];
|
|
10632
12246
|
const movedNodesAfterMove = movedIdOrder
|
|
10633
|
-
.map((draggedId) =>
|
|
10634
|
-
|
|
10635
|
-
node && finalDestProjection
|
|
10636
|
-
?
|
|
10637
|
-
|
|
10638
|
-
|
|
12247
|
+
.map((draggedId) => {
|
|
12248
|
+
const node = movedNodeSnapshots.get(draggedId);
|
|
12249
|
+
return node && finalDestProjection
|
|
12250
|
+
? findMovedCodeLayerNodeInProjection(
|
|
12251
|
+
finalDestProjection,
|
|
12252
|
+
node,
|
|
12253
|
+
movedNodeIdByDraggedId.get(draggedId),
|
|
12254
|
+
)
|
|
12255
|
+
: null;
|
|
12256
|
+
})
|
|
10639
12257
|
.filter((node): node is CodeLayerNode => Boolean(node));
|
|
10640
12258
|
|
|
10641
12259
|
if (movedNodesAfterMove.length > 0) {
|
|
@@ -10707,6 +12325,10 @@ ${serializedHtml}
|
|
|
10707
12325
|
},
|
|
10708
12326
|
) => {
|
|
10709
12327
|
const nextLayerIds = ids.filter((layerId) => !layerId.startsWith("__"));
|
|
12328
|
+
pendingOverviewScreenSelectionRef.current = null;
|
|
12329
|
+
pendingOverviewLayerSelectionRef.current = null;
|
|
12330
|
+
clearPendingOverviewLayerSelectionTimer();
|
|
12331
|
+
setCreatedOverviewLayerSelection(null);
|
|
10710
12332
|
if (intent.additive && !intent.range) {
|
|
10711
12333
|
const currentLayerIds = (
|
|
10712
12334
|
intent.currentSelectedIds && intent.currentSelectedIds.length > 0
|
|
@@ -10723,14 +12345,7 @@ ${serializedHtml}
|
|
|
10723
12345
|
fileIds,
|
|
10724
12346
|
layerIds: additiveLayerIds,
|
|
10725
12347
|
});
|
|
10726
|
-
|
|
10727
|
-
getOverviewScreenIdsFromLayerSelection({
|
|
10728
|
-
fileIds,
|
|
10729
|
-
layerIds: [intent.id],
|
|
10730
|
-
}).length > 0;
|
|
10731
|
-
if (toggledScreen || selectedScreenIds.length > 0) {
|
|
10732
|
-
setOverviewSelectedScreenIds(selectedScreenIds);
|
|
10733
|
-
}
|
|
12348
|
+
setOverviewSelectedScreenIds(selectedScreenIds);
|
|
10734
12349
|
}
|
|
10735
12350
|
setSelectedElement(null);
|
|
10736
12351
|
focusDesignInspectorForSelection();
|
|
@@ -10746,11 +12361,16 @@ ${serializedHtml}
|
|
|
10746
12361
|
}
|
|
10747
12362
|
const codeLayerOwner = codeLayerOwnerByNodeId.get(selectedId);
|
|
10748
12363
|
if (codeLayerOwner) {
|
|
12364
|
+
if (viewModeRef.current === "overview") {
|
|
12365
|
+
pendingOverviewScreenSelectionRef.current = codeLayerOwner.fileId;
|
|
12366
|
+
pendingOverviewLayerSelectionRef.current = selectedId;
|
|
12367
|
+
}
|
|
10749
12368
|
if (codeLayerOwner.fileId !== activeFile?.id) {
|
|
10750
12369
|
setActiveFileId(codeLayerOwner.fileId);
|
|
10751
12370
|
}
|
|
10752
12371
|
const nextSelectionState = getSidebarCodeLayerSelectionState({
|
|
10753
12372
|
currentViewMode: viewModeRef.current,
|
|
12373
|
+
ownerFileId: codeLayerOwner.fileId,
|
|
10754
12374
|
overviewSelectedScreenIds,
|
|
10755
12375
|
});
|
|
10756
12376
|
viewModeRef.current = nextSelectionState.viewMode;
|
|
@@ -10795,6 +12415,7 @@ ${serializedHtml}
|
|
|
10795
12415
|
},
|
|
10796
12416
|
[
|
|
10797
12417
|
activeFile?.id,
|
|
12418
|
+
clearPendingOverviewLayerSelectionTimer,
|
|
10798
12419
|
codeLayerOwnerByNodeId,
|
|
10799
12420
|
effectiveCodeLayerState,
|
|
10800
12421
|
files,
|
|
@@ -11001,6 +12622,51 @@ ${serializedHtml}
|
|
|
11001
12622
|
|
|
11002
12623
|
const zoomLabel = `${Math.round(zoom)}%`;
|
|
11003
12624
|
|
|
12625
|
+
const handleTokensApplied = useCallback(
|
|
12626
|
+
(resolvedCssVars: Record<string, string>) => {
|
|
12627
|
+
if (!canEditDesign || !id) return;
|
|
12628
|
+
setTweakSelections((prev) => ({
|
|
12629
|
+
...prev,
|
|
12630
|
+
...resolvedCssVars,
|
|
12631
|
+
}));
|
|
12632
|
+
queryClient.setQueryData(["action", "get-design", { id }], (old: any) => {
|
|
12633
|
+
if (!old || typeof old !== "object") return old;
|
|
12634
|
+
let currentData: Record<string, unknown> = {};
|
|
12635
|
+
if (typeof old.data === "string" && old.data) {
|
|
12636
|
+
try {
|
|
12637
|
+
const parsed = JSON.parse(old.data);
|
|
12638
|
+
if (
|
|
12639
|
+
parsed &&
|
|
12640
|
+
typeof parsed === "object" &&
|
|
12641
|
+
!Array.isArray(parsed)
|
|
12642
|
+
) {
|
|
12643
|
+
currentData = parsed;
|
|
12644
|
+
}
|
|
12645
|
+
} catch {
|
|
12646
|
+
currentData = {};
|
|
12647
|
+
}
|
|
12648
|
+
}
|
|
12649
|
+
const currentSelections =
|
|
12650
|
+
currentData.tweakSelections &&
|
|
12651
|
+
typeof currentData.tweakSelections === "object" &&
|
|
12652
|
+
!Array.isArray(currentData.tweakSelections)
|
|
12653
|
+
? currentData.tweakSelections
|
|
12654
|
+
: {};
|
|
12655
|
+
return {
|
|
12656
|
+
...old,
|
|
12657
|
+
data: JSON.stringify({
|
|
12658
|
+
...currentData,
|
|
12659
|
+
tweakSelections: {
|
|
12660
|
+
...currentSelections,
|
|
12661
|
+
...resolvedCssVars,
|
|
12662
|
+
},
|
|
12663
|
+
}),
|
|
12664
|
+
};
|
|
12665
|
+
});
|
|
12666
|
+
},
|
|
12667
|
+
[canEditDesign, id, queryClient],
|
|
12668
|
+
);
|
|
12669
|
+
|
|
11004
12670
|
// Hooks must not be called conditionally; keep navigate as an effect so the
|
|
11005
12671
|
// render phase stays pure. This branch is unreachable in practice because the
|
|
11006
12672
|
// design.$id.tsx route always supplies an id param.
|
|
@@ -11065,7 +12731,7 @@ ${serializedHtml}
|
|
|
11065
12731
|
<Button
|
|
11066
12732
|
variant="ghost"
|
|
11067
12733
|
size="sm"
|
|
11068
|
-
className="h-8 shrink-0 cursor-pointer gap-0.5 rounded-md px-0 text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
12734
|
+
className="ml-1 h-8 shrink-0 cursor-pointer gap-0.5 rounded-md px-0 text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
11069
12735
|
aria-label={t("designEditor.devicePreview")}
|
|
11070
12736
|
>
|
|
11071
12737
|
{deviceFrameIcon}
|
|
@@ -11107,10 +12773,10 @@ ${serializedHtml}
|
|
|
11107
12773
|
<Button
|
|
11108
12774
|
variant="ghost"
|
|
11109
12775
|
size="icon"
|
|
11110
|
-
className="size-
|
|
12776
|
+
className="size-8 shrink-0 cursor-pointer rounded-md text-muted-foreground hover:bg-accent hover:text-foreground [&_svg]:size-[calc(var(--spacing)*5.5)]"
|
|
11111
12777
|
aria-label={t("designEditor.more")}
|
|
11112
12778
|
>
|
|
11113
|
-
<AgentNativeMenuMark className="size-[calc(var(--spacing)*
|
|
12779
|
+
<AgentNativeMenuMark className="size-[calc(var(--spacing)*5.5)] text-foreground dark:text-white" />
|
|
11114
12780
|
</Button>
|
|
11115
12781
|
</DropdownMenuTrigger>
|
|
11116
12782
|
<DropdownMenuContent
|
|
@@ -11385,43 +13051,124 @@ ${serializedHtml}
|
|
|
11385
13051
|
</div>
|
|
11386
13052
|
|
|
11387
13053
|
<div className="flex shrink-0 items-center gap-1">
|
|
11388
|
-
<
|
|
11389
|
-
|
|
11390
|
-
|
|
11391
|
-
|
|
11392
|
-
|
|
11393
|
-
|
|
11394
|
-
|
|
11395
|
-
|
|
11396
|
-
|
|
11397
|
-
|
|
11398
|
-
<IconPlayerPlay className="size-5" />
|
|
11399
|
-
</Button>
|
|
11400
|
-
</TooltipTrigger>
|
|
11401
|
-
<TooltipContent>{t("designEditor.designPreview")}</TooltipContent>
|
|
11402
|
-
</Tooltip>
|
|
11403
|
-
|
|
11404
|
-
{/* §6.6 — "Make this a real app" shortcut button (signed-in only).
|
|
11405
|
-
Surfaces the migration CTA without requiring the project menu. */}
|
|
11406
|
-
{isSignedIn && (
|
|
13054
|
+
<Popover
|
|
13055
|
+
open={publishWaitlistPopoverOpen}
|
|
13056
|
+
onOpenChange={(open) => {
|
|
13057
|
+
setPublishWaitlistPopoverOpen(open);
|
|
13058
|
+
setPublishWaitlistPopoverView("actions");
|
|
13059
|
+
if (open) {
|
|
13060
|
+
setPublishWaitlistError(null);
|
|
13061
|
+
}
|
|
13062
|
+
}}
|
|
13063
|
+
>
|
|
11407
13064
|
<Tooltip>
|
|
11408
13065
|
<TooltipTrigger asChild>
|
|
11409
|
-
<
|
|
11410
|
-
|
|
11411
|
-
|
|
11412
|
-
|
|
11413
|
-
|
|
11414
|
-
|
|
11415
|
-
|
|
11416
|
-
|
|
11417
|
-
|
|
11418
|
-
|
|
13066
|
+
<PopoverTrigger asChild>
|
|
13067
|
+
<Button
|
|
13068
|
+
variant="ghost"
|
|
13069
|
+
size="sm"
|
|
13070
|
+
className="h-8 cursor-pointer gap-1 rounded-md px-2 text-foreground hover:bg-accent hover:text-foreground"
|
|
13071
|
+
aria-label={"Preview or publish app" /* i18n-ignore */}
|
|
13072
|
+
>
|
|
13073
|
+
<IconPlayerPlay className="size-5" />
|
|
13074
|
+
<IconChevronDown className="size-3 opacity-70" />
|
|
13075
|
+
</Button>
|
|
13076
|
+
</PopoverTrigger>
|
|
11419
13077
|
</TooltipTrigger>
|
|
11420
13078
|
<TooltipContent>
|
|
11421
|
-
{"
|
|
13079
|
+
{"Preview or publish app" /* i18n-ignore */}
|
|
11422
13080
|
</TooltipContent>
|
|
11423
13081
|
</Tooltip>
|
|
11424
|
-
|
|
13082
|
+
<PopoverContent
|
|
13083
|
+
align="end"
|
|
13084
|
+
sideOffset={8}
|
|
13085
|
+
className="z-[100010] w-72 space-y-3 p-3"
|
|
13086
|
+
>
|
|
13087
|
+
{publishWaitlistPopoverView === "actions" ? (
|
|
13088
|
+
<div className="space-y-1">
|
|
13089
|
+
<Button
|
|
13090
|
+
variant="ghost"
|
|
13091
|
+
className="h-9 w-full justify-start gap-2 px-2 text-sm"
|
|
13092
|
+
onClick={() => {
|
|
13093
|
+
handleOpenDesignPreview();
|
|
13094
|
+
setPublishWaitlistPopoverOpen(false);
|
|
13095
|
+
}}
|
|
13096
|
+
disabled={!activeScreenPreviewUrl && !activeContent.trim()}
|
|
13097
|
+
>
|
|
13098
|
+
<IconPlayerPlay className="size-4" />
|
|
13099
|
+
{t("designEditor.designPreview")}
|
|
13100
|
+
</Button>
|
|
13101
|
+
<Button
|
|
13102
|
+
variant="ghost"
|
|
13103
|
+
className="h-9 w-full justify-start gap-2 px-2 text-sm"
|
|
13104
|
+
onClick={() => setPublishWaitlistPopoverView("waitlist")}
|
|
13105
|
+
>
|
|
13106
|
+
<IconArrowUpRight className="size-4" />
|
|
13107
|
+
{"Publish app" /* i18n-ignore */}
|
|
13108
|
+
</Button>
|
|
13109
|
+
</div>
|
|
13110
|
+
) : (
|
|
13111
|
+
<>
|
|
13112
|
+
<div className="space-y-1">
|
|
13113
|
+
<p className="text-sm font-medium text-foreground">
|
|
13114
|
+
{
|
|
13115
|
+
publishWaitlistJoined
|
|
13116
|
+
? "You're on the waitlist" /* i18n-ignore */
|
|
13117
|
+
: "Publish app" /* i18n-ignore */
|
|
13118
|
+
}
|
|
13119
|
+
</p>
|
|
13120
|
+
<p className="text-xs leading-5 text-muted-foreground">
|
|
13121
|
+
{
|
|
13122
|
+
publishWaitlistJoined
|
|
13123
|
+
? "We'll follow up when app publishing is ready for your workspace." /* i18n-ignore */
|
|
13124
|
+
: isSignedIn
|
|
13125
|
+
? "Publish directly from Design is opening soon. Want early access?" /* i18n-ignore */
|
|
13126
|
+
: "Publish directly from Design is opening soon. Sign in to join the waitlist." /* i18n-ignore */
|
|
13127
|
+
}
|
|
13128
|
+
</p>
|
|
13129
|
+
</div>
|
|
13130
|
+
{publishWaitlistError ? (
|
|
13131
|
+
<p role="alert" className="text-xs text-destructive">
|
|
13132
|
+
{publishWaitlistError}
|
|
13133
|
+
</p>
|
|
13134
|
+
) : null}
|
|
13135
|
+
<div className="flex justify-end gap-2">
|
|
13136
|
+
<Button
|
|
13137
|
+
variant="outline"
|
|
13138
|
+
size="sm"
|
|
13139
|
+
className="h-8 cursor-pointer"
|
|
13140
|
+
onClick={() => setPublishWaitlistPopoverOpen(false)}
|
|
13141
|
+
>
|
|
13142
|
+
{
|
|
13143
|
+
publishWaitlistJoined
|
|
13144
|
+
? "Done" /* i18n-ignore */
|
|
13145
|
+
: "Not now" /* i18n-ignore */
|
|
13146
|
+
}
|
|
13147
|
+
</Button>
|
|
13148
|
+
{!publishWaitlistJoined && (
|
|
13149
|
+
<Button
|
|
13150
|
+
size="sm"
|
|
13151
|
+
className="h-8 cursor-pointer"
|
|
13152
|
+
onClick={() => void handleJoinPublishWaitlist()}
|
|
13153
|
+
disabled={joiningPublishWaitlist}
|
|
13154
|
+
>
|
|
13155
|
+
{joiningPublishWaitlist ? (
|
|
13156
|
+
<>
|
|
13157
|
+
<Spinner className="mr-1.5 size-3.5" />
|
|
13158
|
+
{"Joining" /* i18n-ignore */}
|
|
13159
|
+
</>
|
|
13160
|
+
) : isSignedIn ? (
|
|
13161
|
+
"Add me to waitlist" /* i18n-ignore */
|
|
13162
|
+
) : (
|
|
13163
|
+
"Sign in to join" /* i18n-ignore */
|
|
13164
|
+
)}
|
|
13165
|
+
</Button>
|
|
13166
|
+
)}
|
|
13167
|
+
</div>
|
|
13168
|
+
</>
|
|
13169
|
+
)}
|
|
13170
|
+
</PopoverContent>
|
|
13171
|
+
</Popover>
|
|
11425
13172
|
|
|
11426
13173
|
{isSignedIn ? (
|
|
11427
13174
|
<ShareButton
|
|
@@ -11434,19 +13181,43 @@ ${serializedHtml}
|
|
|
11434
13181
|
shareUrlLabel={t("designEditor.shareEditorLink")}
|
|
11435
13182
|
shareUrlDescription={t("designEditor.shareEditorLinkDescription")}
|
|
11436
13183
|
shareTabs={designShareTabs}
|
|
11437
|
-
popoverClassName=
|
|
13184
|
+
popoverClassName={designSharePopoverClassName}
|
|
11438
13185
|
triggerClassName="h-8 rounded-md !border-[var(--design-editor-accent-color)] !bg-[var(--design-editor-accent-color)] px-3 text-sm !text-[var(--design-editor-accent-contrast-color)] shadow-none hover:!border-[var(--design-editor-accent-hover-color)] hover:!bg-[var(--design-editor-accent-hover-color)] hover:!text-[var(--design-editor-accent-contrast-color)] focus-visible:ring-[var(--design-editor-accent-color)] [&_svg]:!text-[var(--design-editor-accent-contrast-color)]"
|
|
11439
13186
|
/>
|
|
11440
13187
|
) : (
|
|
11441
13188
|
signedOutPersistenceActions
|
|
11442
13189
|
)}
|
|
11443
|
-
|
|
11444
|
-
{isSignedIn && <AgentToggleButton />}
|
|
11445
13190
|
</div>
|
|
11446
13191
|
</div>
|
|
11447
13192
|
</div>
|
|
11448
13193
|
);
|
|
11449
13194
|
|
|
13195
|
+
const leftContentWidth = Math.max(leftSidebarWidth, 320);
|
|
13196
|
+
const motionDockLauncherHidden = motionDockMounted || motionDockOpen;
|
|
13197
|
+
const motionDockLauncher =
|
|
13198
|
+
!embedded && activeFile ? (
|
|
13199
|
+
<div
|
|
13200
|
+
aria-hidden={motionDockLauncherHidden ? true : undefined}
|
|
13201
|
+
className={cn(
|
|
13202
|
+
"overflow-hidden border-t border-[var(--design-editor-panel-divider-color)] transition-[max-height,opacity,border-color] duration-200 ease-out",
|
|
13203
|
+
motionDockLauncherHidden
|
|
13204
|
+
? "max-h-0 border-transparent opacity-0"
|
|
13205
|
+
: "max-h-9 opacity-100",
|
|
13206
|
+
)}
|
|
13207
|
+
>
|
|
13208
|
+
<button
|
|
13209
|
+
type="button"
|
|
13210
|
+
aria-label={"Expand motion dock" /* i18n-ignore */}
|
|
13211
|
+
className="flex h-8 w-full cursor-pointer items-center gap-2 bg-[var(--design-editor-panel-bg)] px-3 text-left text-[11px] font-medium uppercase tracking-wide text-muted-foreground outline-none transition-colors hover:bg-[var(--design-editor-panel-raised-bg)] hover:text-foreground focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-[var(--design-editor-accent-color)] disabled:cursor-default"
|
|
13212
|
+
disabled={motionDockLauncherHidden}
|
|
13213
|
+
onClick={() => setMotionDockOpenAnimated(true)}
|
|
13214
|
+
>
|
|
13215
|
+
<IconChevronRight className="size-3.5 shrink-0" />
|
|
13216
|
+
<span className="min-w-0 flex-1 truncate">Motion</span>
|
|
13217
|
+
</button>
|
|
13218
|
+
</div>
|
|
13219
|
+
) : null;
|
|
13220
|
+
|
|
11450
13221
|
return (
|
|
11451
13222
|
// h-full not flex-1: the parent <main> uses overflow-y-auto, not flex,
|
|
11452
13223
|
// so flex-1 on the child doesn't resolve to the available height. h-full
|
|
@@ -11834,14 +13605,12 @@ ${serializedHtml}
|
|
|
11834
13605
|
"designEditor.shareEditorLinkDescription",
|
|
11835
13606
|
)}
|
|
11836
13607
|
shareTabs={designShareTabs}
|
|
11837
|
-
popoverClassName=
|
|
13608
|
+
popoverClassName={designSharePopoverClassName}
|
|
11838
13609
|
triggerClassName="h-8 rounded-md !border-[var(--design-editor-accent-color)] !bg-[var(--design-editor-accent-color)] px-3 !text-[var(--design-editor-accent-contrast-color)] shadow-none hover:!border-[var(--design-editor-accent-hover-color)] hover:!bg-[var(--design-editor-accent-hover-color)] hover:!text-[var(--design-editor-accent-contrast-color)] focus-visible:ring-[var(--design-editor-accent-color)] [&_svg]:!text-[var(--design-editor-accent-contrast-color)]"
|
|
11839
13610
|
/>
|
|
11840
13611
|
) : !embedded ? (
|
|
11841
13612
|
signedOutPersistenceActions
|
|
11842
13613
|
) : null}
|
|
11843
|
-
|
|
11844
|
-
{!embedded && isSignedIn && <AgentToggleButton />}
|
|
11845
13614
|
</div>
|
|
11846
13615
|
</div>
|
|
11847
13616
|
</header>
|
|
@@ -11849,42 +13618,130 @@ ${serializedHtml}
|
|
|
11849
13618
|
{/* Main canvas area */}
|
|
11850
13619
|
<div className="flex-1 flex overflow-hidden relative">
|
|
11851
13620
|
{!embedded ? (
|
|
11852
|
-
<div
|
|
11853
|
-
|
|
11854
|
-
|
|
11855
|
-
|
|
11856
|
-
|
|
11857
|
-
|
|
11858
|
-
|
|
11859
|
-
|
|
11860
|
-
|
|
11861
|
-
|
|
11862
|
-
|
|
11863
|
-
|
|
11864
|
-
|
|
11865
|
-
|
|
11866
|
-
|
|
11867
|
-
|
|
11868
|
-
|
|
11869
|
-
|
|
11870
|
-
|
|
11871
|
-
|
|
11872
|
-
|
|
11873
|
-
|
|
11874
|
-
|
|
11875
|
-
|
|
11876
|
-
|
|
11877
|
-
|
|
11878
|
-
|
|
11879
|
-
|
|
11880
|
-
|
|
11881
|
-
|
|
11882
|
-
|
|
11883
|
-
|
|
11884
|
-
|
|
11885
|
-
|
|
11886
|
-
|
|
11887
|
-
|
|
13621
|
+
<div className="relative flex min-h-0 shrink-0 border-r border-[var(--design-editor-panel-divider-color)] bg-[var(--design-editor-panel-bg)]">
|
|
13622
|
+
<DesignWorkspaceRail
|
|
13623
|
+
activePanel={activeLeftPanel}
|
|
13624
|
+
projectMenu={projectMenu}
|
|
13625
|
+
onPanelChange={setActiveLeftPanel}
|
|
13626
|
+
/>
|
|
13627
|
+
<div
|
|
13628
|
+
className="flex min-h-0 shrink-0 flex-col bg-[var(--design-editor-panel-bg)] transition-[width] duration-150 ease-out"
|
|
13629
|
+
style={{ width: leftContentWidth }}
|
|
13630
|
+
>
|
|
13631
|
+
<div
|
|
13632
|
+
className={cn(
|
|
13633
|
+
"min-h-0 flex-1 flex-col overflow-hidden",
|
|
13634
|
+
activeLeftPanel === "file" ? "flex" : "hidden",
|
|
13635
|
+
)}
|
|
13636
|
+
>
|
|
13637
|
+
<div className="flex h-10 shrink-0 items-center gap-1.5 border-b border-border px-3">
|
|
13638
|
+
{projectTitleControl}
|
|
13639
|
+
</div>
|
|
13640
|
+
<div className="min-h-0 flex-1">
|
|
13641
|
+
<LayersPanel
|
|
13642
|
+
screens={layerPanelFiles}
|
|
13643
|
+
activeScreenId={activeFileId ?? undefined}
|
|
13644
|
+
screenOverviewActive={viewMode === "overview"}
|
|
13645
|
+
files={
|
|
13646
|
+
viewMode === "overview"
|
|
13647
|
+
? overviewLayerPanelFiles
|
|
13648
|
+
: undefined
|
|
13649
|
+
}
|
|
13650
|
+
layers={
|
|
13651
|
+
viewMode === "overview"
|
|
13652
|
+
? undefined
|
|
13653
|
+
: activeLayerPanelNodes
|
|
13654
|
+
}
|
|
13655
|
+
selectedIds={layerPanelSelectedIds}
|
|
13656
|
+
expandedIds={layerPanelExpandedIds}
|
|
13657
|
+
searchQuery={layersSearchQuery}
|
|
13658
|
+
footer={motionDockLauncher}
|
|
13659
|
+
onScreenSelect={handleSidebarScreenSelect}
|
|
13660
|
+
onScreenOverview={handleSidebarScreenOverview}
|
|
13661
|
+
onAddScreen={handleAddScreen}
|
|
13662
|
+
onSearchQueryChange={setLayersSearchQuery}
|
|
13663
|
+
onExpandedIdsChange={setExpandedLayerIds}
|
|
13664
|
+
onSelectionChange={handleLayerSelectionChange}
|
|
13665
|
+
onRename={handleLayerRename}
|
|
13666
|
+
onToggleLocked={handleToggleLayerLocked}
|
|
13667
|
+
onToggleHidden={handleToggleLayerHidden}
|
|
13668
|
+
onHoverLayer={handleLayerHover}
|
|
13669
|
+
onLeaveLayer={handleLayerLeave}
|
|
13670
|
+
onMoveLayer={handleLayerMove}
|
|
13671
|
+
canMoveLayer={canMoveLayer}
|
|
13672
|
+
boardElements={
|
|
13673
|
+
viewMode === "overview" ? boardElements : undefined
|
|
13674
|
+
}
|
|
13675
|
+
/>
|
|
13676
|
+
</div>
|
|
13677
|
+
</div>
|
|
13678
|
+
<div
|
|
13679
|
+
data-design-agent-panel
|
|
13680
|
+
className={cn(
|
|
13681
|
+
"min-h-0 flex-1 flex-col overflow-hidden",
|
|
13682
|
+
activeLeftPanel === "agent" ? "flex" : "hidden",
|
|
13683
|
+
)}
|
|
13684
|
+
>
|
|
13685
|
+
<AgentChatSurface
|
|
13686
|
+
mode="panel"
|
|
13687
|
+
className="min-h-0 flex-1 border-0 bg-transparent shadow-none"
|
|
13688
|
+
emptyStateText={t("chat.emptyState")}
|
|
13689
|
+
suggestions={[
|
|
13690
|
+
t("chat.suggestionLandingPage"),
|
|
13691
|
+
t("chat.suggestionBrandMatch"),
|
|
13692
|
+
t("chat.suggestionMobile"),
|
|
13693
|
+
]}
|
|
13694
|
+
scope={designChatScope}
|
|
13695
|
+
browserTabId={browserTabId}
|
|
13696
|
+
/>
|
|
13697
|
+
</div>
|
|
13698
|
+
<div
|
|
13699
|
+
className={cn(
|
|
13700
|
+
"min-h-0 flex-1 flex-col overflow-hidden",
|
|
13701
|
+
activeLeftPanel === "assets" ? "flex" : "hidden",
|
|
13702
|
+
)}
|
|
13703
|
+
>
|
|
13704
|
+
<div className="flex min-h-8 shrink-0 items-center border-b border-border/60 px-3">
|
|
13705
|
+
<h3 className="min-w-0 flex-1 truncate text-xs font-semibold text-foreground">
|
|
13706
|
+
{t("designEditor.leftRail.assets")}
|
|
13707
|
+
</h3>
|
|
13708
|
+
</div>
|
|
13709
|
+
<AssetLibraryPanel context={designExtensionContext} />
|
|
13710
|
+
</div>
|
|
13711
|
+
<div
|
|
13712
|
+
className={cn(
|
|
13713
|
+
"min-h-0 flex-1 flex-col overflow-hidden",
|
|
13714
|
+
activeLeftPanel === "tools" ? "flex" : "hidden",
|
|
13715
|
+
)}
|
|
13716
|
+
>
|
|
13717
|
+
<DesignExtensionsPanel
|
|
13718
|
+
context={designExtensionContext}
|
|
13719
|
+
hideAssetLibrary
|
|
13720
|
+
title={t("designEditor.leftRail.tools")}
|
|
13721
|
+
/>
|
|
13722
|
+
</div>
|
|
13723
|
+
<div
|
|
13724
|
+
className={cn(
|
|
13725
|
+
"min-h-0 flex-1 flex-col overflow-hidden",
|
|
13726
|
+
activeLeftPanel === "tokens" ? "flex" : "hidden",
|
|
13727
|
+
)}
|
|
13728
|
+
>
|
|
13729
|
+
{id ? (
|
|
13730
|
+
<>
|
|
13731
|
+
<div className="flex min-h-8 shrink-0 items-center border-b border-border/60 px-3">
|
|
13732
|
+
<h3 className="min-w-0 flex-1 truncate text-xs font-semibold text-foreground">
|
|
13733
|
+
{t("designEditor.tokens.title")}
|
|
13734
|
+
</h3>
|
|
13735
|
+
</div>
|
|
13736
|
+
<div className="design-inspector-scroll min-h-0 flex-1 overflow-y-auto overscroll-contain">
|
|
13737
|
+
<TokensPanel
|
|
13738
|
+
designId={id}
|
|
13739
|
+
onTokensApplied={handleTokensApplied}
|
|
13740
|
+
/>
|
|
13741
|
+
</div>
|
|
13742
|
+
</>
|
|
13743
|
+
) : null}
|
|
13744
|
+
</div>
|
|
11888
13745
|
</div>
|
|
11889
13746
|
<div
|
|
11890
13747
|
role="separator"
|
|
@@ -12022,9 +13879,34 @@ ${serializedHtml}
|
|
|
12022
13879
|
onDismiss={() => setLocalSourceBannerDismissed(true)}
|
|
12023
13880
|
/>
|
|
12024
13881
|
)}
|
|
13882
|
+
{/* "Apply to source" affordance: write the current editor
|
|
13883
|
+
content back to the local HTML/CSS file via the bridge.
|
|
13884
|
+
Only shown for localhost-backed screens where the route
|
|
13885
|
+
maps to an .html/.htm/.css file and the user has editor
|
|
13886
|
+
access. Opens the consent dialog on first use. */}
|
|
13887
|
+
{activeLocalhostRouteIsWritable && canEditDesign && id && (
|
|
13888
|
+
<div className="flex items-center gap-2 border-b border-border/50 bg-muted/30 px-3 py-1.5">
|
|
13889
|
+
<Button
|
|
13890
|
+
size="sm"
|
|
13891
|
+
variant="outline"
|
|
13892
|
+
className="h-6 gap-1.5 px-2 text-[11px]"
|
|
13893
|
+
disabled={applyToSourcePending}
|
|
13894
|
+
onClick={handleApplyToSource}
|
|
13895
|
+
>
|
|
13896
|
+
<IconDeviceFloppy className="size-3 shrink-0" />
|
|
13897
|
+
{applyToSourcePending
|
|
13898
|
+
? t("designEditor.writingToSource")
|
|
13899
|
+
: activeLocalhostRelPath
|
|
13900
|
+
? t("designEditor.applyToSourcePath", {
|
|
13901
|
+
path: activeLocalhostRelPath,
|
|
13902
|
+
})
|
|
13903
|
+
: t("designEditor.applyToSource")}
|
|
13904
|
+
</Button>
|
|
13905
|
+
</div>
|
|
13906
|
+
)}
|
|
12025
13907
|
<div
|
|
12026
13908
|
ref={canvasContainerRef}
|
|
12027
|
-
className="relative
|
|
13909
|
+
className="relative min-w-0 flex-1 overflow-hidden bg-[var(--design-editor-canvas-bg)]"
|
|
12028
13910
|
onPointerMove={handleCanvasPointerMove}
|
|
12029
13911
|
>
|
|
12030
13912
|
{/* Transparent shield that blocks pointer events reaching the
|
|
@@ -12074,11 +13956,87 @@ ${serializedHtml}
|
|
|
12074
13956
|
onPrimitiveCreated={handlePrimitiveCreated}
|
|
12075
13957
|
onPrimitiveReparent={handleOverviewPrimitiveReparent}
|
|
12076
13958
|
onCrossScreenElementDrop={handleCrossScreenElementDrop}
|
|
13959
|
+
boardFileId={boardFileId}
|
|
13960
|
+
boardFileContent={boardFileContent}
|
|
13961
|
+
boardFrameGeometry={boardFrameGeometry}
|
|
13962
|
+
onBoardDrawPrimitive={
|
|
13963
|
+
canEditDesign ? handleBoardDrawPrimitive : undefined
|
|
13964
|
+
}
|
|
13965
|
+
boardEditMode={canEditDesign}
|
|
13966
|
+
onBoardElementSelect={
|
|
13967
|
+
boardFileId
|
|
13968
|
+
? (info) =>
|
|
13969
|
+
handleScreenElementSelect(boardFileId, info)
|
|
13970
|
+
: undefined
|
|
13971
|
+
}
|
|
13972
|
+
onBoardElementHover={
|
|
13973
|
+
boardFileId
|
|
13974
|
+
? (info) =>
|
|
13975
|
+
handleScreenElementHover(boardFileId, info)
|
|
13976
|
+
: undefined
|
|
13977
|
+
}
|
|
13978
|
+
onBoardVisualStyleChange={
|
|
13979
|
+
boardFileId
|
|
13980
|
+
? (selector, styles, info) =>
|
|
13981
|
+
handleScreenVisualStyleChange(
|
|
13982
|
+
boardFileId,
|
|
13983
|
+
selector,
|
|
13984
|
+
styles,
|
|
13985
|
+
info,
|
|
13986
|
+
)
|
|
13987
|
+
: undefined
|
|
13988
|
+
}
|
|
13989
|
+
onBoardVisualStructureChange={
|
|
13990
|
+
boardFileId
|
|
13991
|
+
? (
|
|
13992
|
+
selector,
|
|
13993
|
+
anchorSelector,
|
|
13994
|
+
placement,
|
|
13995
|
+
info,
|
|
13996
|
+
details,
|
|
13997
|
+
) =>
|
|
13998
|
+
handleScreenVisualStructureChange(
|
|
13999
|
+
boardFileId,
|
|
14000
|
+
selector,
|
|
14001
|
+
anchorSelector,
|
|
14002
|
+
placement,
|
|
14003
|
+
info,
|
|
14004
|
+
details,
|
|
14005
|
+
)
|
|
14006
|
+
: undefined
|
|
14007
|
+
}
|
|
14008
|
+
onBoardVisualDuplicateChange={
|
|
14009
|
+
boardFileId
|
|
14010
|
+
? (selector, cloneHtml, info, details) =>
|
|
14011
|
+
handleScreenVisualDuplicateChange(
|
|
14012
|
+
boardFileId,
|
|
14013
|
+
selector,
|
|
14014
|
+
cloneHtml,
|
|
14015
|
+
info,
|
|
14016
|
+
details,
|
|
14017
|
+
)
|
|
14018
|
+
: undefined
|
|
14019
|
+
}
|
|
14020
|
+
onBoardTextContentChange={
|
|
14021
|
+
boardFileId
|
|
14022
|
+
? (selector, value, info, details) =>
|
|
14023
|
+
handleScreenTextContentChange(
|
|
14024
|
+
boardFileId,
|
|
14025
|
+
selector,
|
|
14026
|
+
value,
|
|
14027
|
+
info,
|
|
14028
|
+
details,
|
|
14029
|
+
)
|
|
14030
|
+
: undefined
|
|
14031
|
+
}
|
|
12077
14032
|
onCreateScreenFrame={handleCreateScreenFrame}
|
|
12078
14033
|
onDeleteSelection={handleDeleteOverviewSelection}
|
|
12079
|
-
onSelectionChange={
|
|
14034
|
+
onSelectionChange={handleOverviewScreenSelectionChange}
|
|
12080
14035
|
onPick={(id) => {
|
|
12081
14036
|
pendingOverviewScreenSelectionRef.current = null;
|
|
14037
|
+
pendingOverviewLayerSelectionRef.current = null;
|
|
14038
|
+
clearPendingOverviewLayerSelectionTimer();
|
|
14039
|
+
setCreatedOverviewLayerSelection(null);
|
|
12082
14040
|
setSelectedElement(null);
|
|
12083
14041
|
setHoveredElement(null);
|
|
12084
14042
|
setSelectedLayerIdsState([id]);
|
|
@@ -12183,7 +14141,7 @@ ${serializedHtml}
|
|
|
12183
14141
|
editorChromeScaleY={overviewCanvasZoom / 100}
|
|
12184
14142
|
editMode={mode === "edit"}
|
|
12185
14143
|
interactMode={false}
|
|
12186
|
-
readOnly={!canEditDesign}
|
|
14144
|
+
readOnly={!screenIsActive || !canEditDesign}
|
|
12187
14145
|
scaleMode={screenIsActive && activeTool === "scale"}
|
|
12188
14146
|
clearSelectionRequest={
|
|
12189
14147
|
overviewClearSelectionRequest
|
|
@@ -12471,53 +14429,10 @@ ${serializedHtml}
|
|
|
12471
14429
|
onActiveTabChange={setActiveInspectorTab}
|
|
12472
14430
|
tweaks={tweaks}
|
|
12473
14431
|
tweakValues={tweakSelections}
|
|
12474
|
-
extensionContext={designExtensionContext}
|
|
12475
14432
|
readOnly={initialGenerationReadOnly}
|
|
14433
|
+
activeContent={activeContent}
|
|
14434
|
+
activeFileUpdatedAt={activeFile?.updatedAt ?? null}
|
|
12476
14435
|
onComponentPropApplied={handleComponentPropApplied}
|
|
12477
|
-
onTokensApplied={(resolvedCssVars) => {
|
|
12478
|
-
if (!canEditDesign || !id) return;
|
|
12479
|
-
setTweakSelections((prev) => ({
|
|
12480
|
-
...prev,
|
|
12481
|
-
...resolvedCssVars,
|
|
12482
|
-
}));
|
|
12483
|
-
queryClient.setQueryData(
|
|
12484
|
-
["action", "get-design", { id }],
|
|
12485
|
-
(old: any) => {
|
|
12486
|
-
if (!old || typeof old !== "object") return old;
|
|
12487
|
-
let currentData: Record<string, unknown> = {};
|
|
12488
|
-
if (typeof old.data === "string" && old.data) {
|
|
12489
|
-
try {
|
|
12490
|
-
const parsed = JSON.parse(old.data);
|
|
12491
|
-
if (
|
|
12492
|
-
parsed &&
|
|
12493
|
-
typeof parsed === "object" &&
|
|
12494
|
-
!Array.isArray(parsed)
|
|
12495
|
-
) {
|
|
12496
|
-
currentData = parsed;
|
|
12497
|
-
}
|
|
12498
|
-
} catch {
|
|
12499
|
-
currentData = {};
|
|
12500
|
-
}
|
|
12501
|
-
}
|
|
12502
|
-
const currentSelections =
|
|
12503
|
-
currentData.tweakSelections &&
|
|
12504
|
-
typeof currentData.tweakSelections === "object" &&
|
|
12505
|
-
!Array.isArray(currentData.tweakSelections)
|
|
12506
|
-
? currentData.tweakSelections
|
|
12507
|
-
: {};
|
|
12508
|
-
return {
|
|
12509
|
-
...old,
|
|
12510
|
-
data: JSON.stringify({
|
|
12511
|
-
...currentData,
|
|
12512
|
-
tweakSelections: {
|
|
12513
|
-
...currentSelections,
|
|
12514
|
-
...resolvedCssVars,
|
|
12515
|
-
},
|
|
12516
|
-
}),
|
|
12517
|
-
};
|
|
12518
|
-
},
|
|
12519
|
-
);
|
|
12520
|
-
}}
|
|
12521
14436
|
onTweakChange={(tweakId, value) =>
|
|
12522
14437
|
setTweakSelections((prev) => {
|
|
12523
14438
|
if (!canEditDesign) return prev;
|
|
@@ -12599,52 +14514,21 @@ ${serializedHtml}
|
|
|
12599
14514
|
) : null}
|
|
12600
14515
|
</div>
|
|
12601
14516
|
|
|
12602
|
-
{/* Motion dock (§6.3) —
|
|
12603
|
-
|
|
14517
|
+
{/* Motion dock (§6.3) — bottom timeline mounted while opening, open, or
|
|
14518
|
+
closing. Canvas remains visible above.
|
|
12604
14519
|
Preview-only scrubbing fires a motion-preview postMessage to the
|
|
12605
|
-
canvas iframe
|
|
12606
|
-
|
|
12607
|
-
{!embedded && activeFile ? (
|
|
14520
|
+
canvas iframe; track/duration edits autosave through apply-motion-edit. */}
|
|
14521
|
+
{!embedded && activeFile && motionDockMounted ? (
|
|
12608
14522
|
<MotionDock
|
|
12609
14523
|
tracks={motionTracks}
|
|
12610
14524
|
durationMs={motionDurationMs}
|
|
12611
14525
|
open={motionDockOpen}
|
|
12612
|
-
onOpenChange={
|
|
12613
|
-
onTracksChange={
|
|
12614
|
-
onDurationChange={
|
|
14526
|
+
onOpenChange={setMotionDockOpenAnimated}
|
|
14527
|
+
onTracksChange={handleMotionTracksChange}
|
|
14528
|
+
onDurationChange={handleMotionDurationChange}
|
|
12615
14529
|
canvasIframeRef={canvasIframeRef}
|
|
12616
14530
|
selectedTarget={motionSelectedTarget}
|
|
12617
|
-
|
|
12618
|
-
if (!id) return;
|
|
12619
|
-
applyMotionEditMutation.mutate(
|
|
12620
|
-
{
|
|
12621
|
-
designId: id,
|
|
12622
|
-
fileId: activeFile.id,
|
|
12623
|
-
tracks: tracks.map(({ label: _label, ...t }) => t),
|
|
12624
|
-
durationMs,
|
|
12625
|
-
includeContent: true,
|
|
12626
|
-
},
|
|
12627
|
-
{
|
|
12628
|
-
onSuccess: (result) => {
|
|
12629
|
-
const response = result as {
|
|
12630
|
-
fileId?: unknown;
|
|
12631
|
-
patchedContent?: unknown;
|
|
12632
|
-
};
|
|
12633
|
-
if (
|
|
12634
|
-
typeof response.fileId === "string" &&
|
|
12635
|
-
typeof response.patchedContent === "string"
|
|
12636
|
-
) {
|
|
12637
|
-
applyFileContentUpdate(
|
|
12638
|
-
response.fileId,
|
|
12639
|
-
response.patchedContent,
|
|
12640
|
-
{ refreshPreview: response.fileId === activeFile.id },
|
|
12641
|
-
);
|
|
12642
|
-
}
|
|
12643
|
-
},
|
|
12644
|
-
},
|
|
12645
|
-
);
|
|
12646
|
-
}}
|
|
12647
|
-
applying={applyMotionEditMutation.isPending}
|
|
14531
|
+
applying={motionAutosavePending}
|
|
12648
14532
|
/>
|
|
12649
14533
|
) : null}
|
|
12650
14534
|
|
|
@@ -12910,6 +14794,25 @@ ${serializedHtml}
|
|
|
12910
14794
|
)}
|
|
12911
14795
|
</DialogContent>
|
|
12912
14796
|
</Dialog>
|
|
14797
|
+
|
|
14798
|
+
{/* Localhost write-consent dialog: shown when the agent or editor wants to
|
|
14799
|
+
persist an edit to a local HTML/CSS source file and no valid grant
|
|
14800
|
+
exists for the active connection yet. */}
|
|
14801
|
+
{id && activeLocalhostConnectionId && (
|
|
14802
|
+
<LocalhostWriteConsentDialog
|
|
14803
|
+
open={localhostWriteConsentOpen}
|
|
14804
|
+
onOpenChange={(next) => {
|
|
14805
|
+
if (!next) {
|
|
14806
|
+
localhostWriteConsentPayload?.onCancel();
|
|
14807
|
+
setLocalhostWriteConsentPayload(null);
|
|
14808
|
+
}
|
|
14809
|
+
setLocalhostWriteConsentOpen(next);
|
|
14810
|
+
}}
|
|
14811
|
+
designId={id}
|
|
14812
|
+
connectionId={localhostConsentConnectionId}
|
|
14813
|
+
payload={localhostWriteConsentPayload}
|
|
14814
|
+
/>
|
|
14815
|
+
)}
|
|
12913
14816
|
</div>
|
|
12914
14817
|
);
|
|
12915
14818
|
}
|