@agent-native/core 0.163.4 → 0.164.3
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/templates/analytics/app/components/agents/FeatureFlagsFleetPanel.tsx +78 -19
- package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +29 -0
- package/corpus/templates/analytics/app/lib/use-auto-focus-select.ts +4 -4
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +3 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +6 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +1 -2
- package/corpus/templates/analytics/app/routes/_index.tsx +3 -2
- package/corpus/templates/analytics/app/routes/adhoc.$id.tsx +7 -2
- package/corpus/templates/analytics/app/routes/dashboard.tsx +3 -2
- package/corpus/templates/analytics/app/routes/overview.tsx +3 -2
- package/corpus/templates/analytics/app/routes/traffic.tsx +3 -2
- package/corpus/templates/analytics/changelog/2026-08-18-verify-cross-app-feature-flag-changes.md +6 -0
- package/corpus/templates/analytics/netlify.toml +2 -0
- package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +196 -48
- package/corpus/templates/analytics/server/plugins/feature-flags.ts +5 -0
- package/corpus/templates/analytics/shared/feature-flags.ts +10 -0
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +5 -1
- package/corpus/templates/assets/app/routes/brand-kits._index.tsx +4 -1
- package/corpus/templates/assets/app/routes/libraries.tsx +4 -1
- package/corpus/templates/assets/app/routes/picker.tsx +4 -1
- package/corpus/templates/assets/netlify.toml +2 -0
- package/corpus/templates/brain/netlify.toml +2 -0
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +2 -1
- package/corpus/templates/calendar/changelog/2026-08-19-calendar-page-loads-stay-fast-after-idle-periods.md +6 -0
- package/corpus/templates/calendar/netlify.toml +2 -0
- package/corpus/templates/chat/netlify.toml +2 -0
- package/corpus/templates/clips/app/components/player/player-controls.tsx +3 -3
- package/corpus/templates/clips/app/components/player/reaction-emojis.ts +12 -0
- package/corpus/templates/clips/app/components/player/reactions-tray.tsx +90 -9
- package/corpus/templates/clips/app/components/player/scrubber.tsx +43 -10
- package/corpus/templates/clips/app/components/player/video-player.tsx +105 -15
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +1 -2
- package/corpus/templates/clips/app/routes/_app.wispr.tsx +2 -1
- package/corpus/templates/clips/app/routes/_index.tsx +7 -3
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +13 -8
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +15 -8
- package/corpus/templates/clips/netlify.toml +2 -0
- package/corpus/templates/clips/server/routes/[...page].head.ts +5 -0
- package/corpus/templates/content/netlify.toml +2 -0
- package/corpus/templates/design/.agents/skills/design-editor-architecture/SKILL.md +103 -0
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +2 -1
- package/corpus/templates/design/AGENTS.md +3 -1
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +3 -2
- package/corpus/templates/design/app/components/design/editor/AgentNativeMenuMark.tsx +25 -0
- package/corpus/templates/design/app/components/design/editor/DesignBottomToolbar.tsx +411 -0
- package/corpus/templates/design/app/components/design/editor/DesignCollaborators.tsx +153 -0
- package/corpus/templates/design/app/components/design/editor/DesignWorkspaceRail.tsx +231 -0
- package/corpus/templates/design/app/components/design/editor/MakeRealDialog.tsx +223 -0
- package/corpus/templates/design/app/components/design/editor/PendingScreenDeletionDialog.tsx +58 -0
- package/corpus/templates/design/app/components/design/editor/PendingVisualStyleWarningDialog.tsx +56 -0
- package/corpus/templates/design/app/components/design/editor/ReadOnlyEditorPanel.tsx +23 -0
- package/corpus/templates/design/app/components/design/editor/SaveTemplateDialog.tsx +161 -0
- package/corpus/templates/design/app/components/design/editor/toolbar-controls.tsx +185 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +20913 -0
- package/corpus/templates/design/app/pages/design-editor/command-types.ts +120 -0
- package/corpus/templates/design/app/pages/design-editor/commands/README.md +193 -0
- package/corpus/templates/design/app/pages/design-editor/commands/add-auto-layout.ts +416 -0
- package/corpus/templates/design/app/pages/design-editor/commands/add-screen.ts +112 -0
- package/corpus/templates/design/app/pages/design-editor/commands/align-selection.ts +183 -0
- package/corpus/templates/design/app/pages/design-editor/commands/apply-design-editor-command.ts +243 -0
- package/corpus/templates/design/app/pages/design-editor/commands/apply-file-content-update.ts +236 -0
- package/corpus/templates/design/app/pages/design-editor/commands/apply-local-content-update.ts +304 -0
- package/corpus/templates/design/app/pages/design-editor/commands/apply-pending-visual-styles-with-agent.ts +376 -0
- package/corpus/templates/design/app/pages/design-editor/commands/apply-to-source.ts +132 -0
- package/corpus/templates/design/app/pages/design-editor/commands/can-move-layer.ts +117 -0
- package/corpus/templates/design/app/pages/design-editor/commands/change-selected-z-index.ts +212 -0
- package/corpus/templates/design/app/pages/design-editor/commands/commit-relative-style-delta-to-selected-layers.ts +176 -0
- package/corpus/templates/design/app/pages/design-editor/commands/commit-styles-to-selected-layers.ts +175 -0
- package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +710 -0
- package/corpus/templates/design/app/pages/design-editor/commands/confirm-make-real.ts +99 -0
- package/corpus/templates/design/app/pages/design-editor/commands/copy-as-figma-svg.ts +95 -0
- package/corpus/templates/design/app/pages/design-editor/commands/copy-selection.ts +156 -0
- package/corpus/templates/design/app/pages/design-editor/commands/create-primitive.ts +291 -0
- package/corpus/templates/design/app/pages/design-editor/commands/create-screen-frame.ts +131 -0
- package/corpus/templates/design/app/pages/design-editor/commands/cross-screen-element-drop.ts +577 -0
- package/corpus/templates/design/app/pages/design-editor/commands/delete-files.ts +364 -0
- package/corpus/templates/design/app/pages/design-editor/commands/delete-selection.ts +521 -0
- package/corpus/templates/design/app/pages/design-editor/commands/detach-instance-menu-action.ts +88 -0
- package/corpus/templates/design/app/pages/design-editor/commands/distribute-selection.ts +114 -0
- package/corpus/templates/design/app/pages/design-editor/commands/download-all-screens-pdf.ts +116 -0
- package/corpus/templates/design/app/pages/design-editor/commands/download-pdf.ts +103 -0
- package/corpus/templates/design/app/pages/design-editor/commands/download-svg.ts +159 -0
- package/corpus/templates/design/app/pages/design-editor/commands/duplicate-screen.ts +201 -0
- package/corpus/templates/design/app/pages/design-editor/commands/duplicate-selection.ts +279 -0
- package/corpus/templates/design/app/pages/design-editor/commands/editor-paste.ts +81 -0
- package/corpus/templates/design/app/pages/design-editor/commands/enter-hotkey.ts +155 -0
- package/corpus/templates/design/app/pages/design-editor/commands/enter-single-screen.ts +168 -0
- package/corpus/templates/design/app/pages/design-editor/commands/escape-hotkey.ts +212 -0
- package/corpus/templates/design/app/pages/design-editor/commands/frame-selection.ts +119 -0
- package/corpus/templates/design/app/pages/design-editor/commands/geometry-commit.ts +169 -0
- package/corpus/templates/design/app/pages/design-editor/commands/get-selected-layer-snapshots.ts +177 -0
- package/corpus/templates/design/app/pages/design-editor/commands/group-selection.ts +127 -0
- package/corpus/templates/design/app/pages/design-editor/commands/iframe-context-menu.ts +121 -0
- package/corpus/templates/design/app/pages/design-editor/commands/import-figma-clipboard-into-design.ts +119 -0
- package/corpus/templates/design/app/pages/design-editor/commands/layer-marquee-selection-change.ts +163 -0
- package/corpus/templates/design/app/pages/design-editor/commands/layer-move-to-screen.ts +331 -0
- package/corpus/templates/design/app/pages/design-editor/commands/layer-move.ts +517 -0
- package/corpus/templates/design/app/pages/design-editor/commands/layer-rename.ts +247 -0
- package/corpus/templates/design/app/pages/design-editor/commands/layer-selection-change.ts +161 -0
- package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +122 -0
- package/corpus/templates/design/app/pages/design-editor/commands/nudge-selection.ts +213 -0
- package/corpus/templates/design/app/pages/design-editor/commands/overview-primitive-reparent.ts +318 -0
- package/corpus/templates/design/app/pages/design-editor/commands/paste-copied-screens.ts +114 -0
- package/corpus/templates/design/app/pages/design-editor/commands/paste-over-selection.ts +70 -0
- package/corpus/templates/design/app/pages/design-editor/commands/paste-selection.ts +436 -0
- package/corpus/templates/design/app/pages/design-editor/commands/paste-to-replace.ts +135 -0
- package/corpus/templates/design/app/pages/design-editor/commands/pasted-image-files.ts +209 -0
- package/corpus/templates/design/app/pages/design-editor/commands/persist-frame-geometry-save.ts +113 -0
- package/corpus/templates/design/app/pages/design-editor/commands/persist-tweak-save.ts +160 -0
- package/corpus/templates/design/app/pages/design-editor/commands/primitive-created.ts +168 -0
- package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-layer-state-edit.ts +149 -0
- package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-structure-edit.ts +200 -0
- package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-text-edit.ts +152 -0
- package/corpus/templates/design/app/pages/design-editor/commands/record-pending-visual-style-edit.ts +247 -0
- package/corpus/templates/design/app/pages/design-editor/commands/redo.ts +1086 -0
- package/corpus/templates/design/app/pages/design-editor/commands/render-png-blob.ts +200 -0
- package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +207 -0
- package/corpus/templates/design/app/pages/design-editor/commands/screen-element-select.ts +262 -0
- package/corpus/templates/design/app/pages/design-editor/commands/screen-text-content-change.ts +216 -0
- package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-duplicate-change.ts +108 -0
- package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-structure-change.ts +280 -0
- package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-style-change.ts +151 -0
- package/corpus/templates/design/app/pages/design-editor/commands/send-overview-annotations.ts +88 -0
- package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-move-semantic-handoff.ts +122 -0
- package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-semantic-handoff.ts +143 -0
- package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-state-semantic-handoff.ts +115 -0
- package/corpus/templates/design/app/pages/design-editor/commands/start-retry-generation.ts +152 -0
- package/corpus/templates/design/app/pages/design-editor/commands/start-sidebar-resize.ts +99 -0
- package/corpus/templates/design/app/pages/design-editor/commands/style-change.ts +152 -0
- package/corpus/templates/design/app/pages/design-editor/commands/styles-change.ts +155 -0
- package/corpus/templates/design/app/pages/design-editor/commands/suggest-auto-layout.ts +125 -0
- package/corpus/templates/design/app/pages/design-editor/commands/text-content-change.ts +201 -0
- package/corpus/templates/design/app/pages/design-editor/commands/tidy-up.ts +123 -0
- package/corpus/templates/design/app/pages/design-editor/commands/toggle-layer-hidden.ts +167 -0
- package/corpus/templates/design/app/pages/design-editor/commands/toggle-layer-locked.ts +175 -0
- package/corpus/templates/design/app/pages/design-editor/commands/toggle-motion-keyframe.ts +99 -0
- package/corpus/templates/design/app/pages/design-editor/commands/tweak-prompt-submit.ts +84 -0
- package/corpus/templates/design/app/pages/design-editor/commands/undo.ts +906 -0
- package/corpus/templates/design/app/pages/design-editor/commands/ungroup-selection.ts +170 -0
- package/corpus/templates/design/app/pages/design-editor/commands/visual-duplicate-change.ts +119 -0
- package/corpus/templates/design/app/pages/design-editor/commands/visual-structure-change.ts +274 -0
- package/corpus/templates/design/app/pages/design-editor/commands/write-frame-geometry-snapshot.ts +99 -0
- package/corpus/templates/design/app/pages/design-editor/derive/design-breakpoints.ts +56 -0
- package/corpus/templates/design/app/pages/design-editor/derive/overview-screens.ts +146 -0
- package/corpus/templates/design/app/pages/design-editor/domains/use-tweaks.ts +326 -0
- package/corpus/templates/design/app/pages/design-editor/editor-constants.ts +54 -0
- package/corpus/templates/design/app/pages/design-editor/editor-helpers.ts +183 -0
- package/corpus/templates/design/app/pages/design-editor/effects/adopt-db-file-content.ts +270 -0
- package/corpus/templates/design/app/pages/design-editor/effects/mirror-selection-to-agent-chat.ts +134 -0
- package/corpus/templates/design/app/pages/design-editor/effects/motion-autosave.ts +335 -0
- package/corpus/templates/design/app/pages/design-editor/effects/observe-collab-text.ts +183 -0
- package/corpus/templates/design/app/pages/design-editor/effects/publish-agent-selection-context.ts +252 -0
- package/corpus/templates/design/app/pages/design-editor/effects/resume-pending-generation.ts +178 -0
- package/corpus/templates/design/app/pages/design-editor/effects/seed-collab-content.ts +157 -0
- package/corpus/templates/design/app/pages/design-editor/preview-html.ts +19 -0
- package/corpus/templates/design/app/routes/examples.tsx +2 -1
- package/corpus/templates/design/netlify.toml +2 -0
- package/corpus/templates/design/shared/board-file.ts +2 -1
- package/corpus/templates/dispatch/netlify.toml +2 -0
- package/corpus/templates/forms/app/routes/_index.tsx +3 -2
- package/corpus/templates/forms/netlify.toml +2 -0
- package/corpus/templates/mail/app/routes/_index.tsx +3 -2
- package/corpus/templates/mail/netlify.toml +2 -0
- package/corpus/templates/plan/netlify.toml +2 -0
- package/corpus/templates/slides/netlify.toml +2 -0
- package/corpus/templates/tasks/app/routes/_index.tsx +2 -1
- package/dist/agent/engine/builder-engine.d.ts +6 -4
- package/dist/agent/engine/builder-engine.js +91 -26
- package/dist/agent/engine/registry.js +16 -0
- package/dist/cli/code-agent-agent-tools.d.ts +2 -0
- package/dist/cli/code-agent-agent-tools.js +188 -0
- package/dist/cli/code-agent-collaboration.d.ts +34 -0
- package/dist/cli/code-agent-collaboration.js +104 -0
- package/dist/cli/code-agent-executor.js +6 -0
- package/dist/cli/code-agent-schedules.d.ts +54 -0
- package/dist/cli/code-agent-schedules.js +198 -0
- package/dist/client/AgentPanel.js +1 -1
- package/dist/client/ConnectBuilderCard.js +5 -2
- package/dist/client/EnvironmentBadge.js +6 -5
- package/dist/client/chat/run-recovery.js +3 -2
- package/dist/client/chat/tool-call-display.js +2 -3
- package/dist/client/components/CodeRequiredDialog.js +4 -2
- package/dist/client/i18n.js +5 -4
- package/dist/client/settings/SettingsPanel.js +1 -1
- package/dist/client/settings/VoiceTranscriptionSection.js +1 -1
- package/dist/client/settings/useBuilderStatus.d.ts +6 -1
- package/dist/client/settings/useBuilderStatus.js +22 -33
- package/dist/client/transcription/BuilderTranscriptionCta.js +1 -1
- package/dist/code-agents/index.d.ts +2 -0
- package/dist/code-agents/index.js +2 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/deploy/build.js +10 -4
- package/dist/localization/shared.d.ts +2 -0
- package/dist/localization/shared.js +5 -0
- package/dist/mcp-client/oauth-client.d.ts +1 -0
- package/dist/mcp-client/oauth-client.js +50 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +1 -1
- package/dist/provider-api/actions/provider-api.d.ts +2 -2
- package/dist/server/auth-marketing-locales.d.ts +7 -0
- package/dist/server/auth-marketing-locales.js +1322 -0
- package/dist/server/auth-marketing.d.ts +1 -0
- package/dist/server/auth-marketing.js +35 -6
- package/dist/server/auth.js +4 -17
- package/dist/server/builder-browser.d.ts +22 -0
- package/dist/server/builder-browser.js +77 -1
- package/dist/server/builder-oauth.d.ts +50 -0
- package/dist/server/builder-oauth.js +263 -0
- package/dist/server/core-routes-plugin.d.ts +11 -0
- package/dist/server/core-routes-plugin.js +291 -415
- package/dist/server/credential-provider.d.ts +1 -1
- package/dist/server/credential-provider.js +1 -1
- package/dist/server/csrf.js +2 -2
- package/dist/server/onboarding-html.js +52 -120
- package/dist/server/ssr-handler.js +13 -4
- package/dist/settings/store.d.ts +9 -0
- package/dist/settings/store.js +19 -0
- package/dist/shared/cache-control.d.ts +22 -0
- package/dist/shared/cache-control.js +25 -0
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/templates/chat/netlify.toml +2 -0
- package/docs/content/deployment.mdx +2 -0
- package/docs/content/multi-app-workspace.mdx +1 -1
- package/package.json +2 -2
- package/src/templates/chat/netlify.toml +2 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import type { CodeLayerNode } from "@shared/code-layer";
|
|
2
|
+
import { buildCodeLayerProjection } from "@shared/code-layer";
|
|
3
|
+
import type { Dispatch, RefObject, SetStateAction } from "react";
|
|
4
|
+
import { toast } from "sonner";
|
|
5
|
+
import * as Y from "yjs";
|
|
6
|
+
|
|
7
|
+
import { trace } from "@/components/design/design-trace";
|
|
8
|
+
import type { ElementInfo } from "@/components/design/types";
|
|
9
|
+
import type { ClipboardContentMutationPublication } from "@/lib/clipboard-content-lineage";
|
|
10
|
+
import {
|
|
11
|
+
extractLayerPosition,
|
|
12
|
+
getElementOuterHtml,
|
|
13
|
+
insertClonedHtmlLayer,
|
|
14
|
+
insertClonedHtmlLayers,
|
|
15
|
+
} from "@/pages/design-editor/clone-and-pen-edit";
|
|
16
|
+
import {
|
|
17
|
+
codeLayerSelectorAliases,
|
|
18
|
+
elementInfoFromCodeLayerNode,
|
|
19
|
+
} from "@/pages/design-editor/code-layer-state";
|
|
20
|
+
import type { SelectedCanvasLayerSnapshot } from "@/pages/design-editor/command-types";
|
|
21
|
+
import type { DesignFile } from "@/pages/design-editor/types";
|
|
22
|
+
|
|
23
|
+
export interface DuplicateSelectionArgs {
|
|
24
|
+
activeFile: DesignFile;
|
|
25
|
+
applyFileContentUpdate: (
|
|
26
|
+
fileId: string,
|
|
27
|
+
nextContent: string,
|
|
28
|
+
options?: {
|
|
29
|
+
refreshPreview?: boolean;
|
|
30
|
+
skipPreview?: boolean;
|
|
31
|
+
forcePreviewFullDocument?: boolean;
|
|
32
|
+
persist?: boolean;
|
|
33
|
+
recordHistory?: boolean;
|
|
34
|
+
updatedAt?: string;
|
|
35
|
+
clipboardMutation?: ClipboardContentMutationPublication;
|
|
36
|
+
},
|
|
37
|
+
) => void;
|
|
38
|
+
applyLocalContentUpdate: (
|
|
39
|
+
nextContent: string,
|
|
40
|
+
options?: {
|
|
41
|
+
refreshPreview?: boolean;
|
|
42
|
+
skipPreview?: boolean;
|
|
43
|
+
forcePreviewFullDocument?: boolean;
|
|
44
|
+
immediateSave?: boolean;
|
|
45
|
+
persist?: boolean;
|
|
46
|
+
recordHistory?: boolean;
|
|
47
|
+
historyBeforeContent?: string;
|
|
48
|
+
updatedAt?: string;
|
|
49
|
+
clipboardMutation?: ClipboardContentMutationPublication;
|
|
50
|
+
},
|
|
51
|
+
) => void;
|
|
52
|
+
canEditDesign: boolean;
|
|
53
|
+
files: DesignFile[];
|
|
54
|
+
getFreshActiveContent: () => string;
|
|
55
|
+
getScreenContent: (screenId: string) => string;
|
|
56
|
+
getSelectedLayerSnapshots: () => SelectedCanvasLayerSnapshot[];
|
|
57
|
+
handleDuplicateScreen: (
|
|
58
|
+
screenId: string,
|
|
59
|
+
request?: { canvasPosition?: { x: number; y: number } },
|
|
60
|
+
) => void;
|
|
61
|
+
lastDuplicateTransformRef: RefObject<{
|
|
62
|
+
rootNodeIds: string[];
|
|
63
|
+
dx: number;
|
|
64
|
+
dy: number;
|
|
65
|
+
} | null>;
|
|
66
|
+
overviewSelectedScreenIds: string[];
|
|
67
|
+
remapMotionTracksForClone: (
|
|
68
|
+
nodeIdMap: Map<string, string>,
|
|
69
|
+
targetFileId: string,
|
|
70
|
+
) => void;
|
|
71
|
+
selectedCanvasSelector: string;
|
|
72
|
+
selectedElement: ElementInfo | null;
|
|
73
|
+
selectedLayerIdsState: string[];
|
|
74
|
+
setOverviewSelectedScreenIds: Dispatch<SetStateAction<string[]>>;
|
|
75
|
+
setSelectedElement: Dispatch<SetStateAction<ElementInfo | null>>;
|
|
76
|
+
setSelectedLayerIdsState: Dispatch<SetStateAction<string[]>>;
|
|
77
|
+
t: (key: string, options?: Record<string, unknown>) => string;
|
|
78
|
+
undoManagerRef: RefObject<Y.UndoManager | null>;
|
|
79
|
+
viewModeRef: RefObject<"single" | "overview">;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function runDuplicateSelection({
|
|
83
|
+
activeFile,
|
|
84
|
+
applyFileContentUpdate,
|
|
85
|
+
applyLocalContentUpdate,
|
|
86
|
+
canEditDesign,
|
|
87
|
+
files,
|
|
88
|
+
getFreshActiveContent,
|
|
89
|
+
getScreenContent,
|
|
90
|
+
getSelectedLayerSnapshots,
|
|
91
|
+
handleDuplicateScreen,
|
|
92
|
+
lastDuplicateTransformRef,
|
|
93
|
+
overviewSelectedScreenIds,
|
|
94
|
+
remapMotionTracksForClone,
|
|
95
|
+
selectedCanvasSelector,
|
|
96
|
+
selectedElement,
|
|
97
|
+
selectedLayerIdsState,
|
|
98
|
+
setOverviewSelectedScreenIds,
|
|
99
|
+
setSelectedElement,
|
|
100
|
+
setSelectedLayerIdsState,
|
|
101
|
+
t,
|
|
102
|
+
undoManagerRef,
|
|
103
|
+
viewModeRef,
|
|
104
|
+
}: DuplicateSelectionArgs) {
|
|
105
|
+
trace("structure", "duplicate-selection", {
|
|
106
|
+
canEdit: canEditDesign,
|
|
107
|
+
selectedLayers: selectedLayerIdsState.length,
|
|
108
|
+
});
|
|
109
|
+
if (!canEditDesign) return;
|
|
110
|
+
// U19: duplicate is a discrete one-shot action — see the matching note
|
|
111
|
+
// in handlePasteSelection.
|
|
112
|
+
undoManagerRef.current?.stopCapturing();
|
|
113
|
+
const snapshots = getSelectedLayerSnapshots();
|
|
114
|
+
if (snapshots.length > 0) {
|
|
115
|
+
const selectedIds: string[] = [];
|
|
116
|
+
const selectedScreenIds: string[] = [];
|
|
117
|
+
let lastActiveNode: CodeLayerNode | null = null;
|
|
118
|
+
|
|
119
|
+
// U7: replay the last recorded move delta when duplicating the exact
|
|
120
|
+
// same selection again (e.g. dup, drag it, dup again repeats the same
|
|
121
|
+
// offset — matching Figma). Otherwise an absolutely-positioned source
|
|
122
|
+
// duplicates with zero offset (lands exactly in place) instead of the
|
|
123
|
+
// previous unconditional stripRootPosition cascade.
|
|
124
|
+
const currentSourceIds = snapshots
|
|
125
|
+
.map((snapshot) => snapshot.rootNodeId ?? snapshot.node.id)
|
|
126
|
+
.sort();
|
|
127
|
+
const repeatTransform =
|
|
128
|
+
lastDuplicateTransformRef.current &&
|
|
129
|
+
lastDuplicateTransformRef.current.rootNodeIds.length ===
|
|
130
|
+
currentSourceIds.length &&
|
|
131
|
+
lastDuplicateTransformRef.current.rootNodeIds.every(
|
|
132
|
+
(id, index) => id === currentSourceIds[index],
|
|
133
|
+
)
|
|
134
|
+
? lastDuplicateTransformRef.current
|
|
135
|
+
: null;
|
|
136
|
+
const nextDuplicateRootNodeIds: string[] = [];
|
|
137
|
+
|
|
138
|
+
for (const file of files) {
|
|
139
|
+
const group = snapshots.filter(
|
|
140
|
+
(snapshot) => snapshot.sourceFileId === file.id,
|
|
141
|
+
);
|
|
142
|
+
if (group.length === 0) continue;
|
|
143
|
+
let content = getScreenContent(file.id);
|
|
144
|
+
const insertedRootNodeIds: string[] = [];
|
|
145
|
+
for (const snapshot of [...group].sort(
|
|
146
|
+
(a, b) => b.sourceIndex - a.sourceIndex,
|
|
147
|
+
)) {
|
|
148
|
+
const projection = buildCodeLayerProjection(content);
|
|
149
|
+
const anchorNode =
|
|
150
|
+
projection.nodes.find(
|
|
151
|
+
(node) =>
|
|
152
|
+
node.id === snapshot.node.id ||
|
|
153
|
+
node.dataAttributes["data-agent-native-node-id"] ===
|
|
154
|
+
snapshot.rootNodeId,
|
|
155
|
+
) ?? snapshot.node;
|
|
156
|
+
const sourcePosition = extractLayerPosition(snapshot.html);
|
|
157
|
+
const result = insertClonedHtmlLayers(content, [snapshot.html], {
|
|
158
|
+
targetSelectors: codeLayerSelectorAliases(anchorNode),
|
|
159
|
+
placement: "after",
|
|
160
|
+
// Absolutely-positioned board items land exactly in place (or at
|
|
161
|
+
// the replayed delta); only in-flow elements (no left/top) use
|
|
162
|
+
// stripRootPosition so they join the document as a plain sibling.
|
|
163
|
+
stripRootPosition: !sourcePosition,
|
|
164
|
+
positions: sourcePosition
|
|
165
|
+
? [
|
|
166
|
+
{
|
|
167
|
+
x: sourcePosition.x + (repeatTransform?.dx ?? 0),
|
|
168
|
+
y: sourcePosition.y + (repeatTransform?.dy ?? 0),
|
|
169
|
+
},
|
|
170
|
+
]
|
|
171
|
+
: undefined,
|
|
172
|
+
});
|
|
173
|
+
if (!result) continue;
|
|
174
|
+
content = result.content;
|
|
175
|
+
insertedRootNodeIds.unshift(...result.rootNodeIds);
|
|
176
|
+
// U14: duplicate keeps the clone's animation.
|
|
177
|
+
remapMotionTracksForClone(result.nodeIdMap, file.id);
|
|
178
|
+
}
|
|
179
|
+
if (insertedRootNodeIds.length === 0) continue;
|
|
180
|
+
applyFileContentUpdate(file.id, content, {
|
|
181
|
+
forcePreviewFullDocument: true,
|
|
182
|
+
refreshPreview: false,
|
|
183
|
+
});
|
|
184
|
+
selectedScreenIds.push(file.id);
|
|
185
|
+
const finalProjection = buildCodeLayerProjection(content);
|
|
186
|
+
insertedRootNodeIds.forEach((rootNodeId) => {
|
|
187
|
+
const insertedNode = finalProjection.nodes.find(
|
|
188
|
+
(node) =>
|
|
189
|
+
node.id === rootNodeId ||
|
|
190
|
+
node.dataAttributes["data-agent-native-node-id"] === rootNodeId,
|
|
191
|
+
);
|
|
192
|
+
if (!insertedNode) return;
|
|
193
|
+
selectedIds.push(insertedNode.id);
|
|
194
|
+
nextDuplicateRootNodeIds.push(
|
|
195
|
+
insertedNode.dataAttributes["data-agent-native-node-id"] ??
|
|
196
|
+
insertedNode.id,
|
|
197
|
+
);
|
|
198
|
+
if (file.id === activeFile?.id) lastActiveNode = insertedNode;
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (selectedIds.length > 0) {
|
|
203
|
+
setSelectedLayerIdsState(selectedIds);
|
|
204
|
+
setSelectedElement(
|
|
205
|
+
lastActiveNode ? elementInfoFromCodeLayerNode(lastActiveNode) : null,
|
|
206
|
+
);
|
|
207
|
+
if (viewModeRef.current === "overview") {
|
|
208
|
+
setOverviewSelectedScreenIds(selectedScreenIds);
|
|
209
|
+
}
|
|
210
|
+
// Track this duplicate as the new "last duplicate" so a subsequent
|
|
211
|
+
// drag-then-Cmd+D can record/replay a delta against it. Preserve the
|
|
212
|
+
// previous delta across repeated Cmd+D so chained duplicates (no drag
|
|
213
|
+
// in between) keep applying the same recorded offset, matching Figma.
|
|
214
|
+
lastDuplicateTransformRef.current = {
|
|
215
|
+
rootNodeIds: [...nextDuplicateRootNodeIds].sort(),
|
|
216
|
+
dx: repeatTransform?.dx ?? 0,
|
|
217
|
+
dy: repeatTransform?.dy ?? 0,
|
|
218
|
+
};
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (selectedElement?.selector) {
|
|
224
|
+
const baseContent = getFreshActiveContent();
|
|
225
|
+
const html = getElementOuterHtml(baseContent, selectedElement.selector);
|
|
226
|
+
if (!html) {
|
|
227
|
+
toast.error(t("designEditor.toasts.duplicateElementFailed"));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
// B7 fix: duplicate inserts the clone as an in-flow sibling right AFTER
|
|
231
|
+
// the original — not as an absolutely-positioned body child. Strip
|
|
232
|
+
// position/left/top so it joins normal document flow.
|
|
233
|
+
const selector = selectedCanvasSelector ?? selectedElement.selector;
|
|
234
|
+
const strippedHtml = (() => {
|
|
235
|
+
try {
|
|
236
|
+
const parser = new DOMParser();
|
|
237
|
+
const tmp = parser.parseFromString(
|
|
238
|
+
`<template>${html}</template>`,
|
|
239
|
+
"text/html",
|
|
240
|
+
);
|
|
241
|
+
const root =
|
|
242
|
+
tmp.querySelector("template")?.content.firstElementChild ??
|
|
243
|
+
tmp.body.firstElementChild;
|
|
244
|
+
if (root && root instanceof HTMLElement) {
|
|
245
|
+
root.style.position = "";
|
|
246
|
+
root.style.left = "";
|
|
247
|
+
root.style.top = "";
|
|
248
|
+
root.style.right = "";
|
|
249
|
+
root.style.bottom = "";
|
|
250
|
+
}
|
|
251
|
+
return root?.outerHTML ?? html;
|
|
252
|
+
} catch {
|
|
253
|
+
return html;
|
|
254
|
+
}
|
|
255
|
+
})();
|
|
256
|
+
const nextContent = insertClonedHtmlLayer(baseContent, strippedHtml, {
|
|
257
|
+
targetSelectors: [selector],
|
|
258
|
+
placement: "after",
|
|
259
|
+
});
|
|
260
|
+
if (nextContent) {
|
|
261
|
+
applyLocalContentUpdate(nextContent, {
|
|
262
|
+
forcePreviewFullDocument: true,
|
|
263
|
+
});
|
|
264
|
+
} else {
|
|
265
|
+
toast.error(t("designEditor.toasts.duplicateElementFailed"));
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
// U17: duplicate every selected screen, not just the active one — a
|
|
270
|
+
// multi-screen overview selection (no deeper layer focus) previously
|
|
271
|
+
// silently duplicated only activeFile.id and dropped the rest.
|
|
272
|
+
const screenIdsToDuplicate =
|
|
273
|
+
viewModeRef.current === "overview" && overviewSelectedScreenIds.length > 1
|
|
274
|
+
? overviewSelectedScreenIds
|
|
275
|
+
: activeFile
|
|
276
|
+
? [activeFile.id]
|
|
277
|
+
: [];
|
|
278
|
+
screenIdsToDuplicate.forEach((screenId) => handleDuplicateScreen(screenId));
|
|
279
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { RefObject } from "react";
|
|
2
|
+
|
|
3
|
+
import { isDesignHotkeyEditableTarget } from "@/hooks/useDesignHotkeys";
|
|
4
|
+
import { readDesignClipboardPayloadFromDataTransfer } from "@/lib/design-clipboard";
|
|
5
|
+
import type { DesignClipboardPayload } from "@/lib/design-import";
|
|
6
|
+
import { getFigmaClipboardContent } from "@/lib/design-import";
|
|
7
|
+
|
|
8
|
+
export interface EditorPasteArgs {
|
|
9
|
+
adoptDesignClipboardPayload: (
|
|
10
|
+
payload: DesignClipboardPayload,
|
|
11
|
+
markerText: string,
|
|
12
|
+
plainText?: string,
|
|
13
|
+
) => void;
|
|
14
|
+
canEditDesign: boolean;
|
|
15
|
+
handlePasteSelection: (position?: { x: number; y: number }) => Promise<void>;
|
|
16
|
+
handlePastedImageFiles: (files: File[]) => boolean;
|
|
17
|
+
hasCanvasClipboard: boolean;
|
|
18
|
+
importFigmaClipboardIntoDesign: (content: string) => Promise<void>;
|
|
19
|
+
lastWrittenClipboardMarkerRef: RefObject<string | null>;
|
|
20
|
+
lastWrittenClipboardPlainTextRef: RefObject<string | null>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function runEditorPaste(
|
|
24
|
+
{
|
|
25
|
+
adoptDesignClipboardPayload,
|
|
26
|
+
canEditDesign,
|
|
27
|
+
handlePasteSelection,
|
|
28
|
+
handlePastedImageFiles,
|
|
29
|
+
hasCanvasClipboard,
|
|
30
|
+
importFigmaClipboardIntoDesign,
|
|
31
|
+
lastWrittenClipboardMarkerRef,
|
|
32
|
+
lastWrittenClipboardPlainTextRef,
|
|
33
|
+
}: EditorPasteArgs,
|
|
34
|
+
event: ClipboardEvent,
|
|
35
|
+
) {
|
|
36
|
+
if (event.defaultPrevented) return;
|
|
37
|
+
if (isDesignHotkeyEditableTarget(event.target)) return;
|
|
38
|
+
// U8/paste-multi: collect every pasted image file, not just the first —
|
|
39
|
+
// see handlePastedImageFiles' doc comment for the full rationale.
|
|
40
|
+
const imageFiles = Array.from(event.clipboardData?.items ?? [])
|
|
41
|
+
.filter((item) => item.kind === "file" && item.type.startsWith("image/"))
|
|
42
|
+
.map((item) => item.getAsFile())
|
|
43
|
+
.filter((file): file is File => Boolean(file));
|
|
44
|
+
if (imageFiles.length > 0 && canEditDesign) {
|
|
45
|
+
if (handlePastedImageFiles(imageFiles)) {
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const figmaContent = getFigmaClipboardContent(event.clipboardData);
|
|
51
|
+
if (figmaContent) {
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
void importFigmaClipboardIntoDesign(figmaContent);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (!canEditDesign) return;
|
|
57
|
+
// The native paste event reflects the current clipboard synchronously.
|
|
58
|
+
// New copies carry their lossless marker in text/html; text/plain remains
|
|
59
|
+
// readable. The helper also accepts legacy markers from text/plain.
|
|
60
|
+
const clipboardResult = readDesignClipboardPayloadFromDataTransfer(
|
|
61
|
+
event.clipboardData,
|
|
62
|
+
);
|
|
63
|
+
if (
|
|
64
|
+
clipboardResult &&
|
|
65
|
+
clipboardResult.markerText !== lastWrittenClipboardMarkerRef.current
|
|
66
|
+
) {
|
|
67
|
+
adoptDesignClipboardPayload(
|
|
68
|
+
clipboardResult.payload,
|
|
69
|
+
clipboardResult.markerText,
|
|
70
|
+
clipboardResult.plainText,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const clipboardPlainText = event.clipboardData?.getData("text/plain") ?? "";
|
|
74
|
+
const matchesInMemoryClipboard =
|
|
75
|
+
lastWrittenClipboardPlainTextRef.current !== null &&
|
|
76
|
+
clipboardPlainText === lastWrittenClipboardPlainTextRef.current;
|
|
77
|
+
if (clipboardResult || (hasCanvasClipboard && matchesInMemoryClipboard)) {
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
void handlePasteSelection();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { CodeLayerNode, CodeLayerTreeNode } from "@shared/code-layer";
|
|
2
|
+
import { buildCodeLayerProjection } from "@shared/code-layer";
|
|
3
|
+
import type { Dispatch, RefObject, SetStateAction } from "react";
|
|
4
|
+
|
|
5
|
+
import { getOverviewEnterTarget } from "@/pages/design-editor/selection-state";
|
|
6
|
+
import { scheduleBeginTextEditForScreen } from "@/pages/design-editor/text-edit-utils";
|
|
7
|
+
import type { DesignFile } from "@/pages/design-editor/types";
|
|
8
|
+
|
|
9
|
+
export interface EnterHotkeyArgs {
|
|
10
|
+
SINGLE_MODE_TEXT_TAGS: Set<string>;
|
|
11
|
+
activeFile: DesignFile;
|
|
12
|
+
activeFileId: string | null;
|
|
13
|
+
boardFileId: string | undefined;
|
|
14
|
+
codeLayerOwnerByNodeIdRef: RefObject<
|
|
15
|
+
Map<
|
|
16
|
+
string,
|
|
17
|
+
{
|
|
18
|
+
fileId: string;
|
|
19
|
+
node: CodeLayerNode;
|
|
20
|
+
tree: CodeLayerTreeNode[];
|
|
21
|
+
runtimeOnly: boolean;
|
|
22
|
+
}
|
|
23
|
+
>
|
|
24
|
+
>;
|
|
25
|
+
enterVectorEditForSelection: (owner: {
|
|
26
|
+
fileId: string;
|
|
27
|
+
node: CodeLayerNode;
|
|
28
|
+
}) => boolean;
|
|
29
|
+
getProjectionContentForScreen: (screenId: string) => string;
|
|
30
|
+
overviewSelectedScreenIds: string[];
|
|
31
|
+
selectCodeLayerNodesForHotkey: (
|
|
32
|
+
fileId: string,
|
|
33
|
+
nodes: CodeLayerNode[],
|
|
34
|
+
expandedIds?: readonly string[],
|
|
35
|
+
) => boolean;
|
|
36
|
+
selectedLayerIdsState: string[];
|
|
37
|
+
setActiveFileId: Dispatch<SetStateAction<string | null>>;
|
|
38
|
+
setSelectedLayerIdsState: Dispatch<SetStateAction<string[]>>;
|
|
39
|
+
viewMode: "single" | "overview";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function runEnterHotkey({
|
|
43
|
+
SINGLE_MODE_TEXT_TAGS,
|
|
44
|
+
activeFile,
|
|
45
|
+
activeFileId,
|
|
46
|
+
boardFileId,
|
|
47
|
+
codeLayerOwnerByNodeIdRef,
|
|
48
|
+
enterVectorEditForSelection,
|
|
49
|
+
getProjectionContentForScreen,
|
|
50
|
+
overviewSelectedScreenIds,
|
|
51
|
+
selectCodeLayerNodesForHotkey,
|
|
52
|
+
selectedLayerIdsState,
|
|
53
|
+
setActiveFileId,
|
|
54
|
+
setSelectedLayerIdsState,
|
|
55
|
+
viewMode,
|
|
56
|
+
}: EnterHotkeyArgs) {
|
|
57
|
+
if (viewMode !== "overview") {
|
|
58
|
+
if (selectedLayerIdsState.length === 1) {
|
|
59
|
+
const layerId = selectedLayerIdsState[0]!;
|
|
60
|
+
const owner = codeLayerOwnerByNodeIdRef.current.get(layerId);
|
|
61
|
+
if (owner && owner.fileId === (activeFile?.id ?? activeFileId)) {
|
|
62
|
+
const isTextNode =
|
|
63
|
+
SINGLE_MODE_TEXT_TAGS.has(owner.node.tag) ||
|
|
64
|
+
owner.node.dataAttributes["data-an-primitive"] === "text";
|
|
65
|
+
if (isTextNode) {
|
|
66
|
+
const nodeAttrId =
|
|
67
|
+
owner.node.dataAttributes["data-agent-native-node-id"] ??
|
|
68
|
+
owner.node.id;
|
|
69
|
+
scheduleBeginTextEditForScreen(owner.fileId, nodeAttrId, {
|
|
70
|
+
boardFileId,
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const childNodes = owner.node.children
|
|
75
|
+
.map(
|
|
76
|
+
(childId) => codeLayerOwnerByNodeIdRef.current.get(childId)?.node,
|
|
77
|
+
)
|
|
78
|
+
.filter((node): node is CodeLayerNode => Boolean(node));
|
|
79
|
+
if (
|
|
80
|
+
selectCodeLayerNodesForHotkey(owner.fileId, childNodes, [
|
|
81
|
+
owner.node.id,
|
|
82
|
+
])
|
|
83
|
+
) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// P5/vector-edit: Enter on a single selected committed pen path (has
|
|
91
|
+
// data-an-pen-nodes — see setPenNodesAttributeOnElement) enters vector
|
|
92
|
+
// edit mode instead of drilling into the owning screen. Vector edit's
|
|
93
|
+
// overlay only exists on MultiScreenCanvas (overview), so this
|
|
94
|
+
// deliberately only fires here — see enterVectorEditForSelection's doc
|
|
95
|
+
// comment for the single-screen deferral.
|
|
96
|
+
if (selectedLayerIdsState.length === 1) {
|
|
97
|
+
const layerId = selectedLayerIdsState[0]!;
|
|
98
|
+
const owner = codeLayerOwnerByNodeIdRef.current.get(layerId);
|
|
99
|
+
const penNodesAttr = owner?.node.dataAttributes["data-an-pen-nodes"];
|
|
100
|
+
if (owner && penNodesAttr && enterVectorEditForSelection(owner)) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// Figma parity: Enter on a selected TEXT layer always begins inline
|
|
104
|
+
// text editing, in overview mode too — not just single-screen mode
|
|
105
|
+
// (see the T22 branch above). Reuses the exact same owner lookup /
|
|
106
|
+
// SINGLE_MODE_TEXT_TAGS / data-an-primitive==="text" classification
|
|
107
|
+
// and scheduleBeginTextEditForScreen path; only the screen-drill
|
|
108
|
+
// fallback below is skipped when the selected layer is text.
|
|
109
|
+
if (owner) {
|
|
110
|
+
const isTextNode =
|
|
111
|
+
SINGLE_MODE_TEXT_TAGS.has(owner.node.tag) ||
|
|
112
|
+
owner.node.dataAttributes["data-an-primitive"] === "text";
|
|
113
|
+
if (isTextNode) {
|
|
114
|
+
const nodeAttrId =
|
|
115
|
+
owner.node.dataAttributes["data-agent-native-node-id"] ??
|
|
116
|
+
owner.node.id;
|
|
117
|
+
scheduleBeginTextEditForScreen(owner.fileId, nodeAttrId, {
|
|
118
|
+
boardFileId,
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const childNodes = owner.node.children
|
|
123
|
+
.map((childId) => codeLayerOwnerByNodeIdRef.current.get(childId)?.node)
|
|
124
|
+
.filter((node): node is CodeLayerNode => Boolean(node));
|
|
125
|
+
if (
|
|
126
|
+
selectCodeLayerNodesForHotkey(owner.fileId, childNodes, [owner.node.id])
|
|
127
|
+
) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const target = getOverviewEnterTarget({
|
|
133
|
+
activeFileId: activeFile?.id ?? activeFileId,
|
|
134
|
+
overviewSelectedScreenIds,
|
|
135
|
+
});
|
|
136
|
+
if (!target) return;
|
|
137
|
+
// Ground-truth Figma: Enter on a screen/container selects ALL of its
|
|
138
|
+
// direct children (not just the first) in addition to drilling in.
|
|
139
|
+
// rootNodeIds from the screen's own code-layer projection are exactly
|
|
140
|
+
// its direct children (nodes with no parent, i.e. direct children of
|
|
141
|
+
// <body>) — the same ids setSelectedLayerIdsState already accepts
|
|
142
|
+
// elsewhere (e.g. after insert/paste).
|
|
143
|
+
const targetProjection = buildCodeLayerProjection(
|
|
144
|
+
getProjectionContentForScreen(target),
|
|
145
|
+
);
|
|
146
|
+
if (targetProjection.rootNodeIds.length > 0) {
|
|
147
|
+
setSelectedLayerIdsState(targetProjection.rootNodeIds);
|
|
148
|
+
}
|
|
149
|
+
// Drill-in selects the screen's children and stays on the infinite canvas.
|
|
150
|
+
// It used to also switch to a focused single-screen view — that view no
|
|
151
|
+
// longer exists (the only two views are the canvas and the responsive
|
|
152
|
+
// interactive one), and routing this through the focused view now would
|
|
153
|
+
// land the user in Interact, where there is no selection at all.
|
|
154
|
+
setActiveFileId(target);
|
|
155
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { CanvasFrameGeometryById } from "@shared/canvas-frames";
|
|
2
|
+
import type { PenPath } from "@shared/pen-path";
|
|
3
|
+
import type { Dispatch, RefObject, SetStateAction } from "react";
|
|
4
|
+
|
|
5
|
+
import { getScreenPreviewViewport } from "@/components/design/multi-screen/frame-geometry";
|
|
6
|
+
import type { ElementInfo } from "@/components/design/types";
|
|
7
|
+
import type { OverviewScreen } from "@/pages/design-editor/derive/overview-screens";
|
|
8
|
+
import { resolveScreenEntryZoom } from "@/pages/design-editor/overview-camera";
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_INTERACT_DEVICE_PRESET,
|
|
11
|
+
resolveInteractDeviceForScreen,
|
|
12
|
+
} from "@/pages/design-editor/responsive-interact";
|
|
13
|
+
import type { DesignTool, EditorMode } from "@/pages/design-editor/types";
|
|
14
|
+
import { FOCUSED_SCREEN_ZOOM } from "@/pages/design-editor/types";
|
|
15
|
+
|
|
16
|
+
export interface EnterSingleScreenArgs {
|
|
17
|
+
activeFileId: string | null;
|
|
18
|
+
canvasFrameGeometryById: CanvasFrameGeometryById;
|
|
19
|
+
clearPendingOverviewLayerSelectionTimer: () => void;
|
|
20
|
+
overviewScreens: OverviewScreen[];
|
|
21
|
+
pendingOverviewLayerSelectionRef: RefObject<string | null>;
|
|
22
|
+
pendingOverviewScreenSelectionRef: RefObject<string | null>;
|
|
23
|
+
runEditorViewTransition: (update: () => void) => void;
|
|
24
|
+
screenZoomByIdRef: RefObject<Map<string, number>>;
|
|
25
|
+
setActiveFileId: Dispatch<SetStateAction<string | null>>;
|
|
26
|
+
setActiveTool: Dispatch<SetStateAction<DesignTool>>;
|
|
27
|
+
setCreatedOverviewLayerSelection: Dispatch<
|
|
28
|
+
SetStateAction<{ screenId: string; layerId: string } | null>
|
|
29
|
+
>;
|
|
30
|
+
setDrawMode: Dispatch<SetStateAction<boolean>>;
|
|
31
|
+
setHoveredElement: Dispatch<SetStateAction<ElementInfo | null>>;
|
|
32
|
+
setInteractDeviceName: Dispatch<SetStateAction<string>>;
|
|
33
|
+
setInteractDeviceSize: Dispatch<
|
|
34
|
+
SetStateAction<{ width: number; height: number }>
|
|
35
|
+
>;
|
|
36
|
+
setMode: Dispatch<SetStateAction<EditorMode>>;
|
|
37
|
+
setPinMode: Dispatch<SetStateAction<boolean>>;
|
|
38
|
+
setScreenZoom: Dispatch<SetStateAction<number>>;
|
|
39
|
+
setSelectedElement: Dispatch<SetStateAction<ElementInfo | null>>;
|
|
40
|
+
setVectorEditingState: Dispatch<
|
|
41
|
+
SetStateAction<{ screenId: string; nodeId: string; path: PenPath } | null>
|
|
42
|
+
>;
|
|
43
|
+
setViewMode: Dispatch<SetStateAction<"single" | "overview">>;
|
|
44
|
+
viewModeRef: RefObject<"single" | "overview">;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface EnterSingleScreenOptions {
|
|
48
|
+
mode?: EditorMode;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function runEnterSingleScreen(
|
|
52
|
+
{
|
|
53
|
+
activeFileId,
|
|
54
|
+
canvasFrameGeometryById,
|
|
55
|
+
clearPendingOverviewLayerSelectionTimer,
|
|
56
|
+
overviewScreens,
|
|
57
|
+
pendingOverviewLayerSelectionRef,
|
|
58
|
+
pendingOverviewScreenSelectionRef,
|
|
59
|
+
runEditorViewTransition,
|
|
60
|
+
screenZoomByIdRef,
|
|
61
|
+
setActiveFileId,
|
|
62
|
+
setActiveTool,
|
|
63
|
+
setCreatedOverviewLayerSelection,
|
|
64
|
+
setDrawMode,
|
|
65
|
+
setHoveredElement,
|
|
66
|
+
setInteractDeviceName,
|
|
67
|
+
setInteractDeviceSize,
|
|
68
|
+
setMode,
|
|
69
|
+
setPinMode,
|
|
70
|
+
setScreenZoom,
|
|
71
|
+
setSelectedElement,
|
|
72
|
+
setVectorEditingState,
|
|
73
|
+
setViewMode,
|
|
74
|
+
viewModeRef,
|
|
75
|
+
}: EnterSingleScreenArgs,
|
|
76
|
+
fileId?: string | null,
|
|
77
|
+
options?: EnterSingleScreenOptions,
|
|
78
|
+
) {
|
|
79
|
+
const entryMode = options?.mode ?? "interact";
|
|
80
|
+
const targetFileId = fileId ?? activeFileId;
|
|
81
|
+
const targetScreen = targetFileId
|
|
82
|
+
? overviewScreens.find((screen) => screen.id === targetFileId)
|
|
83
|
+
: undefined;
|
|
84
|
+
const targetMetadataSize = targetScreen
|
|
85
|
+
? {
|
|
86
|
+
width: targetScreen.width ?? DEFAULT_INTERACT_DEVICE_PRESET.width,
|
|
87
|
+
height: targetScreen.height ?? DEFAULT_INTERACT_DEVICE_PRESET.height,
|
|
88
|
+
}
|
|
89
|
+
: undefined;
|
|
90
|
+
const targetViewport =
|
|
91
|
+
targetFileId && targetMetadataSize
|
|
92
|
+
? getScreenPreviewViewport(targetMetadataSize, {
|
|
93
|
+
width:
|
|
94
|
+
canvasFrameGeometryById[targetFileId]?.width ??
|
|
95
|
+
targetMetadataSize.width,
|
|
96
|
+
height:
|
|
97
|
+
canvasFrameGeometryById[targetFileId]?.height ??
|
|
98
|
+
targetMetadataSize.height,
|
|
99
|
+
})
|
|
100
|
+
: undefined;
|
|
101
|
+
const nextInteractDevice = resolveInteractDeviceForScreen(
|
|
102
|
+
targetViewport
|
|
103
|
+
? {
|
|
104
|
+
width: targetViewport.viewportWidth,
|
|
105
|
+
height: targetViewport.viewportHeight,
|
|
106
|
+
}
|
|
107
|
+
: undefined,
|
|
108
|
+
);
|
|
109
|
+
if (
|
|
110
|
+
viewModeRef.current === "single" &&
|
|
111
|
+
(!fileId || fileId === activeFileId)
|
|
112
|
+
) {
|
|
113
|
+
if (fileId && fileId === activeFileId) {
|
|
114
|
+
// Re-focusing the screen that's already active is a deliberate
|
|
115
|
+
// "reset view" affordance (e.g. re-clicking the same screen's
|
|
116
|
+
// Interact button) — reset to the default zoom rather than
|
|
117
|
+
// restoring the remembered one, mirroring the previous behavior.
|
|
118
|
+
setScreenZoom(FOCUSED_SCREEN_ZOOM);
|
|
119
|
+
}
|
|
120
|
+
// The early return used to swallow a requested mode change, so
|
|
121
|
+
// re-clicking a screen after closing Interact left it in whatever
|
|
122
|
+
// mode it had drifted to instead of reopening the responsive view.
|
|
123
|
+
setMode(entryMode);
|
|
124
|
+
setInteractDeviceName(nextInteractDevice.name);
|
|
125
|
+
setInteractDeviceSize({
|
|
126
|
+
width: nextInteractDevice.width,
|
|
127
|
+
height: nextInteractDevice.height,
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
viewModeRef.current = "single";
|
|
132
|
+
pendingOverviewScreenSelectionRef.current = null;
|
|
133
|
+
pendingOverviewLayerSelectionRef.current = null;
|
|
134
|
+
clearPendingOverviewLayerSelectionTimer();
|
|
135
|
+
setCreatedOverviewLayerSelection(null);
|
|
136
|
+
// P5/vector-edit: MultiScreenCanvas (the only place the vectorEdit
|
|
137
|
+
// overlay renders) unmounts on leaving overview, so an active
|
|
138
|
+
// vector-edit session has nothing left to render into — clear it
|
|
139
|
+
// rather than leaving a stale/orphaned session in memory that would
|
|
140
|
+
// resurface if the user returns to overview later.
|
|
141
|
+
setVectorEditingState(null);
|
|
142
|
+
// Per-screen zoom memory: restore the target screen's last-remembered
|
|
143
|
+
// zoom (recorded by the screenZoomByIdRef effect above) instead of
|
|
144
|
+
// always resetting to FOCUSED_SCREEN_ZOOM, so leaving and re-entering a
|
|
145
|
+
// screen preserves where the user left off. Falls back to
|
|
146
|
+
// FOCUSED_SCREEN_ZOOM for a screen's first visit.
|
|
147
|
+
const restoredZoom = resolveScreenEntryZoom(
|
|
148
|
+
targetFileId,
|
|
149
|
+
screenZoomByIdRef.current,
|
|
150
|
+
FOCUSED_SCREEN_ZOOM,
|
|
151
|
+
);
|
|
152
|
+
runEditorViewTransition(() => {
|
|
153
|
+
if (fileId) setActiveFileId(fileId);
|
|
154
|
+
setDrawMode(false);
|
|
155
|
+
setPinMode(false);
|
|
156
|
+
setMode(entryMode);
|
|
157
|
+
setSelectedElement(null);
|
|
158
|
+
setHoveredElement(null);
|
|
159
|
+
setActiveTool("move");
|
|
160
|
+
setScreenZoom(restoredZoom);
|
|
161
|
+
setInteractDeviceName(nextInteractDevice.name);
|
|
162
|
+
setInteractDeviceSize({
|
|
163
|
+
width: nextInteractDevice.width,
|
|
164
|
+
height: nextInteractDevice.height,
|
|
165
|
+
});
|
|
166
|
+
setViewMode("single");
|
|
167
|
+
});
|
|
168
|
+
}
|