@agent-native/core 0.149.5 → 0.151.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/analytics/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +83 -45
- package/corpus/templates/analytics/changelog/2026-08-11-analytics-dashboards-keep-the-selected-tab-when-reopened.md +6 -0
- package/corpus/templates/assets/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/assets/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/brain/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/brain/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/calendar/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/calendar/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/chat/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/chat/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/clips/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/clips/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/clips/changelog/2026-08-11-clips-recovers-from-transient-updates-and-avoids-macos-priva.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +32 -176
- package/corpus/templates/clips/desktop/src/lib/updater.ts +52 -37
- package/corpus/templates/clips/desktop/src/styles.css +1 -166
- package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +2 -0
- package/corpus/templates/clips/desktop/src-tauri/Info.plist +4 -0
- package/corpus/templates/clips/desktop/src-tauri/src/eventkit.rs +28 -1
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +2 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +48 -1
- package/corpus/templates/clips/desktop/src-tauri/src/permission_status.rs +3 -0
- package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +5 -0
- package/corpus/templates/clips/server/plugins/auth.ts +3 -0
- package/corpus/templates/content/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/content/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/content/actions/_document-discovery-query.ts +127 -0
- package/corpus/templates/content/actions/list-documents.ts +189 -198
- package/corpus/templates/content/actions/search-documents.ts +95 -24
- package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +2 -1
- package/corpus/templates/content/app/hooks/use-documents.ts +84 -6
- package/corpus/templates/content/shared/api.ts +10 -0
- package/corpus/templates/design/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/design/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +5 -1
- package/corpus/templates/design/changelog/2026-08-11-design-agent-prompts-copy-successfully-in-the-desktop-app.md +6 -0
- package/corpus/templates/dispatch/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/dispatch/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/dispatch/app/global.css +2 -2
- package/corpus/templates/dispatch/changelog/2026-08-10-desktop-workspace-sign-in.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-08-11-dispatch-no-longer-shows-the-generic-chat-starter-among-defa.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-08-11-integrations-and-scheduled-work-open-as-first-class-electron-pages.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-08-11-sidebar-text-is-slightly-easier-to-read-in-light-and-dark-mo.md +6 -0
- package/corpus/templates/dispatch/server/lib/identity-sso.ts +57 -31
- package/corpus/templates/dispatch/server/plugins/feature-flags.ts +5 -0
- package/corpus/templates/dispatch/server/plugins/identity-sso.ts +82 -33
- package/corpus/templates/dispatch/server/plugins/org-apps-directory.ts +7 -15
- package/corpus/templates/dispatch/server/plugins/setup-dispatch.ts +9 -1
- package/corpus/templates/dispatch/shared/feature-flags.ts +15 -0
- package/corpus/templates/factory/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/factory/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/forms/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/forms/.agents/skills/form-responses/SKILL.md +8 -7
- package/corpus/templates/forms/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/forms/app/hooks/use-forms.ts +8 -1
- package/corpus/templates/forms/changelog/2026-08-11-fixed-public-forms-so-conditional-questions-stay-hidden-unti.md +6 -0
- package/corpus/templates/forms/server/handlers/submissions.ts +2 -1
- package/corpus/templates/forms/server/lib/public-form-ssr.ts +37 -2
- package/corpus/templates/forms/shared/page-url.ts +54 -0
- package/corpus/templates/forms/shared/types.ts +4 -4
- package/corpus/templates/macros/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/mail/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/mail/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/plan/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/plan/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/slides/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/slides/.agents/skills/frontend-design/SKILL.md +5 -0
- package/corpus/templates/slides/actions/add-slide.ts +0 -27
- package/corpus/templates/slides/actions/import-file.ts +28 -9
- package/corpus/templates/slides/actions/view-screen.ts +4 -4
- package/corpus/templates/slides/app/components/design-system/DesignSystemSetup.tsx +8 -1
- package/corpus/templates/slides/app/components/editor/AiEditingMarker.tsx +25 -0
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +43 -48
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +9 -6
- package/corpus/templates/slides/app/components/editor/GeneratingSlidePreview.tsx +1 -9
- package/corpus/templates/slides/app/components/editor/ImageDropPromptPopover.tsx +2 -0
- package/corpus/templates/slides/app/components/editor/ImageOverlay.tsx +0 -26
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +904 -217
- package/corpus/templates/slides/app/components/editor/canvas-interactions/index.ts +1 -0
- package/corpus/templates/slides/app/components/editor/canvas-interactions/slides-canvas-adapter.ts +11 -0
- package/corpus/templates/slides/app/components/editor/inline-edit-session.ts +16 -0
- package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +22 -6
- package/corpus/templates/slides/app/components/editor/style-clipboard.ts +61 -0
- package/corpus/templates/slides/app/components/layout/Layout.tsx +26 -2
- package/corpus/templates/slides/app/components/ui/context-menu.tsx +1 -0
- package/corpus/templates/slides/app/context/DeckContext.tsx +75 -25
- package/corpus/templates/slides/app/global.css +1 -1
- package/corpus/templates/slides/app/i18n/en-US.ts +6 -1
- package/corpus/templates/slides/app/lib/create-deck-generation.ts +1 -1
- package/corpus/templates/slides/app/lib/image-drop-to-agent.ts +12 -12
- package/corpus/templates/slides/app/lib/slide-agent-context.ts +52 -0
- package/corpus/templates/slides/app/lib/slide-image-replacement.ts +61 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +55 -116
- package/corpus/templates/slides/app/pages/Index.tsx +28 -3
- package/corpus/templates/slides/changelog/2026-08-11-agent-chat-now-follows-the-current-slide-and-selection.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-deck-titles-now-use-slide-content-instead-of-placeholder-filenames.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-history-now-opens-from-the-deck-overflow-menu.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-images-can-be-dropped-directly-onto-slides-and-resized-or-re.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-new-decks-now-transition-directly-to-a-full-page-editor-load.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-selected-slide-layers-can-be-nudged-precisely-with-arrow-key.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-slides-can-copy-and-paste-element-styles-with-cmd-option-c-v.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-slides-shows-one-compact-ai-marker-and-keeps-your-current-sl.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-text-edits-preserve-slide-layout-when-no-changes-are-made.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-the-style-toolbar-now-uses-the-full-available-width.md +6 -0
- package/corpus/templates/slides/changelog/2026-08-11-undo-and-redo-now-move-one-visible-slide-state-at-a-time.md +6 -0
- package/corpus/templates/slides/server/lib/chat-attachments.ts +6 -2
- package/corpus/templates/slides/shared/deck-title.ts +56 -0
- package/corpus/templates/tasks/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/corpus/templates/tasks/.agents/skills/frontend-design/SKILL.md +5 -0
- package/dist/agent/run-store.js +6 -1
- package/dist/agent/thread-data-builder.js +19 -44
- package/dist/agent/types.d.ts +13 -0
- package/dist/catalog.json +1 -0
- package/dist/cli/create.js +1 -1
- package/dist/client/AssistantChat.js +58 -10
- package/dist/client/FileStorageSetupCard.d.ts +6 -0
- package/dist/client/FileStorageSetupCard.js +18 -0
- package/dist/client/NewWorkspaceAppFlow.js +10 -0
- package/dist/client/agent-chat-adapter.js +87 -11
- package/dist/client/agent-chat.d.ts +1 -1
- package/dist/client/agent-chat.js +8 -3
- package/dist/client/chat/attachment-adapters.js +10 -0
- package/dist/client/chat/message-components.d.ts +4 -1
- package/dist/client/chat/message-components.js +37 -13
- package/dist/client/chat/tool-call-display.js +14 -0
- package/dist/client/chat-first/apps-rail.d.ts +1 -1
- package/dist/client/chat-first/apps-rail.js +6 -6
- package/dist/client/chat-first/copy.js +1 -0
- package/dist/client/chat-first/types.d.ts +1 -0
- package/dist/client/extensions/EmbeddedExtension.js +1 -1
- package/dist/client/extensions/ExtensionViewer.js +1 -1
- package/dist/client/extensions/InlineExtensionFrame.js +1 -1
- package/dist/client/onboarding/FirstRunOnboarding.js +17 -2
- package/dist/client/settings/FileStorageSettingsForm.d.ts +2 -0
- package/dist/client/settings/FileStorageSettingsForm.js +113 -0
- package/dist/client/settings/SettingsPanel.js +2 -1
- package/dist/client/sse-event-processor.js +21 -0
- package/dist/extensions/actions.js +2 -2
- package/dist/extensions/html-shell.js +3 -1
- package/dist/file-upload/pre-upload-attachments.d.ts +15 -10
- package/dist/file-upload/pre-upload-attachments.js +68 -24
- package/dist/file-upload/registry.d.ts +4 -5
- package/dist/file-upload/registry.js +7 -14
- package/dist/file-upload/s3.d.ts +9 -0
- package/dist/file-upload/s3.js +168 -0
- package/dist/file-upload/types.d.ts +3 -3
- package/dist/file-upload/types.js +2 -2
- package/dist/onboarding/app-profile.js +23 -1
- package/dist/onboarding/default-steps.js +69 -0
- package/dist/onboarding/types.d.ts +2 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +6 -6
- package/dist/server/agent-chat/browser-team-tools.js +20 -0
- package/dist/server/agent-chat-plugin.js +12 -3
- package/dist/server/auth.js +45 -21
- package/dist/server/better-auth-instance.d.ts +2 -0
- package/dist/server/better-auth-instance.js +2 -2
- package/dist/server/core-routes-plugin.d.ts +1 -0
- package/dist/server/core-routes-plugin.js +26 -16
- package/dist/server/identity-sso-store.d.ts +3 -0
- package/dist/server/identity-sso-store.js +45 -0
- package/dist/server/identity-sso.d.ts +20 -10
- package/dist/server/identity-sso.js +79 -31
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +1 -1
- package/dist/templates/chat/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/dist/templates/chat/.agents/skills/frontend-design/SKILL.md +5 -0
- package/dist/templates/default/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/dist/templates/default/.agents/skills/frontend-design/SKILL.md +5 -0
- package/dist/templates/workspace-core/.agents/skills/authentication/SKILL.md +24 -1
- package/dist/templates/workspace-core/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -2
- package/dist/templates/workspace-core/.agents/skills/extensions/references/api.md +1 -1
- package/dist/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +5 -0
- package/dist/templates/workspace-core/.agents/skills/generative-ui/SKILL.md +3 -1
- package/docs/content/authentication.mdx +27 -0
- package/docs/content/cross-app-sso.mdx +38 -8
- package/docs/content/extensions.mdx +15 -15
- package/docs/content/file-uploads.mdx +10 -12
- package/docs/content/generative-ui.mdx +6 -1
- package/package.json +2 -2
- package/src/templates/chat/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/src/templates/chat/.agents/skills/frontend-design/SKILL.md +5 -0
- package/src/templates/default/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/src/templates/default/.agents/skills/frontend-design/SKILL.md +5 -0
- package/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +24 -1
- package/src/templates/workspace-core/.agents/skills/delegate-to-agent/SKILL.md +28 -0
- package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +6 -2
- package/src/templates/workspace-core/.agents/skills/extensions/references/api.md +1 -1
- package/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +5 -0
- package/src/templates/workspace-core/.agents/skills/generative-ui/SKILL.md +3 -1
|
@@ -18,7 +18,13 @@ import {
|
|
|
18
18
|
import { appStateKeyForBrowserTab } from "@shared/app-state-tabs";
|
|
19
19
|
import { hashSlideContent } from "@shared/slide-fit";
|
|
20
20
|
import {
|
|
21
|
+
IconBrush,
|
|
22
|
+
IconClipboard,
|
|
23
|
+
IconCopy,
|
|
21
24
|
IconMaximize,
|
|
25
|
+
IconStackBack,
|
|
26
|
+
IconStackFront,
|
|
27
|
+
IconTrash,
|
|
22
28
|
IconX,
|
|
23
29
|
IconZoomIn,
|
|
24
30
|
IconZoomOut,
|
|
@@ -45,6 +51,14 @@ import {
|
|
|
45
51
|
ZERO_WIDTH_SPACE,
|
|
46
52
|
} from "@/components/editor/bullet-editing";
|
|
47
53
|
import { Button } from "@/components/ui/button";
|
|
54
|
+
import {
|
|
55
|
+
ContextMenu,
|
|
56
|
+
ContextMenuContent,
|
|
57
|
+
ContextMenuItem,
|
|
58
|
+
ContextMenuSeparator,
|
|
59
|
+
ContextMenuShortcut,
|
|
60
|
+
ContextMenuTrigger,
|
|
61
|
+
} from "@/components/ui/context-menu";
|
|
48
62
|
import {
|
|
49
63
|
Tooltip,
|
|
50
64
|
TooltipContent,
|
|
@@ -63,11 +77,14 @@ import {
|
|
|
63
77
|
MIN_CANVAS_ZOOM,
|
|
64
78
|
} from "@/lib/canvas-zoom";
|
|
65
79
|
import { extractMermaidBlocks } from "@/lib/mermaid-blocks";
|
|
80
|
+
import { publishSlidesSelection } from "@/lib/slide-agent-context";
|
|
66
81
|
import {
|
|
67
82
|
createPlaceholderImageTarget,
|
|
68
83
|
imageFileLooksSupported,
|
|
84
|
+
type SlideImageDropPosition,
|
|
69
85
|
} from "@/lib/slide-image-replacement";
|
|
70
86
|
import { TAB_ID } from "@/lib/tab-id";
|
|
87
|
+
import { shortcutLabel } from "@/lib/utils";
|
|
71
88
|
import { enterSelectionMode } from "@/root";
|
|
72
89
|
|
|
73
90
|
import type { DesignSystemData } from "../../../shared/api";
|
|
@@ -77,9 +94,14 @@ import {
|
|
|
77
94
|
isWithinSlidesCanvasEdgeMoveBand,
|
|
78
95
|
resolveSlidesCanvasPointerIntent,
|
|
79
96
|
resolveSlidesCanvasDragTarget,
|
|
97
|
+
resolveSlidesCanvasNudge,
|
|
80
98
|
slidesCanvasInteractionCore,
|
|
81
99
|
} from "./canvas-interactions";
|
|
82
100
|
import ImageOverlay from "./ImageOverlay";
|
|
101
|
+
import {
|
|
102
|
+
shouldPersistInlineEditContent,
|
|
103
|
+
type InlineEditContentSnapshot,
|
|
104
|
+
} from "./inline-edit-session";
|
|
83
105
|
import {
|
|
84
106
|
detectSlideListKind,
|
|
85
107
|
toggleSlideList,
|
|
@@ -121,6 +143,7 @@ import {
|
|
|
121
143
|
getSlideSelectionMode,
|
|
122
144
|
findPersistedImageObject,
|
|
123
145
|
isDeletableFlowImage,
|
|
146
|
+
isDeletableSlideElement,
|
|
124
147
|
removeSlideObjectAndLayoutSpacer,
|
|
125
148
|
resolveSlideObjectContainingBlock,
|
|
126
149
|
type CopiedSlideObjects,
|
|
@@ -142,6 +165,12 @@ import {
|
|
|
142
165
|
import { SlideContextToolbar } from "./SlideContextToolbar";
|
|
143
166
|
import { SlideOverflowWarning } from "./SlideOverflowWarning";
|
|
144
167
|
import { SpeakerNotesPanel } from "./SpeakerNotesPanel";
|
|
168
|
+
import {
|
|
169
|
+
copiedElementStyleFromSnapshot,
|
|
170
|
+
getCopiedElementStyle,
|
|
171
|
+
setCopiedElementStyle,
|
|
172
|
+
type CopiedElementStyle,
|
|
173
|
+
} from "./style-clipboard";
|
|
145
174
|
|
|
146
175
|
function ExcalidrawExitButton(props: { onExit: () => void; label: string }) {
|
|
147
176
|
return (
|
|
@@ -166,10 +195,9 @@ let builderIdCounter = 0;
|
|
|
166
195
|
const CANVAS_ZOOM_PRESETS = [10, 25, 50, 75, 100, 125, 150, 200] as const;
|
|
167
196
|
|
|
168
197
|
/**
|
|
169
|
-
*
|
|
170
|
-
* selection. A flow-layout node
|
|
171
|
-
*
|
|
172
|
-
* object.
|
|
198
|
+
* Operations that require an already-persisted freeform object are narrower
|
|
199
|
+
* than ordinary canvas selection. A flow-layout node can still be promoted by
|
|
200
|
+
* a thresholded drag, but it is not eligible for copy or arrange beforehand.
|
|
173
201
|
*/
|
|
174
202
|
function isPersistedFreeformObject(element: HTMLElement): boolean {
|
|
175
203
|
return (
|
|
@@ -237,7 +265,14 @@ function stripBuilderIds(html: string): string {
|
|
|
237
265
|
parent.removeChild(wrapper);
|
|
238
266
|
}
|
|
239
267
|
const stripRoot = doc.querySelector("[data-strip-root]");
|
|
240
|
-
if (stripRoot)
|
|
268
|
+
if (stripRoot) {
|
|
269
|
+
stripPlaceholderZws(stripRoot);
|
|
270
|
+
// Layout spacers only keep an in-flow element's slot stable while a
|
|
271
|
+
// drag is live. They are editor chrome, not part of the persisted slide.
|
|
272
|
+
stripRoot.querySelectorAll(".fmd-layout-spacer").forEach((spacer) => {
|
|
273
|
+
spacer.remove();
|
|
274
|
+
});
|
|
275
|
+
}
|
|
241
276
|
cleaned = stripRoot?.innerHTML ?? doc.body.innerHTML;
|
|
242
277
|
}
|
|
243
278
|
|
|
@@ -417,7 +452,10 @@ function buildStyleSnapshot(
|
|
|
417
452
|
isText:
|
|
418
453
|
element.tagName !== "IMG" &&
|
|
419
454
|
(!!textPreview || element.classList.contains("fmd-text-box")),
|
|
420
|
-
isImage:
|
|
455
|
+
isImage:
|
|
456
|
+
element.tagName === "IMG" ||
|
|
457
|
+
element.classList.contains("fmd-pptx-image") ||
|
|
458
|
+
element.getAttribute("data-pptx-element-kind") === "image",
|
|
421
459
|
color:
|
|
422
460
|
selectedColor === null || selectedColor === undefined
|
|
423
461
|
? normalizedColor(computed.color)
|
|
@@ -488,7 +526,7 @@ function selectionItemForElement(
|
|
|
488
526
|
imageSrc:
|
|
489
527
|
element instanceof HTMLImageElement
|
|
490
528
|
? (element.getAttribute("src") ?? undefined)
|
|
491
|
-
: undefined,
|
|
529
|
+
: (element.querySelector("img")?.getAttribute("src") ?? undefined),
|
|
492
530
|
style: snapshot ? { ...snapshot, selector: identity.selector } : undefined,
|
|
493
531
|
};
|
|
494
532
|
}
|
|
@@ -508,13 +546,22 @@ function syncSelectionToAppState(state: SlidesSelectionState | null) {
|
|
|
508
546
|
requestSource: TAB_ID,
|
|
509
547
|
}).catch(() => {});
|
|
510
548
|
}
|
|
511
|
-
const generic = state
|
|
549
|
+
const generic = state
|
|
550
|
+
? {
|
|
551
|
+
deckId: state.deckId,
|
|
552
|
+
slideId: state.slideId,
|
|
553
|
+
slideIndex: state.slideIndex,
|
|
554
|
+
slideNumber: state.slideNumber,
|
|
555
|
+
items: state.items,
|
|
556
|
+
}
|
|
557
|
+
: null;
|
|
512
558
|
for (const key of genericKeys) {
|
|
513
559
|
setClientAppState(key, generic, {
|
|
514
560
|
keepalive: true,
|
|
515
561
|
requestSource: TAB_ID,
|
|
516
562
|
}).catch(() => {});
|
|
517
563
|
}
|
|
564
|
+
publishSlidesSelection(state);
|
|
518
565
|
}
|
|
519
566
|
|
|
520
567
|
interface SlideEditorProps {
|
|
@@ -539,12 +586,10 @@ interface SlideEditorProps {
|
|
|
539
586
|
onGenerateImage: () => void;
|
|
540
587
|
onOpenAssetLibrary: (replaceSrc: string) => void;
|
|
541
588
|
onUploadImage: (replaceSrc: string) => void;
|
|
542
|
-
onSearchImage: (replaceSrc: string) => void;
|
|
543
|
-
onLogoSearch: (replaceSrc: string) => void;
|
|
544
589
|
onDropImage?: (
|
|
545
590
|
replaceSrc: string | null,
|
|
546
591
|
file: File,
|
|
547
|
-
position?:
|
|
592
|
+
position?: SlideImageDropPosition,
|
|
548
593
|
) => void;
|
|
549
594
|
/** Fired when an image is dragged from elsewhere in the app (e.g. a
|
|
550
595
|
* generated-image preview in the agent chat panel) and dropped on the
|
|
@@ -552,7 +597,7 @@ interface SlideEditorProps {
|
|
|
552
597
|
onDropImageUrl?: (
|
|
553
598
|
replaceSrc: string | null,
|
|
554
599
|
url: string,
|
|
555
|
-
position?:
|
|
600
|
+
position?: SlideImageDropPosition,
|
|
556
601
|
) => void;
|
|
557
602
|
onToggleObjectFit: (imgSrc: string, newFit: string) => void;
|
|
558
603
|
/** Current user display info for cursor caret */
|
|
@@ -1003,8 +1048,6 @@ export default function SlideEditor({
|
|
|
1003
1048
|
onGenerateImage,
|
|
1004
1049
|
onOpenAssetLibrary,
|
|
1005
1050
|
onUploadImage,
|
|
1006
|
-
onSearchImage,
|
|
1007
|
-
onLogoSearch,
|
|
1008
1051
|
onDropImage,
|
|
1009
1052
|
onDropImageUrl,
|
|
1010
1053
|
onToggleObjectFit,
|
|
@@ -1104,6 +1147,10 @@ export default function SlideEditor({
|
|
|
1104
1147
|
);
|
|
1105
1148
|
const [selectedStyleSnapshot, setSelectedStyleSnapshot] =
|
|
1106
1149
|
useState<SlideStyleSnapshot | null>(null);
|
|
1150
|
+
const [hasCopiedElementStyle, setHasCopiedElementStyle] = useState(
|
|
1151
|
+
() => getCopiedElementStyle() !== null,
|
|
1152
|
+
);
|
|
1153
|
+
const contextMenuTargetRef = useRef<HTMLElement | null>(null);
|
|
1107
1154
|
const selectionOverlayMeasurementKey = createSelectionOverlayMeasurementKey({
|
|
1108
1155
|
slideId: slide.id,
|
|
1109
1156
|
content,
|
|
@@ -1144,8 +1191,12 @@ export default function SlideEditor({
|
|
|
1144
1191
|
const repairRequestTimerRef = useRef<ReturnType<typeof setTimeout> | null>(
|
|
1145
1192
|
null,
|
|
1146
1193
|
);
|
|
1147
|
-
const [
|
|
1148
|
-
useState(
|
|
1194
|
+
const [dismissedOverflowWarningKey, setDismissedOverflowWarningKey] =
|
|
1195
|
+
useState<string | null>(null);
|
|
1196
|
+
const overflowWarningKey = `${slide.id}:${hashSlideContent(slide.content)}`;
|
|
1197
|
+
const warningVisible = dismissedOverflowWarningKey !== overflowWarningKey;
|
|
1198
|
+
const dismissWarning = () =>
|
|
1199
|
+
setDismissedOverflowWarningKey(overflowWarningKey);
|
|
1149
1200
|
const dims = getAspectRatioDims(aspectRatio);
|
|
1150
1201
|
const [fitCanvasZoom, setFitCanvasZoom] = useState(100);
|
|
1151
1202
|
const userSetCanvasZoomRef = useRef(false);
|
|
@@ -1260,7 +1311,9 @@ export default function SlideEditor({
|
|
|
1260
1311
|
}, [dims.width]);
|
|
1261
1312
|
|
|
1262
1313
|
// Reset overflow state whenever the slide changes — the renderer will
|
|
1263
|
-
// report the next measurement (or stay null if the new slide fits).
|
|
1314
|
+
// report the next measurement (or stay null if the new slide fits). The
|
|
1315
|
+
// dismissal key is derived from slide content, so a delayed save cannot
|
|
1316
|
+
// make the same warning reappear after the user closes it.
|
|
1264
1317
|
useEffect(() => {
|
|
1265
1318
|
if (repairRequestTimerRef.current) {
|
|
1266
1319
|
clearTimeout(repairRequestTimerRef.current);
|
|
@@ -1268,7 +1321,6 @@ export default function SlideEditor({
|
|
|
1268
1321
|
}
|
|
1269
1322
|
setOverflowInfo(null);
|
|
1270
1323
|
setIsAskingAgentToFix(false);
|
|
1271
|
-
setIsOverflowWarningDismissed(false);
|
|
1272
1324
|
syncOverflowToAppState(null);
|
|
1273
1325
|
}, [slide.id, slide.content]);
|
|
1274
1326
|
|
|
@@ -1316,7 +1368,7 @@ export default function SlideEditor({
|
|
|
1316
1368
|
horizontalOverflow: info.horizontalOverflow,
|
|
1317
1369
|
});
|
|
1318
1370
|
},
|
|
1319
|
-
[slide.id, deckId],
|
|
1371
|
+
[slide.id, slide.content, deckId],
|
|
1320
1372
|
);
|
|
1321
1373
|
|
|
1322
1374
|
const handleAskAgentToFixLayout = useCallback(() => {
|
|
@@ -1404,10 +1456,10 @@ export default function SlideEditor({
|
|
|
1404
1456
|
useEffect(() => {
|
|
1405
1457
|
onUpdateSlideRef.current = onUpdateSlide;
|
|
1406
1458
|
}, [onUpdateSlide]);
|
|
1407
|
-
const inlineEditDraftRef = useRef<
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1459
|
+
const inlineEditDraftRef = useRef<InlineEditContentSnapshot | null>(null);
|
|
1460
|
+
const inlineEditInitialContentRef = useRef<InlineEditContentSnapshot | null>(
|
|
1461
|
+
null,
|
|
1462
|
+
);
|
|
1411
1463
|
const previousSlideIdRef = useRef(slide.id);
|
|
1412
1464
|
|
|
1413
1465
|
const readCurrentSlideContentHtml = useCallback(() => {
|
|
@@ -1466,6 +1518,12 @@ export default function SlideEditor({
|
|
|
1466
1518
|
(slideId = slide.id) => {
|
|
1467
1519
|
const html = readCurrentSlideContentHtml();
|
|
1468
1520
|
if (html !== null) {
|
|
1521
|
+
if (
|
|
1522
|
+
!inlineEditInitialContentRef.current ||
|
|
1523
|
+
inlineEditInitialContentRef.current.slideId !== slideId
|
|
1524
|
+
) {
|
|
1525
|
+
inlineEditInitialContentRef.current = { slideId, content: html };
|
|
1526
|
+
}
|
|
1469
1527
|
inlineEditDraftRef.current = { slideId, content: html };
|
|
1470
1528
|
}
|
|
1471
1529
|
},
|
|
@@ -1483,8 +1541,8 @@ export default function SlideEditor({
|
|
|
1483
1541
|
|
|
1484
1542
|
/**
|
|
1485
1543
|
* Turn a normal layout block into a freeform object at its current visual
|
|
1486
|
-
* coordinates. A hidden same-size sibling
|
|
1487
|
-
* the rest of the slide does not reflow
|
|
1544
|
+
* coordinates once the user starts moving it. A hidden same-size sibling
|
|
1545
|
+
* retains the flex/grid slot, so the rest of the slide does not reflow.
|
|
1488
1546
|
*/
|
|
1489
1547
|
const freezeElementForFreeformSelection = useCallback(
|
|
1490
1548
|
(
|
|
@@ -1715,7 +1773,7 @@ export default function SlideEditor({
|
|
|
1715
1773
|
],
|
|
1716
1774
|
);
|
|
1717
1775
|
|
|
1718
|
-
/** Exit edit mode, saving
|
|
1776
|
+
/** Exit edit mode, saving changed content without changing its layout. */
|
|
1719
1777
|
const exitInlineEdit = useCallback(() => {
|
|
1720
1778
|
const el = editingElRef.current;
|
|
1721
1779
|
if (!el) return;
|
|
@@ -1724,16 +1782,25 @@ export default function SlideEditor({
|
|
|
1724
1782
|
el.contentEditable = "false";
|
|
1725
1783
|
el.removeAttribute("data-editing-block");
|
|
1726
1784
|
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1785
|
+
// Selection and freeform promotion are separate operations. Merely ending
|
|
1786
|
+
// text editing must not turn a flow-layout block into an absolutely
|
|
1787
|
+
// positioned object, because that changes its available wrapping width.
|
|
1788
|
+
const selected = el;
|
|
1789
|
+
const selector = getBuilderSelector(selected);
|
|
1730
1790
|
|
|
1731
1791
|
const html = selected ? readCurrentSlideContentHtml() : null;
|
|
1732
|
-
|
|
1733
|
-
if (
|
|
1792
|
+
const initial = inlineEditInitialContentRef.current;
|
|
1793
|
+
if (
|
|
1794
|
+
html !== null &&
|
|
1795
|
+
shouldPersistInlineEditContent(initial, {
|
|
1796
|
+
slideId: slide.id,
|
|
1797
|
+
content: html,
|
|
1798
|
+
})
|
|
1799
|
+
) {
|
|
1734
1800
|
onUpdateSlideRef.current({ content: html });
|
|
1735
1801
|
}
|
|
1736
1802
|
inlineEditDraftRef.current = null;
|
|
1803
|
+
inlineEditInitialContentRef.current = null;
|
|
1737
1804
|
const escape = slidesCanvasInteractionCore.escape({
|
|
1738
1805
|
editingObjectId: "inline-editing",
|
|
1739
1806
|
selectedObjectIds: resolveSelectedElement() ? ["selected"] : [],
|
|
@@ -1751,7 +1818,7 @@ export default function SlideEditor({
|
|
|
1751
1818
|
readCurrentSlideContentHtml,
|
|
1752
1819
|
resolveSelectedElement,
|
|
1753
1820
|
selectElementForStyling,
|
|
1754
|
-
|
|
1821
|
+
slide.id,
|
|
1755
1822
|
clearSelectedElement,
|
|
1756
1823
|
syncSelectionToAppState,
|
|
1757
1824
|
]);
|
|
@@ -1805,10 +1872,15 @@ export default function SlideEditor({
|
|
|
1805
1872
|
richTextSelectionRef.current = null;
|
|
1806
1873
|
setEditingEl(null);
|
|
1807
1874
|
|
|
1808
|
-
|
|
1875
|
+
const initial = inlineEditInitialContentRef.current;
|
|
1876
|
+
if (
|
|
1877
|
+
draft?.slideId === previousSlideId &&
|
|
1878
|
+
shouldPersistInlineEditContent(initial, draft)
|
|
1879
|
+
) {
|
|
1809
1880
|
onUpdateSlideRef.current({ content: draft.content }, previousSlideId);
|
|
1810
1881
|
inlineEditDraftRef.current = null;
|
|
1811
1882
|
}
|
|
1883
|
+
inlineEditInitialContentRef.current = null;
|
|
1812
1884
|
|
|
1813
1885
|
previousSlideIdRef.current = slide.id;
|
|
1814
1886
|
}, [slide.id]);
|
|
@@ -2082,6 +2154,7 @@ export default function SlideEditor({
|
|
|
2082
2154
|
return;
|
|
2083
2155
|
setSelectedImg(null);
|
|
2084
2156
|
setImageOverlay(null);
|
|
2157
|
+
syncSelectionToAppState(null);
|
|
2085
2158
|
};
|
|
2086
2159
|
document.addEventListener("mousedown", handleClick);
|
|
2087
2160
|
return () => document.removeEventListener("mousedown", handleClick);
|
|
@@ -2091,6 +2164,7 @@ export default function SlideEditor({
|
|
|
2091
2164
|
useEffect(() => {
|
|
2092
2165
|
setSelectedImg(null);
|
|
2093
2166
|
setImageOverlay(null);
|
|
2167
|
+
syncSelectionToAppState(null);
|
|
2094
2168
|
}, [slide.id]);
|
|
2095
2169
|
|
|
2096
2170
|
// Stamp all elements with data-builder-id after render
|
|
@@ -2127,15 +2201,22 @@ export default function SlideEditor({
|
|
|
2127
2201
|
`[data-builder-id="${id}"]`,
|
|
2128
2202
|
) as HTMLElement | null;
|
|
2129
2203
|
if (!el) return;
|
|
2130
|
-
const
|
|
2204
|
+
const runtimeSelector = `[data-builder-id="${id}"]`;
|
|
2131
2205
|
const text = (el.textContent || "").trim().slice(0, 200);
|
|
2132
|
-
|
|
2133
|
-
|
|
2206
|
+
const identity = getSlideSelectionIdentity(el, runtimeSelector);
|
|
2207
|
+
rects.set(id, {
|
|
2208
|
+
rect: el.getBoundingClientRect(),
|
|
2209
|
+
text,
|
|
2210
|
+
selector: identity.selector,
|
|
2211
|
+
});
|
|
2212
|
+
items.push(selectionItemForElement(el, runtimeSelector));
|
|
2134
2213
|
});
|
|
2135
2214
|
}
|
|
2136
2215
|
setMultiSelection(ids);
|
|
2137
2216
|
setMultiSelectionRects(rects);
|
|
2138
2217
|
if (ids.size > 0) clearSelectedElement();
|
|
2218
|
+
setSelectedImg(null);
|
|
2219
|
+
setImageOverlay(null);
|
|
2139
2220
|
// Anchor the chip to the slide canvas (clickable wrapper)
|
|
2140
2221
|
const canvas = containerRef.current?.querySelector(
|
|
2141
2222
|
".slide-image-clickable",
|
|
@@ -2328,7 +2409,8 @@ export default function SlideEditor({
|
|
|
2328
2409
|
next.set(id, {
|
|
2329
2410
|
rect: el.getBoundingClientRect(),
|
|
2330
2411
|
text: (el.textContent || "").trim().slice(0, 200),
|
|
2331
|
-
selector: `[data-builder-id="${id}"]
|
|
2412
|
+
selector: getSlideSelectionIdentity(el, `[data-builder-id="${id}"]`)
|
|
2413
|
+
.selector,
|
|
2332
2414
|
});
|
|
2333
2415
|
});
|
|
2334
2416
|
setMultiSelectionRects(next);
|
|
@@ -2365,10 +2447,74 @@ export default function SlideEditor({
|
|
|
2365
2447
|
syncSelectionToAppState(null);
|
|
2366
2448
|
}, [clearSelectedElement, slide.id]);
|
|
2367
2449
|
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2450
|
+
const deleteSelectedElements = useCallback(() => {
|
|
2451
|
+
const slideContent = getSlideContent();
|
|
2452
|
+
if (!slideContent) return false;
|
|
2453
|
+
|
|
2454
|
+
if (multiSelection.size > 0) {
|
|
2455
|
+
const selected = Array.from(multiSelection)
|
|
2456
|
+
.map(
|
|
2457
|
+
(id) =>
|
|
2458
|
+
slideContent.querySelector(
|
|
2459
|
+
`[data-builder-id="${id}"]`,
|
|
2460
|
+
) as HTMLElement | null,
|
|
2461
|
+
)
|
|
2462
|
+
.filter((element): element is HTMLElement => element !== null);
|
|
2463
|
+
const roots = selected.filter(
|
|
2464
|
+
(element) =>
|
|
2465
|
+
!selected.some(
|
|
2466
|
+
(candidate) => candidate !== element && candidate.contains(element),
|
|
2467
|
+
),
|
|
2468
|
+
);
|
|
2469
|
+
if (
|
|
2470
|
+
roots.length === 0 ||
|
|
2471
|
+
roots.some((element) => !isDeletableSlideElement(element))
|
|
2472
|
+
) {
|
|
2473
|
+
return false;
|
|
2474
|
+
}
|
|
2475
|
+
for (const element of roots) {
|
|
2476
|
+
removeSlideObjectAndLayoutSpacer(element);
|
|
2477
|
+
}
|
|
2478
|
+
clearMultiSelection();
|
|
2479
|
+
} else {
|
|
2480
|
+
const element = resolveSelectedElement();
|
|
2481
|
+
if (!element) return false;
|
|
2482
|
+
if (!isDeletableSlideElement(element)) {
|
|
2483
|
+
return false;
|
|
2484
|
+
}
|
|
2485
|
+
if (isDeletableFlowImage(element)) {
|
|
2486
|
+
// Imported images can be nested inside an absolutely-positioned owner;
|
|
2487
|
+
// remove that owner so its persisted metadata cannot become a ghost.
|
|
2488
|
+
const owner = findPersistedImageObject(element, slideContent);
|
|
2489
|
+
if (owner) removeSlideObjectAndLayoutSpacer(owner);
|
|
2490
|
+
else element.remove();
|
|
2491
|
+
setSelectedImg(null);
|
|
2492
|
+
setImageOverlay(null);
|
|
2493
|
+
} else {
|
|
2494
|
+
removeSlideObjectAndLayoutSpacer(element);
|
|
2495
|
+
}
|
|
2496
|
+
clearSelectedElement();
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
const html = readCurrentSlideContentHtml();
|
|
2500
|
+
if (html !== null) {
|
|
2501
|
+
onUpdateSlideRef.current({ content: html, animations: [] });
|
|
2502
|
+
}
|
|
2503
|
+
syncSelectionToAppState(null);
|
|
2504
|
+
return true;
|
|
2505
|
+
}, [
|
|
2506
|
+
clearMultiSelection,
|
|
2507
|
+
clearSelectedElement,
|
|
2508
|
+
getSlideContent,
|
|
2509
|
+
multiSelection,
|
|
2510
|
+
readCurrentSlideContentHtml,
|
|
2511
|
+
resolveSelectedElement,
|
|
2512
|
+
]);
|
|
2513
|
+
|
|
2514
|
+
// Delete/Backspace removes the selected slide content (single or
|
|
2515
|
+
// multi-select). Only active when something is selected for styling (not
|
|
2516
|
+
// while inline-editing text, where Backspace should delete a character) and
|
|
2517
|
+
// not while the browser focus is in an unrelated input.
|
|
2372
2518
|
useEffect(() => {
|
|
2373
2519
|
if (editingEl) return;
|
|
2374
2520
|
if (multiSelection.size === 0 && !selectedElementSelector) return;
|
|
@@ -2378,77 +2524,15 @@ export default function SlideEditor({
|
|
|
2378
2524
|
const tag = active?.tagName;
|
|
2379
2525
|
if (tag === "INPUT" || tag === "TEXTAREA") return;
|
|
2380
2526
|
if (active instanceof HTMLElement && active.isContentEditable) return;
|
|
2381
|
-
|
|
2382
|
-
const slideContent = getSlideContent();
|
|
2383
|
-
if (!slideContent) return;
|
|
2384
|
-
|
|
2385
|
-
if (multiSelection.size > 0) {
|
|
2386
|
-
const selected = Array.from(multiSelection)
|
|
2387
|
-
.map(
|
|
2388
|
-
(id) =>
|
|
2389
|
-
slideContent.querySelector(
|
|
2390
|
-
`[data-builder-id="${id}"]`,
|
|
2391
|
-
) as HTMLElement | null,
|
|
2392
|
-
)
|
|
2393
|
-
.filter((element): element is HTMLElement => element !== null);
|
|
2394
|
-
const roots = selected.filter(
|
|
2395
|
-
(element) =>
|
|
2396
|
-
!selected.some(
|
|
2397
|
-
(candidate) =>
|
|
2398
|
-
candidate !== element && candidate.contains(element),
|
|
2399
|
-
),
|
|
2400
|
-
);
|
|
2401
|
-
if (
|
|
2402
|
-
roots.length === 0 ||
|
|
2403
|
-
roots.some((element) => !isPersistedFreeformObject(element))
|
|
2404
|
-
) {
|
|
2405
|
-
return;
|
|
2406
|
-
}
|
|
2407
|
-
e.preventDefault();
|
|
2408
|
-
for (const element of roots) {
|
|
2409
|
-
removeSlideObjectAndLayoutSpacer(element);
|
|
2410
|
-
}
|
|
2411
|
-
clearMultiSelection();
|
|
2412
|
-
} else {
|
|
2413
|
-
const element = resolveSelectedElement();
|
|
2414
|
-
if (!element) return;
|
|
2415
|
-
if (isPersistedFreeformObject(element)) {
|
|
2416
|
-
e.preventDefault();
|
|
2417
|
-
removeSlideObjectAndLayoutSpacer(element);
|
|
2418
|
-
} else if (isDeletableFlowImage(element)) {
|
|
2419
|
-
e.preventDefault();
|
|
2420
|
-
// An imported PPTX/PDF image is an <img> (or placeholder) nested in
|
|
2421
|
-
// an absolutely positioned wrapper that carries the persisted object
|
|
2422
|
-
// id. Removing only the inner node would leave that wrapper and its
|
|
2423
|
-
// durable metadata behind as an invisible ghost object.
|
|
2424
|
-
const owner = findPersistedImageObject(element, slideContent);
|
|
2425
|
-
if (owner) removeSlideObjectAndLayoutSpacer(owner);
|
|
2426
|
-
else element.remove();
|
|
2427
|
-
setSelectedImg(null);
|
|
2428
|
-
setImageOverlay(null);
|
|
2429
|
-
} else {
|
|
2430
|
-
return;
|
|
2431
|
-
}
|
|
2432
|
-
clearSelectedElement();
|
|
2433
|
-
}
|
|
2434
|
-
|
|
2435
|
-
const html = readCurrentSlideContentHtml();
|
|
2436
|
-
if (html !== null) onUpdateSlideRef.current({ content: html });
|
|
2437
|
-
syncSelectionToAppState(null);
|
|
2527
|
+
if (deleteSelectedElements()) e.preventDefault();
|
|
2438
2528
|
};
|
|
2439
2529
|
window.addEventListener("keydown", onKey);
|
|
2440
2530
|
return () => window.removeEventListener("keydown", onKey);
|
|
2441
2531
|
}, [
|
|
2532
|
+
deleteSelectedElements,
|
|
2442
2533
|
editingEl,
|
|
2443
2534
|
multiSelection,
|
|
2444
2535
|
selectedElementSelector,
|
|
2445
|
-
getSlideContent,
|
|
2446
|
-
clearMultiSelection,
|
|
2447
|
-
resolveSelectedElement,
|
|
2448
|
-
clearSelectedElement,
|
|
2449
|
-
readCurrentSlideContentHtml,
|
|
2450
|
-
removeSlideObjectAndLayoutSpacer,
|
|
2451
|
-
syncSelectionToAppState,
|
|
2452
2536
|
]);
|
|
2453
2537
|
|
|
2454
2538
|
/**
|
|
@@ -2461,6 +2545,17 @@ export default function SlideEditor({
|
|
|
2461
2545
|
let el: HTMLElement | null = target;
|
|
2462
2546
|
while (el && slideContent.contains(el) && el !== slideContent) {
|
|
2463
2547
|
if (el.classList.contains("fmd-layout-spacer")) return null;
|
|
2548
|
+
if (isSlideCanvasShell(el)) {
|
|
2549
|
+
el = el.parentElement;
|
|
2550
|
+
continue;
|
|
2551
|
+
}
|
|
2552
|
+
if (
|
|
2553
|
+
el.tagName === "IMG" ||
|
|
2554
|
+
el.classList.contains("fmd-img-placeholder")
|
|
2555
|
+
) {
|
|
2556
|
+
const imageObject = findPersistedImageObject(el, slideContent);
|
|
2557
|
+
if (imageObject) return imageObject.getAttribute("data-builder-id");
|
|
2558
|
+
}
|
|
2464
2559
|
if (isInlineTextElement(el)) {
|
|
2465
2560
|
el = el.parentElement;
|
|
2466
2561
|
continue;
|
|
@@ -2479,6 +2574,17 @@ export default function SlideEditor({
|
|
|
2479
2574
|
let el: HTMLElement | null = target;
|
|
2480
2575
|
while (el && slideContent.contains(el) && el !== slideContent) {
|
|
2481
2576
|
if (el.classList.contains("fmd-layout-spacer")) return null;
|
|
2577
|
+
if (isSlideCanvasShell(el)) {
|
|
2578
|
+
el = el.parentElement;
|
|
2579
|
+
continue;
|
|
2580
|
+
}
|
|
2581
|
+
if (
|
|
2582
|
+
el.tagName === "IMG" ||
|
|
2583
|
+
el.classList.contains("fmd-img-placeholder")
|
|
2584
|
+
) {
|
|
2585
|
+
const imageObject = findPersistedImageObject(el, slideContent);
|
|
2586
|
+
if (imageObject) return imageObject;
|
|
2587
|
+
}
|
|
2482
2588
|
if (isInlineTextElement(el)) {
|
|
2483
2589
|
el = el.parentElement;
|
|
2484
2590
|
continue;
|
|
@@ -2634,6 +2740,211 @@ export default function SlideEditor({
|
|
|
2634
2740
|
],
|
|
2635
2741
|
);
|
|
2636
2742
|
|
|
2743
|
+
const getStyleTargets = useCallback((): HTMLElement[] => {
|
|
2744
|
+
const slideContent = getSlideContent();
|
|
2745
|
+
if (!slideContent) return [];
|
|
2746
|
+
if (contextMenuTargetRef.current) {
|
|
2747
|
+
return [contextMenuTargetRef.current];
|
|
2748
|
+
}
|
|
2749
|
+
if (multiSelection.size > 0) {
|
|
2750
|
+
return Array.from(multiSelection)
|
|
2751
|
+
.map(
|
|
2752
|
+
(id) =>
|
|
2753
|
+
slideContent.querySelector(
|
|
2754
|
+
`[data-builder-id="${id}"]`,
|
|
2755
|
+
) as HTMLElement | null,
|
|
2756
|
+
)
|
|
2757
|
+
.filter((element): element is HTMLElement => element !== null);
|
|
2758
|
+
}
|
|
2759
|
+
const element = editingElRef.current ?? resolveSelectedElement();
|
|
2760
|
+
return element ? [element] : [];
|
|
2761
|
+
}, [getSlideContent, multiSelection, resolveSelectedElement]);
|
|
2762
|
+
|
|
2763
|
+
const styleSnapshotForElement = useCallback(
|
|
2764
|
+
(element: HTMLElement): SlideStyleSnapshot => {
|
|
2765
|
+
const selector =
|
|
2766
|
+
getBuilderSelector(element) ??
|
|
2767
|
+
selectedElementSelector ??
|
|
2768
|
+
element.tagName;
|
|
2769
|
+
const inlineTextStyle =
|
|
2770
|
+
editingElRef.current === element
|
|
2771
|
+
? getInlineTextStyleSnapshotForRange(
|
|
2772
|
+
element,
|
|
2773
|
+
richTextSelectionRef.current,
|
|
2774
|
+
)
|
|
2775
|
+
: undefined;
|
|
2776
|
+
return buildStyleSnapshot(element, selector, inlineTextStyle);
|
|
2777
|
+
},
|
|
2778
|
+
[selectedElementSelector],
|
|
2779
|
+
);
|
|
2780
|
+
|
|
2781
|
+
const applyStylePatchToElement = useCallback(
|
|
2782
|
+
(
|
|
2783
|
+
element: HTMLElement,
|
|
2784
|
+
patch: CopiedElementStyle,
|
|
2785
|
+
range: Range | null = null,
|
|
2786
|
+
): Range | null => {
|
|
2787
|
+
const inlinePatch = inlineInspectorStylePatch(patch);
|
|
2788
|
+
let styledRange: Range | null = null;
|
|
2789
|
+
if (
|
|
2790
|
+
range &&
|
|
2791
|
+
restoreEditableTextRange(element, range) &&
|
|
2792
|
+
Object.keys(inlinePatch).length > 0
|
|
2793
|
+
) {
|
|
2794
|
+
const result = applyInlineTextStyle(element, inlinePatch);
|
|
2795
|
+
if (result.scope === "selection" && result.range) {
|
|
2796
|
+
styledRange = result.range.cloneRange();
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
if (!styledRange && Object.keys(inlinePatch).length > 0) {
|
|
2801
|
+
applyDescendantTextStyle(element, inlinePatch);
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2804
|
+
for (const [property, value] of Object.entries(patch)) {
|
|
2805
|
+
if (value === undefined) continue;
|
|
2806
|
+
if (
|
|
2807
|
+
styledRange &&
|
|
2808
|
+
INLINE_INSPECTOR_STYLE_KEYS.includes(
|
|
2809
|
+
property as (typeof INLINE_INSPECTOR_STYLE_KEYS)[number],
|
|
2810
|
+
)
|
|
2811
|
+
) {
|
|
2812
|
+
continue;
|
|
2813
|
+
}
|
|
2814
|
+
element.style.setProperty(stylePropertyName(property), value);
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
if (
|
|
2818
|
+
patch.borderWidth &&
|
|
2819
|
+
patch.borderWidth !== "0" &&
|
|
2820
|
+
patch.borderWidth !== "0px" &&
|
|
2821
|
+
window.getComputedStyle(element).borderStyle === "none"
|
|
2822
|
+
) {
|
|
2823
|
+
element.style.borderStyle = "solid";
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
return styledRange;
|
|
2827
|
+
},
|
|
2828
|
+
[],
|
|
2829
|
+
);
|
|
2830
|
+
|
|
2831
|
+
const copySelectedElementStyle = useCallback(() => {
|
|
2832
|
+
const source = getStyleTargets()[0];
|
|
2833
|
+
if (!source) return false;
|
|
2834
|
+
setCopiedElementStyle(
|
|
2835
|
+
copiedElementStyleFromSnapshot(styleSnapshotForElement(source)),
|
|
2836
|
+
);
|
|
2837
|
+
setHasCopiedElementStyle(true);
|
|
2838
|
+
return true;
|
|
2839
|
+
}, [getStyleTargets, styleSnapshotForElement]);
|
|
2840
|
+
|
|
2841
|
+
const pasteCopiedElementStyle = useCallback(() => {
|
|
2842
|
+
const copied = getCopiedElementStyle();
|
|
2843
|
+
if (!copied) return false;
|
|
2844
|
+
const targets = getStyleTargets();
|
|
2845
|
+
if (targets.length === 0) return false;
|
|
2846
|
+
|
|
2847
|
+
const editing = editingElRef.current;
|
|
2848
|
+
const savedRange = editing
|
|
2849
|
+
? (richTextSelectionRef.current ?? snapshotEditableTextRange(editing))
|
|
2850
|
+
: null;
|
|
2851
|
+
const nextRange = editing
|
|
2852
|
+
? applyStylePatchToElement(editing, copied, savedRange)
|
|
2853
|
+
: null;
|
|
2854
|
+
if (editing && nextRange) richTextSelectionRef.current = nextRange;
|
|
2855
|
+
|
|
2856
|
+
for (const target of editing ? targets.slice(1) : targets) {
|
|
2857
|
+
applyStylePatchToElement(target, copied);
|
|
2858
|
+
}
|
|
2859
|
+
|
|
2860
|
+
if (editing) {
|
|
2861
|
+
captureInlineEditDraft(slide.id);
|
|
2862
|
+
const selector = selectedElementSelector ?? getBuilderSelector(editing);
|
|
2863
|
+
if (selector) {
|
|
2864
|
+
setSelectedStyleSnapshot(
|
|
2865
|
+
buildStyleSnapshot(
|
|
2866
|
+
editing,
|
|
2867
|
+
selector,
|
|
2868
|
+
getInlineTextStyleSnapshotForRange(
|
|
2869
|
+
editing,
|
|
2870
|
+
richTextSelectionRef.current,
|
|
2871
|
+
),
|
|
2872
|
+
),
|
|
2873
|
+
);
|
|
2874
|
+
}
|
|
2875
|
+
} else {
|
|
2876
|
+
const html = readCurrentSlideContentHtml();
|
|
2877
|
+
if (html !== null) onUpdateSlideRef.current({ content: html });
|
|
2878
|
+
if (targets.length === 1) {
|
|
2879
|
+
const selector = getBuilderSelector(targets[0]);
|
|
2880
|
+
if (selector) selectElementForStyling(targets[0], selector);
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
return true;
|
|
2884
|
+
}, [
|
|
2885
|
+
applyStylePatchToElement,
|
|
2886
|
+
captureInlineEditDraft,
|
|
2887
|
+
getStyleTargets,
|
|
2888
|
+
readCurrentSlideContentHtml,
|
|
2889
|
+
selectedElementSelector,
|
|
2890
|
+
selectElementForStyling,
|
|
2891
|
+
slide.id,
|
|
2892
|
+
]);
|
|
2893
|
+
|
|
2894
|
+
const copySelectedObjects = useCallback(() => {
|
|
2895
|
+
const selection = getObjectOperationSelection();
|
|
2896
|
+
if (!selection) return false;
|
|
2897
|
+
const copied = copySlideObjects(selection.elements);
|
|
2898
|
+
const sourceObjectId = selection.elements[0].getAttribute(
|
|
2899
|
+
"data-slide-object-id",
|
|
2900
|
+
);
|
|
2901
|
+
if (!sourceObjectId) return false;
|
|
2902
|
+
copiedObjectClipboardRef.current = {
|
|
2903
|
+
copied,
|
|
2904
|
+
deckId,
|
|
2905
|
+
slideId: slide.id,
|
|
2906
|
+
sourceObjectId,
|
|
2907
|
+
};
|
|
2908
|
+
return true;
|
|
2909
|
+
}, [deckId, getObjectOperationSelection, slide.id]);
|
|
2910
|
+
|
|
2911
|
+
const pasteSelectedObjects = useCallback(() => {
|
|
2912
|
+
const clipboard = copiedObjectClipboardRef.current;
|
|
2913
|
+
const selection = getObjectOperationSelection();
|
|
2914
|
+
if (
|
|
2915
|
+
!clipboard ||
|
|
2916
|
+
!selection ||
|
|
2917
|
+
clipboard.deckId !== deckId ||
|
|
2918
|
+
clipboard.slideId !== slide.id
|
|
2919
|
+
) {
|
|
2920
|
+
return false;
|
|
2921
|
+
}
|
|
2922
|
+
const slideContent = getSlideContent();
|
|
2923
|
+
const source =
|
|
2924
|
+
slideContent &&
|
|
2925
|
+
findSlideObjectById(slideContent, clipboard.sourceObjectId);
|
|
2926
|
+
if (
|
|
2927
|
+
!source ||
|
|
2928
|
+
resolveSlideObjectContainingBlock(source, selection.positioningLayer) !==
|
|
2929
|
+
selection.containingBlock
|
|
2930
|
+
) {
|
|
2931
|
+
return false;
|
|
2932
|
+
}
|
|
2933
|
+
pasteSlideObjects(clipboard.copied, selection.elements[0]);
|
|
2934
|
+
return true;
|
|
2935
|
+
}, [
|
|
2936
|
+
deckId,
|
|
2937
|
+
getObjectOperationSelection,
|
|
2938
|
+
getSlideContent,
|
|
2939
|
+
pasteSlideObjects,
|
|
2940
|
+
slide.id,
|
|
2941
|
+
]);
|
|
2942
|
+
|
|
2943
|
+
const duplicateSelectedObjects = useCallback(() => {
|
|
2944
|
+
if (!copySelectedObjects()) return false;
|
|
2945
|
+
return pasteSelectedObjects();
|
|
2946
|
+
}, [copySelectedObjects, pasteSelectedObjects]);
|
|
2947
|
+
|
|
2637
2948
|
// Object clipboard contents are editor-local. It intentionally does not
|
|
2638
2949
|
// survive a deck switch, unlike the browser's native text clipboard.
|
|
2639
2950
|
useEffect(() => {
|
|
@@ -2648,6 +2959,7 @@ export default function SlideEditor({
|
|
|
2648
2959
|
if (readOnly) return;
|
|
2649
2960
|
const onKey = (e: KeyboardEvent) => {
|
|
2650
2961
|
if (!(e.metaKey || e.ctrlKey)) return;
|
|
2962
|
+
if (e.altKey) return;
|
|
2651
2963
|
const key = e.key.toLowerCase();
|
|
2652
2964
|
if (key !== "c" && key !== "v" && key !== "d") return;
|
|
2653
2965
|
|
|
@@ -2730,6 +3042,48 @@ export default function SlideEditor({
|
|
|
2730
3042
|
slide.id,
|
|
2731
3043
|
]);
|
|
2732
3044
|
|
|
3045
|
+
// Appearance clipboard shortcuts are deliberately separate from object
|
|
3046
|
+
// copy/paste. The Alt modifier keeps Cmd/Ctrl+C/V available for duplicating
|
|
3047
|
+
// objects and for native text editing.
|
|
3048
|
+
useEffect(() => {
|
|
3049
|
+
if (readOnly) return;
|
|
3050
|
+
const onKey = (e: KeyboardEvent) => {
|
|
3051
|
+
if (!(e.metaKey || e.ctrlKey) || !e.altKey) return;
|
|
3052
|
+
const key =
|
|
3053
|
+
e.code === "KeyC" ? "c" : e.code === "KeyV" ? "v" : e.key.toLowerCase();
|
|
3054
|
+
if (key !== "c" && key !== "v") return;
|
|
3055
|
+
|
|
3056
|
+
const active = document.activeElement;
|
|
3057
|
+
if (
|
|
3058
|
+
active instanceof HTMLInputElement ||
|
|
3059
|
+
active instanceof HTMLTextAreaElement ||
|
|
3060
|
+
active instanceof HTMLSelectElement
|
|
3061
|
+
) {
|
|
3062
|
+
return;
|
|
3063
|
+
}
|
|
3064
|
+
if (
|
|
3065
|
+
active instanceof HTMLElement &&
|
|
3066
|
+
active.isContentEditable &&
|
|
3067
|
+
!editingEl?.contains(active)
|
|
3068
|
+
) {
|
|
3069
|
+
return;
|
|
3070
|
+
}
|
|
3071
|
+
if (getStyleTargets().length === 0) return;
|
|
3072
|
+
|
|
3073
|
+
const handled =
|
|
3074
|
+
key === "c" ? copySelectedElementStyle() : pasteCopiedElementStyle();
|
|
3075
|
+
if (handled) e.preventDefault();
|
|
3076
|
+
};
|
|
3077
|
+
window.addEventListener("keydown", onKey);
|
|
3078
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
3079
|
+
}, [
|
|
3080
|
+
copySelectedElementStyle,
|
|
3081
|
+
editingEl,
|
|
3082
|
+
getStyleTargets,
|
|
3083
|
+
pasteCopiedElementStyle,
|
|
3084
|
+
readOnly,
|
|
3085
|
+
]);
|
|
3086
|
+
|
|
2733
3087
|
const placeTextBoxAt = useCallback(
|
|
2734
3088
|
(clientX: number, clientY: number, target: HTMLElement | null) => {
|
|
2735
3089
|
const canvas = containerRef.current
|
|
@@ -3241,44 +3595,24 @@ export default function SlideEditor({
|
|
|
3241
3595
|
) as HTMLElement | null,
|
|
3242
3596
|
)
|
|
3243
3597
|
.filter((el): el is HTMLElement => el !== null);
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3598
|
+
const roots = elements.filter(
|
|
3599
|
+
(element) =>
|
|
3600
|
+
!elements.some(
|
|
3601
|
+
(candidate) => candidate !== element && candidate.contains(element),
|
|
3602
|
+
),
|
|
3603
|
+
);
|
|
3604
|
+
if (roots.length === 0) return;
|
|
3249
3605
|
|
|
3250
|
-
//
|
|
3251
|
-
//
|
|
3252
|
-
//
|
|
3253
|
-
const
|
|
3254
|
-
".fmd-slide",
|
|
3606
|
+
// Capture the viewport before promotion. A normal-flow element becomes
|
|
3607
|
+
// an absolute child only after the drag threshold is crossed, so a
|
|
3608
|
+
// stationary click never mutates the slide's layout or HTML.
|
|
3609
|
+
const slideCanvas = roots[0].closest(
|
|
3610
|
+
".fmd-slide, [data-slide-canvas]",
|
|
3255
3611
|
) as HTMLElement | null;
|
|
3256
|
-
if (!
|
|
3257
|
-
const
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
child instanceof HTMLElement &&
|
|
3261
|
-
child.hasAttribute("data-fmd-autofit-content"),
|
|
3262
|
-
) ?? fmdSlide;
|
|
3263
|
-
const containingBlock = resolveSlideObjectContainingBlock(
|
|
3264
|
-
members[0].element,
|
|
3265
|
-
positioningLayer,
|
|
3266
|
-
);
|
|
3267
|
-
if (
|
|
3268
|
-
members.some(
|
|
3269
|
-
(member) =>
|
|
3270
|
-
member.element.closest(".fmd-slide") !== fmdSlide ||
|
|
3271
|
-
resolveSlideObjectContainingBlock(
|
|
3272
|
-
member.element,
|
|
3273
|
-
positioningLayer,
|
|
3274
|
-
) !== containingBlock,
|
|
3275
|
-
)
|
|
3276
|
-
) {
|
|
3277
|
-
return;
|
|
3278
|
-
}
|
|
3279
|
-
const slideRect = fmdSlide.getBoundingClientRect();
|
|
3280
|
-
const slideWidth = fmdSlide.offsetWidth;
|
|
3281
|
-
const slideHeight = fmdSlide.offsetHeight;
|
|
3612
|
+
if (!slideCanvas) return;
|
|
3613
|
+
const slideRect = slideCanvas.getBoundingClientRect();
|
|
3614
|
+
const slideWidth = slideCanvas.offsetWidth;
|
|
3615
|
+
const slideHeight = slideCanvas.offsetHeight;
|
|
3282
3616
|
if (
|
|
3283
3617
|
!slideRect.width ||
|
|
3284
3618
|
!slideRect.height ||
|
|
@@ -3288,6 +3622,155 @@ export default function SlideEditor({
|
|
|
3288
3622
|
return;
|
|
3289
3623
|
}
|
|
3290
3624
|
|
|
3625
|
+
const promotions: Array<{
|
|
3626
|
+
element: HTMLElement;
|
|
3627
|
+
originalClassName: string;
|
|
3628
|
+
originalStyle: string | null;
|
|
3629
|
+
originalObjectId: string | null;
|
|
3630
|
+
originalContentEditable: string | null;
|
|
3631
|
+
originalEditingBlock: string | null;
|
|
3632
|
+
restoreMarkdownTree?: () => void;
|
|
3633
|
+
}> = [];
|
|
3634
|
+
let members: ReturnType<typeof collectMovableSlideObjects> = [];
|
|
3635
|
+
let prepared = false;
|
|
3636
|
+
let promotionsRestored = false;
|
|
3637
|
+
|
|
3638
|
+
const removeFreeformLayoutSpacer = (element: HTMLElement) => {
|
|
3639
|
+
const objectId = element.getAttribute("data-slide-object-id");
|
|
3640
|
+
if (!objectId) return;
|
|
3641
|
+
const spacer = Array.from(
|
|
3642
|
+
element.parentElement?.querySelectorAll<HTMLElement>(
|
|
3643
|
+
"[data-slide-layout-spacer-for]",
|
|
3644
|
+
) ?? [],
|
|
3645
|
+
).find(
|
|
3646
|
+
(candidate) =>
|
|
3647
|
+
candidate.getAttribute("data-slide-layout-spacer-for") === objectId,
|
|
3648
|
+
);
|
|
3649
|
+
spacer?.remove();
|
|
3650
|
+
};
|
|
3651
|
+
|
|
3652
|
+
const restoreGroupPromotions = () => {
|
|
3653
|
+
if (promotionsRestored) return;
|
|
3654
|
+
promotionsRestored = true;
|
|
3655
|
+
|
|
3656
|
+
for (const promotion of promotions) {
|
|
3657
|
+
removeFreeformLayoutSpacer(promotion.element);
|
|
3658
|
+
}
|
|
3659
|
+
|
|
3660
|
+
const restoredMarkdownTrees = new Set<() => void>();
|
|
3661
|
+
for (const promotion of promotions) {
|
|
3662
|
+
const restoreMarkdownTree = promotion.restoreMarkdownTree;
|
|
3663
|
+
if (
|
|
3664
|
+
restoreMarkdownTree &&
|
|
3665
|
+
!restoredMarkdownTrees.has(restoreMarkdownTree)
|
|
3666
|
+
) {
|
|
3667
|
+
restoredMarkdownTrees.add(restoreMarkdownTree);
|
|
3668
|
+
restoreMarkdownTree();
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3672
|
+
for (const promotion of promotions) {
|
|
3673
|
+
const {
|
|
3674
|
+
element,
|
|
3675
|
+
originalClassName,
|
|
3676
|
+
originalStyle,
|
|
3677
|
+
originalObjectId,
|
|
3678
|
+
originalContentEditable,
|
|
3679
|
+
originalEditingBlock,
|
|
3680
|
+
} = promotion;
|
|
3681
|
+
element.className = originalClassName;
|
|
3682
|
+
if (originalStyle === null) element.removeAttribute("style");
|
|
3683
|
+
else element.setAttribute("style", originalStyle);
|
|
3684
|
+
if (originalContentEditable === null) {
|
|
3685
|
+
element.removeAttribute("contenteditable");
|
|
3686
|
+
} else {
|
|
3687
|
+
element.setAttribute("contenteditable", originalContentEditable);
|
|
3688
|
+
}
|
|
3689
|
+
if (originalEditingBlock === null) {
|
|
3690
|
+
element.removeAttribute("data-editing-block");
|
|
3691
|
+
} else {
|
|
3692
|
+
element.setAttribute("data-editing-block", originalEditingBlock);
|
|
3693
|
+
}
|
|
3694
|
+
if (originalObjectId) {
|
|
3695
|
+
element.setAttribute("data-slide-object-id", originalObjectId);
|
|
3696
|
+
} else {
|
|
3697
|
+
element.removeAttribute("data-slide-object-id");
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
};
|
|
3701
|
+
|
|
3702
|
+
const prepareGroup = () => {
|
|
3703
|
+
if (prepared) return members.length > 0;
|
|
3704
|
+
prepared = true;
|
|
3705
|
+
|
|
3706
|
+
for (const element of roots) {
|
|
3707
|
+
const originalClassName = element.className;
|
|
3708
|
+
const originalStyle = element.getAttribute("style");
|
|
3709
|
+
const originalObjectId = element.getAttribute("data-slide-object-id");
|
|
3710
|
+
const originalContentEditable =
|
|
3711
|
+
element.getAttribute("contenteditable");
|
|
3712
|
+
const originalEditingBlock =
|
|
3713
|
+
element.getAttribute("data-editing-block");
|
|
3714
|
+
const frozen = freezeElementForFreeformSelection(element);
|
|
3715
|
+
if (!frozen) {
|
|
3716
|
+
restoreGroupPromotions();
|
|
3717
|
+
return false;
|
|
3718
|
+
}
|
|
3719
|
+
promotions.push({
|
|
3720
|
+
element,
|
|
3721
|
+
originalClassName,
|
|
3722
|
+
originalStyle,
|
|
3723
|
+
originalObjectId,
|
|
3724
|
+
originalContentEditable,
|
|
3725
|
+
originalEditingBlock,
|
|
3726
|
+
restoreMarkdownTree: frozen.restoreMarkdownTree,
|
|
3727
|
+
});
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
const nextMembers = collectMovableSlideObjects(
|
|
3731
|
+
roots,
|
|
3732
|
+
getObjectGeometry,
|
|
3733
|
+
);
|
|
3734
|
+
if (nextMembers.length !== roots.length) {
|
|
3735
|
+
restoreGroupPromotions();
|
|
3736
|
+
return false;
|
|
3737
|
+
}
|
|
3738
|
+
|
|
3739
|
+
const fmdSlide = nextMembers[0].element.closest(
|
|
3740
|
+
".fmd-slide",
|
|
3741
|
+
) as HTMLElement | null;
|
|
3742
|
+
if (!fmdSlide) {
|
|
3743
|
+
restoreGroupPromotions();
|
|
3744
|
+
return false;
|
|
3745
|
+
}
|
|
3746
|
+
const positioningLayer =
|
|
3747
|
+
Array.from(fmdSlide.children).find(
|
|
3748
|
+
(child): child is HTMLElement =>
|
|
3749
|
+
child instanceof HTMLElement &&
|
|
3750
|
+
child.hasAttribute("data-fmd-autofit-content"),
|
|
3751
|
+
) ?? fmdSlide;
|
|
3752
|
+
const containingBlock = resolveSlideObjectContainingBlock(
|
|
3753
|
+
nextMembers[0].element,
|
|
3754
|
+
positioningLayer,
|
|
3755
|
+
);
|
|
3756
|
+
if (
|
|
3757
|
+
nextMembers.some(
|
|
3758
|
+
(member) =>
|
|
3759
|
+
member.element.closest(".fmd-slide") !== fmdSlide ||
|
|
3760
|
+
resolveSlideObjectContainingBlock(
|
|
3761
|
+
member.element,
|
|
3762
|
+
positioningLayer,
|
|
3763
|
+
) !== containingBlock,
|
|
3764
|
+
)
|
|
3765
|
+
) {
|
|
3766
|
+
restoreGroupPromotions();
|
|
3767
|
+
return false;
|
|
3768
|
+
}
|
|
3769
|
+
|
|
3770
|
+
members = nextMembers;
|
|
3771
|
+
return true;
|
|
3772
|
+
};
|
|
3773
|
+
|
|
3291
3774
|
const stop = () => {
|
|
3292
3775
|
window.removeEventListener("pointermove", onMove);
|
|
3293
3776
|
window.removeEventListener("pointerup", onUp);
|
|
@@ -3299,6 +3782,9 @@ export default function SlideEditor({
|
|
|
3299
3782
|
|
|
3300
3783
|
const controller = createSlidesCanvasGestureController({
|
|
3301
3784
|
preview: (gesture) => {
|
|
3785
|
+
if (!prepareGroup()) {
|
|
3786
|
+
return { handled: false, reason: "unhandled" };
|
|
3787
|
+
}
|
|
3302
3788
|
// Claim the click only once a real drag starts, so a click without
|
|
3303
3789
|
// movement on a multi-selected object remains normal editor input.
|
|
3304
3790
|
suppressNextClickRef.current = true;
|
|
@@ -3312,18 +3798,49 @@ export default function SlideEditor({
|
|
|
3312
3798
|
return { handled: true };
|
|
3313
3799
|
},
|
|
3314
3800
|
commit: () => {
|
|
3315
|
-
|
|
3801
|
+
if (members.length === 0) {
|
|
3802
|
+
return { handled: false, reason: "unhandled" };
|
|
3803
|
+
}
|
|
3804
|
+
// Serialize while the promoted elements still live in their fmd
|
|
3805
|
+
// canvas. Markdown promotion restores the React tree below, but
|
|
3806
|
+
// the persisted HTML must retain the canvas and absolute geometry.
|
|
3807
|
+
const html = readCurrentSlideContentHtml();
|
|
3808
|
+
for (const promotion of promotions) {
|
|
3809
|
+
removeFreeformLayoutSpacer(promotion.element);
|
|
3810
|
+
}
|
|
3811
|
+
const restoredMarkdownTrees = new Set<() => void>();
|
|
3812
|
+
for (const promotion of promotions) {
|
|
3813
|
+
const restoreMarkdownTree = promotion.restoreMarkdownTree;
|
|
3814
|
+
if (
|
|
3815
|
+
restoreMarkdownTree &&
|
|
3816
|
+
!restoredMarkdownTrees.has(restoreMarkdownTree)
|
|
3817
|
+
) {
|
|
3818
|
+
restoredMarkdownTrees.add(restoreMarkdownTree);
|
|
3819
|
+
restoreMarkdownTree();
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
if (html !== null) {
|
|
3823
|
+
pendingMultiSelectionResyncRef.current = members.map(
|
|
3824
|
+
(member) => member.objectId,
|
|
3825
|
+
);
|
|
3826
|
+
onUpdateSlideRef.current({ content: html }, undefined, {
|
|
3827
|
+
persistence: "immediate",
|
|
3828
|
+
});
|
|
3829
|
+
}
|
|
3316
3830
|
return { handled: true };
|
|
3317
3831
|
},
|
|
3318
3832
|
cancel: () => {
|
|
3319
|
-
|
|
3320
|
-
|
|
3833
|
+
if (members.length > 0) {
|
|
3834
|
+
applySlideObjectMoveDelta(members, 0, 0, applyObjectGeometry);
|
|
3835
|
+
refreshMultiSelectionRects(ids);
|
|
3836
|
+
}
|
|
3837
|
+
restoreGroupPromotions();
|
|
3321
3838
|
return { handled: true };
|
|
3322
3839
|
},
|
|
3323
3840
|
});
|
|
3324
3841
|
controller.pointerDown({
|
|
3325
3842
|
kind: "move",
|
|
3326
|
-
objectIds:
|
|
3843
|
+
objectIds: Array.from(ids),
|
|
3327
3844
|
pointer: {
|
|
3328
3845
|
x: e.clientX,
|
|
3329
3846
|
y: e.clientY,
|
|
@@ -3382,10 +3899,11 @@ export default function SlideEditor({
|
|
|
3382
3899
|
},
|
|
3383
3900
|
[
|
|
3384
3901
|
applyObjectGeometry,
|
|
3385
|
-
|
|
3902
|
+
freezeElementForFreeformSelection,
|
|
3386
3903
|
refreshMultiSelectionRects,
|
|
3387
3904
|
getObjectGeometry,
|
|
3388
3905
|
getSlideContent,
|
|
3906
|
+
readCurrentSlideContentHtml,
|
|
3389
3907
|
readOnly,
|
|
3390
3908
|
],
|
|
3391
3909
|
);
|
|
@@ -3404,11 +3922,11 @@ export default function SlideEditor({
|
|
|
3404
3922
|
) {
|
|
3405
3923
|
return;
|
|
3406
3924
|
}
|
|
3407
|
-
const nudge =
|
|
3925
|
+
const nudge = resolveSlidesCanvasNudge(e);
|
|
3408
3926
|
if (!nudge) return;
|
|
3409
3927
|
const { x: dx, y: dy } = nudge.delta;
|
|
3410
3928
|
|
|
3411
|
-
if (multiSelection.size > 0) {
|
|
3929
|
+
if (multiSelection.size > 0 && !e.shiftKey && !e.metaKey && !e.ctrlKey) {
|
|
3412
3930
|
const slideContent = getSlideContent();
|
|
3413
3931
|
if (!slideContent) return;
|
|
3414
3932
|
const elements = Array.from(multiSelection)
|
|
@@ -3661,6 +4179,7 @@ export default function SlideEditor({
|
|
|
3661
4179
|
candidates.forEach((node) => {
|
|
3662
4180
|
const el = node as HTMLElement;
|
|
3663
4181
|
if (el.classList.contains("fmd-layout-spacer")) return;
|
|
4182
|
+
if (isSlideCanvasShell(el)) return;
|
|
3664
4183
|
if (isInlineTextElement(el)) return;
|
|
3665
4184
|
const id = el.getAttribute("data-builder-id");
|
|
3666
4185
|
if (!id) return;
|
|
@@ -3689,11 +4208,27 @@ export default function SlideEditor({
|
|
|
3689
4208
|
const list = Array.from(multiSelectionRects.values())
|
|
3690
4209
|
.map((v) => v.selector)
|
|
3691
4210
|
.join(", ");
|
|
3692
|
-
agentChat.prefill(
|
|
3693
|
-
|
|
4211
|
+
agentChat.prefill(
|
|
4212
|
+
`[Current selection on slide ${slideIndex + 1} (${slide.id}): ${list}]\n`,
|
|
4213
|
+
);
|
|
4214
|
+
}, [multiSelection.size, multiSelectionRects, slide.id, slideIndex]);
|
|
3694
4215
|
|
|
3695
4216
|
const showImageOverlay = useCallback(
|
|
3696
4217
|
(target: HTMLElement) => {
|
|
4218
|
+
const publishImageSelection = (element: HTMLElement) => {
|
|
4219
|
+
const selector =
|
|
4220
|
+
getBuilderSelector(element) ??
|
|
4221
|
+
`[data-builder-id="${ensureBuilderId(element)}"]`;
|
|
4222
|
+
const snapshot = buildStyleSnapshot(element, selector);
|
|
4223
|
+
syncSelectionToAppState(
|
|
4224
|
+
buildSelectionState("image", [
|
|
4225
|
+
selectionItemForElement(element, selector, {
|
|
4226
|
+
...snapshot,
|
|
4227
|
+
isImage: true,
|
|
4228
|
+
}),
|
|
4229
|
+
]),
|
|
4230
|
+
);
|
|
4231
|
+
};
|
|
3697
4232
|
if (target.tagName === "IMG") {
|
|
3698
4233
|
const img = target as HTMLImageElement;
|
|
3699
4234
|
const rect = img.getBoundingClientRect();
|
|
@@ -3705,6 +4240,7 @@ export default function SlideEditor({
|
|
|
3705
4240
|
) as "cover" | "contain";
|
|
3706
4241
|
setSelectedImg(img);
|
|
3707
4242
|
setImageOverlay({ rect, src, objectFit: fit });
|
|
4243
|
+
publishImageSelection(img);
|
|
3708
4244
|
return;
|
|
3709
4245
|
}
|
|
3710
4246
|
// Also handle placeholder divs (dashed border boxes meant for images)
|
|
@@ -3719,9 +4255,10 @@ export default function SlideEditor({
|
|
|
3719
4255
|
src: getPlaceholderTarget(placeholder),
|
|
3720
4256
|
objectFit: "cover",
|
|
3721
4257
|
});
|
|
4258
|
+
publishImageSelection(placeholder);
|
|
3722
4259
|
}
|
|
3723
4260
|
},
|
|
3724
|
-
[getPlaceholderTarget],
|
|
4261
|
+
[buildSelectionState, getPlaceholderTarget],
|
|
3725
4262
|
);
|
|
3726
4263
|
|
|
3727
4264
|
// Browsers put the dragged element's outerHTML on the "text/html" data
|
|
@@ -3750,6 +4287,29 @@ export default function SlideEditor({
|
|
|
3750
4287
|
[],
|
|
3751
4288
|
);
|
|
3752
4289
|
|
|
4290
|
+
const getSlideDropPosition = useCallback(
|
|
4291
|
+
(clientX: number, clientY: number): SlideImageDropPosition | undefined => {
|
|
4292
|
+
const slideCanvas =
|
|
4293
|
+
slideCanvasRef.current?.querySelector<HTMLElement>(".fmd-slide") ??
|
|
4294
|
+
slideCanvasRef.current?.querySelector<HTMLElement>(
|
|
4295
|
+
"[data-slide-canvas]",
|
|
4296
|
+
);
|
|
4297
|
+
if (!slideCanvas) return undefined;
|
|
4298
|
+
const rect = slideCanvas.getBoundingClientRect();
|
|
4299
|
+
const width = slideCanvas.offsetWidth || rect.width;
|
|
4300
|
+
const height = slideCanvas.offsetHeight || rect.height;
|
|
4301
|
+
if (!rect.width || !rect.height || !width || !height) return undefined;
|
|
4302
|
+
return clientPointToSlideCoordinates(
|
|
4303
|
+
clientX,
|
|
4304
|
+
clientY,
|
|
4305
|
+
rect,
|
|
4306
|
+
width,
|
|
4307
|
+
height,
|
|
4308
|
+
);
|
|
4309
|
+
},
|
|
4310
|
+
[],
|
|
4311
|
+
);
|
|
4312
|
+
|
|
3753
4313
|
const handleSlideDragOver = useCallback((e: React.DragEvent) => {
|
|
3754
4314
|
const files = Array.from(e.dataTransfer.files ?? []);
|
|
3755
4315
|
const items = Array.from(e.dataTransfer.items ?? []);
|
|
@@ -3781,7 +4341,7 @@ export default function SlideEditor({
|
|
|
3781
4341
|
onDropImage?.(
|
|
3782
4342
|
getImageReplacementTarget(e.target as HTMLElement),
|
|
3783
4343
|
file,
|
|
3784
|
-
|
|
4344
|
+
getSlideDropPosition(e.clientX, e.clientY),
|
|
3785
4345
|
);
|
|
3786
4346
|
return;
|
|
3787
4347
|
}
|
|
@@ -3794,15 +4354,13 @@ export default function SlideEditor({
|
|
|
3794
4354
|
onDropImageUrl?.(
|
|
3795
4355
|
getImageReplacementTarget(e.target as HTMLElement),
|
|
3796
4356
|
url,
|
|
3797
|
-
|
|
3798
|
-
x: e.clientX,
|
|
3799
|
-
y: e.clientY,
|
|
3800
|
-
},
|
|
4357
|
+
getSlideDropPosition(e.clientX, e.clientY),
|
|
3801
4358
|
);
|
|
3802
4359
|
},
|
|
3803
4360
|
[
|
|
3804
4361
|
extractDraggedImageUrl,
|
|
3805
4362
|
getImageReplacementTarget,
|
|
4363
|
+
getSlideDropPosition,
|
|
3806
4364
|
onDropImage,
|
|
3807
4365
|
onDropImageUrl,
|
|
3808
4366
|
],
|
|
@@ -3845,6 +4403,13 @@ export default function SlideEditor({
|
|
|
3845
4403
|
e.preventDefault();
|
|
3846
4404
|
e.stopPropagation();
|
|
3847
4405
|
const next = new Set(multiSelection);
|
|
4406
|
+
if (next.size === 0) {
|
|
4407
|
+
const selectedElement = resolveSelectedElement();
|
|
4408
|
+
const selectedId =
|
|
4409
|
+
selectedElement?.getAttribute("data-builder-id") ??
|
|
4410
|
+
selectedImg?.getAttribute("data-builder-id");
|
|
4411
|
+
if (selectedId && selectedId !== id) next.add(selectedId);
|
|
4412
|
+
}
|
|
3848
4413
|
if (next.has(id)) next.delete(id);
|
|
3849
4414
|
else next.add(id);
|
|
3850
4415
|
applyMultiSelection(next);
|
|
@@ -3909,6 +4474,8 @@ export default function SlideEditor({
|
|
|
3909
4474
|
findSelectableElement,
|
|
3910
4475
|
isSlideWhitespaceTarget,
|
|
3911
4476
|
multiSelection,
|
|
4477
|
+
resolveSelectedElement,
|
|
4478
|
+
selectedImg,
|
|
3912
4479
|
applyMultiSelection,
|
|
3913
4480
|
clearMultiSelection,
|
|
3914
4481
|
clearCanvasSelection,
|
|
@@ -3932,12 +4499,41 @@ export default function SlideEditor({
|
|
|
3932
4499
|
const handleSlideContextMenu = useCallback(
|
|
3933
4500
|
(e: React.MouseEvent) => {
|
|
3934
4501
|
const target = e.target as HTMLElement;
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
4502
|
+
const slideContent = getSlideContent();
|
|
4503
|
+
if (!slideContent) {
|
|
4504
|
+
contextMenuTargetRef.current = null;
|
|
4505
|
+
return;
|
|
4506
|
+
}
|
|
4507
|
+
|
|
4508
|
+
const selectable = findSelectableElement(target, slideContent);
|
|
4509
|
+
if (!selectable) {
|
|
4510
|
+
contextMenuTargetRef.current = null;
|
|
4511
|
+
clearCanvasSelection();
|
|
4512
|
+
return;
|
|
4513
|
+
}
|
|
4514
|
+
|
|
4515
|
+
const builderId = selectable.getAttribute("data-builder-id");
|
|
4516
|
+
const isMultiSelectionTarget = Boolean(
|
|
4517
|
+
builderId && multiSelection.has(builderId),
|
|
4518
|
+
);
|
|
4519
|
+
contextMenuTargetRef.current = isMultiSelectionTarget ? null : selectable;
|
|
4520
|
+
if (!isMultiSelectionTarget) {
|
|
4521
|
+
if (multiSelection.size > 0) clearMultiSelection();
|
|
4522
|
+
const selector = getBuilderSelector(selectable);
|
|
4523
|
+
if (selector) {
|
|
4524
|
+
selectElementForStyling(selectable, selector);
|
|
4525
|
+
enterSelectionMode("agentNative.enterStyleEditing", { selector });
|
|
4526
|
+
}
|
|
3938
4527
|
}
|
|
3939
4528
|
},
|
|
3940
|
-
[
|
|
4529
|
+
[
|
|
4530
|
+
clearCanvasSelection,
|
|
4531
|
+
clearMultiSelection,
|
|
4532
|
+
findSelectableElement,
|
|
4533
|
+
getSlideContent,
|
|
4534
|
+
multiSelection,
|
|
4535
|
+
selectElementForStyling,
|
|
4536
|
+
],
|
|
3941
4537
|
);
|
|
3942
4538
|
|
|
3943
4539
|
const preserveRichTextSelection = useCallback(() => {
|
|
@@ -4240,6 +4836,8 @@ export default function SlideEditor({
|
|
|
4240
4836
|
</div>
|
|
4241
4837
|
) : null;
|
|
4242
4838
|
|
|
4839
|
+
const hasObjectSelection = getObjectOperationSelection() !== null;
|
|
4840
|
+
|
|
4243
4841
|
return (
|
|
4244
4842
|
<div
|
|
4245
4843
|
className="relative flex h-full min-h-0 flex-1 flex-col overflow-hidden rounded-l-lg bg-[var(--slides-editor-surface)]"
|
|
@@ -4346,64 +4944,155 @@ export default function SlideEditor({
|
|
|
4346
4944
|
className="shrink-0"
|
|
4347
4945
|
style={{ width: canvasWidth, maxWidth: canvasWidth }}
|
|
4348
4946
|
>
|
|
4349
|
-
<
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
onClick={handleSlideClick}
|
|
4354
|
-
onContextMenu={handleSlideContextMenu}
|
|
4355
|
-
onDoubleClick={handleSlideDoubleClick}
|
|
4356
|
-
onPointerDown={handleSlidePointerDown}
|
|
4357
|
-
onPointerMove={handleSlidePointerMove}
|
|
4358
|
-
onPointerLeave={clearEdgeMoveCursor}
|
|
4359
|
-
onDragStart={(event) => event.preventDefault()}
|
|
4360
|
-
onDragOver={handleSlideDragOver}
|
|
4361
|
-
onDrop={handleSlideDrop}
|
|
4947
|
+
<ContextMenu
|
|
4948
|
+
onOpenChange={(open) => {
|
|
4949
|
+
if (!open) contextMenuTargetRef.current = null;
|
|
4950
|
+
}}
|
|
4362
4951
|
>
|
|
4363
|
-
<
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4952
|
+
<ContextMenuTrigger asChild disabled={readOnly}>
|
|
4953
|
+
<div
|
|
4954
|
+
ref={slideCanvasRef}
|
|
4955
|
+
className="slide-image-clickable relative"
|
|
4956
|
+
data-editable={!readOnly ? "true" : undefined}
|
|
4957
|
+
onClick={handleSlideClick}
|
|
4958
|
+
onContextMenu={handleSlideContextMenu}
|
|
4959
|
+
onDoubleClick={handleSlideDoubleClick}
|
|
4960
|
+
onPointerDown={handleSlidePointerDown}
|
|
4961
|
+
onPointerMove={handleSlidePointerMove}
|
|
4962
|
+
onPointerLeave={clearEdgeMoveCursor}
|
|
4963
|
+
onDragStart={(event) => event.preventDefault()}
|
|
4964
|
+
onDragOver={handleSlideDragOver}
|
|
4965
|
+
onDrop={handleSlideDrop}
|
|
4966
|
+
>
|
|
4967
|
+
<SlideRenderer
|
|
4968
|
+
slide={slide}
|
|
4969
|
+
className="shadow-2xl shadow-black/40"
|
|
4970
|
+
designSystem={designSystem}
|
|
4971
|
+
aspectRatio={aspectRatio}
|
|
4972
|
+
onOverflowChange={handleOverflowChange}
|
|
4973
|
+
onAutofitSettled={handleAutofitSettled}
|
|
4974
|
+
/>
|
|
4975
|
+
{/* Fading "AI edited" ring around the canvas when the
|
|
4976
|
+
agent just edited THIS slide (component handles fade). */}
|
|
4977
|
+
{activeSlideEdits.length > 0 && (
|
|
4978
|
+
<RecentEditHighlights
|
|
4979
|
+
edits={activeSlideEdits}
|
|
4980
|
+
resolveRect={resolveCanvasRect}
|
|
4981
|
+
containerRef={slideCanvasRef}
|
|
4982
|
+
outlineOnly
|
|
4387
4983
|
/>
|
|
4388
4984
|
)}
|
|
4985
|
+
{(agentActive || passivePresentUsers.length > 0) && (
|
|
4986
|
+
<div className="absolute right-2 top-2 z-10 flex items-center gap-2">
|
|
4987
|
+
<AgentPresenceChip
|
|
4988
|
+
active={Boolean(agentActive)}
|
|
4989
|
+
/>
|
|
4990
|
+
{passivePresentUsers.length > 0 && (
|
|
4991
|
+
<SameSlidePresenceIndicator
|
|
4992
|
+
users={passivePresentUsers}
|
|
4993
|
+
/>
|
|
4994
|
+
)}
|
|
4995
|
+
</div>
|
|
4996
|
+
)}
|
|
4997
|
+
{overflowInfo &&
|
|
4998
|
+
!readOnly &&
|
|
4999
|
+
!agentActive &&
|
|
5000
|
+
warningVisible && (
|
|
5001
|
+
<SlideOverflowWarning
|
|
5002
|
+
verticalOverflow={overflowInfo.verticalOverflow}
|
|
5003
|
+
horizontalOverflow={
|
|
5004
|
+
overflowInfo.horizontalOverflow
|
|
5005
|
+
}
|
|
5006
|
+
isAskingAgentToFix={isAskingAgentToFix}
|
|
5007
|
+
dismissLabel={t(
|
|
5008
|
+
"deckEditor.dismissLayoutWarning",
|
|
5009
|
+
)}
|
|
5010
|
+
onFix={handleAskAgentToFixLayout}
|
|
5011
|
+
onDismiss={dismissWarning}
|
|
5012
|
+
/>
|
|
5013
|
+
)}
|
|
4389
5014
|
</div>
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
5015
|
+
</ContextMenuTrigger>
|
|
5016
|
+
<ContextMenuContent>
|
|
5017
|
+
<ContextMenuItem
|
|
5018
|
+
disabled={!hasObjectSelection}
|
|
5019
|
+
onSelect={copySelectedObjects}
|
|
5020
|
+
>
|
|
5021
|
+
<IconCopy className="me-2 h-4 w-4" />
|
|
5022
|
+
{t("styleInspector.copy")}
|
|
5023
|
+
<ContextMenuShortcut>
|
|
5024
|
+
{shortcutLabel("cmd+c")}
|
|
5025
|
+
</ContextMenuShortcut>
|
|
5026
|
+
</ContextMenuItem>
|
|
5027
|
+
<ContextMenuItem
|
|
5028
|
+
disabled={!hasObjectSelection}
|
|
5029
|
+
onSelect={pasteSelectedObjects}
|
|
5030
|
+
>
|
|
5031
|
+
<IconClipboard className="me-2 h-4 w-4" />
|
|
5032
|
+
{t("styleInspector.paste")}
|
|
5033
|
+
<ContextMenuShortcut>
|
|
5034
|
+
{shortcutLabel("cmd+v")}
|
|
5035
|
+
</ContextMenuShortcut>
|
|
5036
|
+
</ContextMenuItem>
|
|
5037
|
+
<ContextMenuItem
|
|
5038
|
+
disabled={!hasObjectSelection}
|
|
5039
|
+
onSelect={duplicateSelectedObjects}
|
|
5040
|
+
>
|
|
5041
|
+
<IconCopy className="me-2 h-4 w-4" />
|
|
5042
|
+
{t("editorSidebar.duplicate")}
|
|
5043
|
+
<ContextMenuShortcut>
|
|
5044
|
+
{shortcutLabel("cmd+d")}
|
|
5045
|
+
</ContextMenuShortcut>
|
|
5046
|
+
</ContextMenuItem>
|
|
5047
|
+
<ContextMenuItem
|
|
5048
|
+
disabled={!hasObjectSelection}
|
|
5049
|
+
onSelect={deleteSelectedElements}
|
|
5050
|
+
>
|
|
5051
|
+
<IconTrash className="me-2 h-4 w-4" />
|
|
5052
|
+
{t("editorSidebar.delete")}
|
|
5053
|
+
<ContextMenuShortcut>⌫</ContextMenuShortcut>
|
|
5054
|
+
</ContextMenuItem>
|
|
5055
|
+
<ContextMenuSeparator />
|
|
5056
|
+
<ContextMenuItem
|
|
5057
|
+
disabled={getStyleTargets().length === 0}
|
|
5058
|
+
onSelect={copySelectedElementStyle}
|
|
5059
|
+
>
|
|
5060
|
+
<IconBrush className="me-2 h-4 w-4" />
|
|
5061
|
+
{t("styleInspector.copyStyle")}
|
|
5062
|
+
<ContextMenuShortcut>
|
|
5063
|
+
{shortcutLabel("cmd+alt+c")}
|
|
5064
|
+
</ContextMenuShortcut>
|
|
5065
|
+
</ContextMenuItem>
|
|
5066
|
+
<ContextMenuItem
|
|
5067
|
+
disabled={
|
|
5068
|
+
!hasCopiedElementStyle ||
|
|
5069
|
+
getStyleTargets().length === 0
|
|
5070
|
+
}
|
|
5071
|
+
onSelect={pasteCopiedElementStyle}
|
|
5072
|
+
>
|
|
5073
|
+
<IconBrush className="me-2 h-4 w-4" />
|
|
5074
|
+
{t("styleInspector.pasteStyle")}
|
|
5075
|
+
<ContextMenuShortcut>
|
|
5076
|
+
{shortcutLabel("cmd+alt+v")}
|
|
5077
|
+
</ContextMenuShortcut>
|
|
5078
|
+
</ContextMenuItem>
|
|
5079
|
+
<ContextMenuSeparator />
|
|
5080
|
+
<ContextMenuItem
|
|
5081
|
+
disabled={!selectedElementSelector}
|
|
5082
|
+
onSelect={() => handleArrangeSelected("front")}
|
|
5083
|
+
>
|
|
5084
|
+
<IconStackFront className="me-2 h-4 w-4" />
|
|
5085
|
+
{t("styleInspector.bringToFront")}
|
|
5086
|
+
</ContextMenuItem>
|
|
5087
|
+
<ContextMenuItem
|
|
5088
|
+
disabled={!selectedElementSelector}
|
|
5089
|
+
onSelect={() => handleArrangeSelected("back")}
|
|
5090
|
+
>
|
|
5091
|
+
<IconStackBack className="me-2 h-4 w-4" />
|
|
5092
|
+
{t("styleInspector.sendToBack")}
|
|
5093
|
+
</ContextMenuItem>
|
|
5094
|
+
</ContextMenuContent>
|
|
5095
|
+
</ContextMenu>
|
|
4407
5096
|
</div>
|
|
4408
5097
|
</div>
|
|
4409
5098
|
</div>
|
|
@@ -4418,7 +5107,7 @@ export default function SlideEditor({
|
|
|
4418
5107
|
readOnly={readOnly}
|
|
4419
5108
|
/>
|
|
4420
5109
|
|
|
4421
|
-
{selectionRect && (
|
|
5110
|
+
{selectionRect && !selectedElementSelector && (
|
|
4422
5111
|
<ImageSelectionOutline
|
|
4423
5112
|
rect={selectionRect}
|
|
4424
5113
|
viewportRect={selectionViewportRect}
|
|
@@ -4483,8 +5172,6 @@ export default function SlideEditor({
|
|
|
4483
5172
|
onGenerate={onGenerateImage}
|
|
4484
5173
|
onLibrary={() => onOpenAssetLibrary(imageOverlay.src)}
|
|
4485
5174
|
onUpload={() => onUploadImage(imageOverlay.src)}
|
|
4486
|
-
onSearch={() => onSearchImage(imageOverlay.src)}
|
|
4487
|
-
onLogo={() => onLogoSearch(imageOverlay.src)}
|
|
4488
5175
|
onToggleObjectFit={() => {
|
|
4489
5176
|
const newFit =
|
|
4490
5177
|
imageOverlay.objectFit === "cover" ? "contain" : "cover";
|