@agent-native/core 0.80.0 → 0.80.2
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 +2 -2
- package/corpus/core/CHANGELOG.md +28 -0
- package/corpus/core/docs/content/locales/ar-SA/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/ar-SA/template-design.mdx +14 -1
- package/corpus/core/docs/content/locales/de-DE/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/de-DE/template-design.mdx +15 -1
- package/corpus/core/docs/content/locales/es-ES/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/es-ES/template-design.mdx +15 -1
- package/corpus/core/docs/content/locales/fr-FR/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/fr-FR/template-design.mdx +15 -1
- package/corpus/core/docs/content/locales/hi-IN/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/hi-IN/template-design.mdx +15 -1
- package/corpus/core/docs/content/locales/ja-JP/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/ja-JP/template-design.mdx +16 -1
- package/corpus/core/docs/content/locales/ko-KR/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/ko-KR/template-design.mdx +14 -1
- package/corpus/core/docs/content/locales/pt-BR/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/pt-BR/template-design.mdx +14 -1
- package/corpus/core/docs/content/locales/zh-CN/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/zh-CN/template-design.mdx +14 -1
- package/corpus/core/docs/content/locales/zh-TW/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/locales/zh-TW/template-design.mdx +14 -1
- package/corpus/core/docs/content/skills-guide.mdx +5 -0
- package/corpus/core/docs/content/template-design.mdx +14 -1
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/application-state/handlers.ts +1 -1
- package/corpus/core/src/brand-kit/index.ts +1 -1
- package/corpus/core/src/cli/skills.ts +18 -0
- package/corpus/core/src/client/AgentPanel.tsx +1 -1
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +1 -1
- package/corpus/core/src/client/extensions/portable-extension.ts +1 -1
- package/corpus/core/src/code-agents/transcript-normalizer.ts +1 -1
- package/corpus/core/src/extensions/path.ts +1 -4
- package/corpus/core/src/integrations/a2a-continuation-processor.ts +1 -1
- package/corpus/core/src/mcp/build-server.ts +2 -2
- package/corpus/core/src/mcp/embed-app.ts +35 -1
- package/corpus/core/src/org/handlers.ts +5 -5
- package/corpus/core/src/org/plugin.ts +2 -2
- package/corpus/core/src/scripts/call-agent.ts +1 -1
- package/corpus/core/src/scripts/db/safety.ts +1 -1
- package/corpus/core/src/server/auth.ts +3 -0
- package/corpus/core/src/server/builder-design-systems.ts +308 -0
- package/corpus/core/src/server/core-routes-plugin.ts +29 -3
- package/corpus/core/src/server/index.ts +8 -0
- package/corpus/core/src/styles/agent-native.css +14 -7
- package/corpus/core/src/usage/store.ts +2 -2
- package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +298 -15
- package/corpus/templates/analytics/actions/get-sql-dashboard.ts +9 -4
- package/corpus/templates/analytics/actions/mutate-dashboard.ts +18 -2
- package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +12 -2
- package/corpus/templates/analytics/app/hooks/use-replay-storage-status.ts +66 -0
- package/corpus/templates/analytics/app/i18n/zh-TW.ts +37 -0
- package/corpus/templates/analytics/app/i18n-data.ts +404 -0
- package/corpus/templates/analytics/app/pages/Settings.tsx +333 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +2 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +121 -3
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +49 -26
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +137 -36
- package/corpus/templates/analytics/changelog/2026-06-29-analytics-agents-can-now-place-new-charts-into-requested-das.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-analytics-queries-now-report-failed-source-query-errors-inst.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-charts-can-now-be-dropped-between-full-dashboard-rows-to-cre.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-dashboard-email-reports-include-the-full-dashboard-image-wit.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-main-surface-border-reaches-the-top.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-session-replay-now-records-playable-sessions-in-production-a.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-29-set-up-session-replay-storage-from-settings-connect-builder-.md +6 -0
- package/corpus/templates/analytics/scripts/cleanup-empty-session-recordings.ts +102 -0
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +49 -26
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +81 -20
- package/corpus/templates/analytics/server/lib/replay-storage.ts +79 -0
- package/corpus/templates/analytics/server/lib/s3-upload-provider.ts +255 -0
- package/corpus/templates/analytics/server/lib/session-replay.ts +29 -8
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +11 -2
- package/corpus/templates/analytics/server/plugins/onboarding.ts +97 -0
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +24 -0
- package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +105 -5
- package/corpus/templates/content/actions/set-image-alt-text.ts +1 -1
- package/corpus/templates/content/shared/document-export.ts +1 -1
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +8 -18
- package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +17 -0
- package/corpus/templates/design/actions/add-localhost-screens.ts +32 -5
- package/corpus/templates/design/actions/index-design-system-with-builder.ts +259 -0
- package/corpus/templates/design/actions/update-file.ts +9 -2
- package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +33 -75
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +72 -9
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +41 -39
- package/corpus/templates/design/app/components/design/EditPanel.tsx +1029 -249
- package/corpus/templates/design/app/components/design/LayersPanel.tsx +450 -101
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +978 -154
- package/corpus/templates/design/app/components/design/TweaksPanel.tsx +7 -9
- package/corpus/templates/design/app/components/design/inspector/AlignmentMatrix.tsx +133 -66
- package/corpus/templates/design/app/components/design/inspector/AutoLayoutMatrix.tsx +1234 -344
- package/corpus/templates/design/app/components/design/inspector/ConstraintsWidget.tsx +144 -94
- package/corpus/templates/design/app/components/design/inspector/ExportSettingsPanel.tsx +301 -72
- package/corpus/templates/design/app/components/design/inspector/FigmaColorPicker.tsx +876 -286
- package/corpus/templates/design/app/components/design/inspector/GradientEditor.tsx +601 -0
- package/corpus/templates/design/app/components/design/inspector/ImageFillControls.tsx +242 -0
- package/corpus/templates/design/app/components/design/inspector/ScrubInput.tsx +30 -7
- package/corpus/templates/design/app/components/design/inspector/ShaderControls.tsx +212 -155
- package/corpus/templates/design/app/components/design/inspector/ShaderFillsPanel.tsx +423 -0
- package/corpus/templates/design/app/components/design/inspector/figma-icons.tsx +487 -107
- package/corpus/templates/design/app/components/design/inspector/index.ts +27 -0
- package/corpus/templates/design/app/global.css +29 -3
- package/corpus/templates/design/app/i18n/zh-TW.ts +13 -0
- package/corpus/templates/design/app/i18n-data.ts +239 -1
- package/corpus/templates/design/app/pages/DesignEditor.tsx +1023 -353
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +57 -221
- package/corpus/templates/design/changelog/2026-06-29-canvas-selection-no-longer-flashes-broken-prototype-html-whe.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-command-menu-selections-are-clearer-in-dark-mode.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-design-mode-top-chrome-now-groups-collaborators-preview-shar.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-element-selection-now-stays-on-the-intended-layer-after-sele.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-full-view-controls-now-sit-above-frames-inst.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-screen-overview-tools-can-draw-shapes-create-screens-and-kee.md +6 -0
- package/corpus/templates/design/changelog/2026-06-29-the-pen-tool-now-creates-anchor-based-bezier-vectors-with-ha.md +6 -0
- package/corpus/templates/design/server/handlers/import-figma-system.ts +38 -85
- package/corpus/templates/design/server/lib/design-snapshot.ts +9 -1
- package/corpus/templates/design/shared/code-layer.ts +209 -9
- package/corpus/templates/design/shared/html-content.ts +21 -0
- package/corpus/templates/design/shared/pen-path.ts +207 -0
- package/corpus/templates/mail/app/lib/utils.ts +2 -2
- package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +9 -10
- package/corpus/templates/slides/AGENTS.md +3 -2
- package/corpus/templates/slides/actions/import-file.ts +26 -17
- package/corpus/templates/slides/actions/index-design-system-with-builder.ts +259 -0
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +35 -167
- package/corpus/templates/slides/app/components/design-system/fig-import-response.ts +9 -13
- package/corpus/templates/slides/app/context/DeckContext.tsx +1 -1
- package/corpus/templates/slides/app/i18n/ar-SA.ts +9 -37
- package/corpus/templates/slides/app/i18n/de-DE.ts +10 -25
- package/corpus/templates/slides/app/i18n/en-US.ts +9 -26
- package/corpus/templates/slides/app/i18n/es-ES.ts +10 -28
- package/corpus/templates/slides/app/i18n/fr-FR.ts +10 -28
- package/corpus/templates/slides/app/i18n/hi-IN.ts +9 -26
- package/corpus/templates/slides/app/i18n/ja-JP.ts +10 -22
- package/corpus/templates/slides/app/i18n/ko-KR.ts +9 -23
- package/corpus/templates/slides/app/i18n/pt-BR.ts +10 -28
- package/corpus/templates/slides/app/i18n/zh-CN.ts +9 -22
- package/corpus/templates/slides/app/i18n/zh-TW.ts +9 -22
- package/corpus/templates/slides/server/handlers/import-figma-system.ts +44 -12
- package/corpus/templates/slides/server/lib/chat-attachments.ts +1 -1
- package/corpus/templates/videos/app/components/NewCompositionPopover.tsx +1 -1
- package/dist/application-state/handlers.js +1 -1
- package/dist/application-state/handlers.js.map +1 -1
- package/dist/brand-kit/index.d.ts +1 -1
- package/dist/brand-kit/index.js +1 -1
- package/dist/brand-kit/index.js.map +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +18 -0
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/AgentPanel.js +1 -1
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +1 -1
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/extensions/portable-extension.d.ts +1 -1
- package/dist/client/extensions/portable-extension.d.ts.map +1 -1
- package/dist/client/extensions/portable-extension.js.map +1 -1
- package/dist/code-agents/transcript-normalizer.js +1 -1
- package/dist/code-agents/transcript-normalizer.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/extensions/path.d.ts +1 -1
- package/dist/extensions/path.d.ts.map +1 -1
- package/dist/extensions/path.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/integrations/a2a-continuation-processor.js +1 -1
- package/dist/integrations/a2a-continuation-processor.js.map +1 -1
- package/dist/mcp/build-server.d.ts +1 -1
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +1 -1
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/embed-app.d.ts.map +1 -1
- package/dist/mcp/embed-app.js +35 -1
- package/dist/mcp/embed-app.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +8 -8
- package/dist/org/handlers.js +5 -5
- package/dist/org/handlers.js.map +1 -1
- package/dist/org/plugin.js +2 -2
- package/dist/org/plugin.js.map +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/scripts/call-agent.js +1 -1
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/scripts/db/safety.js +1 -1
- package/dist/scripts/db/safety.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +3 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/builder-design-systems.d.ts +28 -0
- package/dist/server/builder-design-systems.d.ts.map +1 -0
- package/dist/server/builder-design-systems.js +217 -0
- package/dist/server/builder-design-systems.js.map +1 -0
- package/dist/server/core-routes-plugin.d.ts +1 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +20 -3
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/styles/agent-native.css +14 -7
- package/dist/usage/store.js +2 -2
- package/dist/usage/store.js.map +1 -1
- package/docs/content/locales/ar-SA/skills-guide.mdx +5 -0
- package/docs/content/locales/ar-SA/template-design.mdx +14 -1
- package/docs/content/locales/de-DE/skills-guide.mdx +5 -0
- package/docs/content/locales/de-DE/template-design.mdx +15 -1
- package/docs/content/locales/es-ES/skills-guide.mdx +5 -0
- package/docs/content/locales/es-ES/template-design.mdx +15 -1
- package/docs/content/locales/fr-FR/skills-guide.mdx +5 -0
- package/docs/content/locales/fr-FR/template-design.mdx +15 -1
- package/docs/content/locales/hi-IN/skills-guide.mdx +5 -0
- package/docs/content/locales/hi-IN/template-design.mdx +15 -1
- package/docs/content/locales/ja-JP/skills-guide.mdx +5 -0
- package/docs/content/locales/ja-JP/template-design.mdx +16 -1
- package/docs/content/locales/ko-KR/skills-guide.mdx +5 -0
- package/docs/content/locales/ko-KR/template-design.mdx +14 -1
- package/docs/content/locales/pt-BR/skills-guide.mdx +5 -0
- package/docs/content/locales/pt-BR/template-design.mdx +14 -1
- package/docs/content/locales/zh-CN/skills-guide.mdx +5 -0
- package/docs/content/locales/zh-CN/template-design.mdx +14 -1
- package/docs/content/locales/zh-TW/skills-guide.mdx +5 -0
- package/docs/content/locales/zh-TW/template-design.mdx +14 -1
- package/docs/content/skills-guide.mdx +5 -0
- package/docs/content/template-design.mdx +14 -1
- package/package.json +1 -1
- package/corpus/templates/design/actions/import-figma-file.ts +0 -166
- package/corpus/templates/slides/server/lib/fig-design-system.ts +0 -110
|
@@ -5,11 +5,14 @@ import {
|
|
|
5
5
|
useCollaborativeDoc,
|
|
6
6
|
isReconcileLeadClient,
|
|
7
7
|
generateTabId,
|
|
8
|
+
dedupeCollabUsersByEmail,
|
|
8
9
|
emailToColor,
|
|
9
10
|
emailToName,
|
|
10
11
|
PresenceBar,
|
|
11
12
|
AgentToggleButton,
|
|
12
13
|
ShareButton,
|
|
14
|
+
agentNativePath,
|
|
15
|
+
ensureEmbedAuthFetchInterceptor,
|
|
13
16
|
isEmbedAuthActive,
|
|
14
17
|
sendToAgentChat,
|
|
15
18
|
getBrowserTabId,
|
|
@@ -22,13 +25,13 @@ import {
|
|
|
22
25
|
useT,
|
|
23
26
|
useChangeVersion,
|
|
24
27
|
useAgentChatContext,
|
|
28
|
+
useAvatarUrl,
|
|
25
29
|
type CollabUser,
|
|
26
30
|
type PromptComposerSubmitOptions,
|
|
27
31
|
} from "@agent-native/core/client";
|
|
28
32
|
import type { TweakDefinition } from "@shared/api";
|
|
29
33
|
import {
|
|
30
34
|
parseCanvasFrameGeometryById,
|
|
31
|
-
type CanvasFrameGeometry,
|
|
32
35
|
type CanvasFrameGeometryById,
|
|
33
36
|
} from "@shared/canvas-frames";
|
|
34
37
|
import {
|
|
@@ -40,12 +43,14 @@ import {
|
|
|
40
43
|
type CodeLayerNode,
|
|
41
44
|
type CodeLayerTreeNode,
|
|
42
45
|
} from "@shared/code-layer";
|
|
46
|
+
import { shouldUseLiveFileContent } from "@shared/html-content";
|
|
43
47
|
import {
|
|
44
48
|
resolveTweaksToCssVars,
|
|
45
49
|
type TweakSelections,
|
|
46
50
|
} from "@shared/resolve-tweaks";
|
|
47
51
|
import {
|
|
48
52
|
IconArrowLeft,
|
|
53
|
+
IconArrowUpRight,
|
|
49
54
|
IconPencil,
|
|
50
55
|
IconMessage,
|
|
51
56
|
IconBrush,
|
|
@@ -57,6 +62,7 @@ import {
|
|
|
57
62
|
IconViewportWide,
|
|
58
63
|
IconPlus,
|
|
59
64
|
IconLayoutGrid,
|
|
65
|
+
IconFrame,
|
|
60
66
|
IconX,
|
|
61
67
|
IconPin,
|
|
62
68
|
IconCode,
|
|
@@ -69,12 +75,19 @@ import {
|
|
|
69
75
|
IconTypography,
|
|
70
76
|
IconHandStop,
|
|
71
77
|
IconSquare,
|
|
78
|
+
IconLine,
|
|
79
|
+
IconCircle,
|
|
80
|
+
IconTriangle,
|
|
81
|
+
IconStar,
|
|
82
|
+
IconPhotoVideo,
|
|
72
83
|
IconVectorBezier,
|
|
73
84
|
IconScale,
|
|
74
85
|
IconScribble,
|
|
75
|
-
|
|
86
|
+
IconHandClick,
|
|
87
|
+
IconTransformPoint,
|
|
76
88
|
IconDownload,
|
|
77
89
|
IconFileExport,
|
|
90
|
+
IconPlayerPlay,
|
|
78
91
|
} from "@tabler/icons-react";
|
|
79
92
|
import { useQueryClient } from "@tanstack/react-query";
|
|
80
93
|
import {
|
|
@@ -121,11 +134,13 @@ import { VariantGrid } from "@/components/design/VariantGrid";
|
|
|
121
134
|
import { VariantHandoffCard } from "@/components/design/VariantHandoffCard";
|
|
122
135
|
import PromptPopover from "@/components/editor/PromptDialog";
|
|
123
136
|
import type { UploadedFile } from "@/components/editor/PromptDialog";
|
|
137
|
+
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
124
138
|
import { Button } from "@/components/ui/button";
|
|
125
139
|
import {
|
|
126
140
|
DropdownMenu,
|
|
127
141
|
DropdownMenuContent,
|
|
128
142
|
DropdownMenuItem,
|
|
143
|
+
DropdownMenuLabel,
|
|
129
144
|
DropdownMenuRadioGroup,
|
|
130
145
|
DropdownMenuRadioItem,
|
|
131
146
|
DropdownMenuSeparator,
|
|
@@ -187,22 +202,60 @@ const STORED_RUN_LIVENESS_GRACE_MS = 20_000;
|
|
|
187
202
|
const MAX_DESIGN_UNDO_STACK = 50;
|
|
188
203
|
const OVERVIEW_ZOOM_THRESHOLD = 60;
|
|
189
204
|
const FOCUSED_SCREEN_ZOOM = 100;
|
|
205
|
+
const KEEPALIVE_FILE_SAVE_MAX_BYTES = 60_000;
|
|
190
206
|
// Higher than the toolbar presets so overview zooming still feels like canvas
|
|
191
207
|
// work; trackpad/pinch zooming past this commits to editing that screen.
|
|
192
208
|
const OVERVIEW_EDIT_ZOOM_THRESHOLD = 250;
|
|
193
209
|
|
|
210
|
+
interface FileContentSaveRequest {
|
|
211
|
+
id: string;
|
|
212
|
+
content: string;
|
|
213
|
+
syncCollab: boolean;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function byteLength(value: string): number {
|
|
217
|
+
if (typeof TextEncoder === "undefined") return value.length;
|
|
218
|
+
return new TextEncoder().encode(value).length;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function sendFileContentSaveKeepalive(pending: FileContentSaveRequest): void {
|
|
222
|
+
if (typeof window === "undefined") return;
|
|
223
|
+
const body = JSON.stringify({
|
|
224
|
+
id: pending.id,
|
|
225
|
+
content: pending.content,
|
|
226
|
+
syncCollab: pending.syncCollab,
|
|
227
|
+
});
|
|
228
|
+
if (byteLength(body) > KEEPALIVE_FILE_SAVE_MAX_BYTES) return;
|
|
229
|
+
ensureEmbedAuthFetchInterceptor();
|
|
230
|
+
void fetch(agentNativePath("/_agent-native/actions/update-file"), {
|
|
231
|
+
method: "POST",
|
|
232
|
+
headers: {
|
|
233
|
+
"Content-Type": "application/json",
|
|
234
|
+
"X-Agent-Native-Frontend": "1",
|
|
235
|
+
},
|
|
236
|
+
body,
|
|
237
|
+
cache: "no-store",
|
|
238
|
+
keepalive: true,
|
|
239
|
+
}).catch(() => {});
|
|
240
|
+
}
|
|
241
|
+
|
|
194
242
|
type EditorMode = "annotate" | "edit" | "interact";
|
|
195
243
|
type DesignTool =
|
|
196
244
|
| "move"
|
|
197
245
|
| "frame"
|
|
198
246
|
| "rect"
|
|
247
|
+
| "line"
|
|
248
|
+
| "arrow"
|
|
249
|
+
| "ellipse"
|
|
250
|
+
| "polygon"
|
|
251
|
+
| "star"
|
|
199
252
|
| "text"
|
|
200
253
|
| "pen"
|
|
201
254
|
| "hand"
|
|
202
255
|
| "comment"
|
|
203
256
|
| "draw"
|
|
204
|
-
| "scale"
|
|
205
|
-
|
|
257
|
+
| "scale";
|
|
258
|
+
type ShapeTool = "rect" | "line" | "arrow" | "ellipse" | "polygon" | "star";
|
|
206
259
|
|
|
207
260
|
interface DesignFile {
|
|
208
261
|
id: string;
|
|
@@ -422,7 +475,7 @@ function applyInlineStylesToHtml(
|
|
|
422
475
|
if (typeof window === "undefined") return null;
|
|
423
476
|
try {
|
|
424
477
|
const doc = new DOMParser().parseFromString(content, "text/html");
|
|
425
|
-
const element = doc
|
|
478
|
+
const element = queryUniqueSelector(doc, selector) as HTMLElement | null;
|
|
426
479
|
if (!element) return null;
|
|
427
480
|
Object.entries(styles).forEach(([property, value]) => {
|
|
428
481
|
(element.style as any)[property] = value;
|
|
@@ -576,6 +629,16 @@ function primitiveLayerName(primitive: CanvasPrimitiveInsert): string {
|
|
|
576
629
|
switch (primitive.kind) {
|
|
577
630
|
case "frame":
|
|
578
631
|
return "Frame";
|
|
632
|
+
case "line":
|
|
633
|
+
return "Line";
|
|
634
|
+
case "arrow":
|
|
635
|
+
return "Arrow";
|
|
636
|
+
case "ellipse":
|
|
637
|
+
return "Ellipse";
|
|
638
|
+
case "polygon":
|
|
639
|
+
return "Polygon";
|
|
640
|
+
case "star":
|
|
641
|
+
return "Star";
|
|
579
642
|
case "path":
|
|
580
643
|
return "Vector";
|
|
581
644
|
case "text":
|
|
@@ -602,9 +665,14 @@ function appendCanvasPrimitiveToHtml(
|
|
|
602
665
|
const nodeId = uniqueLayerId(primitive.kind);
|
|
603
666
|
const layerName = primitiveLayerName(primitive);
|
|
604
667
|
|
|
605
|
-
if (
|
|
668
|
+
if (
|
|
669
|
+
primitive.kind === "path" ||
|
|
670
|
+
primitive.kind === "line" ||
|
|
671
|
+
primitive.kind === "arrow"
|
|
672
|
+
) {
|
|
606
673
|
const svg = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
607
674
|
const path = doc.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
675
|
+
const markerId = `${nodeId}-arrow`;
|
|
608
676
|
const points = primitive.points?.length
|
|
609
677
|
? primitive.points
|
|
610
678
|
: [
|
|
@@ -615,14 +683,15 @@ function appendCanvasPrimitiveToHtml(
|
|
|
615
683
|
const originY = Math.min(...points.map((point) => point.y));
|
|
616
684
|
path.setAttribute(
|
|
617
685
|
"d",
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
point.
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
686
|
+
primitive.pathData ??
|
|
687
|
+
points
|
|
688
|
+
.map((point, index) => {
|
|
689
|
+
const command = index === 0 ? "M" : "L";
|
|
690
|
+
return `${command} ${Math.round(point.x - originX)} ${Math.round(
|
|
691
|
+
point.y - originY,
|
|
692
|
+
)}`;
|
|
693
|
+
})
|
|
694
|
+
.join(" "),
|
|
626
695
|
);
|
|
627
696
|
path.setAttribute("fill", "none");
|
|
628
697
|
path.setAttribute(
|
|
@@ -632,6 +701,33 @@ function appendCanvasPrimitiveToHtml(
|
|
|
632
701
|
path.setAttribute("stroke-width", String(primitive.strokeWidth ?? 3));
|
|
633
702
|
path.setAttribute("stroke-linecap", "round");
|
|
634
703
|
path.setAttribute("stroke-linejoin", "round");
|
|
704
|
+
if (primitive.kind === "arrow") {
|
|
705
|
+
const defs = doc.createElementNS("http://www.w3.org/2000/svg", "defs");
|
|
706
|
+
const marker = doc.createElementNS(
|
|
707
|
+
"http://www.w3.org/2000/svg",
|
|
708
|
+
"marker",
|
|
709
|
+
);
|
|
710
|
+
const arrowHead = doc.createElementNS(
|
|
711
|
+
"http://www.w3.org/2000/svg",
|
|
712
|
+
"path",
|
|
713
|
+
);
|
|
714
|
+
marker.setAttribute("id", markerId);
|
|
715
|
+
marker.setAttribute("markerWidth", "10");
|
|
716
|
+
marker.setAttribute("markerHeight", "10");
|
|
717
|
+
marker.setAttribute("refX", "8");
|
|
718
|
+
marker.setAttribute("refY", "5");
|
|
719
|
+
marker.setAttribute("orient", "auto");
|
|
720
|
+
marker.setAttribute("markerUnits", "strokeWidth");
|
|
721
|
+
arrowHead.setAttribute("d", "M 0 0 L 10 5 L 0 10 z");
|
|
722
|
+
arrowHead.setAttribute(
|
|
723
|
+
"fill",
|
|
724
|
+
primitive.stroke ?? "var(--primary, #2563eb)",
|
|
725
|
+
);
|
|
726
|
+
marker.appendChild(arrowHead);
|
|
727
|
+
defs.appendChild(marker);
|
|
728
|
+
svg.appendChild(defs);
|
|
729
|
+
path.setAttribute("marker-end", `url(#${markerId})`);
|
|
730
|
+
}
|
|
635
731
|
svg.setAttribute("data-agent-native-node-id", nodeId);
|
|
636
732
|
svg.setAttribute("data-agent-native-layer-name", layerName);
|
|
637
733
|
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
|
@@ -654,14 +750,55 @@ function appendCanvasPrimitiveToHtml(
|
|
|
654
750
|
return `<!DOCTYPE html>\n${doc.documentElement.outerHTML}`;
|
|
655
751
|
}
|
|
656
752
|
|
|
753
|
+
if (primitive.kind === "polygon" || primitive.kind === "star") {
|
|
754
|
+
const svg = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
755
|
+
const polygon = doc.createElementNS(
|
|
756
|
+
"http://www.w3.org/2000/svg",
|
|
757
|
+
"polygon",
|
|
758
|
+
);
|
|
759
|
+
polygon.setAttribute(
|
|
760
|
+
"points",
|
|
761
|
+
polygonPointsForHtmlShape(primitive.kind, width, height),
|
|
762
|
+
);
|
|
763
|
+
polygon.setAttribute("fill", primitive.fill ?? "rgba(37, 99, 235, 0.16)");
|
|
764
|
+
polygon.setAttribute("stroke", primitive.stroke ?? "rgb(37, 99, 235)");
|
|
765
|
+
polygon.setAttribute(
|
|
766
|
+
"stroke-width",
|
|
767
|
+
String(primitive.strokeWidth ?? 1.5),
|
|
768
|
+
);
|
|
769
|
+
polygon.setAttribute("stroke-linejoin", "round");
|
|
770
|
+
svg.setAttribute("data-agent-native-node-id", nodeId);
|
|
771
|
+
svg.setAttribute("data-agent-native-layer-name", layerName);
|
|
772
|
+
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
|
773
|
+
svg.setAttribute(
|
|
774
|
+
"style",
|
|
775
|
+
[
|
|
776
|
+
"position:absolute",
|
|
777
|
+
`left:${left}px`,
|
|
778
|
+
`top:${top}px`,
|
|
779
|
+
`width:${width}px`,
|
|
780
|
+
`height:${height}px`,
|
|
781
|
+
"overflow:visible",
|
|
782
|
+
geometry.rotation ? `transform:rotate(${geometry.rotation}deg)` : "",
|
|
783
|
+
]
|
|
784
|
+
.filter(Boolean)
|
|
785
|
+
.join(";"),
|
|
786
|
+
);
|
|
787
|
+
svg.appendChild(polygon);
|
|
788
|
+
doc.body.appendChild(svg);
|
|
789
|
+
return `<!DOCTYPE html>\n${doc.documentElement.outerHTML}`;
|
|
790
|
+
}
|
|
791
|
+
|
|
657
792
|
const element = doc.createElement("div");
|
|
658
793
|
element.setAttribute("data-agent-native-node-id", nodeId);
|
|
659
794
|
element.setAttribute("data-agent-native-layer-name", layerName);
|
|
660
795
|
element.style.position = "absolute";
|
|
661
796
|
element.style.left = `${left}px`;
|
|
662
797
|
element.style.top = `${top}px`;
|
|
663
|
-
|
|
664
|
-
|
|
798
|
+
if (!(primitive.kind === "text" && primitive.autoSize)) {
|
|
799
|
+
element.style.width = `${width}px`;
|
|
800
|
+
element.style.height = `${height}px`;
|
|
801
|
+
}
|
|
665
802
|
if (geometry.rotation) {
|
|
666
803
|
element.style.transform = `rotate(${geometry.rotation}deg)`;
|
|
667
804
|
}
|
|
@@ -675,12 +812,20 @@ function appendCanvasPrimitiveToHtml(
|
|
|
675
812
|
element.style.overflow = "hidden";
|
|
676
813
|
} else if (primitive.kind === "text") {
|
|
677
814
|
element.textContent = primitive.text ?? "Text";
|
|
678
|
-
element.style.display = "flex";
|
|
679
|
-
|
|
815
|
+
element.style.display = primitive.autoSize ? "inline-block" : "flex";
|
|
816
|
+
if (!primitive.autoSize) {
|
|
817
|
+
element.style.alignItems = "center";
|
|
818
|
+
}
|
|
680
819
|
element.style.color = primitive.fill ?? "currentColor";
|
|
681
820
|
element.style.fontSize = "16px";
|
|
682
821
|
element.style.lineHeight = "1.2";
|
|
683
822
|
element.style.whiteSpace = "pre-wrap";
|
|
823
|
+
} else if (primitive.kind === "ellipse") {
|
|
824
|
+
element.style.background = primitive.fill ?? "rgba(37, 99, 235, 0.16)";
|
|
825
|
+
element.style.border = `${primitive.strokeWidth ?? 1}px solid ${
|
|
826
|
+
primitive.stroke ?? "rgb(37, 99, 235)"
|
|
827
|
+
}`;
|
|
828
|
+
element.style.borderRadius = "9999px";
|
|
684
829
|
} else {
|
|
685
830
|
element.style.background = primitive.fill ?? "rgba(37, 99, 235, 0.16)";
|
|
686
831
|
element.style.border = `${primitive.strokeWidth ?? 1}px solid ${
|
|
@@ -696,6 +841,45 @@ function appendCanvasPrimitiveToHtml(
|
|
|
696
841
|
}
|
|
697
842
|
}
|
|
698
843
|
|
|
844
|
+
function polygonPointsForHtmlShape(
|
|
845
|
+
kind: "polygon" | "star",
|
|
846
|
+
width: number,
|
|
847
|
+
height: number,
|
|
848
|
+
): string {
|
|
849
|
+
const safeWidth = Math.max(1, width);
|
|
850
|
+
const safeHeight = Math.max(1, height);
|
|
851
|
+
const cx = safeWidth / 2;
|
|
852
|
+
const cy = safeHeight / 2;
|
|
853
|
+
const radius = Math.max(1, Math.min(safeWidth, safeHeight) / 2);
|
|
854
|
+
const points: Array<{ x: number; y: number }> = [];
|
|
855
|
+
|
|
856
|
+
if (kind === "polygon") {
|
|
857
|
+
for (let index = 0; index < 3; index += 1) {
|
|
858
|
+
const angle = -Math.PI / 2 + (index * Math.PI * 2) / 3;
|
|
859
|
+
points.push({
|
|
860
|
+
x: cx + Math.cos(angle) * radius,
|
|
861
|
+
y: cy + Math.sin(angle) * radius,
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
} else {
|
|
865
|
+
for (let index = 0; index < 10; index += 1) {
|
|
866
|
+
const angle = -Math.PI / 2 + (index * Math.PI) / 5;
|
|
867
|
+
const pointRadius = index % 2 === 0 ? radius : radius * 0.45;
|
|
868
|
+
points.push({
|
|
869
|
+
x: cx + Math.cos(angle) * pointRadius,
|
|
870
|
+
y: cy + Math.sin(angle) * pointRadius,
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
return points
|
|
876
|
+
.map(
|
|
877
|
+
(point) =>
|
|
878
|
+
`${Math.round(point.x * 10) / 10},${Math.round(point.y * 10) / 10}`,
|
|
879
|
+
)
|
|
880
|
+
.join(" ");
|
|
881
|
+
}
|
|
882
|
+
|
|
699
883
|
function cloneHtmlLayerAtPosition(
|
|
700
884
|
content: string,
|
|
701
885
|
layerHtml: string,
|
|
@@ -758,6 +942,18 @@ function queryFirstSelector(
|
|
|
758
942
|
return null;
|
|
759
943
|
}
|
|
760
944
|
|
|
945
|
+
function queryUniqueSelector(
|
|
946
|
+
root: ParentNode,
|
|
947
|
+
selector: string,
|
|
948
|
+
): Element | null {
|
|
949
|
+
try {
|
|
950
|
+
const matches = root.querySelectorAll(selector);
|
|
951
|
+
return matches.length === 1 ? (matches[0] ?? null) : null;
|
|
952
|
+
} catch {
|
|
953
|
+
return null;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
761
957
|
function insertClonedHtmlLayer(
|
|
762
958
|
content: string,
|
|
763
959
|
cloneHtml: string,
|
|
@@ -819,7 +1015,7 @@ function getElementOuterHtml(content: string, selector: string): string | null {
|
|
|
819
1015
|
if (typeof window === "undefined") return null;
|
|
820
1016
|
try {
|
|
821
1017
|
const doc = new DOMParser().parseFromString(content, "text/html");
|
|
822
|
-
return doc
|
|
1018
|
+
return queryUniqueSelector(doc, selector)?.outerHTML ?? null;
|
|
823
1019
|
} catch {
|
|
824
1020
|
return null;
|
|
825
1021
|
}
|
|
@@ -832,7 +1028,7 @@ function removeElementFromHtml(
|
|
|
832
1028
|
if (typeof window === "undefined") return null;
|
|
833
1029
|
try {
|
|
834
1030
|
const doc = new DOMParser().parseFromString(content, "text/html");
|
|
835
|
-
const element = doc
|
|
1031
|
+
const element = queryUniqueSelector(doc, selector);
|
|
836
1032
|
if (!element) return null;
|
|
837
1033
|
element.remove();
|
|
838
1034
|
return `<!DOCTYPE html>\n${doc.documentElement.outerHTML}`;
|
|
@@ -892,7 +1088,7 @@ function updateElementContentInHtml(
|
|
|
892
1088
|
if (typeof window === "undefined") return null;
|
|
893
1089
|
try {
|
|
894
1090
|
const doc = new DOMParser().parseFromString(content, "text/html");
|
|
895
|
-
const element = doc
|
|
1091
|
+
const element = queryUniqueSelector(doc, selector);
|
|
896
1092
|
if (!element) return null;
|
|
897
1093
|
if (html !== undefined) {
|
|
898
1094
|
element.innerHTML = sanitizeEditableInnerHtml(html);
|
|
@@ -959,12 +1155,15 @@ function codeLayerSelectorMatches(
|
|
|
959
1155
|
): boolean {
|
|
960
1156
|
if (!node || !selector) return false;
|
|
961
1157
|
const target = normalizeCodeLayerSelector(selector);
|
|
1158
|
+
const targetHasDirectPath = target.includes(" > ");
|
|
962
1159
|
return codeLayerSelectorAliases(node).some((candidate) => {
|
|
963
1160
|
const normalized = normalizeCodeLayerSelector(candidate);
|
|
964
1161
|
return (
|
|
965
1162
|
normalized === target ||
|
|
966
1163
|
normalized.endsWith(` > ${target}`) ||
|
|
967
|
-
|
|
1164
|
+
(targetHasDirectPath &&
|
|
1165
|
+
normalized.includes(" > ") &&
|
|
1166
|
+
target.endsWith(` > ${normalized}`))
|
|
968
1167
|
);
|
|
969
1168
|
});
|
|
970
1169
|
}
|
|
@@ -985,6 +1184,7 @@ function codeLayerTreeToPanelNodes(
|
|
|
985
1184
|
id: node.id,
|
|
986
1185
|
name: node.name,
|
|
987
1186
|
type: layerTypeForCodeLayer(node),
|
|
1187
|
+
layout: node.layout,
|
|
988
1188
|
detail: node.detail,
|
|
989
1189
|
badge: node.badge,
|
|
990
1190
|
selectable: !locked && !hidden,
|
|
@@ -1070,6 +1270,30 @@ function resolveCodeLayerNodeFromBridge(
|
|
|
1070
1270
|
);
|
|
1071
1271
|
}
|
|
1072
1272
|
|
|
1273
|
+
function elementInfoExistsInContent(
|
|
1274
|
+
content: string,
|
|
1275
|
+
info: ElementInfo | null,
|
|
1276
|
+
): boolean {
|
|
1277
|
+
if (!info) return false;
|
|
1278
|
+
const projection = buildCodeLayerProjection(content);
|
|
1279
|
+
if (
|
|
1280
|
+
resolveCodeLayerNodeFromBridge(
|
|
1281
|
+
projection,
|
|
1282
|
+
info.selector,
|
|
1283
|
+
info.sourceId ?? info.id,
|
|
1284
|
+
)
|
|
1285
|
+
) {
|
|
1286
|
+
return true;
|
|
1287
|
+
}
|
|
1288
|
+
if (!info.selector || typeof window === "undefined") return false;
|
|
1289
|
+
try {
|
|
1290
|
+
const doc = new DOMParser().parseFromString(content, "text/html");
|
|
1291
|
+
return Boolean(queryUniqueSelector(doc, info.selector));
|
|
1292
|
+
} catch {
|
|
1293
|
+
return false;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1073
1297
|
function collectCodeLayerAncestors(
|
|
1074
1298
|
nodes: CodeLayerTreeNode[],
|
|
1075
1299
|
targetId: string,
|
|
@@ -1112,6 +1336,147 @@ function AgentNativeMenuMark({ className }: { className?: string }) {
|
|
|
1112
1336
|
);
|
|
1113
1337
|
}
|
|
1114
1338
|
|
|
1339
|
+
interface DesignCollaborator {
|
|
1340
|
+
user: CollabUser;
|
|
1341
|
+
image?: string;
|
|
1342
|
+
isCurrent?: boolean;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
function userInitial(nameOrEmail: string): string {
|
|
1346
|
+
const trimmed = nameOrEmail.trim();
|
|
1347
|
+
if (!trimmed) return "?";
|
|
1348
|
+
return trimmed.charAt(0).toUpperCase();
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function userColor(user: CollabUser): string {
|
|
1352
|
+
return user.color || emailToColor(user.email);
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
function DesignCollaboratorAvatar({
|
|
1356
|
+
collaborator,
|
|
1357
|
+
className,
|
|
1358
|
+
}: {
|
|
1359
|
+
collaborator: DesignCollaborator;
|
|
1360
|
+
className?: string;
|
|
1361
|
+
}) {
|
|
1362
|
+
const label = collaborator.user.name || emailToName(collaborator.user.email);
|
|
1363
|
+
const storedAvatarUrl = useAvatarUrl(collaborator.user.email);
|
|
1364
|
+
const avatarUrl = storedAvatarUrl ?? collaborator.image;
|
|
1365
|
+
|
|
1366
|
+
return (
|
|
1367
|
+
<Avatar
|
|
1368
|
+
className={cn(
|
|
1369
|
+
"size-7 border-2 border-[var(--design-editor-panel-bg)] shadow-sm",
|
|
1370
|
+
className,
|
|
1371
|
+
)}
|
|
1372
|
+
>
|
|
1373
|
+
{avatarUrl ? <AvatarImage src={avatarUrl} alt={label} /> : null}
|
|
1374
|
+
<AvatarFallback
|
|
1375
|
+
className="text-[10px] font-semibold text-white"
|
|
1376
|
+
style={{ backgroundColor: userColor(collaborator.user) }}
|
|
1377
|
+
>
|
|
1378
|
+
{userInitial(label || collaborator.user.email)}
|
|
1379
|
+
</AvatarFallback>
|
|
1380
|
+
</Avatar>
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
function DesignCollaboratorsMenu({
|
|
1385
|
+
collaborators,
|
|
1386
|
+
followingEmail,
|
|
1387
|
+
label,
|
|
1388
|
+
onAvatarClick,
|
|
1389
|
+
}: {
|
|
1390
|
+
collaborators: DesignCollaborator[];
|
|
1391
|
+
followingEmail?: string | null;
|
|
1392
|
+
label: string;
|
|
1393
|
+
onAvatarClick?: (user: CollabUser | null) => void;
|
|
1394
|
+
}) {
|
|
1395
|
+
if (collaborators.length === 0) return null;
|
|
1396
|
+
|
|
1397
|
+
const visibleCollaborators = collaborators.slice(0, 3);
|
|
1398
|
+
const hasMultipleCollaborators = collaborators.length > 1;
|
|
1399
|
+
const followingLower = followingEmail?.trim().toLowerCase() ?? null;
|
|
1400
|
+
|
|
1401
|
+
return (
|
|
1402
|
+
<DropdownMenu>
|
|
1403
|
+
<DropdownMenuTrigger asChild>
|
|
1404
|
+
<button
|
|
1405
|
+
type="button"
|
|
1406
|
+
className="flex h-8 min-w-0 cursor-pointer items-center rounded-md pr-1 text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground"
|
|
1407
|
+
aria-label={label}
|
|
1408
|
+
>
|
|
1409
|
+
<span className="flex items-center">
|
|
1410
|
+
{visibleCollaborators.map((collaborator, index) => (
|
|
1411
|
+
<DesignCollaboratorAvatar
|
|
1412
|
+
key={`${collaborator.user.email}:${index}`}
|
|
1413
|
+
collaborator={collaborator}
|
|
1414
|
+
className={index === 0 ? undefined : "-ml-2"}
|
|
1415
|
+
/>
|
|
1416
|
+
))}
|
|
1417
|
+
</span>
|
|
1418
|
+
{hasMultipleCollaborators ? (
|
|
1419
|
+
<IconChevronDown className="ml-0.5 size-3 opacity-70" />
|
|
1420
|
+
) : null}
|
|
1421
|
+
</button>
|
|
1422
|
+
</DropdownMenuTrigger>
|
|
1423
|
+
<DropdownMenuContent align="start" className="w-64">
|
|
1424
|
+
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
|
1425
|
+
{label}
|
|
1426
|
+
</DropdownMenuLabel>
|
|
1427
|
+
{collaborators.map((collaborator) => {
|
|
1428
|
+
const user = collaborator.user;
|
|
1429
|
+
const email = user.email.trim().toLowerCase();
|
|
1430
|
+
const isFollowing =
|
|
1431
|
+
followingLower != null && email === followingLower;
|
|
1432
|
+
const name = user.name || emailToName(user.email);
|
|
1433
|
+
|
|
1434
|
+
return (
|
|
1435
|
+
<DropdownMenuItem
|
|
1436
|
+
key={user.email}
|
|
1437
|
+
onSelect={() => {
|
|
1438
|
+
if (!collaborator.isCurrent) onAvatarClick?.(user);
|
|
1439
|
+
}}
|
|
1440
|
+
className="gap-2"
|
|
1441
|
+
>
|
|
1442
|
+
<DesignCollaboratorAvatar collaborator={collaborator} />
|
|
1443
|
+
<span className="min-w-0 flex-1">
|
|
1444
|
+
<span className="block truncate text-sm font-medium">
|
|
1445
|
+
{name}
|
|
1446
|
+
</span>
|
|
1447
|
+
<span className="block truncate text-xs text-muted-foreground">
|
|
1448
|
+
{user.email}
|
|
1449
|
+
</span>
|
|
1450
|
+
</span>
|
|
1451
|
+
{isFollowing ? (
|
|
1452
|
+
<IconCheck className="size-3.5 text-[var(--design-editor-accent-color)]" />
|
|
1453
|
+
) : null}
|
|
1454
|
+
</DropdownMenuItem>
|
|
1455
|
+
);
|
|
1456
|
+
})}
|
|
1457
|
+
</DropdownMenuContent>
|
|
1458
|
+
</DropdownMenu>
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
function externalPreviewUrlForContent(content: string): string | null {
|
|
1463
|
+
const trimmed = content.trim();
|
|
1464
|
+
if (!/^https?:\/\//i.test(trimmed)) return null;
|
|
1465
|
+
try {
|
|
1466
|
+
const url = new URL(trimmed);
|
|
1467
|
+
url.hash = "";
|
|
1468
|
+
return url.toString();
|
|
1469
|
+
} catch {
|
|
1470
|
+
return null;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
function fullPreviewHtml(content: string): string {
|
|
1475
|
+
const trimmed = content.trim();
|
|
1476
|
+
if (/<!doctype html|<html[\s>]/i.test(trimmed)) return content;
|
|
1477
|
+
return `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head><body>${content}</body></html>`;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1115
1480
|
type FigmaToolbarOption = {
|
|
1116
1481
|
key: string;
|
|
1117
1482
|
label: string;
|
|
@@ -1135,6 +1500,7 @@ function FigmaToolbarTool({
|
|
|
1135
1500
|
options: FigmaToolbarOption[];
|
|
1136
1501
|
onPrimary: () => void;
|
|
1137
1502
|
}) {
|
|
1503
|
+
const hasOptionsMenu = options.length > 1;
|
|
1138
1504
|
return (
|
|
1139
1505
|
<div
|
|
1140
1506
|
className={cn(
|
|
@@ -1159,45 +1525,51 @@ function FigmaToolbarTool({
|
|
|
1159
1525
|
<TooltipContent side="top">{label}</TooltipContent>
|
|
1160
1526
|
</Tooltip>
|
|
1161
1527
|
|
|
1162
|
-
|
|
1163
|
-
<
|
|
1164
|
-
<
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
aria-label={`${label} options`}
|
|
1171
|
-
>
|
|
1172
|
-
<IconChevronDown className="size-3" />
|
|
1173
|
-
</button>
|
|
1174
|
-
</DropdownMenuTrigger>
|
|
1175
|
-
<DropdownMenuContent
|
|
1176
|
-
side="top"
|
|
1177
|
-
align="center"
|
|
1178
|
-
sideOffset={12}
|
|
1179
|
-
className="w-56 rounded-2xl border-white/10 bg-neutral-950 p-2 text-neutral-50 shadow-[0_24px_70px_-24px_rgba(0,0,0,0.9)]"
|
|
1180
|
-
>
|
|
1181
|
-
{options.map((option) => (
|
|
1182
|
-
<DropdownMenuItem
|
|
1183
|
-
key={option.key}
|
|
1184
|
-
disabled={option.disabled}
|
|
1185
|
-
onSelect={option.onSelect}
|
|
1186
|
-
className="h-10 rounded-lg text-sm text-neutral-100 focus:bg-white/10 focus:text-white disabled:text-neutral-500"
|
|
1187
|
-
>
|
|
1188
|
-
<span className="mr-2 flex size-5 items-center justify-center text-neutral-100">
|
|
1189
|
-
{option.active ? <IconCheck className="size-4" /> : option.icon}
|
|
1190
|
-
</span>
|
|
1191
|
-
<span className="min-w-0 flex-1 truncate">{option.label}</span>
|
|
1192
|
-
{option.shortcut && (
|
|
1193
|
-
<DropdownMenuShortcut className="ml-3 text-neutral-400">
|
|
1194
|
-
{option.shortcut}
|
|
1195
|
-
</DropdownMenuShortcut>
|
|
1528
|
+
{hasOptionsMenu ? (
|
|
1529
|
+
<DropdownMenu>
|
|
1530
|
+
<DropdownMenuTrigger asChild>
|
|
1531
|
+
<button
|
|
1532
|
+
type="button"
|
|
1533
|
+
className={cn(
|
|
1534
|
+
"flex h-8 w-4 cursor-pointer items-center justify-center rounded-r-md transition-colors",
|
|
1535
|
+
active ? "hover:bg-white/15" : "hover:bg-white/10",
|
|
1196
1536
|
)}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1537
|
+
aria-label={`${label} options`}
|
|
1538
|
+
>
|
|
1539
|
+
<IconChevronDown className="size-3" />
|
|
1540
|
+
</button>
|
|
1541
|
+
</DropdownMenuTrigger>
|
|
1542
|
+
<DropdownMenuContent
|
|
1543
|
+
side="top"
|
|
1544
|
+
align="center"
|
|
1545
|
+
sideOffset={12}
|
|
1546
|
+
className="w-56 rounded-2xl border-white/10 bg-neutral-950 p-2 text-neutral-50 shadow-[0_24px_70px_-24px_rgba(0,0,0,0.9)]"
|
|
1547
|
+
>
|
|
1548
|
+
{options.map((option) => (
|
|
1549
|
+
<DropdownMenuItem
|
|
1550
|
+
key={option.key}
|
|
1551
|
+
disabled={option.disabled}
|
|
1552
|
+
onSelect={option.onSelect}
|
|
1553
|
+
className="h-10 rounded-lg text-sm text-neutral-100 focus:bg-white/10 focus:text-white disabled:text-neutral-500"
|
|
1554
|
+
>
|
|
1555
|
+
<span className="mr-2 flex size-5 items-center justify-center text-neutral-100">
|
|
1556
|
+
{option.active ? (
|
|
1557
|
+
<IconCheck className="size-4" />
|
|
1558
|
+
) : (
|
|
1559
|
+
option.icon
|
|
1560
|
+
)}
|
|
1561
|
+
</span>
|
|
1562
|
+
<span className="min-w-0 flex-1 truncate">{option.label}</span>
|
|
1563
|
+
{option.shortcut && (
|
|
1564
|
+
<DropdownMenuShortcut className="ml-3 text-neutral-400">
|
|
1565
|
+
{option.shortcut}
|
|
1566
|
+
</DropdownMenuShortcut>
|
|
1567
|
+
)}
|
|
1568
|
+
</DropdownMenuItem>
|
|
1569
|
+
))}
|
|
1570
|
+
</DropdownMenuContent>
|
|
1571
|
+
</DropdownMenu>
|
|
1572
|
+
) : null}
|
|
1201
1573
|
</div>
|
|
1202
1574
|
);
|
|
1203
1575
|
}
|
|
@@ -1245,25 +1617,22 @@ function FigmaBottomToolbar({
|
|
|
1245
1617
|
activeTool,
|
|
1246
1618
|
onMove,
|
|
1247
1619
|
onFrame,
|
|
1248
|
-
|
|
1620
|
+
onShape,
|
|
1249
1621
|
onText,
|
|
1250
1622
|
onPen,
|
|
1251
1623
|
onHand,
|
|
1252
1624
|
onDraw,
|
|
1253
1625
|
onScale,
|
|
1254
1626
|
onCommentPin,
|
|
1255
|
-
overviewActive,
|
|
1256
1627
|
onModeChange,
|
|
1257
|
-
onScreensToggle,
|
|
1258
1628
|
}: {
|
|
1259
1629
|
mode: EditorMode;
|
|
1260
1630
|
pinMode: boolean;
|
|
1261
1631
|
drawMode: boolean;
|
|
1262
1632
|
activeTool: DesignTool;
|
|
1263
|
-
overviewActive: boolean;
|
|
1264
1633
|
onMove: () => void;
|
|
1265
1634
|
onFrame: () => void;
|
|
1266
|
-
|
|
1635
|
+
onShape: (tool: ShapeTool) => void;
|
|
1267
1636
|
onText: () => void;
|
|
1268
1637
|
onPen: () => void;
|
|
1269
1638
|
onHand: () => void;
|
|
@@ -1271,9 +1640,95 @@ function FigmaBottomToolbar({
|
|
|
1271
1640
|
onScale: () => void;
|
|
1272
1641
|
onCommentPin: () => void;
|
|
1273
1642
|
onModeChange: (mode: EditorMode) => void;
|
|
1274
|
-
onScreensToggle: () => void;
|
|
1275
1643
|
}) {
|
|
1276
1644
|
const t = useT();
|
|
1645
|
+
const shapeTools = new Set<DesignTool>([
|
|
1646
|
+
"rect",
|
|
1647
|
+
"line",
|
|
1648
|
+
"arrow",
|
|
1649
|
+
"ellipse",
|
|
1650
|
+
"polygon",
|
|
1651
|
+
"star",
|
|
1652
|
+
]);
|
|
1653
|
+
const activeShape = shapeTools.has(activeTool)
|
|
1654
|
+
? (activeTool as ShapeTool)
|
|
1655
|
+
: "rect";
|
|
1656
|
+
const shapeIcon = (tool: ShapeTool, className: string) => {
|
|
1657
|
+
switch (tool) {
|
|
1658
|
+
case "line":
|
|
1659
|
+
return <IconLine className={className} />;
|
|
1660
|
+
case "arrow":
|
|
1661
|
+
return <IconArrowUpRight className={className} />;
|
|
1662
|
+
case "ellipse":
|
|
1663
|
+
return <IconCircle className={className} />;
|
|
1664
|
+
case "polygon":
|
|
1665
|
+
return <IconTriangle className={className} />;
|
|
1666
|
+
case "star":
|
|
1667
|
+
return <IconStar className={className} />;
|
|
1668
|
+
case "rect":
|
|
1669
|
+
default:
|
|
1670
|
+
return <IconSquare className={className} />;
|
|
1671
|
+
}
|
|
1672
|
+
};
|
|
1673
|
+
const shapeOptions: FigmaToolbarOption[] = [
|
|
1674
|
+
{
|
|
1675
|
+
key: "rect",
|
|
1676
|
+
label: t("designEditor.tools.rect"),
|
|
1677
|
+
icon: shapeIcon("rect", "size-4"),
|
|
1678
|
+
shortcut: "R",
|
|
1679
|
+
active: activeTool === "rect",
|
|
1680
|
+
onSelect: () => onShape("rect"),
|
|
1681
|
+
},
|
|
1682
|
+
{
|
|
1683
|
+
key: "line",
|
|
1684
|
+
label: t("designEditor.tools.line"),
|
|
1685
|
+
icon: shapeIcon("line", "size-4"),
|
|
1686
|
+
shortcut: "L",
|
|
1687
|
+
active: activeTool === "line",
|
|
1688
|
+
onSelect: () => onShape("line"),
|
|
1689
|
+
},
|
|
1690
|
+
{
|
|
1691
|
+
key: "arrow",
|
|
1692
|
+
label: t("designEditor.tools.arrow"),
|
|
1693
|
+
icon: shapeIcon("arrow", "size-4"),
|
|
1694
|
+
shortcut: "⇧L",
|
|
1695
|
+
active: activeTool === "arrow",
|
|
1696
|
+
onSelect: () => onShape("arrow"),
|
|
1697
|
+
},
|
|
1698
|
+
{
|
|
1699
|
+
key: "ellipse",
|
|
1700
|
+
label: t("designEditor.tools.ellipse"),
|
|
1701
|
+
icon: shapeIcon("ellipse", "size-4"),
|
|
1702
|
+
shortcut: "O",
|
|
1703
|
+
active: activeTool === "ellipse",
|
|
1704
|
+
onSelect: () => onShape("ellipse"),
|
|
1705
|
+
},
|
|
1706
|
+
{
|
|
1707
|
+
key: "polygon",
|
|
1708
|
+
label: t("designEditor.tools.polygon"),
|
|
1709
|
+
icon: shapeIcon("polygon", "size-4"),
|
|
1710
|
+
active: activeTool === "polygon",
|
|
1711
|
+
onSelect: () => onShape("polygon"),
|
|
1712
|
+
},
|
|
1713
|
+
{
|
|
1714
|
+
key: "star",
|
|
1715
|
+
label: t("designEditor.tools.star"),
|
|
1716
|
+
icon: shapeIcon("star", "size-4"),
|
|
1717
|
+
active: activeTool === "star",
|
|
1718
|
+
onSelect: () => onShape("star"),
|
|
1719
|
+
},
|
|
1720
|
+
{
|
|
1721
|
+
key: "image-video",
|
|
1722
|
+
label: t("designEditor.tools.imageVideo"),
|
|
1723
|
+
icon: <IconPhotoVideo className="size-4" />,
|
|
1724
|
+
shortcut: "⇧⌘K",
|
|
1725
|
+
disabled: true,
|
|
1726
|
+
onSelect: () => {},
|
|
1727
|
+
},
|
|
1728
|
+
];
|
|
1729
|
+
const activeShapeOption =
|
|
1730
|
+
shapeOptions.find((option) => option.key === activeShape) ??
|
|
1731
|
+
shapeOptions[0]!;
|
|
1277
1732
|
const tools: Array<{
|
|
1278
1733
|
key: string;
|
|
1279
1734
|
active: boolean;
|
|
@@ -1319,42 +1774,26 @@ function FigmaBottomToolbar({
|
|
|
1319
1774
|
key: "frame",
|
|
1320
1775
|
active: activeTool === "frame",
|
|
1321
1776
|
label: t("designEditor.tools.frame"),
|
|
1322
|
-
icon: <
|
|
1777
|
+
icon: <IconFrame className="size-[18px]" />,
|
|
1323
1778
|
onClick: onFrame,
|
|
1324
1779
|
options: [
|
|
1325
1780
|
{
|
|
1326
1781
|
key: "frame",
|
|
1327
1782
|
label: t("designEditor.tools.frame"),
|
|
1328
|
-
icon: <
|
|
1783
|
+
icon: <IconFrame className="size-4" />,
|
|
1329
1784
|
shortcut: "F",
|
|
1330
1785
|
active: activeTool === "frame",
|
|
1331
1786
|
onSelect: onFrame,
|
|
1332
1787
|
},
|
|
1333
|
-
{
|
|
1334
|
-
key: "screens",
|
|
1335
|
-
label: t("designEditor.modes.screens"),
|
|
1336
|
-
icon: <IconLayoutGrid className="size-4" />,
|
|
1337
|
-
active: overviewActive,
|
|
1338
|
-
onSelect: onScreensToggle,
|
|
1339
|
-
},
|
|
1340
1788
|
],
|
|
1341
1789
|
},
|
|
1342
1790
|
{
|
|
1343
|
-
key: "
|
|
1344
|
-
active: activeTool
|
|
1345
|
-
label:
|
|
1346
|
-
icon:
|
|
1347
|
-
onClick:
|
|
1348
|
-
options:
|
|
1349
|
-
{
|
|
1350
|
-
key: "rect",
|
|
1351
|
-
label: t("designEditor.tools.rect"),
|
|
1352
|
-
icon: <IconSquare className="size-4" />,
|
|
1353
|
-
shortcut: "R",
|
|
1354
|
-
active: activeTool === "rect",
|
|
1355
|
-
onSelect: onRect,
|
|
1356
|
-
},
|
|
1357
|
-
],
|
|
1791
|
+
key: "shape",
|
|
1792
|
+
active: shapeTools.has(activeTool),
|
|
1793
|
+
label: activeShapeOption.label,
|
|
1794
|
+
icon: shapeIcon(activeShape, "size-[18px]"),
|
|
1795
|
+
onClick: () => onShape(activeShape),
|
|
1796
|
+
options: shapeOptions,
|
|
1358
1797
|
},
|
|
1359
1798
|
{
|
|
1360
1799
|
key: "text",
|
|
@@ -1424,7 +1863,7 @@ function FigmaBottomToolbar({
|
|
|
1424
1863
|
];
|
|
1425
1864
|
|
|
1426
1865
|
const modes: Array<{
|
|
1427
|
-
key: EditorMode
|
|
1866
|
+
key: EditorMode;
|
|
1428
1867
|
active: boolean;
|
|
1429
1868
|
label: string;
|
|
1430
1869
|
icon: ReactNode;
|
|
@@ -1432,32 +1871,25 @@ function FigmaBottomToolbar({
|
|
|
1432
1871
|
}> = [
|
|
1433
1872
|
{
|
|
1434
1873
|
key: "annotate",
|
|
1435
|
-
active: mode === "annotate"
|
|
1874
|
+
active: mode === "annotate",
|
|
1436
1875
|
label: t("designEditor.modes.annotate"),
|
|
1437
1876
|
icon: <IconScribble className="size-[18px]" />,
|
|
1438
1877
|
onClick: () => onModeChange("annotate"),
|
|
1439
1878
|
},
|
|
1440
1879
|
{
|
|
1441
1880
|
key: "edit",
|
|
1442
|
-
active: mode === "edit"
|
|
1881
|
+
active: mode === "edit",
|
|
1443
1882
|
label: t("designEditor.modes.edit"),
|
|
1444
|
-
icon: <
|
|
1883
|
+
icon: <IconTransformPoint className="size-[18px]" />,
|
|
1445
1884
|
onClick: () => onModeChange("edit"),
|
|
1446
1885
|
},
|
|
1447
1886
|
{
|
|
1448
1887
|
key: "interact",
|
|
1449
|
-
active: mode === "interact"
|
|
1888
|
+
active: mode === "interact",
|
|
1450
1889
|
label: t("designEditor.modes.interact"),
|
|
1451
|
-
icon: <
|
|
1890
|
+
icon: <IconHandClick className="size-[18px]" />,
|
|
1452
1891
|
onClick: () => onModeChange("interact"),
|
|
1453
1892
|
},
|
|
1454
|
-
{
|
|
1455
|
-
key: "screens",
|
|
1456
|
-
active: overviewActive,
|
|
1457
|
-
label: t("designEditor.modes.screens"),
|
|
1458
|
-
icon: <IconLayoutGrid className="size-[18px]" />,
|
|
1459
|
-
onClick: onScreensToggle,
|
|
1460
|
-
},
|
|
1461
1893
|
];
|
|
1462
1894
|
|
|
1463
1895
|
return (
|
|
@@ -2049,13 +2481,17 @@ export default function DesignEditor() {
|
|
|
2049
2481
|
]);
|
|
2050
2482
|
|
|
2051
2483
|
// Current user info for collaborative presence
|
|
2052
|
-
const currentUser: CollabUser | undefined =
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2484
|
+
const currentUser: CollabUser | undefined = useMemo(
|
|
2485
|
+
() =>
|
|
2486
|
+
session?.email
|
|
2487
|
+
? {
|
|
2488
|
+
name: session.name?.trim() || emailToName(session.email),
|
|
2489
|
+
email: session.email,
|
|
2490
|
+
color: emailToColor(session.email),
|
|
2491
|
+
}
|
|
2492
|
+
: undefined,
|
|
2493
|
+
[session?.email, session?.name],
|
|
2494
|
+
);
|
|
2059
2495
|
|
|
2060
2496
|
// Data fetching
|
|
2061
2497
|
useEffect(() => {
|
|
@@ -2135,14 +2571,83 @@ export default function DesignEditor() {
|
|
|
2135
2571
|
const exportHtmlMutation = useActionMutation("export-html");
|
|
2136
2572
|
const exportZipMutation = useActionMutation("export-zip");
|
|
2137
2573
|
const [, setPatchProof] = useState<PatchProofState | null>(null);
|
|
2138
|
-
const pendingFileSaveRef = useRef<{
|
|
2139
|
-
|
|
2140
|
-
|
|
2574
|
+
const pendingFileSaveRef = useRef<{
|
|
2575
|
+
id: string;
|
|
2576
|
+
content: string;
|
|
2577
|
+
syncCollab: boolean;
|
|
2578
|
+
} | null>(null);
|
|
2579
|
+
const fileSaveChainsRef = useRef<Record<string, Promise<void>>>({});
|
|
2580
|
+
const latestFileSaveForUnloadRef = useRef<
|
|
2581
|
+
Record<string, FileContentSaveRequest>
|
|
2582
|
+
>({});
|
|
2141
2583
|
const fileSaveTimerRef = useRef<number | null>(null);
|
|
2142
2584
|
|
|
2585
|
+
const saveFileContent = useCallback(
|
|
2586
|
+
(pending: FileContentSaveRequest) => {
|
|
2587
|
+
latestFileSaveForUnloadRef.current[pending.id] = pending;
|
|
2588
|
+
const previous =
|
|
2589
|
+
fileSaveChainsRef.current[pending.id] ?? Promise.resolve();
|
|
2590
|
+
const current = previous
|
|
2591
|
+
.catch(() => {})
|
|
2592
|
+
.then(async () => {
|
|
2593
|
+
try {
|
|
2594
|
+
await updateFileMutation.mutateAsync({
|
|
2595
|
+
id: pending.id,
|
|
2596
|
+
content: pending.content,
|
|
2597
|
+
syncCollab: pending.syncCollab,
|
|
2598
|
+
} as any);
|
|
2599
|
+
setPatchProof((prev) =>
|
|
2600
|
+
prev && prev.fileId === pending.id && prev.status === "queued"
|
|
2601
|
+
? { ...prev, status: "applied" }
|
|
2602
|
+
: prev,
|
|
2603
|
+
);
|
|
2604
|
+
} catch (error) {
|
|
2605
|
+
setPatchProof((prev) =>
|
|
2606
|
+
prev && prev.fileId === pending.id && prev.status === "queued"
|
|
2607
|
+
? {
|
|
2608
|
+
...prev,
|
|
2609
|
+
status: "failed",
|
|
2610
|
+
error:
|
|
2611
|
+
error instanceof Error
|
|
2612
|
+
? error.message
|
|
2613
|
+
: t("common.genericError"),
|
|
2614
|
+
}
|
|
2615
|
+
: prev,
|
|
2616
|
+
);
|
|
2617
|
+
}
|
|
2618
|
+
});
|
|
2619
|
+
fileSaveChainsRef.current[pending.id] = current;
|
|
2620
|
+
void current.finally(() => {
|
|
2621
|
+
if (fileSaveChainsRef.current[pending.id] === current) {
|
|
2622
|
+
delete fileSaveChainsRef.current[pending.id];
|
|
2623
|
+
}
|
|
2624
|
+
});
|
|
2625
|
+
},
|
|
2626
|
+
[t, updateFileMutation],
|
|
2627
|
+
);
|
|
2628
|
+
|
|
2143
2629
|
const queueFileContentSave = useCallback(
|
|
2144
|
-
(
|
|
2145
|
-
|
|
2630
|
+
(
|
|
2631
|
+
fileId: string,
|
|
2632
|
+
content: string,
|
|
2633
|
+
options: { syncCollab?: boolean; immediate?: boolean } = {},
|
|
2634
|
+
) => {
|
|
2635
|
+
const pending = {
|
|
2636
|
+
id: fileId,
|
|
2637
|
+
content,
|
|
2638
|
+
syncCollab: options.syncCollab ?? true,
|
|
2639
|
+
};
|
|
2640
|
+
latestFileSaveForUnloadRef.current[fileId] = pending;
|
|
2641
|
+
if (options.immediate) {
|
|
2642
|
+
if (fileSaveTimerRef.current) {
|
|
2643
|
+
window.clearTimeout(fileSaveTimerRef.current);
|
|
2644
|
+
fileSaveTimerRef.current = null;
|
|
2645
|
+
}
|
|
2646
|
+
pendingFileSaveRef.current = null;
|
|
2647
|
+
saveFileContent(pending);
|
|
2648
|
+
return;
|
|
2649
|
+
}
|
|
2650
|
+
pendingFileSaveRef.current = pending;
|
|
2146
2651
|
if (fileSaveTimerRef.current) {
|
|
2147
2652
|
window.clearTimeout(fileSaveTimerRef.current);
|
|
2148
2653
|
}
|
|
@@ -2151,42 +2656,25 @@ export default function DesignEditor() {
|
|
|
2151
2656
|
pendingFileSaveRef.current = null;
|
|
2152
2657
|
fileSaveTimerRef.current = null;
|
|
2153
2658
|
if (!pending) return;
|
|
2154
|
-
|
|
2155
|
-
{
|
|
2156
|
-
id: pending.id,
|
|
2157
|
-
content: pending.content,
|
|
2158
|
-
} as any,
|
|
2159
|
-
{
|
|
2160
|
-
onSuccess: () => {
|
|
2161
|
-
setPatchProof((prev) =>
|
|
2162
|
-
prev && prev.fileId === pending.id && prev.status === "queued"
|
|
2163
|
-
? { ...prev, status: "applied" }
|
|
2164
|
-
: prev,
|
|
2165
|
-
);
|
|
2166
|
-
},
|
|
2167
|
-
onError: (error) => {
|
|
2168
|
-
setPatchProof((prev) =>
|
|
2169
|
-
prev && prev.fileId === pending.id && prev.status === "queued"
|
|
2170
|
-
? {
|
|
2171
|
-
...prev,
|
|
2172
|
-
status: "failed",
|
|
2173
|
-
error:
|
|
2174
|
-
error instanceof Error
|
|
2175
|
-
? error.message
|
|
2176
|
-
: t("common.genericError"),
|
|
2177
|
-
}
|
|
2178
|
-
: prev,
|
|
2179
|
-
);
|
|
2180
|
-
},
|
|
2181
|
-
},
|
|
2182
|
-
);
|
|
2659
|
+
saveFileContent(pending);
|
|
2183
2660
|
}, 400);
|
|
2184
2661
|
},
|
|
2185
|
-
[
|
|
2662
|
+
[saveFileContent],
|
|
2186
2663
|
);
|
|
2187
2664
|
|
|
2188
2665
|
useEffect(() => {
|
|
2666
|
+
const handlePageHide = () => {
|
|
2667
|
+
if (pendingFileSaveRef.current) {
|
|
2668
|
+
latestFileSaveForUnloadRef.current[pendingFileSaveRef.current.id] =
|
|
2669
|
+
pendingFileSaveRef.current;
|
|
2670
|
+
}
|
|
2671
|
+
Object.values(latestFileSaveForUnloadRef.current).forEach(
|
|
2672
|
+
sendFileContentSaveKeepalive,
|
|
2673
|
+
);
|
|
2674
|
+
};
|
|
2675
|
+
window.addEventListener("pagehide", handlePageHide);
|
|
2189
2676
|
return () => {
|
|
2677
|
+
window.removeEventListener("pagehide", handlePageHide);
|
|
2190
2678
|
if (fileSaveTimerRef.current) {
|
|
2191
2679
|
window.clearTimeout(fileSaveTimerRef.current);
|
|
2192
2680
|
}
|
|
@@ -2607,7 +3095,7 @@ export default function DesignEditor() {
|
|
|
2607
3095
|
setPinMode(false);
|
|
2608
3096
|
setMode("edit");
|
|
2609
3097
|
setSelectedElement(null);
|
|
2610
|
-
setActiveTool("
|
|
3098
|
+
setActiveTool("move");
|
|
2611
3099
|
setViewMode("overview");
|
|
2612
3100
|
} else if (editorView === "single") {
|
|
2613
3101
|
viewModeRef.current = "single";
|
|
@@ -2688,7 +3176,7 @@ export default function DesignEditor() {
|
|
|
2688
3176
|
});
|
|
2689
3177
|
if (nextId) {
|
|
2690
3178
|
setActiveFileId(nextId);
|
|
2691
|
-
setActiveTool("
|
|
3179
|
+
setActiveTool("move");
|
|
2692
3180
|
setViewMode("overview");
|
|
2693
3181
|
if (request?.canvasPosition) {
|
|
2694
3182
|
queueFrameGeometrySave({
|
|
@@ -2760,6 +3248,63 @@ export default function DesignEditor() {
|
|
|
2760
3248
|
);
|
|
2761
3249
|
}, [createFileMutation, files, id, queryClient, t]);
|
|
2762
3250
|
|
|
3251
|
+
const handleCreateScreenFrame = useCallback(
|
|
3252
|
+
(geometry: { x: number; y: number; width: number; height: number }) => {
|
|
3253
|
+
if (!id) return;
|
|
3254
|
+
const filename = nextBlankScreenFilename(files);
|
|
3255
|
+
const nextGeometry = {
|
|
3256
|
+
x: Math.round(geometry.x),
|
|
3257
|
+
y: Math.round(geometry.y),
|
|
3258
|
+
width: Math.max(64, Math.round(geometry.width)),
|
|
3259
|
+
height: Math.max(64, Math.round(geometry.height)),
|
|
3260
|
+
};
|
|
3261
|
+
createFileMutation.mutate(
|
|
3262
|
+
{
|
|
3263
|
+
designId: id,
|
|
3264
|
+
filename,
|
|
3265
|
+
content: blankScreenHtml(prettyScreenName(filename)),
|
|
3266
|
+
fileType: "html",
|
|
3267
|
+
} as any,
|
|
3268
|
+
{
|
|
3269
|
+
onSuccess: (result: any) => {
|
|
3270
|
+
const nextId = typeof result?.id === "string" ? result.id : null;
|
|
3271
|
+
queryClient.invalidateQueries({
|
|
3272
|
+
queryKey: ["action", "get-design"],
|
|
3273
|
+
});
|
|
3274
|
+
if (nextId) {
|
|
3275
|
+
setActiveFileId(nextId);
|
|
3276
|
+
setSelectedElement(null);
|
|
3277
|
+
setSelectedLayerIdsState([nextId]);
|
|
3278
|
+
setActiveTool("move");
|
|
3279
|
+
setMode("edit");
|
|
3280
|
+
setViewMode("overview");
|
|
3281
|
+
writeFrameGeometrySnapshot({
|
|
3282
|
+
...canvasFrameGeometryById,
|
|
3283
|
+
[nextId]: nextGeometry,
|
|
3284
|
+
});
|
|
3285
|
+
}
|
|
3286
|
+
},
|
|
3287
|
+
onError: (error) => {
|
|
3288
|
+
toast.error(
|
|
3289
|
+
error instanceof Error
|
|
3290
|
+
? error.message
|
|
3291
|
+
: t("designEditor.toasts.screenDuplicateError"),
|
|
3292
|
+
);
|
|
3293
|
+
},
|
|
3294
|
+
},
|
|
3295
|
+
);
|
|
3296
|
+
},
|
|
3297
|
+
[
|
|
3298
|
+
canvasFrameGeometryById,
|
|
3299
|
+
createFileMutation,
|
|
3300
|
+
files,
|
|
3301
|
+
id,
|
|
3302
|
+
queryClient,
|
|
3303
|
+
t,
|
|
3304
|
+
writeFrameGeometrySnapshot,
|
|
3305
|
+
],
|
|
3306
|
+
);
|
|
3307
|
+
|
|
2763
3308
|
// Collaborative editing for the active file
|
|
2764
3309
|
const { ydoc, awareness, isSynced, activeUsers, agentActive } =
|
|
2765
3310
|
useCollaborativeDoc({
|
|
@@ -2836,13 +3381,30 @@ export default function DesignEditor() {
|
|
|
2836
3381
|
const ytext = ydoc.getText("content");
|
|
2837
3382
|
const text = ytext.toString();
|
|
2838
3383
|
if (text.length > 0) {
|
|
3384
|
+
const storedContent = activeFile?.content ?? "";
|
|
3385
|
+
if (
|
|
3386
|
+
!shouldUseLiveFileContent({
|
|
3387
|
+
liveContent: text,
|
|
3388
|
+
storedContent,
|
|
3389
|
+
fileType: activeFile?.fileType ?? "html",
|
|
3390
|
+
})
|
|
3391
|
+
) {
|
|
3392
|
+
setCollabContent(storedContent);
|
|
3393
|
+
lastLocalContentRef.current = storedContent;
|
|
3394
|
+
setContentRenderRevision((revision) => revision + 1);
|
|
3395
|
+
ydoc.transact(() => {
|
|
3396
|
+
ytext.delete(0, ytext.length);
|
|
3397
|
+
ytext.insert(0, storedContent);
|
|
3398
|
+
}, TAB_ID);
|
|
3399
|
+
return;
|
|
3400
|
+
}
|
|
2839
3401
|
// Y.Doc snapshots are a render seed, not the SQL source of truth; the
|
|
2840
3402
|
// reconcile effect below advances the updatedAt watermark only after it
|
|
2841
3403
|
// confirms or applies the current DB content.
|
|
2842
3404
|
setCollabContent(text);
|
|
2843
3405
|
setContentRenderRevision((revision) => revision + 1);
|
|
2844
3406
|
}
|
|
2845
|
-
}, [ydoc, isSynced, activeFileId]);
|
|
3407
|
+
}, [ydoc, isSynced, activeFileId, activeFile?.content, activeFile?.fileType]);
|
|
2846
3408
|
|
|
2847
3409
|
// Keep the freshest DB `updatedAt` in a ref the observe handler can read.
|
|
2848
3410
|
useEffect(() => {
|
|
@@ -2887,34 +3449,11 @@ export default function DesignEditor() {
|
|
|
2887
3449
|
if (!isLocalEdit) {
|
|
2888
3450
|
setSelectedElement((prev) => {
|
|
2889
3451
|
if (!prev) return prev;
|
|
2890
|
-
|
|
2891
|
-
const iframe = document.querySelector<HTMLIFrameElement>(
|
|
2892
|
-
"iframe[data-design-preview-iframe]",
|
|
2893
|
-
);
|
|
2894
|
-
const doc = iframe?.contentDocument;
|
|
2895
|
-
if (doc && (!prev.selector || !doc.querySelector(prev.selector))) {
|
|
2896
|
-
return null;
|
|
2897
|
-
}
|
|
2898
|
-
} catch {
|
|
2899
|
-
// iframe not accessible yet — clear defensively
|
|
2900
|
-
return null;
|
|
2901
|
-
}
|
|
2902
|
-
return prev;
|
|
3452
|
+
return elementInfoExistsInContent(next, prev) ? prev : null;
|
|
2903
3453
|
});
|
|
2904
3454
|
setHoveredElement((prev) => {
|
|
2905
3455
|
if (!prev) return prev;
|
|
2906
|
-
|
|
2907
|
-
const iframe = document.querySelector<HTMLIFrameElement>(
|
|
2908
|
-
"iframe[data-design-preview-iframe]",
|
|
2909
|
-
);
|
|
2910
|
-
const doc = iframe?.contentDocument;
|
|
2911
|
-
if (doc && (!prev.selector || !doc.querySelector(prev.selector))) {
|
|
2912
|
-
return null;
|
|
2913
|
-
}
|
|
2914
|
-
} catch {
|
|
2915
|
-
return null;
|
|
2916
|
-
}
|
|
2917
|
-
return prev;
|
|
3456
|
+
return elementInfoExistsInContent(next, prev) ? prev : null;
|
|
2918
3457
|
});
|
|
2919
3458
|
}
|
|
2920
3459
|
};
|
|
@@ -2981,6 +3520,31 @@ export default function DesignEditor() {
|
|
|
2981
3520
|
if (!activeFile || !isSynced) return;
|
|
2982
3521
|
const dbContent = activeFile.content ?? "";
|
|
2983
3522
|
const dbUpdatedAt = activeFile.updatedAt ?? null;
|
|
3523
|
+
if (
|
|
3524
|
+
typeof collabContent === "string" &&
|
|
3525
|
+
!shouldUseLiveFileContent({
|
|
3526
|
+
liveContent: collabContent,
|
|
3527
|
+
storedContent: dbContent,
|
|
3528
|
+
fileType: activeFile.fileType,
|
|
3529
|
+
})
|
|
3530
|
+
) {
|
|
3531
|
+
clearStaleAgentCollabRecovery();
|
|
3532
|
+
setCollabContent(dbContent);
|
|
3533
|
+
lastLocalContentRef.current = dbContent;
|
|
3534
|
+
if (dbUpdatedAt) lastAppliedFileUpdatedAtRef.current = dbUpdatedAt;
|
|
3535
|
+
setContentRenderRevision((revision) => revision + 1);
|
|
3536
|
+
|
|
3537
|
+
if (isLeadClient && ydoc) {
|
|
3538
|
+
const ytext = ydoc.getText("content");
|
|
3539
|
+
if (ytext.toString() !== dbContent) {
|
|
3540
|
+
ydoc.transact(() => {
|
|
3541
|
+
ytext.delete(0, ytext.length);
|
|
3542
|
+
ytext.insert(0, dbContent);
|
|
3543
|
+
}, TAB_ID);
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
return;
|
|
3547
|
+
}
|
|
2984
3548
|
|
|
2985
3549
|
// Already reflecting this exact content (our own echo or Yjs already
|
|
2986
3550
|
// delivered it) — just advance the watermark and stop.
|
|
@@ -3174,6 +3738,29 @@ export default function DesignEditor() {
|
|
|
3174
3738
|
[followingEmail, stopFollowing],
|
|
3175
3739
|
);
|
|
3176
3740
|
|
|
3741
|
+
const designCollaborators = useMemo<DesignCollaborator[]>(() => {
|
|
3742
|
+
const currentEmail = currentUser?.email.trim().toLowerCase() ?? null;
|
|
3743
|
+
const humans = dedupeCollabUsersByEmail([
|
|
3744
|
+
...(currentUser ? [currentUser] : []),
|
|
3745
|
+
...activeUsers,
|
|
3746
|
+
]).filter((user) => user.email.trim().toLowerCase() !== "agent@system");
|
|
3747
|
+
const otherHumans = humans.filter(
|
|
3748
|
+
(user) => user.email.trim().toLowerCase() !== currentEmail,
|
|
3749
|
+
);
|
|
3750
|
+
const collaborators = otherHumans.map((user) => ({ user }));
|
|
3751
|
+
|
|
3752
|
+
if (!currentUser) return collaborators;
|
|
3753
|
+
|
|
3754
|
+
return [
|
|
3755
|
+
{
|
|
3756
|
+
user: currentUser,
|
|
3757
|
+
image: session?.image,
|
|
3758
|
+
isCurrent: true,
|
|
3759
|
+
},
|
|
3760
|
+
...collaborators,
|
|
3761
|
+
];
|
|
3762
|
+
}, [activeUsers, currentUser, session?.image]);
|
|
3763
|
+
|
|
3177
3764
|
// Resolve the content to render: prefer collab content, fall back to DB
|
|
3178
3765
|
const activeContent = collabContent ?? activeFile?.content ?? "";
|
|
3179
3766
|
const pageStyles = useMemo(
|
|
@@ -3248,11 +3835,38 @@ export default function DesignEditor() {
|
|
|
3248
3835
|
const applyLocalContentUpdate = useCallback(
|
|
3249
3836
|
(
|
|
3250
3837
|
nextContent: string,
|
|
3251
|
-
options: {
|
|
3838
|
+
options: {
|
|
3839
|
+
refreshPreview?: boolean;
|
|
3840
|
+
skipPreview?: boolean;
|
|
3841
|
+
immediateSave?: boolean;
|
|
3842
|
+
} = {},
|
|
3252
3843
|
) => {
|
|
3253
3844
|
if (!activeFile) return;
|
|
3254
3845
|
setCollabContent(nextContent);
|
|
3255
3846
|
lastLocalContentRef.current = nextContent;
|
|
3847
|
+
if (id) {
|
|
3848
|
+
const optimisticUpdatedAt = new Date().toISOString();
|
|
3849
|
+
queryClient.setQueryData(
|
|
3850
|
+
["action", "get-design", { id }],
|
|
3851
|
+
(old: any) => {
|
|
3852
|
+
if (!old || typeof old !== "object" || !Array.isArray(old.files)) {
|
|
3853
|
+
return old;
|
|
3854
|
+
}
|
|
3855
|
+
return {
|
|
3856
|
+
...old,
|
|
3857
|
+
files: old.files.map((file: DesignFile) =>
|
|
3858
|
+
file.id === activeFile.id
|
|
3859
|
+
? {
|
|
3860
|
+
...file,
|
|
3861
|
+
content: nextContent,
|
|
3862
|
+
updatedAt: optimisticUpdatedAt,
|
|
3863
|
+
}
|
|
3864
|
+
: file,
|
|
3865
|
+
),
|
|
3866
|
+
};
|
|
3867
|
+
},
|
|
3868
|
+
);
|
|
3869
|
+
}
|
|
3256
3870
|
const forceRefresh = options.refreshPreview === true;
|
|
3257
3871
|
const replacedPreview = options.skipPreview
|
|
3258
3872
|
? true
|
|
@@ -3271,9 +3885,20 @@ export default function DesignEditor() {
|
|
|
3271
3885
|
}, LOCAL_EDIT_ORIGIN);
|
|
3272
3886
|
}
|
|
3273
3887
|
}
|
|
3274
|
-
queueFileContentSave(activeFile.id, nextContent
|
|
3888
|
+
queueFileContentSave(activeFile.id, nextContent, {
|
|
3889
|
+
syncCollab: !(ydoc && isSynced),
|
|
3890
|
+
immediate: options.immediateSave,
|
|
3891
|
+
});
|
|
3275
3892
|
},
|
|
3276
|
-
[
|
|
3893
|
+
[
|
|
3894
|
+
activeFile,
|
|
3895
|
+
id,
|
|
3896
|
+
isSynced,
|
|
3897
|
+
queryClient,
|
|
3898
|
+
queueFileContentSave,
|
|
3899
|
+
replacePreviewContent,
|
|
3900
|
+
ydoc,
|
|
3901
|
+
],
|
|
3277
3902
|
);
|
|
3278
3903
|
|
|
3279
3904
|
const applyFileContentUpdate = useCallback(
|
|
@@ -3297,21 +3922,13 @@ export default function DesignEditor() {
|
|
|
3297
3922
|
),
|
|
3298
3923
|
};
|
|
3299
3924
|
});
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
});
|
|
3305
|
-
},
|
|
3925
|
+
saveFileContent({
|
|
3926
|
+
id: fileId,
|
|
3927
|
+
content: nextContent,
|
|
3928
|
+
syncCollab: true,
|
|
3306
3929
|
});
|
|
3307
3930
|
},
|
|
3308
|
-
[
|
|
3309
|
-
activeFile?.id,
|
|
3310
|
-
applyLocalContentUpdate,
|
|
3311
|
-
id,
|
|
3312
|
-
queryClient,
|
|
3313
|
-
updateFileMutation,
|
|
3314
|
-
],
|
|
3931
|
+
[activeFile?.id, applyLocalContentUpdate, id, queryClient, saveFileContent],
|
|
3315
3932
|
);
|
|
3316
3933
|
|
|
3317
3934
|
const handleCreatePrimitive = useCallback(
|
|
@@ -3319,7 +3936,11 @@ export default function DesignEditor() {
|
|
|
3319
3936
|
const targetFile = files.find((file) => file.id === screenId);
|
|
3320
3937
|
if (!targetFile) return false;
|
|
3321
3938
|
const baseContent =
|
|
3322
|
-
targetFile.id === activeFile?.id
|
|
3939
|
+
targetFile.id === activeFile?.id
|
|
3940
|
+
? ydoc && isSynced
|
|
3941
|
+
? ydoc.getText("content").toString()
|
|
3942
|
+
: (collabContentRef.current ?? activeContent)
|
|
3943
|
+
: targetFile.content;
|
|
3323
3944
|
const nextContent = appendCanvasPrimitiveToHtml(baseContent, primitive);
|
|
3324
3945
|
if (!nextContent) {
|
|
3325
3946
|
toast.error(t("designEditor.toasts.primitiveInsertFailed"));
|
|
@@ -3327,7 +3948,7 @@ export default function DesignEditor() {
|
|
|
3327
3948
|
}
|
|
3328
3949
|
|
|
3329
3950
|
if (targetFile.id === activeFile?.id) {
|
|
3330
|
-
applyLocalContentUpdate(nextContent);
|
|
3951
|
+
applyLocalContentUpdate(nextContent, { immediateSave: true });
|
|
3331
3952
|
} else {
|
|
3332
3953
|
queryClient.setQueryData(
|
|
3333
3954
|
["action", "get-design", { id }],
|
|
@@ -3345,16 +3966,11 @@ export default function DesignEditor() {
|
|
|
3345
3966
|
};
|
|
3346
3967
|
},
|
|
3347
3968
|
);
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
queryKey: ["action", "get-design"],
|
|
3354
|
-
});
|
|
3355
|
-
},
|
|
3356
|
-
},
|
|
3357
|
-
);
|
|
3969
|
+
saveFileContent({
|
|
3970
|
+
id: targetFile.id,
|
|
3971
|
+
content: nextContent,
|
|
3972
|
+
syncCollab: true,
|
|
3973
|
+
});
|
|
3358
3974
|
}
|
|
3359
3975
|
|
|
3360
3976
|
return true;
|
|
@@ -3365,9 +3981,11 @@ export default function DesignEditor() {
|
|
|
3365
3981
|
applyLocalContentUpdate,
|
|
3366
3982
|
files,
|
|
3367
3983
|
id,
|
|
3984
|
+
isSynced,
|
|
3368
3985
|
queryClient,
|
|
3986
|
+
saveFileContent,
|
|
3369
3987
|
t,
|
|
3370
|
-
|
|
3988
|
+
ydoc,
|
|
3371
3989
|
],
|
|
3372
3990
|
);
|
|
3373
3991
|
|
|
@@ -3396,8 +4014,8 @@ export default function DesignEditor() {
|
|
|
3396
4014
|
setSelectedElement(null);
|
|
3397
4015
|
}, []);
|
|
3398
4016
|
|
|
3399
|
-
const
|
|
3400
|
-
setActiveTool(
|
|
4017
|
+
const handleShapeTool = useCallback((tool: ShapeTool) => {
|
|
4018
|
+
setActiveTool(tool);
|
|
3401
4019
|
setViewMode("overview");
|
|
3402
4020
|
setMode("edit");
|
|
3403
4021
|
setDrawMode(false);
|
|
@@ -3405,6 +4023,10 @@ export default function DesignEditor() {
|
|
|
3405
4023
|
setSelectedElement(null);
|
|
3406
4024
|
}, []);
|
|
3407
4025
|
|
|
4026
|
+
const handleRectTool = useCallback(() => {
|
|
4027
|
+
handleShapeTool("rect");
|
|
4028
|
+
}, [handleShapeTool]);
|
|
4029
|
+
|
|
3408
4030
|
const handlePenTool = useCallback(() => {
|
|
3409
4031
|
setActiveTool("pen");
|
|
3410
4032
|
setViewMode("overview");
|
|
@@ -3908,7 +4530,9 @@ export default function DesignEditor() {
|
|
|
3908
4530
|
}, LOCAL_EDIT_ORIGIN);
|
|
3909
4531
|
}
|
|
3910
4532
|
}
|
|
3911
|
-
queueFileContentSave(activeFile.id, resolvedNextContent
|
|
4533
|
+
queueFileContentSave(activeFile.id, resolvedNextContent, {
|
|
4534
|
+
syncCollab: !(ydoc && isSynced),
|
|
4535
|
+
});
|
|
3912
4536
|
if (resolvedNode) setSelectedLayerIdsState([resolvedNode.id]);
|
|
3913
4537
|
setSelectedElement((prev) => {
|
|
3914
4538
|
if (options.elementInfo) return options.elementInfo;
|
|
@@ -4030,16 +4654,31 @@ export default function DesignEditor() {
|
|
|
4030
4654
|
);
|
|
4031
4655
|
return false;
|
|
4032
4656
|
}
|
|
4033
|
-
|
|
4034
|
-
|
|
4657
|
+
const movedNode =
|
|
4658
|
+
(patch.result.after?.nodeId
|
|
4659
|
+
? patch.projection.nodes.find(
|
|
4660
|
+
(node) => node.id === patch.result.after?.nodeId,
|
|
4661
|
+
)
|
|
4662
|
+
: null) ??
|
|
4663
|
+
resolveCodeLayerNodeFromBridge(
|
|
4664
|
+
patch.projection,
|
|
4665
|
+
selector,
|
|
4666
|
+
details?.sourceId ??
|
|
4667
|
+
elementInfo?.sourceId ??
|
|
4668
|
+
(targetNode
|
|
4669
|
+
? bridgeSourceIdForCodeLayerNode(targetNode)
|
|
4670
|
+
: undefined),
|
|
4671
|
+
);
|
|
4672
|
+
applyLocalContentUpdate(patch.content, { skipPreview: true });
|
|
4673
|
+
if (movedNode) setSelectedLayerIdsState([movedNode.id]);
|
|
4035
4674
|
if (elementInfo) {
|
|
4036
4675
|
setSelectedElement({
|
|
4037
4676
|
...elementInfo,
|
|
4038
|
-
sourceId:
|
|
4039
|
-
? bridgeSourceIdForCodeLayerNode(
|
|
4677
|
+
sourceId: movedNode
|
|
4678
|
+
? bridgeSourceIdForCodeLayerNode(movedNode)
|
|
4040
4679
|
: elementInfo.sourceId,
|
|
4041
|
-
selector:
|
|
4042
|
-
? preferredCodeLayerSelector(
|
|
4680
|
+
selector: movedNode
|
|
4681
|
+
? preferredCodeLayerSelector(movedNode)
|
|
4043
4682
|
: elementInfo.selector,
|
|
4044
4683
|
});
|
|
4045
4684
|
}
|
|
@@ -4433,7 +5072,9 @@ export default function DesignEditor() {
|
|
|
4433
5072
|
if (ydoc && activeFile) {
|
|
4434
5073
|
const next = ydoc.getText("content").toString();
|
|
4435
5074
|
lastLocalContentRef.current = next;
|
|
4436
|
-
queueFileContentSave(activeFile.id, next
|
|
5075
|
+
queueFileContentSave(activeFile.id, next, {
|
|
5076
|
+
syncCollab: !(ydoc && isSynced),
|
|
5077
|
+
});
|
|
4437
5078
|
if (!replacePreviewContent(next)) {
|
|
4438
5079
|
setContentRenderRevision((revision) => revision + 1);
|
|
4439
5080
|
}
|
|
@@ -4472,6 +5113,7 @@ export default function DesignEditor() {
|
|
|
4472
5113
|
}, [
|
|
4473
5114
|
ydoc,
|
|
4474
5115
|
activeFile,
|
|
5116
|
+
isSynced,
|
|
4475
5117
|
queueFileContentSave,
|
|
4476
5118
|
replacePreviewContent,
|
|
4477
5119
|
syncUndoRedoState,
|
|
@@ -4486,7 +5128,9 @@ export default function DesignEditor() {
|
|
|
4486
5128
|
if (ydoc && activeFile) {
|
|
4487
5129
|
const next = ydoc.getText("content").toString();
|
|
4488
5130
|
lastLocalContentRef.current = next;
|
|
4489
|
-
queueFileContentSave(activeFile.id, next
|
|
5131
|
+
queueFileContentSave(activeFile.id, next, {
|
|
5132
|
+
syncCollab: !(ydoc && isSynced),
|
|
5133
|
+
});
|
|
4490
5134
|
if (!replacePreviewContent(next)) {
|
|
4491
5135
|
setContentRenderRevision((revision) => revision + 1);
|
|
4492
5136
|
}
|
|
@@ -4525,6 +5169,7 @@ export default function DesignEditor() {
|
|
|
4525
5169
|
}, [
|
|
4526
5170
|
ydoc,
|
|
4527
5171
|
activeFile,
|
|
5172
|
+
isSynced,
|
|
4528
5173
|
queueFileContentSave,
|
|
4529
5174
|
replacePreviewContent,
|
|
4530
5175
|
syncUndoRedoState,
|
|
@@ -4575,7 +5220,7 @@ export default function DesignEditor() {
|
|
|
4575
5220
|
setPinMode(false);
|
|
4576
5221
|
setMode("edit");
|
|
4577
5222
|
setSelectedElement(null);
|
|
4578
|
-
setActiveTool("
|
|
5223
|
+
setActiveTool("move");
|
|
4579
5224
|
setViewMode("overview");
|
|
4580
5225
|
});
|
|
4581
5226
|
}, [runEditorViewTransition]);
|
|
@@ -4767,7 +5412,7 @@ export default function DesignEditor() {
|
|
|
4767
5412
|
setDrawMode(false);
|
|
4768
5413
|
setPinMode(false);
|
|
4769
5414
|
setMode("edit");
|
|
4770
|
-
setActiveTool("
|
|
5415
|
+
setActiveTool("move");
|
|
4771
5416
|
setViewMode("overview");
|
|
4772
5417
|
setOverviewSelectAllRequest((request) => request + 1);
|
|
4773
5418
|
}, [files.length]);
|
|
@@ -4813,7 +5458,7 @@ export default function DesignEditor() {
|
|
|
4813
5458
|
onZoomReset: () => setZoom(100),
|
|
4814
5459
|
onZoomToFit: () => {
|
|
4815
5460
|
setViewMode("overview");
|
|
4816
|
-
setActiveTool("
|
|
5461
|
+
setActiveTool("move");
|
|
4817
5462
|
setZoom(100);
|
|
4818
5463
|
},
|
|
4819
5464
|
onZoomToSelection: () => {
|
|
@@ -5514,6 +6159,32 @@ ${serializedHtml}
|
|
|
5514
6159
|
});
|
|
5515
6160
|
}, [designDataJson, files]);
|
|
5516
6161
|
|
|
6162
|
+
const activeScreenPreviewUrl = useMemo(() => {
|
|
6163
|
+
if (builderPreviewUrl) return builderPreviewUrl;
|
|
6164
|
+
const screen = overviewScreens.find((item) => item.id === activeFile?.id);
|
|
6165
|
+
return (
|
|
6166
|
+
screen?.url ||
|
|
6167
|
+
screen?.previewUrl ||
|
|
6168
|
+
externalPreviewUrlForContent(activeContent)
|
|
6169
|
+
);
|
|
6170
|
+
}, [activeContent, activeFile?.id, builderPreviewUrl, overviewScreens]);
|
|
6171
|
+
|
|
6172
|
+
const handleOpenDesignPreview = useCallback(() => {
|
|
6173
|
+
if (activeScreenPreviewUrl) {
|
|
6174
|
+
window.open(activeScreenPreviewUrl, "_blank", "noopener,noreferrer");
|
|
6175
|
+
return;
|
|
6176
|
+
}
|
|
6177
|
+
|
|
6178
|
+
const content = activeContent.trim();
|
|
6179
|
+
if (!content) return;
|
|
6180
|
+
|
|
6181
|
+
const blobUrl = URL.createObjectURL(
|
|
6182
|
+
new Blob([fullPreviewHtml(activeContent)], { type: "text/html" }),
|
|
6183
|
+
);
|
|
6184
|
+
window.open(blobUrl, "_blank", "noopener,noreferrer");
|
|
6185
|
+
window.setTimeout(() => URL.revokeObjectURL(blobUrl), 60_000);
|
|
6186
|
+
}, [activeContent, activeScreenPreviewUrl]);
|
|
6187
|
+
|
|
5517
6188
|
const activeLayerId =
|
|
5518
6189
|
selectedLayerIds[selectedLayerIds.length - 1] ??
|
|
5519
6190
|
selectedElementLayerId ??
|
|
@@ -5571,7 +6242,9 @@ ${serializedHtml}
|
|
|
5571
6242
|
moved = true;
|
|
5572
6243
|
}
|
|
5573
6244
|
if (!moved || nextContent === sourceContent) return;
|
|
5574
|
-
applyFileContentUpdate(targetOwner.fileId, nextContent
|
|
6245
|
+
applyFileContentUpdate(targetOwner.fileId, nextContent, {
|
|
6246
|
+
refreshPreview: targetOwner.fileId === activeFile?.id,
|
|
6247
|
+
});
|
|
5575
6248
|
},
|
|
5576
6249
|
[
|
|
5577
6250
|
activeContent,
|
|
@@ -5808,16 +6481,60 @@ ${serializedHtml}
|
|
|
5808
6481
|
|
|
5809
6482
|
const deviceFrameIcon =
|
|
5810
6483
|
deviceFrame === "desktop" ? (
|
|
5811
|
-
<IconDeviceDesktop className="
|
|
6484
|
+
<IconDeviceDesktop className="size-3" />
|
|
5812
6485
|
) : deviceFrame === "tablet" ? (
|
|
5813
|
-
<IconDeviceTablet className="
|
|
6486
|
+
<IconDeviceTablet className="size-3" />
|
|
5814
6487
|
) : deviceFrame === "mobile" ? (
|
|
5815
|
-
<IconDeviceMobile className="
|
|
6488
|
+
<IconDeviceMobile className="size-3" />
|
|
5816
6489
|
) : (
|
|
5817
|
-
<IconViewportWide className="
|
|
6490
|
+
<IconViewportWide className="size-3" />
|
|
5818
6491
|
);
|
|
5819
6492
|
const questionFlowActive = pendingQuestionsVisible;
|
|
5820
6493
|
|
|
6494
|
+
const deviceFrameControl = (
|
|
6495
|
+
<DropdownMenu>
|
|
6496
|
+
<Tooltip>
|
|
6497
|
+
<TooltipTrigger asChild>
|
|
6498
|
+
<DropdownMenuTrigger asChild>
|
|
6499
|
+
<Button
|
|
6500
|
+
variant="ghost"
|
|
6501
|
+
size="sm"
|
|
6502
|
+
className="h-7 shrink-0 gap-0.5 px-1.5 cursor-pointer text-muted-foreground hover:text-foreground"
|
|
6503
|
+
disabled={viewMode === "overview"}
|
|
6504
|
+
>
|
|
6505
|
+
{deviceFrameIcon}
|
|
6506
|
+
<IconChevronDown className="size-2.5 opacity-60" />
|
|
6507
|
+
</Button>
|
|
6508
|
+
</DropdownMenuTrigger>
|
|
6509
|
+
</TooltipTrigger>
|
|
6510
|
+
<TooltipContent>{t("designEditor.devicePreview")}</TooltipContent>
|
|
6511
|
+
</Tooltip>
|
|
6512
|
+
<DropdownMenuContent align="end" className="w-44">
|
|
6513
|
+
<DropdownMenuRadioGroup
|
|
6514
|
+
value={deviceFrame}
|
|
6515
|
+
onValueChange={(v) => setDeviceFrame(v as DeviceFrameType)}
|
|
6516
|
+
>
|
|
6517
|
+
<DropdownMenuRadioItem value="none">
|
|
6518
|
+
<IconViewportWide className="mr-2 h-4 w-4" />
|
|
6519
|
+
{t("designEditor.devices.responsive")}
|
|
6520
|
+
</DropdownMenuRadioItem>
|
|
6521
|
+
<DropdownMenuRadioItem value="desktop">
|
|
6522
|
+
<IconDeviceDesktop className="mr-2 h-4 w-4" />
|
|
6523
|
+
{t("designEditor.devices.desktop")}
|
|
6524
|
+
</DropdownMenuRadioItem>
|
|
6525
|
+
<DropdownMenuRadioItem value="tablet">
|
|
6526
|
+
<IconDeviceTablet className="mr-2 h-4 w-4" />
|
|
6527
|
+
{t("designEditor.devices.tablet")}
|
|
6528
|
+
</DropdownMenuRadioItem>
|
|
6529
|
+
<DropdownMenuRadioItem value="mobile">
|
|
6530
|
+
<IconDeviceMobile className="mr-2 h-4 w-4" />
|
|
6531
|
+
{t("designEditor.devices.mobile")}
|
|
6532
|
+
</DropdownMenuRadioItem>
|
|
6533
|
+
</DropdownMenuRadioGroup>
|
|
6534
|
+
</DropdownMenuContent>
|
|
6535
|
+
</DropdownMenu>
|
|
6536
|
+
);
|
|
6537
|
+
|
|
5821
6538
|
const projectMenu = (
|
|
5822
6539
|
<DropdownMenu>
|
|
5823
6540
|
<DropdownMenuTrigger asChild>
|
|
@@ -5966,7 +6683,7 @@ ${serializedHtml}
|
|
|
5966
6683
|
setTitleEditing(false);
|
|
5967
6684
|
}
|
|
5968
6685
|
}}
|
|
5969
|
-
className="h-7 min-w-0 flex-1 text-
|
|
6686
|
+
className="-mx-1 h-7 min-w-0 flex-1 border-transparent bg-[var(--design-editor-panel-raised-bg)] px-1 py-0 text-[13px] font-medium text-foreground shadow-none ring-offset-0 focus-visible:border-[var(--design-editor-control-border)] focus-visible:ring-1 focus-visible:ring-[var(--design-editor-accent-color)] focus-visible:ring-offset-0"
|
|
5970
6687
|
/>
|
|
5971
6688
|
) : (
|
|
5972
6689
|
<Tooltip>
|
|
@@ -5977,7 +6694,7 @@ ${serializedHtml}
|
|
|
5977
6694
|
setTitleDraft(design.title);
|
|
5978
6695
|
setTitleEditing(true);
|
|
5979
6696
|
}}
|
|
5980
|
-
className="-mx-1 min-w-0 flex-1 cursor-text truncate rounded px-1 text-left text-
|
|
6697
|
+
className="-mx-1 min-w-0 flex-1 cursor-text truncate rounded px-1 text-left text-[13px] font-medium text-foreground/90 hover:bg-accent/50"
|
|
5981
6698
|
>
|
|
5982
6699
|
{design.title}
|
|
5983
6700
|
</button>
|
|
@@ -5986,111 +6703,84 @@ ${serializedHtml}
|
|
|
5986
6703
|
</Tooltip>
|
|
5987
6704
|
);
|
|
5988
6705
|
|
|
5989
|
-
const
|
|
5990
|
-
<
|
|
5991
|
-
<
|
|
5992
|
-
<
|
|
5993
|
-
<
|
|
5994
|
-
<
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
size="sm"
|
|
5999
|
-
className="h-7 gap-1 px-2 cursor-pointer"
|
|
6000
|
-
disabled={viewMode === "overview"}
|
|
6001
|
-
>
|
|
6002
|
-
{deviceFrameIcon}
|
|
6003
|
-
<IconChevronDown className="w-3 h-3 opacity-60" />
|
|
6004
|
-
</Button>
|
|
6005
|
-
</DropdownMenuTrigger>
|
|
6006
|
-
</TooltipTrigger>
|
|
6007
|
-
<TooltipContent>{t("designEditor.devicePreview")}</TooltipContent>
|
|
6008
|
-
</Tooltip>
|
|
6009
|
-
<DropdownMenuContent align="end" className="w-44">
|
|
6010
|
-
<DropdownMenuRadioGroup
|
|
6011
|
-
value={deviceFrame}
|
|
6012
|
-
onValueChange={(v) => setDeviceFrame(v as DeviceFrameType)}
|
|
6706
|
+
const zoomControl = (
|
|
6707
|
+
<DropdownMenu>
|
|
6708
|
+
<Tooltip>
|
|
6709
|
+
<TooltipTrigger asChild>
|
|
6710
|
+
<DropdownMenuTrigger asChild>
|
|
6711
|
+
<Button
|
|
6712
|
+
variant="ghost"
|
|
6713
|
+
size="sm"
|
|
6714
|
+
className="h-6 gap-0.5 px-1 text-[10px] tabular-nums text-muted-foreground cursor-pointer hover:text-foreground"
|
|
6013
6715
|
>
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6716
|
+
{zoomLabel}
|
|
6717
|
+
<IconChevronDown className="size-2.5 opacity-60" />
|
|
6718
|
+
</Button>
|
|
6719
|
+
</DropdownMenuTrigger>
|
|
6720
|
+
</TooltipTrigger>
|
|
6721
|
+
<TooltipContent>{t("designEditor.zoom")}</TooltipContent>
|
|
6722
|
+
</Tooltip>
|
|
6723
|
+
<DropdownMenuContent align="end" className="w-40">
|
|
6724
|
+
<DropdownMenuItem onClick={handleZoomOut}>
|
|
6725
|
+
<IconZoomOut className="mr-2 h-4 w-4" />
|
|
6726
|
+
{t("designEditor.zoomOut")}
|
|
6727
|
+
</DropdownMenuItem>
|
|
6728
|
+
<DropdownMenuItem onClick={handleZoomIn}>
|
|
6729
|
+
<IconZoomIn className="mr-2 h-4 w-4" />
|
|
6730
|
+
{t("designEditor.zoomIn")}
|
|
6731
|
+
</DropdownMenuItem>
|
|
6732
|
+
<DropdownMenuSeparator />
|
|
6733
|
+
{ZOOM_PRESETS.map((preset) => (
|
|
6734
|
+
<DropdownMenuItem
|
|
6735
|
+
key={preset}
|
|
6736
|
+
onClick={() => setZoom(preset)}
|
|
6737
|
+
className="justify-between"
|
|
6738
|
+
>
|
|
6739
|
+
<span>{preset}%</span>
|
|
6740
|
+
{Math.round(zoom) === preset && <IconCheck className="h-4 w-4" />}
|
|
6741
|
+
</DropdownMenuItem>
|
|
6742
|
+
))}
|
|
6743
|
+
</DropdownMenuContent>
|
|
6744
|
+
</DropdownMenu>
|
|
6745
|
+
);
|
|
6033
6746
|
|
|
6034
|
-
|
|
6747
|
+
const rightSidebarActions = (
|
|
6748
|
+
<div className="shrink-0 border-b border-border bg-[var(--design-editor-panel-bg)] px-2 py-1.5">
|
|
6749
|
+
<div className="flex min-h-8 items-center justify-between gap-2">
|
|
6750
|
+
<DesignCollaboratorsMenu
|
|
6751
|
+
collaborators={designCollaborators}
|
|
6752
|
+
followingEmail={followingEmail}
|
|
6753
|
+
label={t("designEditor.collaborators")}
|
|
6754
|
+
onAvatarClick={handleAvatarClick}
|
|
6755
|
+
/>
|
|
6756
|
+
|
|
6757
|
+
<div className="ml-auto flex shrink-0 items-center gap-1">
|
|
6035
6758
|
<Tooltip>
|
|
6036
6759
|
<TooltipTrigger asChild>
|
|
6037
|
-
<
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
</
|
|
6760
|
+
<Button
|
|
6761
|
+
variant="ghost"
|
|
6762
|
+
size="icon"
|
|
6763
|
+
className="size-8 cursor-pointer rounded-md text-foreground hover:bg-accent hover:text-foreground"
|
|
6764
|
+
onClick={handleOpenDesignPreview}
|
|
6765
|
+
disabled={!activeScreenPreviewUrl && !activeContent.trim()}
|
|
6766
|
+
aria-label={t("designEditor.designPreview")}
|
|
6767
|
+
>
|
|
6768
|
+
<IconPlayerPlay className="size-5" />
|
|
6769
|
+
</Button>
|
|
6047
6770
|
</TooltipTrigger>
|
|
6048
|
-
<TooltipContent>{t("designEditor.
|
|
6771
|
+
<TooltipContent>{t("designEditor.designPreview")}</TooltipContent>
|
|
6049
6772
|
</Tooltip>
|
|
6050
|
-
<DropdownMenuContent align="end" className="w-40">
|
|
6051
|
-
<DropdownMenuItem onClick={handleZoomOut}>
|
|
6052
|
-
<IconZoomOut className="mr-2 h-4 w-4" />
|
|
6053
|
-
{t("designEditor.zoomOut")}
|
|
6054
|
-
</DropdownMenuItem>
|
|
6055
|
-
<DropdownMenuItem onClick={handleZoomIn}>
|
|
6056
|
-
<IconZoomIn className="mr-2 h-4 w-4" />
|
|
6057
|
-
{t("designEditor.zoomIn")}
|
|
6058
|
-
</DropdownMenuItem>
|
|
6059
|
-
<DropdownMenuSeparator />
|
|
6060
|
-
{ZOOM_PRESETS.map((preset) => (
|
|
6061
|
-
<DropdownMenuItem
|
|
6062
|
-
key={preset}
|
|
6063
|
-
onClick={() => setZoom(preset)}
|
|
6064
|
-
className="justify-between"
|
|
6065
|
-
>
|
|
6066
|
-
<span>{preset}%</span>
|
|
6067
|
-
{Math.round(zoom) === preset && (
|
|
6068
|
-
<IconCheck className="h-4 w-4" />
|
|
6069
|
-
)}
|
|
6070
|
-
</DropdownMenuItem>
|
|
6071
|
-
))}
|
|
6072
|
-
</DropdownMenuContent>
|
|
6073
|
-
</DropdownMenu>
|
|
6074
6773
|
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
followingEmail={followingEmail}
|
|
6083
|
-
/>
|
|
6084
|
-
|
|
6085
|
-
<ShareButton
|
|
6086
|
-
resourceType="design"
|
|
6087
|
-
resourceId={id}
|
|
6088
|
-
resourceTitle={design.title}
|
|
6089
|
-
hideTriggerIcon
|
|
6090
|
-
triggerClassName="h-7 rounded-md !border-[var(--design-editor-accent-color)] !bg-[var(--design-editor-accent-color)] px-2 text-xs !text-[var(--design-editor-accent-contrast-color)] shadow-none hover:!border-[var(--design-editor-accent-hover-color)] hover:!bg-[var(--design-editor-accent-hover-color)] hover:!text-[var(--design-editor-accent-contrast-color)] focus-visible:ring-[var(--design-editor-accent-color)] [&_svg]:!text-[var(--design-editor-accent-contrast-color)]"
|
|
6091
|
-
/>
|
|
6774
|
+
<ShareButton
|
|
6775
|
+
resourceType="design"
|
|
6776
|
+
resourceId={id}
|
|
6777
|
+
resourceTitle={design.title}
|
|
6778
|
+
hideTriggerIcon
|
|
6779
|
+
triggerClassName="h-8 rounded-md !border-[var(--design-editor-accent-color)] !bg-[var(--design-editor-accent-color)] px-3 text-sm !text-[var(--design-editor-accent-contrast-color)] shadow-none hover:!border-[var(--design-editor-accent-hover-color)] hover:!bg-[var(--design-editor-accent-hover-color)] hover:!text-[var(--design-editor-accent-contrast-color)] focus-visible:ring-[var(--design-editor-accent-color)] [&_svg]:!text-[var(--design-editor-accent-contrast-color)]"
|
|
6780
|
+
/>
|
|
6092
6781
|
|
|
6093
|
-
|
|
6782
|
+
<AgentToggleButton />
|
|
6783
|
+
</div>
|
|
6094
6784
|
</div>
|
|
6095
6785
|
</div>
|
|
6096
6786
|
);
|
|
@@ -6304,7 +6994,9 @@ ${serializedHtml}
|
|
|
6304
6994
|
>
|
|
6305
6995
|
<TabsList className="pointer-events-auto h-8">
|
|
6306
6996
|
<TabsTrigger value="edit" className="h-6 gap-1 px-2 text-xs">
|
|
6307
|
-
{mode === "edit" &&
|
|
6997
|
+
{mode === "edit" && (
|
|
6998
|
+
<IconTransformPoint className="h-3 w-3" />
|
|
6999
|
+
)}
|
|
6308
7000
|
{t("designEditor.modes.edit")}
|
|
6309
7001
|
</TabsTrigger>
|
|
6310
7002
|
<TabsTrigger
|
|
@@ -6312,7 +7004,9 @@ ${serializedHtml}
|
|
|
6312
7004
|
className="h-6 gap-1 px-2 text-xs"
|
|
6313
7005
|
disabled={!activeFile || viewMode === "overview"}
|
|
6314
7006
|
>
|
|
6315
|
-
{mode === "interact" &&
|
|
7007
|
+
{mode === "interact" && (
|
|
7008
|
+
<IconHandClick className="h-3 w-3" />
|
|
7009
|
+
)}
|
|
6316
7010
|
{t("designEditor.modes.interact")}
|
|
6317
7011
|
</TabsTrigger>
|
|
6318
7012
|
<TabsTrigger
|
|
@@ -6328,31 +7022,6 @@ ${serializedHtml}
|
|
|
6328
7022
|
</div>
|
|
6329
7023
|
)}
|
|
6330
7024
|
<div className="ml-auto flex shrink-0 items-center gap-1 pl-2">
|
|
6331
|
-
{/* Overview / single-screen toggle. Clicking Overview shows every
|
|
6332
|
-
file in the design as a Figma-style pannable lineup. */}
|
|
6333
|
-
<Tooltip>
|
|
6334
|
-
<TooltipTrigger asChild>
|
|
6335
|
-
<Button
|
|
6336
|
-
variant={viewMode === "overview" ? "secondary" : "ghost"}
|
|
6337
|
-
size="icon"
|
|
6338
|
-
className="h-7 w-7 cursor-pointer"
|
|
6339
|
-
onClick={handleViewModeToggle}
|
|
6340
|
-
aria-label={
|
|
6341
|
-
viewMode === "overview"
|
|
6342
|
-
? t("designEditor.returnToCurrentScreen")
|
|
6343
|
-
: t("designEditor.openScreenOverview")
|
|
6344
|
-
}
|
|
6345
|
-
>
|
|
6346
|
-
<IconLayoutGrid className="w-3.5 h-3.5" />
|
|
6347
|
-
</Button>
|
|
6348
|
-
</TooltipTrigger>
|
|
6349
|
-
<TooltipContent>
|
|
6350
|
-
{viewMode === "overview"
|
|
6351
|
-
? t("designEditor.currentScreen")
|
|
6352
|
-
: t("designEditor.screenOverview")}
|
|
6353
|
-
</TooltipContent>
|
|
6354
|
-
</Tooltip>
|
|
6355
|
-
|
|
6356
7025
|
{!embedded && (
|
|
6357
7026
|
<>
|
|
6358
7027
|
{/* Device preview — collapsed into a single menu. */}
|
|
@@ -6488,9 +7157,10 @@ ${serializedHtml}
|
|
|
6488
7157
|
className="relative flex min-h-0 shrink-0 flex-col bg-[var(--design-editor-panel-bg)]"
|
|
6489
7158
|
style={{ width: leftSidebarWidth }}
|
|
6490
7159
|
>
|
|
6491
|
-
<div className="flex h-
|
|
7160
|
+
<div className="flex h-10 shrink-0 items-center gap-1.5 border-b border-border px-2">
|
|
6492
7161
|
{projectMenu}
|
|
6493
7162
|
{projectTitleControl}
|
|
7163
|
+
{deviceFrameControl}
|
|
6494
7164
|
</div>
|
|
6495
7165
|
<div className="min-h-0 flex-1">
|
|
6496
7166
|
<LayersPanel
|
|
@@ -6617,10 +7287,9 @@ ${serializedHtml}
|
|
|
6617
7287
|
pinMode={pinMode}
|
|
6618
7288
|
drawMode={drawMode}
|
|
6619
7289
|
activeTool={activeTool}
|
|
6620
|
-
overviewActive={viewMode === "overview"}
|
|
6621
7290
|
onMove={handleMoveTool}
|
|
6622
7291
|
onFrame={handleFrameTool}
|
|
6623
|
-
|
|
7292
|
+
onShape={handleShapeTool}
|
|
6624
7293
|
onText={handleTextTool}
|
|
6625
7294
|
onPen={handlePenTool}
|
|
6626
7295
|
onHand={handleHandTool}
|
|
@@ -6628,7 +7297,6 @@ ${serializedHtml}
|
|
|
6628
7297
|
onScale={handleScaleTool}
|
|
6629
7298
|
onCommentPin={handlePinToolToggle}
|
|
6630
7299
|
onModeChange={handleModeChange}
|
|
6631
|
-
onScreensToggle={handleViewModeToggle}
|
|
6632
7300
|
/>
|
|
6633
7301
|
)}
|
|
6634
7302
|
|
|
@@ -6672,7 +7340,7 @@ ${serializedHtml}
|
|
|
6672
7340
|
onSelectAll={handleSelectAllFrames}
|
|
6673
7341
|
onZoomToFit={() => {
|
|
6674
7342
|
setViewMode("overview");
|
|
6675
|
-
setActiveTool("
|
|
7343
|
+
setActiveTool("move");
|
|
6676
7344
|
setZoom(100);
|
|
6677
7345
|
}}
|
|
6678
7346
|
onZoomToSelection={() => setZoom(150)}
|
|
@@ -6721,12 +7389,13 @@ ${serializedHtml}
|
|
|
6721
7389
|
onGeometryChange={queueFrameGeometrySave}
|
|
6722
7390
|
onGeometryCommit={handleGeometryCommit}
|
|
6723
7391
|
onCreatePrimitive={handleCreatePrimitive}
|
|
7392
|
+
onCreateScreenFrame={handleCreateScreenFrame}
|
|
6724
7393
|
onDeleteSelection={handleDeleteOverviewSelection}
|
|
6725
7394
|
onPick={(id) => {
|
|
6726
7395
|
setSelectedElement(null);
|
|
6727
7396
|
setSelectedLayerIdsState([id]);
|
|
6728
7397
|
setActiveFileId(id);
|
|
6729
|
-
setActiveTool("
|
|
7398
|
+
setActiveTool("move");
|
|
6730
7399
|
setMode("edit");
|
|
6731
7400
|
}}
|
|
6732
7401
|
onEdit={enterSingleScreen}
|
|
@@ -6935,6 +7604,7 @@ ${serializedHtml}
|
|
|
6935
7604
|
selectedElement={selectedElement}
|
|
6936
7605
|
pageStyles={pageStyles}
|
|
6937
7606
|
zoom={zoom}
|
|
7607
|
+
headerTrailing={zoomControl}
|
|
6938
7608
|
width={rightSidebarWidth}
|
|
6939
7609
|
activeTab={activeInspectorTab}
|
|
6940
7610
|
onActiveTabChange={setActiveInspectorTab}
|