@agent-native/core 0.163.4 → 0.164.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/templates/analytics/app/hooks/use-dashboard-chat-context.ts +29 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +1 -2
- package/corpus/templates/analytics/netlify.toml +2 -0
- 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/_index.tsx +10 -2
- 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/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/netlify.toml +2 -0
- 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/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/routes.d.ts +1 -1
- 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/observability/routes.d.ts +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/provider-api.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- 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/settings/store.d.ts +9 -0
- package/dist/settings/store.js +19 -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,906 @@
|
|
|
1
|
+
import { useActionMutation } from "@agent-native/core/client/hooks";
|
|
2
|
+
import type { CanvasFrameGeometryById } from "@shared/canvas-frames";
|
|
3
|
+
import type { QueryClient } from "@tanstack/react-query";
|
|
4
|
+
import type { Dispatch, RefObject, SetStateAction } from "react";
|
|
5
|
+
import { toast } from "sonner";
|
|
6
|
+
import * as Y from "yjs";
|
|
7
|
+
|
|
8
|
+
import { trace } from "@/components/design/design-trace";
|
|
9
|
+
import type { ElementInfo } from "@/components/design/types";
|
|
10
|
+
import type {
|
|
11
|
+
ClipboardContentLineage,
|
|
12
|
+
ClipboardContentMutationOrigin,
|
|
13
|
+
ClipboardContentMutationPublication,
|
|
14
|
+
} from "@/lib/clipboard-content-lineage";
|
|
15
|
+
import {
|
|
16
|
+
refreshElementInfoFromContent,
|
|
17
|
+
refreshSelectedLayerIdsFromContent,
|
|
18
|
+
} from "@/pages/design-editor/code-layer-state";
|
|
19
|
+
import type { LiveScreenSnapshot } from "@/pages/design-editor/command-types";
|
|
20
|
+
import {
|
|
21
|
+
getCanvasFrameGeometry,
|
|
22
|
+
staleGeometryFrameIds,
|
|
23
|
+
viewportChangedFrameIds,
|
|
24
|
+
} from "@/pages/design-editor/design-data-geometry-utils";
|
|
25
|
+
import type {
|
|
26
|
+
PreviewContentReplaceResult,
|
|
27
|
+
UndoRedoOrderKind,
|
|
28
|
+
} from "@/pages/design-editor/editor-state";
|
|
29
|
+
import { previewContentReplaceNeedsRenderFallback } from "@/pages/design-editor/editor-state";
|
|
30
|
+
import type {
|
|
31
|
+
ContentHistoryChange,
|
|
32
|
+
ContentHistoryEntry,
|
|
33
|
+
FileCreationHistoryEntry,
|
|
34
|
+
FileDeletionHistoryEntry,
|
|
35
|
+
GeometryHistoryEntry,
|
|
36
|
+
GeometryHistorySelection,
|
|
37
|
+
} from "@/pages/design-editor/history";
|
|
38
|
+
import {
|
|
39
|
+
MAX_DESIGN_UNDO_STACK,
|
|
40
|
+
applyGeometryHistoryDiff,
|
|
41
|
+
findLastContentHistoryChangeIndex,
|
|
42
|
+
getAvailableContentHistoryChanges,
|
|
43
|
+
remapFileDeletionHistoryEntryIds,
|
|
44
|
+
} from "@/pages/design-editor/history";
|
|
45
|
+
import type {
|
|
46
|
+
PendingLiveNonStyleEdit,
|
|
47
|
+
PendingLiveNonStyleUndoEntry,
|
|
48
|
+
PendingVisualStyleEdit,
|
|
49
|
+
PendingVisualStyleUndoEntry,
|
|
50
|
+
} from "@/pages/design-editor/pending-edits";
|
|
51
|
+
import {
|
|
52
|
+
mergePendingLiveNonStyleEdits,
|
|
53
|
+
mergePendingVisualStyleEdits,
|
|
54
|
+
} from "@/pages/design-editor/pending-edits";
|
|
55
|
+
import { pendingEditTargetsSelectedElement } from "@/pages/design-editor/selection-state";
|
|
56
|
+
import type { DesignFile } from "@/pages/design-editor/types";
|
|
57
|
+
|
|
58
|
+
export interface UndoArgs {
|
|
59
|
+
activeEditorDragRef: RefObject<boolean>;
|
|
60
|
+
activeFile: DesignFile;
|
|
61
|
+
applyFileContentUpdate: (
|
|
62
|
+
fileId: string,
|
|
63
|
+
nextContent: string,
|
|
64
|
+
options?: {
|
|
65
|
+
refreshPreview?: boolean;
|
|
66
|
+
skipPreview?: boolean;
|
|
67
|
+
forcePreviewFullDocument?: boolean;
|
|
68
|
+
persist?: boolean;
|
|
69
|
+
recordHistory?: boolean;
|
|
70
|
+
updatedAt?: string;
|
|
71
|
+
clipboardMutation?: ClipboardContentMutationPublication;
|
|
72
|
+
},
|
|
73
|
+
) => void;
|
|
74
|
+
applyLocalContentUpdate: (
|
|
75
|
+
nextContent: string,
|
|
76
|
+
options?: {
|
|
77
|
+
refreshPreview?: boolean;
|
|
78
|
+
skipPreview?: boolean;
|
|
79
|
+
forcePreviewFullDocument?: boolean;
|
|
80
|
+
immediateSave?: boolean;
|
|
81
|
+
persist?: boolean;
|
|
82
|
+
recordHistory?: boolean;
|
|
83
|
+
historyBeforeContent?: string;
|
|
84
|
+
updatedAt?: string;
|
|
85
|
+
clipboardMutation?: ClipboardContentMutationPublication;
|
|
86
|
+
},
|
|
87
|
+
) => void;
|
|
88
|
+
canEditDesign: boolean;
|
|
89
|
+
clipboardPasteRedoStackRef: RefObject<ContentHistoryChange[]>;
|
|
90
|
+
clipboardPasteUndoStackRef: RefObject<ContentHistoryChange[]>;
|
|
91
|
+
contentRedoSelectionStackRef: RefObject<
|
|
92
|
+
(GeometryHistorySelection | undefined)[]
|
|
93
|
+
>;
|
|
94
|
+
contentRedoStackRef: RefObject<ContentHistoryEntry[]>;
|
|
95
|
+
contentUndoSelectionStackRef: RefObject<
|
|
96
|
+
(GeometryHistorySelection | undefined)[]
|
|
97
|
+
>;
|
|
98
|
+
contentUndoStackRef: RefObject<ContentHistoryEntry[]>;
|
|
99
|
+
createFileMutation: ReturnType<
|
|
100
|
+
typeof useActionMutation<undefined, undefined, "create-file">
|
|
101
|
+
>;
|
|
102
|
+
deleteFileMutation: ReturnType<
|
|
103
|
+
typeof useActionMutation<undefined, undefined, "delete-file">
|
|
104
|
+
>;
|
|
105
|
+
designDataJsonRef: RefObject<Record<string, unknown>>;
|
|
106
|
+
fileCreationRedoStackRef: RefObject<FileCreationHistoryEntry[]>;
|
|
107
|
+
fileCreationUndoStackRef: RefObject<FileCreationHistoryEntry[]>;
|
|
108
|
+
fileDeletionRedoStackRef: RefObject<FileDeletionHistoryEntry[]>;
|
|
109
|
+
fileDeletionUndoStackRef: RefObject<FileDeletionHistoryEntry[]>;
|
|
110
|
+
fileHistoryMutationPendingRef: RefObject<boolean>;
|
|
111
|
+
files: DesignFile[];
|
|
112
|
+
geometryRedoStackRef: RefObject<GeometryHistoryEntry[]>;
|
|
113
|
+
geometryUndoStackRef: RefObject<GeometryHistoryEntry[]>;
|
|
114
|
+
getFreshActiveContent: () => string;
|
|
115
|
+
getScreenContent: (screenId: string) => string;
|
|
116
|
+
historyOrderRef: RefObject<UndoRedoOrderKind[]>;
|
|
117
|
+
id: string | undefined;
|
|
118
|
+
isSynced: boolean;
|
|
119
|
+
lastLocalContentRef: RefObject<string | null>;
|
|
120
|
+
latestClipboardMutationContentRef: RefObject<
|
|
121
|
+
Map<string, ClipboardContentLineage>
|
|
122
|
+
>;
|
|
123
|
+
liveFrameGeometryRef: RefObject<CanvasFrameGeometryById>;
|
|
124
|
+
liveScreenSnapshotsById: Record<string, LiveScreenSnapshot>;
|
|
125
|
+
localContentRedoStackRef: RefObject<ContentHistoryChange[]>;
|
|
126
|
+
localContentUndoStackRef: RefObject<ContentHistoryChange[]>;
|
|
127
|
+
markPendingLocalFileContent: (
|
|
128
|
+
fileId: string,
|
|
129
|
+
content: string,
|
|
130
|
+
baseUpdatedAt?: string | null,
|
|
131
|
+
) => void;
|
|
132
|
+
pendingLiveNonStyleEditsRef: RefObject<PendingLiveNonStyleEdit[]>;
|
|
133
|
+
pendingLiveNonStyleRedoStackRef: RefObject<PendingLiveNonStyleUndoEntry[]>;
|
|
134
|
+
pendingLiveNonStyleUndoStackRef: RefObject<PendingLiveNonStyleUndoEntry[]>;
|
|
135
|
+
pendingLocalFileContentsRef: RefObject<
|
|
136
|
+
Map<
|
|
137
|
+
string,
|
|
138
|
+
{ content: string; startedAt: number; baseUpdatedAt?: string | null }
|
|
139
|
+
>
|
|
140
|
+
>;
|
|
141
|
+
pendingVisualStyleEditsRef: RefObject<PendingVisualStyleEdit[]>;
|
|
142
|
+
pendingVisualStyleRedoStackRef: RefObject<PendingVisualStyleUndoEntry[]>;
|
|
143
|
+
pendingVisualStyleUndoStackRef: RefObject<PendingVisualStyleUndoEntry[]>;
|
|
144
|
+
performDeleteFiles: (
|
|
145
|
+
filesToDelete: DesignFile[],
|
|
146
|
+
options?: {
|
|
147
|
+
skipFileCreationRedoPrune?: boolean;
|
|
148
|
+
recordDeletionHistory?: boolean;
|
|
149
|
+
onMutationSettled?: (
|
|
150
|
+
deletedFiles: DesignFile[],
|
|
151
|
+
failedFiles: DesignFile[],
|
|
152
|
+
) => void;
|
|
153
|
+
},
|
|
154
|
+
) => void;
|
|
155
|
+
publishAuthoritativeClipboardMutation: (args: {
|
|
156
|
+
fileId: string;
|
|
157
|
+
baseContent: string;
|
|
158
|
+
nextContent: string;
|
|
159
|
+
origin: ClipboardContentMutationOrigin;
|
|
160
|
+
}) => ClipboardContentMutationPublication | null;
|
|
161
|
+
queryClient: QueryClient;
|
|
162
|
+
queueFileContentSave: (
|
|
163
|
+
fileId: string,
|
|
164
|
+
content: string,
|
|
165
|
+
options?: { syncCollab?: boolean; immediate?: boolean },
|
|
166
|
+
) => void;
|
|
167
|
+
redoOrderRef: RefObject<UndoRedoOrderKind[]>;
|
|
168
|
+
replacePreviewContent: (
|
|
169
|
+
nextContent: string,
|
|
170
|
+
selector?: string | null,
|
|
171
|
+
options?: { forceFullDocument?: boolean },
|
|
172
|
+
) => PreviewContentReplaceResult;
|
|
173
|
+
requestPendingLiveNonStyleRevert: (
|
|
174
|
+
edits: readonly PendingLiveNonStyleEdit[],
|
|
175
|
+
) => void;
|
|
176
|
+
requestPendingVisualStyleRevert: (
|
|
177
|
+
edits: readonly PendingVisualStyleEdit[],
|
|
178
|
+
) => void;
|
|
179
|
+
restoreSelectionSnapshot: (
|
|
180
|
+
selection: GeometryHistorySelection | undefined,
|
|
181
|
+
) => void;
|
|
182
|
+
setActiveFileId: Dispatch<SetStateAction<string | null>>;
|
|
183
|
+
setContentRenderRevision: Dispatch<SetStateAction<number>>;
|
|
184
|
+
setHoveredElement: Dispatch<SetStateAction<ElementInfo | null>>;
|
|
185
|
+
setOverviewSelectedScreenIds: Dispatch<SetStateAction<string[]>>;
|
|
186
|
+
setPendingLiveNonStyleEdits: Dispatch<
|
|
187
|
+
SetStateAction<PendingLiveNonStyleEdit[]>
|
|
188
|
+
>;
|
|
189
|
+
setPendingVisualStyleEdits: Dispatch<
|
|
190
|
+
SetStateAction<PendingVisualStyleEdit[]>
|
|
191
|
+
>;
|
|
192
|
+
setSelectedElement: Dispatch<SetStateAction<ElementInfo | null>>;
|
|
193
|
+
setSelectedLayerIdsState: Dispatch<SetStateAction<string[]>>;
|
|
194
|
+
suppressContentHistoryRef: RefObject<boolean>;
|
|
195
|
+
syncLiveScreenSnapshotPreview: (screenId: string, html: string) => void;
|
|
196
|
+
syncUndoRedoState: () => void;
|
|
197
|
+
t: (key: string, options?: Record<string, unknown>) => string;
|
|
198
|
+
undoManagerRef: RefObject<Y.UndoManager | null>;
|
|
199
|
+
updateLiveScreenSnapshotContent: (
|
|
200
|
+
screenId: string,
|
|
201
|
+
html: string,
|
|
202
|
+
options?: { recordHistory?: boolean },
|
|
203
|
+
) => boolean;
|
|
204
|
+
viewModeRef: RefObject<"single" | "overview">;
|
|
205
|
+
writeFrameGeometrySnapshot: (
|
|
206
|
+
geometryById: CanvasFrameGeometryById,
|
|
207
|
+
options?: { syncViewportFrameIds?: string[]; pinHeightFrameIds?: string[] },
|
|
208
|
+
) => void;
|
|
209
|
+
ydoc: Y.Doc | null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function runUndo({
|
|
213
|
+
activeEditorDragRef,
|
|
214
|
+
activeFile,
|
|
215
|
+
applyFileContentUpdate,
|
|
216
|
+
applyLocalContentUpdate,
|
|
217
|
+
canEditDesign,
|
|
218
|
+
clipboardPasteRedoStackRef,
|
|
219
|
+
clipboardPasteUndoStackRef,
|
|
220
|
+
contentRedoSelectionStackRef,
|
|
221
|
+
contentRedoStackRef,
|
|
222
|
+
contentUndoSelectionStackRef,
|
|
223
|
+
contentUndoStackRef,
|
|
224
|
+
createFileMutation,
|
|
225
|
+
deleteFileMutation,
|
|
226
|
+
designDataJsonRef,
|
|
227
|
+
fileCreationRedoStackRef,
|
|
228
|
+
fileCreationUndoStackRef,
|
|
229
|
+
fileDeletionRedoStackRef,
|
|
230
|
+
fileDeletionUndoStackRef,
|
|
231
|
+
fileHistoryMutationPendingRef,
|
|
232
|
+
files,
|
|
233
|
+
geometryRedoStackRef,
|
|
234
|
+
geometryUndoStackRef,
|
|
235
|
+
getFreshActiveContent,
|
|
236
|
+
getScreenContent,
|
|
237
|
+
historyOrderRef,
|
|
238
|
+
id,
|
|
239
|
+
isSynced,
|
|
240
|
+
lastLocalContentRef,
|
|
241
|
+
latestClipboardMutationContentRef,
|
|
242
|
+
liveFrameGeometryRef,
|
|
243
|
+
liveScreenSnapshotsById,
|
|
244
|
+
localContentRedoStackRef,
|
|
245
|
+
localContentUndoStackRef,
|
|
246
|
+
markPendingLocalFileContent,
|
|
247
|
+
pendingLiveNonStyleEditsRef,
|
|
248
|
+
pendingLiveNonStyleRedoStackRef,
|
|
249
|
+
pendingLiveNonStyleUndoStackRef,
|
|
250
|
+
pendingLocalFileContentsRef,
|
|
251
|
+
pendingVisualStyleEditsRef,
|
|
252
|
+
pendingVisualStyleRedoStackRef,
|
|
253
|
+
pendingVisualStyleUndoStackRef,
|
|
254
|
+
performDeleteFiles,
|
|
255
|
+
publishAuthoritativeClipboardMutation,
|
|
256
|
+
queryClient,
|
|
257
|
+
queueFileContentSave,
|
|
258
|
+
redoOrderRef,
|
|
259
|
+
replacePreviewContent,
|
|
260
|
+
requestPendingLiveNonStyleRevert,
|
|
261
|
+
requestPendingVisualStyleRevert,
|
|
262
|
+
restoreSelectionSnapshot,
|
|
263
|
+
setActiveFileId,
|
|
264
|
+
setContentRenderRevision,
|
|
265
|
+
setHoveredElement,
|
|
266
|
+
setOverviewSelectedScreenIds,
|
|
267
|
+
setPendingLiveNonStyleEdits,
|
|
268
|
+
setPendingVisualStyleEdits,
|
|
269
|
+
setSelectedElement,
|
|
270
|
+
setSelectedLayerIdsState,
|
|
271
|
+
suppressContentHistoryRef,
|
|
272
|
+
syncLiveScreenSnapshotPreview,
|
|
273
|
+
syncUndoRedoState,
|
|
274
|
+
t,
|
|
275
|
+
undoManagerRef,
|
|
276
|
+
updateLiveScreenSnapshotContent,
|
|
277
|
+
viewModeRef,
|
|
278
|
+
writeFrameGeometrySnapshot,
|
|
279
|
+
ydoc,
|
|
280
|
+
}: UndoArgs) {
|
|
281
|
+
trace("history", "undo", {});
|
|
282
|
+
if (!canEditDesign) return;
|
|
283
|
+
// U10: an in-progress drag hasn't been committed yet (onGeometryCommit /
|
|
284
|
+
// the content update fires on drag END), so undoing mid-drag would pop a
|
|
285
|
+
// PRIOR entry while the live-but-uncommitted drag is still moving the
|
|
286
|
+
// element — the drag's eventual commit would then stomp the undo. Block
|
|
287
|
+
// until the drag finishes (or is cancelled).
|
|
288
|
+
if (activeEditorDragRef.current) return;
|
|
289
|
+
if (fileHistoryMutationPendingRef.current) return;
|
|
290
|
+
const pendingStyleUndoStack = pendingVisualStyleUndoStackRef.current;
|
|
291
|
+
const pendingStyleUndo =
|
|
292
|
+
pendingStyleUndoStack[pendingStyleUndoStack.length - 1];
|
|
293
|
+
const pendingNonStyleUndoStack = pendingLiveNonStyleUndoStackRef.current;
|
|
294
|
+
const pendingNonStyleUndo =
|
|
295
|
+
pendingNonStyleUndoStack[pendingNonStyleUndoStack.length - 1];
|
|
296
|
+
if (
|
|
297
|
+
pendingNonStyleUndo &&
|
|
298
|
+
(!pendingStyleUndo ||
|
|
299
|
+
pendingNonStyleUndo.edit.updatedAt > pendingStyleUndo.edit.updatedAt)
|
|
300
|
+
) {
|
|
301
|
+
const nextUndoStack = pendingNonStyleUndoStack.slice(0, -1);
|
|
302
|
+
pendingLiveNonStyleUndoStackRef.current = nextUndoStack;
|
|
303
|
+
const nextPending = mergePendingLiveNonStyleEdits(
|
|
304
|
+
nextUndoStack.map((entry) => entry.edit),
|
|
305
|
+
);
|
|
306
|
+
pendingLiveNonStyleEditsRef.current = nextPending;
|
|
307
|
+
pendingLiveNonStyleRedoStackRef.current = [
|
|
308
|
+
...pendingLiveNonStyleRedoStackRef.current.slice(
|
|
309
|
+
-(MAX_DESIGN_UNDO_STACK - 1),
|
|
310
|
+
),
|
|
311
|
+
pendingNonStyleUndo,
|
|
312
|
+
];
|
|
313
|
+
requestPendingLiveNonStyleRevert([
|
|
314
|
+
pendingNonStyleUndo.kind === "text"
|
|
315
|
+
? {
|
|
316
|
+
...pendingNonStyleUndo.edit,
|
|
317
|
+
originalValue: pendingNonStyleUndo.revertValue,
|
|
318
|
+
originalHtml: pendingNonStyleUndo.revertHtml,
|
|
319
|
+
}
|
|
320
|
+
: pendingNonStyleUndo.kind === "layer-state"
|
|
321
|
+
? {
|
|
322
|
+
...pendingNonStyleUndo.edit,
|
|
323
|
+
originalEnabled: pendingNonStyleUndo.revertEnabled,
|
|
324
|
+
}
|
|
325
|
+
: pendingNonStyleUndo.edit,
|
|
326
|
+
]);
|
|
327
|
+
setPendingLiveNonStyleEdits(nextPending);
|
|
328
|
+
// Bug fix — undo reverted the DOM via requestPendingLiveNonStyleRevert
|
|
329
|
+
// above but never resynced the inspector panel's selectedElement, so
|
|
330
|
+
// the right panel kept showing pre-undo text until deselect/reselect.
|
|
331
|
+
// Mirrors recordPendingVisualStyleEdit's direct object-patch resync
|
|
332
|
+
// (~line 9514): a plain merge of the revert payload already on this
|
|
333
|
+
// undo entry, not a DOM re-query or content-string rebuild (those
|
|
334
|
+
// don't exist for pending live edits, which never touch
|
|
335
|
+
// ydoc/activeFile.content).
|
|
336
|
+
if (
|
|
337
|
+
pendingNonStyleUndo.kind === "text" &&
|
|
338
|
+
pendingNonStyleUndo.edit.screenId === activeFile?.id
|
|
339
|
+
) {
|
|
340
|
+
const { sourceId: revertedSourceId, selector: revertedSelector } =
|
|
341
|
+
pendingNonStyleUndo.edit;
|
|
342
|
+
const { revertValue, revertHtml } = pendingNonStyleUndo;
|
|
343
|
+
setSelectedElement((prev) => {
|
|
344
|
+
if (!prev) return prev;
|
|
345
|
+
if (
|
|
346
|
+
!pendingEditTargetsSelectedElement({
|
|
347
|
+
editSourceId: revertedSourceId,
|
|
348
|
+
editSelector: revertedSelector,
|
|
349
|
+
selectedSourceId: prev.sourceId,
|
|
350
|
+
selectedSelector: prev.selector,
|
|
351
|
+
})
|
|
352
|
+
) {
|
|
353
|
+
return prev;
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
...prev,
|
|
357
|
+
textContent: revertValue,
|
|
358
|
+
htmlContent: revertHtml ?? prev.htmlContent,
|
|
359
|
+
};
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
syncUndoRedoState();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (pendingStyleUndo) {
|
|
366
|
+
const nextUndoStack = pendingStyleUndoStack.slice(0, -1);
|
|
367
|
+
pendingVisualStyleUndoStackRef.current = nextUndoStack;
|
|
368
|
+
const nextPending = mergePendingVisualStyleEdits(
|
|
369
|
+
nextUndoStack.map((entry) => entry.edit),
|
|
370
|
+
);
|
|
371
|
+
pendingVisualStyleEditsRef.current = nextPending;
|
|
372
|
+
pendingVisualStyleRedoStackRef.current = [
|
|
373
|
+
...pendingVisualStyleRedoStackRef.current.slice(
|
|
374
|
+
-(MAX_DESIGN_UNDO_STACK - 1),
|
|
375
|
+
),
|
|
376
|
+
pendingStyleUndo,
|
|
377
|
+
];
|
|
378
|
+
requestPendingVisualStyleRevert([
|
|
379
|
+
{
|
|
380
|
+
...pendingStyleUndo.edit,
|
|
381
|
+
originalStyles: pendingStyleUndo.revertStyles,
|
|
382
|
+
},
|
|
383
|
+
]);
|
|
384
|
+
setPendingVisualStyleEdits(nextPending);
|
|
385
|
+
// Bug fix — same stale-inspector-panel issue as the pendingNonStyleUndo
|
|
386
|
+
// branch above, for style undo. Merge the reverted style values
|
|
387
|
+
// (already computed as pendingStyleUndo.revertStyles) into
|
|
388
|
+
// selectedElement.computedStyles, guarded to the currently-selected
|
|
389
|
+
// element so an undo on a different/background screen doesn't
|
|
390
|
+
// clobber the panel for whatever the user has selected right now.
|
|
391
|
+
if (pendingStyleUndo.edit.screenId === activeFile?.id) {
|
|
392
|
+
const {
|
|
393
|
+
sourceId: revertedStyleSourceId,
|
|
394
|
+
selector: revertedStyleSelector,
|
|
395
|
+
} = pendingStyleUndo.edit;
|
|
396
|
+
const revertedStyles = pendingStyleUndo.edit.interactionState
|
|
397
|
+
? Object.fromEntries(
|
|
398
|
+
Object.entries(pendingStyleUndo.revertStyles).map(
|
|
399
|
+
([property, value]) => [
|
|
400
|
+
property,
|
|
401
|
+
value || pendingStyleUndo.edit.baseStyles?.[property] || "",
|
|
402
|
+
],
|
|
403
|
+
),
|
|
404
|
+
)
|
|
405
|
+
: pendingStyleUndo.revertStyles;
|
|
406
|
+
setSelectedElement((prev) => {
|
|
407
|
+
if (!prev) return prev;
|
|
408
|
+
if (
|
|
409
|
+
!pendingEditTargetsSelectedElement({
|
|
410
|
+
editSourceId: revertedStyleSourceId,
|
|
411
|
+
editSelector: revertedStyleSelector,
|
|
412
|
+
selectedSourceId: prev.sourceId,
|
|
413
|
+
selectedSelector: prev.selector,
|
|
414
|
+
})
|
|
415
|
+
) {
|
|
416
|
+
return prev;
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
...prev,
|
|
420
|
+
computedStyles: {
|
|
421
|
+
...prev.computedStyles,
|
|
422
|
+
...revertedStyles,
|
|
423
|
+
},
|
|
424
|
+
};
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
syncUndoRedoState();
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const clipboardPasteUndo =
|
|
431
|
+
clipboardPasteUndoStackRef.current[
|
|
432
|
+
clipboardPasteUndoStackRef.current.length - 1
|
|
433
|
+
];
|
|
434
|
+
if (clipboardPasteUndo) {
|
|
435
|
+
const currentContent =
|
|
436
|
+
latestClipboardMutationContentRef.current.get(clipboardPasteUndo.fileId)
|
|
437
|
+
?.content ??
|
|
438
|
+
pendingLocalFileContentsRef.current.get(clipboardPasteUndo.fileId)
|
|
439
|
+
?.content ??
|
|
440
|
+
(clipboardPasteUndo.fileId === activeFile?.id
|
|
441
|
+
? getFreshActiveContent()
|
|
442
|
+
: (getScreenContent(clipboardPasteUndo.fileId) ?? ""));
|
|
443
|
+
// Only claim the command when this paste is still the top document
|
|
444
|
+
// state. If another edit followed it, the ordinary chronological
|
|
445
|
+
// history below gets first chance; once that edit is undone back to
|
|
446
|
+
// `after`, the next Cmd+Z reaches this immutable paste entry.
|
|
447
|
+
if (currentContent === clipboardPasteUndo.after) {
|
|
448
|
+
const clipboardMutation = publishAuthoritativeClipboardMutation({
|
|
449
|
+
fileId: clipboardPasteUndo.fileId,
|
|
450
|
+
baseContent: clipboardPasteUndo.after,
|
|
451
|
+
nextContent: clipboardPasteUndo.before,
|
|
452
|
+
origin: "clipboard-undo",
|
|
453
|
+
});
|
|
454
|
+
if (!clipboardMutation) return;
|
|
455
|
+
clipboardPasteUndoStackRef.current =
|
|
456
|
+
clipboardPasteUndoStackRef.current.slice(0, -1);
|
|
457
|
+
clipboardPasteRedoStackRef.current = [
|
|
458
|
+
...clipboardPasteRedoStackRef.current.slice(
|
|
459
|
+
-(MAX_DESIGN_UNDO_STACK - 1),
|
|
460
|
+
),
|
|
461
|
+
clipboardPasteUndo,
|
|
462
|
+
];
|
|
463
|
+
if (clipboardPasteUndo.fileId === activeFile?.id) {
|
|
464
|
+
applyLocalContentUpdate(clipboardPasteUndo.before, {
|
|
465
|
+
recordHistory: false,
|
|
466
|
+
forcePreviewFullDocument: true,
|
|
467
|
+
immediateSave: true,
|
|
468
|
+
clipboardMutation,
|
|
469
|
+
});
|
|
470
|
+
} else {
|
|
471
|
+
applyFileContentUpdate(
|
|
472
|
+
clipboardPasteUndo.fileId,
|
|
473
|
+
clipboardPasteUndo.before,
|
|
474
|
+
{
|
|
475
|
+
recordHistory: false,
|
|
476
|
+
forcePreviewFullDocument: true,
|
|
477
|
+
clipboardMutation,
|
|
478
|
+
},
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
setSelectedElement((previous) =>
|
|
482
|
+
previous
|
|
483
|
+
? refreshElementInfoFromContent(clipboardPasteUndo.before, previous)
|
|
484
|
+
: previous,
|
|
485
|
+
);
|
|
486
|
+
setSelectedLayerIdsState((previous) =>
|
|
487
|
+
refreshSelectedLayerIdsFromContent(clipboardPasteUndo.before, previous),
|
|
488
|
+
);
|
|
489
|
+
syncUndoRedoState();
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const um = undoManagerRef.current;
|
|
494
|
+
const canUseOverviewHistory = viewModeRef.current === "overview";
|
|
495
|
+
let prunedUndoHistory = 0;
|
|
496
|
+
const undoContent = (scope: "any" | "local" | "global" = "any") => {
|
|
497
|
+
if (scope !== "global" && um?.canUndo()) {
|
|
498
|
+
um.undo();
|
|
499
|
+
if (ydoc && activeFile) {
|
|
500
|
+
const next = ydoc.getText("content").toString();
|
|
501
|
+
markPendingLocalFileContent(activeFile.id, next, activeFile.updatedAt);
|
|
502
|
+
lastLocalContentRef.current = next;
|
|
503
|
+
queueFileContentSave(activeFile.id, next, {
|
|
504
|
+
syncCollab: !(ydoc && isSynced),
|
|
505
|
+
});
|
|
506
|
+
// Holistic flash pipeline: only fall back to a full srcdoc rebuild
|
|
507
|
+
// (real iframe reload) when the live in-place patch genuinely
|
|
508
|
+
// failed — replaceRuntimeDocument's forceFullDocument branch already
|
|
509
|
+
// swaps content inside the SAME live iframe (no navigation), so
|
|
510
|
+
// bumping contentRenderRevision unconditionally right after a
|
|
511
|
+
// successful in-place replace was a redundant second reload and the
|
|
512
|
+
// dominant cause of "undo/redo flashes heavily".
|
|
513
|
+
if (
|
|
514
|
+
previewContentReplaceNeedsRenderFallback(
|
|
515
|
+
replacePreviewContent(next, null, {
|
|
516
|
+
forceFullDocument: true,
|
|
517
|
+
}),
|
|
518
|
+
)
|
|
519
|
+
) {
|
|
520
|
+
setContentRenderRevision((revision) => revision + 1);
|
|
521
|
+
}
|
|
522
|
+
// Clear stale selection if the undo removed the selected element.
|
|
523
|
+
setSelectedElement((prev) => {
|
|
524
|
+
if (!prev) return prev;
|
|
525
|
+
return refreshElementInfoFromContent(next, prev);
|
|
526
|
+
});
|
|
527
|
+
setHoveredElement((prev) => {
|
|
528
|
+
if (!prev) return prev;
|
|
529
|
+
return refreshElementInfoFromContent(next, prev);
|
|
530
|
+
});
|
|
531
|
+
// U18: keep the layers-panel highlight in sync too.
|
|
532
|
+
setSelectedLayerIdsState((prev) =>
|
|
533
|
+
refreshSelectedLayerIdsFromContent(next, prev),
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
// Drop the matching local fallback mirror (see U3) so it can't be
|
|
537
|
+
// replayed a second time via the fallthrough path below once the Yjs
|
|
538
|
+
// UndoManager for this file is later torn down.
|
|
539
|
+
const mirroredIndex = findLastContentHistoryChangeIndex(
|
|
540
|
+
localContentUndoStackRef.current,
|
|
541
|
+
activeFile?.id,
|
|
542
|
+
);
|
|
543
|
+
if (mirroredIndex !== -1) {
|
|
544
|
+
localContentUndoStackRef.current =
|
|
545
|
+
localContentUndoStackRef.current.filter(
|
|
546
|
+
(_, index) => index !== mirroredIndex,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
redoOrderRef.current = [
|
|
550
|
+
...redoOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
|
|
551
|
+
"content",
|
|
552
|
+
];
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (!canUseOverviewHistory && scope !== "global" && activeFile?.id) {
|
|
557
|
+
const localIndex = findLastContentHistoryChangeIndex(
|
|
558
|
+
localContentUndoStackRef.current,
|
|
559
|
+
activeFile.id,
|
|
560
|
+
);
|
|
561
|
+
if (localIndex !== -1) {
|
|
562
|
+
const [entry] = localContentUndoStackRef.current.splice(localIndex, 1);
|
|
563
|
+
if (entry) {
|
|
564
|
+
localContentRedoStackRef.current = [
|
|
565
|
+
...localContentRedoStackRef.current.slice(
|
|
566
|
+
-(MAX_DESIGN_UNDO_STACK - 1),
|
|
567
|
+
),
|
|
568
|
+
entry,
|
|
569
|
+
];
|
|
570
|
+
// U20: route a live-snapshot screen's replay through
|
|
571
|
+
// updateLiveScreenSnapshotContent — see the matching note above.
|
|
572
|
+
if (liveScreenSnapshotsById[entry.fileId]) {
|
|
573
|
+
updateLiveScreenSnapshotContent(entry.fileId, entry.before, {
|
|
574
|
+
recordHistory: false,
|
|
575
|
+
});
|
|
576
|
+
syncLiveScreenSnapshotPreview(entry.fileId, entry.before);
|
|
577
|
+
} else {
|
|
578
|
+
applyLocalContentUpdate(entry.before, {
|
|
579
|
+
refreshPreview: false,
|
|
580
|
+
forcePreviewFullDocument: true,
|
|
581
|
+
immediateSave: true,
|
|
582
|
+
recordHistory: false,
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
setSelectedElement((prev) => {
|
|
586
|
+
if (!prev) return prev;
|
|
587
|
+
return refreshElementInfoFromContent(entry.before, prev);
|
|
588
|
+
});
|
|
589
|
+
setHoveredElement((prev) => {
|
|
590
|
+
if (!prev) return prev;
|
|
591
|
+
return refreshElementInfoFromContent(entry.before, prev);
|
|
592
|
+
});
|
|
593
|
+
// U18: keep the layers-panel highlight in sync too.
|
|
594
|
+
setSelectedLayerIdsState((prev) =>
|
|
595
|
+
refreshSelectedLayerIdsFromContent(entry.before, prev),
|
|
596
|
+
);
|
|
597
|
+
return true;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (scope === "local") return false;
|
|
603
|
+
if (!canUseOverviewHistory) return false;
|
|
604
|
+
const entry =
|
|
605
|
+
contentUndoStackRef.current[contentUndoStackRef.current.length - 1];
|
|
606
|
+
if (!entry) return false;
|
|
607
|
+
const entrySelection =
|
|
608
|
+
contentUndoSelectionStackRef.current[
|
|
609
|
+
contentUndoSelectionStackRef.current.length - 1
|
|
610
|
+
];
|
|
611
|
+
const changes = getAvailableContentHistoryChanges(
|
|
612
|
+
entry,
|
|
613
|
+
files.map((file) => file.id),
|
|
614
|
+
activeFile?.id,
|
|
615
|
+
);
|
|
616
|
+
if (changes.length === 0) {
|
|
617
|
+
contentUndoStackRef.current.pop();
|
|
618
|
+
contentUndoSelectionStackRef.current.pop();
|
|
619
|
+
prunedUndoHistory += 1;
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
contentUndoStackRef.current.pop();
|
|
623
|
+
contentUndoSelectionStackRef.current.pop();
|
|
624
|
+
contentRedoStackRef.current = [
|
|
625
|
+
...contentRedoStackRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
|
|
626
|
+
entry,
|
|
627
|
+
];
|
|
628
|
+
contentRedoSelectionStackRef.current = [
|
|
629
|
+
...contentRedoSelectionStackRef.current.slice(
|
|
630
|
+
-(MAX_DESIGN_UNDO_STACK - 1),
|
|
631
|
+
),
|
|
632
|
+
entrySelection,
|
|
633
|
+
];
|
|
634
|
+
redoOrderRef.current = [
|
|
635
|
+
...redoOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
|
|
636
|
+
"file-content",
|
|
637
|
+
];
|
|
638
|
+
suppressContentHistoryRef.current = true;
|
|
639
|
+
try {
|
|
640
|
+
for (const change of changes) {
|
|
641
|
+
// U20: a live-snapshot (URL-backed/localhost) screen's visible
|
|
642
|
+
// content lives in liveScreenSnapshotsById, not DesignFile.content
|
|
643
|
+
// — route replay there instead of the regular content path, which
|
|
644
|
+
// that screen's edits never actually write to.
|
|
645
|
+
if (liveScreenSnapshotsById[change.fileId]) {
|
|
646
|
+
updateLiveScreenSnapshotContent(change.fileId, change.before, {
|
|
647
|
+
recordHistory: false,
|
|
648
|
+
});
|
|
649
|
+
syncLiveScreenSnapshotPreview(change.fileId, change.before);
|
|
650
|
+
} else if (change.fileId === activeFile?.id) {
|
|
651
|
+
applyLocalContentUpdate(change.before, {
|
|
652
|
+
refreshPreview: false,
|
|
653
|
+
forcePreviewFullDocument: true,
|
|
654
|
+
immediateSave: true,
|
|
655
|
+
recordHistory: false,
|
|
656
|
+
});
|
|
657
|
+
} else {
|
|
658
|
+
applyFileContentUpdate(change.fileId, change.before, {
|
|
659
|
+
recordHistory: false,
|
|
660
|
+
refreshPreview: false,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
} finally {
|
|
665
|
+
suppressContentHistoryRef.current = false;
|
|
666
|
+
}
|
|
667
|
+
const activeChange = changes.find(
|
|
668
|
+
(change) => change.fileId === activeFile?.id,
|
|
669
|
+
);
|
|
670
|
+
if (activeChange) {
|
|
671
|
+
setSelectedElement((prev) => {
|
|
672
|
+
if (!prev) return prev;
|
|
673
|
+
return refreshElementInfoFromContent(activeChange.before, prev);
|
|
674
|
+
});
|
|
675
|
+
setHoveredElement((prev) => {
|
|
676
|
+
if (!prev) return prev;
|
|
677
|
+
return refreshElementInfoFromContent(activeChange.before, prev);
|
|
678
|
+
});
|
|
679
|
+
// U18: keep the layers-panel highlight in sync too.
|
|
680
|
+
setSelectedLayerIdsState((prev) =>
|
|
681
|
+
refreshSelectedLayerIdsFromContent(activeChange.before, prev),
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
// Figma-parity undo/redo selection restore: overrides the
|
|
685
|
+
// refreshSelectedLayerIdsFromContent heuristic just above with the
|
|
686
|
+
// actual captured selection, when one was recorded for this entry.
|
|
687
|
+
restoreSelectionSnapshot(entrySelection);
|
|
688
|
+
return true;
|
|
689
|
+
};
|
|
690
|
+
const undoGeometry = () => {
|
|
691
|
+
if (!canUseOverviewHistory) return false;
|
|
692
|
+
const entry = geometryUndoStackRef.current.pop();
|
|
693
|
+
if (!entry) return false;
|
|
694
|
+
// Freshness guard: this entry last wrote `entry.after`. If a peer/agent
|
|
695
|
+
// has since moved any of the frames it touched, replaying `entry.before`
|
|
696
|
+
// would silently clobber their change — drop this entry instead. The pop
|
|
697
|
+
// above already removed it, so undo skips forward to the next entry.
|
|
698
|
+
const stale = staleGeometryFrameIds(
|
|
699
|
+
entry,
|
|
700
|
+
liveFrameGeometryRef.current,
|
|
701
|
+
entry.after,
|
|
702
|
+
);
|
|
703
|
+
if (stale.length > 0) {
|
|
704
|
+
console.debug(
|
|
705
|
+
"[design] skipping stale geometry undo; frames changed since capture:",
|
|
706
|
+
stale,
|
|
707
|
+
);
|
|
708
|
+
toast.info(t("designEditor.toasts.undoSkippedConcurrentEdit"));
|
|
709
|
+
// Try the next undo entry rather than swallowing the whole gesture.
|
|
710
|
+
return undoGeometry();
|
|
711
|
+
}
|
|
712
|
+
geometryRedoStackRef.current = [
|
|
713
|
+
...geometryRedoStackRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
|
|
714
|
+
entry,
|
|
715
|
+
];
|
|
716
|
+
redoOrderRef.current = [
|
|
717
|
+
...redoOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
|
|
718
|
+
"geometry",
|
|
719
|
+
];
|
|
720
|
+
// U11: merge only this entry's per-frame diff onto the CURRENT live
|
|
721
|
+
// map (read fresh from the ref) instead of replacing the whole board
|
|
722
|
+
// with the entry's stale whole-board snapshot — otherwise a frame
|
|
723
|
+
// created after this entry was recorded has no key in entry.before
|
|
724
|
+
// and would be wiped out by a full-map replace.
|
|
725
|
+
writeFrameGeometrySnapshot(
|
|
726
|
+
applyGeometryHistoryDiff(
|
|
727
|
+
getCanvasFrameGeometry(designDataJsonRef.current),
|
|
728
|
+
entry,
|
|
729
|
+
"undo",
|
|
730
|
+
),
|
|
731
|
+
{
|
|
732
|
+
syncViewportFrameIds: viewportChangedFrameIds(
|
|
733
|
+
entry.after,
|
|
734
|
+
entry.before,
|
|
735
|
+
),
|
|
736
|
+
},
|
|
737
|
+
);
|
|
738
|
+
// Figma parity: undo re-selects whatever was selected when this
|
|
739
|
+
// gesture's change was originally made.
|
|
740
|
+
restoreSelectionSnapshot(entry.selectionBefore);
|
|
741
|
+
return true;
|
|
742
|
+
};
|
|
743
|
+
// U12: undo a screen create/duplicate by soft-deleting the file it
|
|
744
|
+
// created (performDeleteFiles already prunes any content/geometry undo
|
|
745
|
+
// entries for that file, mirroring U2's screen-deletion cleanup).
|
|
746
|
+
// Resolved by filename at undo time (filenames are unique) since the
|
|
747
|
+
// entry itself doesn't carry the id assigned by the create mutation.
|
|
748
|
+
const undoFileCreation = () => {
|
|
749
|
+
if (!canUseOverviewHistory) return false;
|
|
750
|
+
const entry = fileCreationUndoStackRef.current.pop();
|
|
751
|
+
if (!entry) return false;
|
|
752
|
+
const createdFile = files.find((file) => file.filename === entry.filename);
|
|
753
|
+
if (!createdFile) return false;
|
|
754
|
+
fileCreationRedoStackRef.current = [
|
|
755
|
+
...fileCreationRedoStackRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
|
|
756
|
+
entry,
|
|
757
|
+
];
|
|
758
|
+
redoOrderRef.current = [
|
|
759
|
+
...redoOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
|
|
760
|
+
"file-created",
|
|
761
|
+
];
|
|
762
|
+
// skipFileCreationRedoPrune: the entry was just pushed onto the redo
|
|
763
|
+
// stack above for this exact filename — without this flag
|
|
764
|
+
// performDeleteFiles' filename-keyed redo prune would immediately pop
|
|
765
|
+
// it back off, leaving redo permanently empty after this undo.
|
|
766
|
+
performDeleteFiles([createdFile], { skipFileCreationRedoPrune: true });
|
|
767
|
+
return true;
|
|
768
|
+
};
|
|
769
|
+
const undoFileDeletion = () => {
|
|
770
|
+
if (!canUseOverviewHistory || !id) return false;
|
|
771
|
+
const entry = fileDeletionUndoStackRef.current.pop();
|
|
772
|
+
if (!entry) return false;
|
|
773
|
+
|
|
774
|
+
fileHistoryMutationPendingRef.current = true;
|
|
775
|
+
syncUndoRedoState();
|
|
776
|
+
void (async () => {
|
|
777
|
+
const recreatedIds: string[] = [];
|
|
778
|
+
try {
|
|
779
|
+
for (const file of entry.files) {
|
|
780
|
+
const result = (await createFileMutation.mutateAsync({
|
|
781
|
+
designId: id,
|
|
782
|
+
filename: file.filename,
|
|
783
|
+
content: file.content,
|
|
784
|
+
fileType: file.fileType,
|
|
785
|
+
} as any)) as { id?: string };
|
|
786
|
+
if (!result.id) {
|
|
787
|
+
throw new Error(`Failed to restore "${file.filename}"`);
|
|
788
|
+
}
|
|
789
|
+
recreatedIds.push(result.id);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
const recreatedEntry = remapFileDeletionHistoryEntryIds(
|
|
793
|
+
entry,
|
|
794
|
+
recreatedIds,
|
|
795
|
+
);
|
|
796
|
+
if (recreatedEntry.files.length !== entry.files.length) {
|
|
797
|
+
throw new Error("Failed to restore every deleted screen");
|
|
798
|
+
}
|
|
799
|
+
fileDeletionRedoStackRef.current = [
|
|
800
|
+
...fileDeletionRedoStackRef.current.slice(
|
|
801
|
+
-(MAX_DESIGN_UNDO_STACK - 1),
|
|
802
|
+
),
|
|
803
|
+
recreatedEntry,
|
|
804
|
+
];
|
|
805
|
+
redoOrderRef.current = [
|
|
806
|
+
...redoOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
|
|
807
|
+
"file-deleted",
|
|
808
|
+
];
|
|
809
|
+
|
|
810
|
+
const nextGeometry = {
|
|
811
|
+
...getCanvasFrameGeometry(designDataJsonRef.current),
|
|
812
|
+
};
|
|
813
|
+
recreatedEntry.files.forEach((file, index) => {
|
|
814
|
+
const geometry = entry.files[index]?.geometry;
|
|
815
|
+
if (geometry) nextGeometry[file.id] = geometry;
|
|
816
|
+
});
|
|
817
|
+
writeFrameGeometrySnapshot(nextGeometry);
|
|
818
|
+
queryClient.invalidateQueries({
|
|
819
|
+
queryKey: ["action", "get-design"],
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
const firstRestoredId = recreatedEntry.files[0]?.id;
|
|
823
|
+
if (firstRestoredId) {
|
|
824
|
+
setActiveFileId(firstRestoredId);
|
|
825
|
+
setOverviewSelectedScreenIds(
|
|
826
|
+
recreatedEntry.files.map((file) => file.id),
|
|
827
|
+
);
|
|
828
|
+
setSelectedLayerIdsState(recreatedEntry.files.map((file) => file.id));
|
|
829
|
+
}
|
|
830
|
+
} catch (error) {
|
|
831
|
+
await Promise.allSettled(
|
|
832
|
+
recreatedIds.map((fileId) =>
|
|
833
|
+
deleteFileMutation.mutateAsync({ id: fileId } as any),
|
|
834
|
+
),
|
|
835
|
+
);
|
|
836
|
+
fileDeletionUndoStackRef.current = [
|
|
837
|
+
...fileDeletionUndoStackRef.current.slice(
|
|
838
|
+
-(MAX_DESIGN_UNDO_STACK - 1),
|
|
839
|
+
),
|
|
840
|
+
entry,
|
|
841
|
+
];
|
|
842
|
+
historyOrderRef.current = [
|
|
843
|
+
...historyOrderRef.current.slice(-(MAX_DESIGN_UNDO_STACK - 1)),
|
|
844
|
+
"file-deleted",
|
|
845
|
+
];
|
|
846
|
+
queryClient.invalidateQueries({
|
|
847
|
+
queryKey: ["action", "get-design"],
|
|
848
|
+
});
|
|
849
|
+
toast.error(
|
|
850
|
+
error instanceof Error ? error.message : t("common.genericError"),
|
|
851
|
+
);
|
|
852
|
+
} finally {
|
|
853
|
+
fileHistoryMutationPendingRef.current = false;
|
|
854
|
+
syncUndoRedoState();
|
|
855
|
+
}
|
|
856
|
+
})();
|
|
857
|
+
return true;
|
|
858
|
+
};
|
|
859
|
+
|
|
860
|
+
const undoByOrder = (preferred?: UndoRedoOrderKind) => {
|
|
861
|
+
if (preferred === "file-deleted") {
|
|
862
|
+
return (
|
|
863
|
+
undoFileDeletion() ||
|
|
864
|
+
undoFileCreation() ||
|
|
865
|
+
undoContent() ||
|
|
866
|
+
undoGeometry()
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
if (preferred === "file-created")
|
|
870
|
+
return (
|
|
871
|
+
undoFileCreation() ||
|
|
872
|
+
undoFileDeletion() ||
|
|
873
|
+
undoContent() ||
|
|
874
|
+
undoGeometry()
|
|
875
|
+
);
|
|
876
|
+
if (preferred === "geometry") return undoGeometry() || undoContent();
|
|
877
|
+
if (preferred === "file-content") {
|
|
878
|
+
const prunedBefore = prunedUndoHistory;
|
|
879
|
+
if (undoContent("global")) return true;
|
|
880
|
+
if (prunedUndoHistory > prunedBefore) return false;
|
|
881
|
+
return undoGeometry();
|
|
882
|
+
}
|
|
883
|
+
if (preferred === "content") {
|
|
884
|
+
const prunedBefore = prunedUndoHistory;
|
|
885
|
+
return (
|
|
886
|
+
undoContent("local") ||
|
|
887
|
+
undoContent("global") ||
|
|
888
|
+
(prunedUndoHistory > prunedBefore ? false : undoGeometry())
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
return undoFileDeletion() || undoContent() || undoGeometry();
|
|
892
|
+
};
|
|
893
|
+
let didUndo = false;
|
|
894
|
+
if (canUseOverviewHistory) {
|
|
895
|
+
while (!didUndo) {
|
|
896
|
+
const preferred = historyOrderRef.current.pop();
|
|
897
|
+
didUndo = undoByOrder(preferred);
|
|
898
|
+
if (didUndo || preferred === undefined) break;
|
|
899
|
+
}
|
|
900
|
+
} else {
|
|
901
|
+
didUndo = undoContent("local");
|
|
902
|
+
}
|
|
903
|
+
if (didUndo || prunedUndoHistory > 0) {
|
|
904
|
+
syncUndoRedoState();
|
|
905
|
+
}
|
|
906
|
+
}
|