@agent-native/core 0.130.2 → 0.131.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +79 -0
- package/corpus/core/docs/content/agent-surfaces.mdx +4 -4
- package/corpus/core/docs/content/automations.mdx +61 -12
- package/corpus/core/docs/content/deployment.mdx +11 -4
- package/corpus/core/docs/content/extensions.mdx +39 -33
- package/corpus/core/docs/content/recurring-jobs.mdx +41 -19
- package/corpus/core/docs/content/template-dispatch-operations.mdx +19 -7
- package/corpus/core/package.json +3 -1
- package/corpus/core/src/a2a/types.ts +6 -0
- package/corpus/core/src/action.ts +68 -0
- package/corpus/core/src/agent/engine/builder-engine.ts +8 -4
- package/corpus/core/src/agent/production-agent.ts +175 -100
- package/corpus/core/src/agent/tool-search.ts +36 -0
- package/corpus/core/src/automations/service.ts +506 -0
- package/corpus/core/src/browser-context/index.ts +392 -0
- package/corpus/core/src/client/AgentPanel.tsx +2 -1
- package/corpus/core/src/client/FeedbackButton.tsx +37 -4
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +21 -0
- package/corpus/core/src/client/agent-chat.ts +31 -3
- package/corpus/core/src/client/agent-page/AgentJobsTab.tsx +438 -310
- package/corpus/core/src/client/agent-page/AgentTabsPage.tsx +4 -4
- package/corpus/core/src/client/agent-page/use-jobs.ts +7 -0
- package/corpus/core/src/client/analytics-session.ts +13 -0
- package/corpus/core/src/client/extensions/ExtensionViewer.tsx +67 -0
- package/corpus/core/src/client/settings/AutomationsSection.tsx +27 -499
- package/corpus/core/src/client/settings/SettingsPanel.tsx +32 -11
- package/corpus/core/src/client/settings/index.ts +2 -0
- package/corpus/core/src/client/use-agent-chat.ts +35 -3
- package/corpus/core/src/client/use-chat-threads.ts +8 -25
- package/corpus/core/src/deploy/build.ts +2 -0
- package/corpus/core/src/deploy/workspace-deploy.ts +2 -0
- package/corpus/core/src/extensions/actions.ts +115 -3
- package/corpus/core/src/extensions/fetch-tool.ts +11 -0
- package/corpus/core/src/extensions/routes.ts +16 -1
- package/corpus/core/src/extensions/web-content.ts +96 -1
- package/corpus/core/src/integrations/computer-supervision.ts +33 -1
- package/corpus/core/src/integrations/index.ts +6 -0
- package/corpus/core/src/integrations/integration-durable-dispatch-config.ts +10 -0
- package/corpus/core/src/integrations/integration-durable-dispatch.ts +4 -0
- package/corpus/core/src/integrations/plugin.ts +94 -41
- package/corpus/core/src/integrations/remote-browser-actions.ts +621 -0
- package/corpus/core/src/jobs/actions/list-recurring-jobs.ts +5 -7
- package/corpus/core/src/jobs/actions/manage-recurring-job.ts +3 -7
- package/corpus/core/src/jobs/background-automation-runner.ts +437 -0
- package/corpus/core/src/jobs/frontmatter.ts +308 -0
- package/corpus/core/src/jobs/scheduler.ts +89 -598
- package/corpus/core/src/jobs/tools.ts +22 -3
- package/corpus/core/src/localization/default-messages.ts +45 -45
- package/corpus/core/src/mcp-client/index.ts +6 -0
- package/corpus/core/src/notifications/actions.ts +5 -0
- package/corpus/core/src/progress/actions.ts +5 -0
- package/corpus/core/src/provider-api/actions/github-repo-files.ts +11 -0
- package/corpus/core/src/provider-api/actions/provider-api.ts +13 -0
- package/corpus/core/src/server/action-discovery.ts +7 -0
- package/corpus/core/src/server/agent-capabilities.ts +43 -8
- package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +63 -1
- package/corpus/core/src/server/agent-chat/browser-team-tools.ts +126 -3
- package/corpus/core/src/server/agent-chat/framework-prompts.ts +7 -11
- package/corpus/core/src/server/agent-chat/plugin-options.ts +3 -3
- package/corpus/core/src/server/agent-chat/script-entries.ts +52 -1
- package/corpus/core/src/server/agent-chat-plugin.ts +25 -5
- package/corpus/core/src/server/attribution.ts +22 -0
- package/corpus/core/src/server/auth.ts +20 -6
- package/corpus/core/src/server/better-auth-instance.ts +10 -2
- package/corpus/core/src/server/core-routes-plugin.ts +10 -1
- package/corpus/core/src/server/csrf.ts +25 -0
- package/corpus/core/src/server/google-oauth.ts +21 -1
- package/corpus/core/src/server/onboarding-html.ts +18 -0
- package/corpus/core/src/server/prompts/framework-core-compact.ts +3 -3
- package/corpus/core/src/server/prompts/framework-core.ts +3 -3
- package/corpus/core/src/server/prompts/shared-rules.ts +3 -3
- package/corpus/core/src/settings/index.ts +1 -0
- package/corpus/core/src/shared/analytics-anonymous-id.ts +35 -0
- package/corpus/core/src/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
- package/corpus/core/src/templates/default/app/i18n/ar-SA.ts +26 -0
- package/corpus/core/src/templates/default/app/i18n/de-DE.ts +28 -0
- package/corpus/core/src/templates/default/app/i18n/en-US.ts +27 -0
- package/corpus/core/src/templates/default/app/i18n/es-ES.ts +28 -0
- package/corpus/core/src/templates/default/app/i18n/fr-FR.ts +28 -0
- package/corpus/core/src/templates/default/app/i18n/hi-IN.ts +25 -0
- package/corpus/core/src/templates/default/app/i18n/ja-JP.ts +27 -0
- package/corpus/core/src/templates/default/app/i18n/ko-KR.ts +27 -0
- package/corpus/core/src/templates/default/app/i18n/pt-BR.ts +27 -0
- package/corpus/core/src/templates/default/app/i18n/zh-CN.ts +24 -0
- package/corpus/core/src/templates/default/app/i18n/zh-TW.ts +24 -0
- package/corpus/core/src/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/agent-page/SKILL.md +10 -7
- package/corpus/core/src/templates/workspace-core/.agents/skills/automations/SKILL.md +53 -8
- package/corpus/core/src/templates/workspace-core/.agents/skills/browser-sessions/SKILL.md +131 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/extension-points/SKILL.md +13 -4
- package/corpus/core/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +23 -15
- package/corpus/core/src/templates/workspace-core/.agents/skills/recurring-jobs/SKILL.md +33 -18
- package/corpus/core/src/tracking/providers.ts +1 -0
- package/corpus/core/src/tracking/registry.ts +2 -1
- package/corpus/core/src/tracking/types.ts +1 -0
- package/corpus/core/src/triggers/actions/list-automations.ts +43 -44
- package/corpus/core/src/triggers/actions/manage-automation.ts +17 -48
- package/corpus/core/src/triggers/actions.ts +218 -127
- package/corpus/core/src/triggers/dispatcher.ts +181 -367
- package/corpus/core/src/triggers/routes.ts +84 -27
- package/corpus/core/src/triggers/types.ts +9 -23
- package/corpus/templates/analytics/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
- package/corpus/templates/analytics/AGENTS.md +5 -0
- package/corpus/templates/analytics/actions/hubspot-deals.ts +1 -0
- package/corpus/templates/analytics/actions/hubspot-records.ts +1 -0
- package/corpus/templates/analytics/actions/install-dashboard-template.ts +13 -4
- package/corpus/templates/analytics/actions/rename-dashboard.ts +8 -3
- package/corpus/templates/analytics/actions/restore-dashboard-revision.ts +8 -3
- package/corpus/templates/analytics/actions/update-dashboard.ts +22 -5
- package/corpus/templates/analytics/app/components/dashboard/DashboardPanelSkeleton.tsx +7 -1
- package/corpus/templates/analytics/app/components/layout/CommandPalette.tsx +1 -82
- package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +4 -4
- package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +23 -13
- package/corpus/templates/analytics/app/global.css +21 -0
- package/corpus/templates/analytics/app/i18n-data.ts +165 -101
- package/corpus/templates/analytics/app/lib/custom-block-promotion.ts +62 -0
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +17 -2
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +3 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +77 -12
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-save-queue.ts +34 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +42 -26
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +15 -0
- package/corpus/templates/analytics/app/pages/analyses/AnalysisDetail.tsx +1 -1
- package/corpus/templates/analytics/changelog/2026-07-29-chat-history-now-closes-smoothly-and-stays-ready-when-you-re.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-29-chat-history-stays-visible-when-you-return-to-the-dashboard.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-29-custom-dashboard-blocks-now-identify-one-off-agent-patches-a.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-29-dashboard-chart-deletions-now-save-in-order-so-removed-rows-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-29-dashboard-chart-loaders-are-easier-to-see-and-animate-more-s.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-29-plan-mode-can-now-query-connected-analytics-sources-while-ke.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-29-plan-mode-can-use-clips-as-read-only-prompt-context.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-29-signup-entry-pages-now-show-the-first-page-viewed-before-eac.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +26 -1
- package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +0 -33
- package/corpus/templates/analytics/server/lib/analytics-connector-catalog.ts +2 -0
- package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +13 -3
- package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +9 -1
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +44 -1
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +96 -16
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +18 -27
- package/corpus/templates/analytics/server/plugins/core-routes.ts +1 -0
- package/corpus/templates/assets/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
- package/corpus/templates/assets/app/components/layout/Sidebar.tsx +4 -8
- package/corpus/templates/assets/changelog/2026-07-29-chat-history-now-closes-smoothly-and-stays-ready-when-you-re.md +6 -0
- package/corpus/templates/assets/changelog/2026-07-29-chat-history-stays-visible-when-you-return-to-the-workspace.md +6 -0
- package/corpus/templates/assets/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
- package/corpus/templates/brain/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
- package/corpus/templates/brain/app/components/layout/Sidebar.tsx +3 -7
- package/corpus/templates/brain/app/root.tsx +0 -3
- package/corpus/templates/brain/changelog/2026-07-29-chat-history-now-closes-smoothly-and-stays-ready-when-you-re.md +6 -0
- package/corpus/templates/brain/changelog/2026-07-29-chat-history-stays-visible-when-you-return-to-ask-brain.md +6 -0
- package/corpus/templates/brain/changelog/2026-07-29-extensions-are-no-longer-shown-as-a-default-command-menu-des.md +6 -0
- package/corpus/templates/brain/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
- package/corpus/templates/calendar/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
- package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +25 -5
- package/corpus/templates/calendar/AGENTS.md +5 -0
- package/corpus/templates/calendar/actions/create-event.ts +43 -13
- package/corpus/templates/calendar/actions/event-action-helpers.ts +131 -1
- package/corpus/templates/calendar/actions/get-event.ts +1 -0
- package/corpus/templates/calendar/actions/manage-event-draft.ts +65 -2
- package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +438 -258
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +19 -3
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +7 -3
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +14 -19
- package/corpus/templates/calendar/app/components/calendar/MonthView.tsx +24 -0
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +15 -2
- package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +1 -5
- package/corpus/templates/calendar/app/hooks/use-events.ts +51 -5
- package/corpus/templates/calendar/app/i18n-data.ts +50 -0
- package/corpus/templates/calendar/app/lib/all-day-layout.ts +9 -2
- package/corpus/templates/calendar/app/lib/event-form-initialization.ts +4 -8
- package/corpus/templates/calendar/app/lib/event-form-utils.ts +43 -8
- package/corpus/templates/calendar/app/lib/location-suggestions.ts +36 -0
- package/corpus/templates/calendar/app/lib/out-of-office.ts +87 -1
- package/corpus/templates/calendar/app/pages/CalendarView.tsx +82 -20
- package/corpus/templates/calendar/changelog/2026-07-26-out-of-office-events-now-default-to-full-days-with-a-ready.md +6 -0
- package/corpus/templates/calendar/changelog/2026-07-29-recent-calendar-locations-now-appear-as-address-suggestions-.md +6 -0
- package/corpus/templates/calendar/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
- package/corpus/templates/calendar/changelog/2026-07-29-unnamed-events-now-use-a-title-placeholder.md +6 -0
- package/corpus/templates/calendar/server/lib/find-time.ts +29 -5
- package/corpus/templates/calendar/server/lib/google-calendar.ts +2 -0
- package/corpus/templates/calendar/server/lib/ical-fetcher.ts +1 -0
- package/corpus/templates/calendar/shared/api.ts +4 -0
- package/corpus/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
- package/corpus/templates/chat/app/components/layout/Sidebar.tsx +1 -5
- package/corpus/templates/chat/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
- package/corpus/templates/clips/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
- package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -5
- package/corpus/templates/clips/changelog/2026-07-29-fixed-system-audio-recordings-picking-up-your-microphone-in-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-29-the-desktop-camera-preview-now-closes-after-a-recording-fin.md +6 -0
- package/corpus/templates/clips/desktop/src/app.tsx +14 -15
- package/corpus/templates/clips/desktop/src/lib/bubble-session.ts +11 -0
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +9 -4
- package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +11 -0
- package/corpus/templates/clips/server/jobs/transactional-emails.ts +47 -23
- package/corpus/templates/clips/server/lib/transactional-email-store.ts +16 -6
- package/corpus/templates/content/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
- package/corpus/templates/content/.agents/skills/content-product-development/SKILL.md +103 -0
- package/corpus/templates/content/.agents/skills/content-product-development/references/verification.md +57 -0
- package/corpus/templates/content/AGENTS.md +2 -0
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +1 -6
- package/corpus/templates/content/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
- package/corpus/templates/content/docs/product/README.md +115 -0
- package/corpus/templates/content/docs/product/architecture.md +98 -0
- package/corpus/templates/content/docs/product/capabilities/content.access.page-database.md +76 -0
- package/corpus/templates/content/docs/product/capabilities/content.access.row-private.md +75 -0
- package/corpus/templates/content/docs/product/capabilities/content.access.safe-aggregate.md +75 -0
- package/corpus/templates/content/docs/product/capabilities/content.access.visibility-closure.md +79 -0
- package/corpus/templates/content/docs/product/capabilities/content.action.button.md +67 -0
- package/corpus/templates/content/docs/product/capabilities/content.agent.action-parity.md +73 -0
- package/corpus/templates/content/docs/product/capabilities/content.agent.audience-safe.md +91 -0
- package/corpus/templates/content/docs/product/capabilities/content.agent.automation.md +67 -0
- package/corpus/templates/content/docs/product/capabilities/content.agent.expression-authoring.md +71 -0
- package/corpus/templates/content/docs/product/capabilities/content.agent.presence.md +103 -0
- package/corpus/templates/content/docs/product/capabilities/content.agent.resource-consent.md +90 -0
- package/corpus/templates/content/docs/product/capabilities/content.agent.skill-catalog.md +67 -0
- package/corpus/templates/content/docs/product/capabilities/content.api.cms.md +70 -0
- package/corpus/templates/content/docs/product/capabilities/content.author.code.md +71 -0
- package/corpus/templates/content/docs/product/capabilities/content.author.document-editor.md +71 -0
- package/corpus/templates/content/docs/product/capabilities/content.author.footnotes.md +71 -0
- package/corpus/templates/content/docs/product/capabilities/content.author.math.md +71 -0
- package/corpus/templates/content/docs/product/capabilities/content.author.media.md +71 -0
- package/corpus/templates/content/docs/product/capabilities/content.author.mermaid.md +71 -0
- package/corpus/templates/content/docs/product/capabilities/content.automation.scheduled.md +67 -0
- package/corpus/templates/content/docs/product/capabilities/content.capture.enrich.md +75 -0
- package/corpus/templates/content/docs/product/capabilities/content.command.fabric.md +67 -0
- package/corpus/templates/content/docs/product/capabilities/content.comment.page-owned.md +75 -0
- package/corpus/templates/content/docs/product/capabilities/content.diff.ai-assist.md +75 -0
- package/corpus/templates/content/docs/product/capabilities/content.diff.filtered-review.md +79 -0
- package/corpus/templates/content/docs/product/capabilities/content.diff.in-place.md +80 -0
- package/corpus/templates/content/docs/product/capabilities/content.discussion.page.md +80 -0
- package/corpus/templates/content/docs/product/capabilities/content.embed.host-grant.md +70 -0
- package/corpus/templates/content/docs/product/capabilities/content.embed.mcp-app.md +69 -0
- package/corpus/templates/content/docs/product/capabilities/content.embed.surface.md +74 -0
- package/corpus/templates/content/docs/product/capabilities/content.event.committed.md +85 -0
- package/corpus/templates/content/docs/product/capabilities/content.expression.cached-result.md +71 -0
- package/corpus/templates/content/docs/product/capabilities/content.expression.catalog.md +71 -0
- package/corpus/templates/content/docs/product/capabilities/content.expression.language.md +76 -0
- package/corpus/templates/content/docs/product/capabilities/content.feedback.signal.md +67 -0
- package/corpus/templates/content/docs/product/capabilities/content.form.shared-engine.md +72 -0
- package/corpus/templates/content/docs/product/capabilities/content.history.queryable.md +79 -0
- package/corpus/templates/content/docs/product/capabilities/content.home.global.md +80 -0
- package/corpus/templates/content/docs/product/capabilities/content.job.durable.md +76 -0
- package/corpus/templates/content/docs/product/capabilities/content.knowledge.graph.md +67 -0
- package/corpus/templates/content/docs/product/capabilities/content.knowledge.links.md +67 -0
- package/corpus/templates/content/docs/product/capabilities/content.knowledge.search.md +67 -0
- package/corpus/templates/content/docs/product/capabilities/content.layout.responsive.md +80 -0
- package/corpus/templates/content/docs/product/capabilities/content.navigation.sidebar.md +86 -0
- package/corpus/templates/content/docs/product/capabilities/content.notification.source.md +71 -0
- package/corpus/templates/content/docs/product/capabilities/content.object.block.md +76 -0
- package/corpus/templates/content/docs/product/capabilities/content.object.blocks-field.md +79 -0
- package/corpus/templates/content/docs/product/capabilities/content.object.database.md +92 -0
- package/corpus/templates/content/docs/product/capabilities/content.object.multi-membership.md +75 -0
- package/corpus/templates/content/docs/product/capabilities/content.object.page.md +76 -0
- package/corpus/templates/content/docs/product/capabilities/content.object.reference.md +75 -0
- package/corpus/templates/content/docs/product/capabilities/content.object.transclusion.md +75 -0
- package/corpus/templates/content/docs/product/capabilities/content.organization.teams.md +71 -0
- package/corpus/templates/content/docs/product/capabilities/content.portability.pdf-export.md +78 -0
- package/corpus/templates/content/docs/product/capabilities/content.portability.roundtrip.md +154 -0
- package/corpus/templates/content/docs/product/capabilities/content.portability.source-representation.md +70 -0
- package/corpus/templates/content/docs/product/capabilities/content.portability.vault-export.md +80 -0
- package/corpus/templates/content/docs/product/capabilities/content.presentation.mode.md +76 -0
- package/corpus/templates/content/docs/product/capabilities/content.preset.catalog.md +71 -0
- package/corpus/templates/content/docs/product/capabilities/content.property.actor.md +75 -0
- package/corpus/templates/content/docs/product/capabilities/content.property.catalog.md +76 -0
- package/corpus/templates/content/docs/product/capabilities/content.property.constraints.md +75 -0
- package/corpus/templates/content/docs/product/capabilities/content.property.guarded-change.md +76 -0
- package/corpus/templates/content/docs/product/capabilities/content.property.location.md +71 -0
- package/corpus/templates/content/docs/product/capabilities/content.property.typed.md +71 -0
- package/corpus/templates/content/docs/product/capabilities/content.publish.public.md +75 -0
- package/corpus/templates/content/docs/product/capabilities/content.publish.reading.md +74 -0
- package/corpus/templates/content/docs/product/capabilities/content.query.object.md +114 -0
- package/corpus/templates/content/docs/product/capabilities/content.reader.documents.md +69 -0
- package/corpus/templates/content/docs/product/capabilities/content.reader.surface.md +75 -0
- package/corpus/templates/content/docs/product/capabilities/content.relationship.edge.md +183 -0
- package/corpus/templates/content/docs/product/capabilities/content.renderer.artifact-block.md +71 -0
- package/corpus/templates/content/docs/product/capabilities/content.renderer.custom-block.md +81 -0
- package/corpus/templates/content/docs/product/capabilities/content.renderer.graph.md +71 -0
- package/corpus/templates/content/docs/product/capabilities/content.renderer.typed.md +92 -0
- package/corpus/templates/content/docs/product/capabilities/content.research.annotation.md +75 -0
- package/corpus/templates/content/docs/product/capabilities/content.research.citation.md +70 -0
- package/corpus/templates/content/docs/product/capabilities/content.review.code.md +67 -0
- package/corpus/templates/content/docs/product/capabilities/content.revision.suggestions.md +76 -0
- package/corpus/templates/content/docs/product/capabilities/content.rule.deterministic.md +79 -0
- package/corpus/templates/content/docs/product/capabilities/content.schedule.constraints.md +81 -0
- package/corpus/templates/content/docs/product/capabilities/content.security.private-vault.md +71 -0
- package/corpus/templates/content/docs/product/capabilities/content.share.views.md +140 -0
- package/corpus/templates/content/docs/product/capabilities/content.source.adapters.md +154 -0
- package/corpus/templates/content/docs/product/capabilities/content.source.builder-codec.md +70 -0
- package/corpus/templates/content/docs/product/capabilities/content.source.catalog.md +149 -0
- package/corpus/templates/content/docs/product/capabilities/content.source.file-folder.md +71 -0
- package/corpus/templates/content/docs/product/capabilities/content.source.local-bridge.md +156 -0
- package/corpus/templates/content/docs/product/capabilities/content.source.local-project.md +70 -0
- package/corpus/templates/content/docs/product/capabilities/content.source.page-link.md +70 -0
- package/corpus/templates/content/docs/product/capabilities/content.source.row-union.md +69 -0
- package/corpus/templates/content/docs/product/capabilities/content.source.spaces-files.md +75 -0
- package/corpus/templates/content/docs/product/capabilities/content.source.sync-policy.md +161 -0
- package/corpus/templates/content/docs/product/capabilities/content.system.dependencies.md +80 -0
- package/corpus/templates/content/docs/product/capabilities/content.system.my-tasks.md +75 -0
- package/corpus/templates/content/docs/product/capabilities/content.system.project-status.md +77 -0
- package/corpus/templates/content/docs/product/capabilities/content.system.research-workspace.md +80 -0
- package/corpus/templates/content/docs/product/capabilities/content.system.task-project.md +76 -0
- package/corpus/templates/content/docs/product/capabilities/content.template.governance.md +76 -0
- package/corpus/templates/content/docs/product/capabilities/content.template.graph.md +76 -0
- package/corpus/templates/content/docs/product/capabilities/content.template.item-body.md +71 -0
- package/corpus/templates/content/docs/product/capabilities/content.template.update.md +71 -0
- package/corpus/templates/content/docs/product/capabilities/content.time.types.md +85 -0
- package/corpus/templates/content/docs/product/capabilities/content.version.branching.md +151 -0
- package/corpus/templates/content/docs/product/capabilities/content.version.field-history.md +103 -0
- package/corpus/templates/content/docs/product/capabilities/content.view.canvas.md +94 -0
- package/corpus/templates/content/docs/product/capabilities/content.view.chart.md +91 -0
- package/corpus/templates/content/docs/product/capabilities/content.view.dynamic-create.md +137 -0
- package/corpus/templates/content/docs/product/capabilities/content.view.fast-capture.md +82 -0
- package/corpus/templates/content/docs/product/capabilities/content.view.graph.md +95 -0
- package/corpus/templates/content/docs/product/capabilities/content.view.grouping-aggregation.md +83 -0
- package/corpus/templates/content/docs/product/capabilities/content.view.map.md +77 -0
- package/corpus/templates/content/docs/product/capabilities/content.view.personal-state.md +99 -0
- package/corpus/templates/content/docs/product/capabilities/content.view.pivot.md +76 -0
- package/corpus/templates/content/docs/product/capabilities/content.view.query.md +106 -0
- package/corpus/templates/content/docs/product/capabilities/content.view.renderer-conformance.md +77 -0
- package/corpus/templates/content/docs/product/capabilities/content.view.scale.md +77 -0
- package/corpus/templates/content/docs/product/capabilities/content.view.source-query.md +159 -0
- package/corpus/templates/content/docs/product/capabilities/content.view.timeline.md +86 -0
- package/corpus/templates/content/docs/product/capabilities/content.view.tree.md +77 -0
- package/corpus/templates/content/docs/product/capabilities/content.workspace.multi-scope.md +80 -0
- package/corpus/templates/content/docs/product/capabilities/content.workspace.session-restore.md +76 -0
- package/corpus/templates/content/docs/product/capabilities/content.workspace.view-instance.md +76 -0
- package/corpus/templates/content/docs/product/capabilities/content.workspace.working-set.md +77 -0
- package/corpus/templates/content/docs/product/chapters/content.chapter.capture-research.md +20 -0
- package/corpus/templates/content/docs/product/chapters/content.chapter.connected-sources.md +20 -0
- package/corpus/templates/content/docs/product/chapters/content.chapter.consensus.md +19 -0
- package/corpus/templates/content/docs/product/chapters/content.chapter.durable-home.md +21 -0
- package/corpus/templates/content/docs/product/chapters/content.chapter.publishing-portability.md +21 -0
- package/corpus/templates/content/docs/product/chapters/content.chapter.working-systems.md +27 -0
- package/corpus/templates/content/docs/product/encyclopedia.md +559 -0
- package/corpus/templates/content/docs/product/features/content.feature.bring-your-local-work.md +39 -0
- package/corpus/templates/content/docs/product/features/content.feature.build-living-dashboards.md +34 -0
- package/corpus/templates/content/docs/product/features/content.feature.build-new-surfaces.md +33 -0
- package/corpus/templates/content/docs/product/features/content.feature.capture-into-action.md +34 -0
- package/corpus/templates/content/docs/product/features/content.feature.cite-what-you-found.md +34 -0
- package/corpus/templates/content/docs/product/features/content.feature.collaborate-in-context.md +59 -0
- package/corpus/templates/content/docs/product/features/content.feature.collect-structured-input.md +38 -0
- package/corpus/templates/content/docs/product/features/content.feature.connect-your-sources.md +38 -0
- package/corpus/templates/content/docs/product/features/content.feature.data-that-keeps-itself-right.md +45 -0
- package/corpus/templates/content/docs/product/features/content.feature.durable-foundations.md +48 -0
- package/corpus/templates/content/docs/product/features/content.feature.evolve-systems-safely.md +38 -0
- package/corpus/templates/content/docs/product/features/content.feature.explore-alternatives-safely.md +39 -0
- package/corpus/templates/content/docs/product/features/content.feature.find-your-place-again.md +40 -0
- package/corpus/templates/content/docs/product/features/content.feature.keep-your-private-vault-private.md +39 -0
- package/corpus/templates/content/docs/product/features/content.feature.living-references.md +38 -0
- package/corpus/templates/content/docs/product/features/content.feature.make-the-workspace-yours.md +40 -0
- package/corpus/templates/content/docs/product/features/content.feature.move-without-starting-over.md +38 -0
- package/corpus/templates/content/docs/product/features/content.feature.plan-work-across-time.md +39 -0
- package/corpus/templates/content/docs/product/features/content.feature.publish-with-confidence.md +43 -0
- package/corpus/templates/content/docs/product/features/content.feature.put-your-organizations-know-how-to-work.md +38 -0
- package/corpus/templates/content/docs/product/features/content.feature.read-and-annotate-anything.md +38 -0
- package/corpus/templates/content/docs/product/features/content.feature.review-changes-in-place.md +39 -0
- package/corpus/templates/content/docs/product/features/content.feature.run-projects-your-way.md +44 -0
- package/corpus/templates/content/docs/product/features/content.feature.see-your-information-your-way.md +44 -0
- package/corpus/templates/content/docs/product/features/content.feature.share-how-your-organization-works.md +38 -0
- package/corpus/templates/content/docs/product/features/content.feature.sketch-connections-keep-whats-true.md +39 -0
- package/corpus/templates/content/docs/product/features/content.feature.take-the-whole-vault-with-you.md +39 -0
- package/corpus/templates/content/docs/product/features/content.feature.trust-your-connected-sources.md +40 -0
- package/corpus/templates/content/docs/product/features/content.feature.understand-what-your-data-says.md +39 -0
- package/corpus/templates/content/docs/product/features/content.feature.when-this-happens-that-follows.md +43 -0
- package/corpus/templates/content/docs/product/features/content.feature.work-across-every-workspace.md +37 -0
- package/corpus/templates/content/docs/product/features/content.feature.work-on-content-inside-another-application.md +37 -0
- package/corpus/templates/content/docs/product/roadmap.md +878 -0
- package/corpus/templates/crm/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
- package/corpus/templates/crm/app/components/layout/CrmSidebar.tsx +1 -7
- package/corpus/templates/crm/app/i18n/en-US.ts +4 -0
- package/corpus/templates/crm/app/routes/settings.tsx +33 -8
- package/corpus/templates/crm/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
- package/corpus/templates/design/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +133 -63
- package/corpus/templates/design/.generated/bridge/embedded-wheel.generated.ts +19 -2
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +51 -14
- package/corpus/templates/design/app/components/design/DesignExtensionsPanel.tsx +41 -26
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +22 -4
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +216 -116
- package/corpus/templates/design/app/components/design/bridge/embedded-wheel.bridge.ts +21 -2
- package/corpus/templates/design/app/components/design/code-workbench/CodeWorkbench.tsx +5 -1
- package/corpus/templates/design/app/components/design/code-workbench/CodeWorkbenchLoader.tsx +10 -2
- package/corpus/templates/design/app/components/design/code-workbench/code-workbench-shell.ts +2 -0
- package/corpus/templates/design/app/components/design/code-workbench/explorer/ExplorerView.tsx +60 -55
- package/corpus/templates/design/app/components/design/design-canvas/content-size-report.ts +88 -2
- package/corpus/templates/design/app/components/design/multi-screen/board-surface-html.ts +3 -2
- package/corpus/templates/design/app/components/design/types.ts +4 -0
- package/corpus/templates/design/app/components/layout/Sidebar.tsx +1 -5
- package/corpus/templates/design/app/pages/design-editor/clipboard-layer-source.ts +28 -0
- package/corpus/templates/design/app/pages/design-editor/clone-and-pen-edit.ts +117 -0
- package/corpus/templates/design/app/pages/design-editor/code-layer-state.ts +3 -1
- package/corpus/templates/design/app/pages/design-editor/pending-edits.ts +120 -74
- package/corpus/templates/design/app/pages/design-editor/pending-structure-verification.ts +11 -0
- package/corpus/templates/design/app/pages/design-editor/react-semantic-handoff.ts +2 -0
- package/corpus/templates/design/app/pages/design-editor/source-location.ts +1 -1
- package/corpus/templates/design/changelog/2026-07-29-copy-rendered-ui-between-live-local-apps-and-design-files-wh.md +6 -0
- package/corpus/templates/design/changelog/2026-07-29-design-boards-stay-dark-and-viewport-height-content-keeps-it.md +6 -0
- package/corpus/templates/design/changelog/2026-07-29-design-frames-no-longer-grow-continuously-when-their-content.md +6 -0
- package/corpus/templates/design/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
- package/corpus/templates/design/changelog/2026-07-29-the-code-panel-and-apply-design-update-control-now-use-clear.md +6 -0
- package/corpus/templates/design/changelog/2026-07-29-the-tools-panel-now-keeps-extension-creation-and-discovery-h.md +6 -0
- package/corpus/templates/design/shared/source-mode.ts +7 -1
- package/corpus/templates/dispatch/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
- package/corpus/templates/dispatch/app/i18n-data.ts +171 -0
- package/corpus/templates/dispatch/app/routes/browser-chat.tsx +4 -0
- package/corpus/templates/dispatch/app/routes/browser-connect.tsx +4 -0
- package/corpus/templates/dispatch/changelog/2026-07-29-browser-chat-can-securely-connect-the-agent-native-chrome-ex.md +6 -0
- package/corpus/templates/forms/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
- package/corpus/templates/forms/app/components/layout/Sidebar.tsx +1 -6
- package/corpus/templates/forms/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
- package/corpus/templates/macros/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
- package/corpus/templates/macros/app/components/layout/AppLayout.tsx +1 -5
- package/corpus/templates/macros/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
- package/corpus/templates/mail/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
- package/corpus/templates/mail/app/components/layout/AppLayout.tsx +1 -9
- package/corpus/templates/mail/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
- package/corpus/templates/plan/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
- package/corpus/templates/plan/app/components/layout/Sidebar.tsx +3 -7
- package/corpus/templates/plan/app/pages/PlansPage.tsx +2 -2
- package/corpus/templates/plan/changelog/2026-07-29-chat-history-now-closes-smoothly-and-stays-ready-when-you-re.md +6 -0
- package/corpus/templates/plan/changelog/2026-07-29-chat-history-stays-visible-when-you-return-to-ask-plan.md +6 -0
- package/corpus/templates/plan/changelog/2026-07-29-fixed-plan-pages-crashing-when-recent-activity-is-unavailabl.md +6 -0
- package/corpus/templates/plan/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
- package/corpus/templates/slides/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
- package/corpus/templates/slides/.agents/skills/analytics-data-for-decks/SKILL.md +48 -0
- package/corpus/templates/slides/.agents/skills/create-deck/SKILL.md +60 -4
- package/corpus/templates/slides/.agents/skills/design-systems/SKILL.md +20 -0
- package/corpus/templates/slides/.agents/skills/slide-editing/SKILL.md +30 -1
- package/corpus/templates/slides/.agents/skills/slide-images/SKILL.md +3 -1
- package/corpus/templates/slides/AGENTS.md +25 -24
- package/corpus/templates/slides/actions/check-image-gen.ts +8 -2
- package/corpus/templates/slides/actions/create-deck.ts +4 -12
- package/corpus/templates/slides/actions/export-pptx.ts +25 -3
- package/corpus/templates/slides/actions/generate-image-api.ts +4 -2
- package/corpus/templates/slides/actions/generate-slides-ai.ts +1 -1
- package/corpus/templates/slides/actions/get-deck-reference-context.ts +136 -0
- package/corpus/templates/slides/actions/get-workspace-defaults.ts +55 -0
- package/corpus/templates/slides/actions/image-gen-status.ts +10 -1
- package/corpus/templates/slides/actions/import-file.ts +53 -38
- package/corpus/templates/slides/actions/list-decks.ts +2 -0
- package/corpus/templates/slides/actions/patch-deck.ts +2 -0
- package/corpus/templates/slides/actions/set-workspace-defaults.ts +50 -0
- package/corpus/templates/slides/actions/view-screen.ts +6 -0
- package/corpus/templates/slides/app/components/comments/SlideCommentsPanel.tsx +1 -1
- package/corpus/templates/slides/app/components/deck/DeckCard.tsx +67 -3
- package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +40 -8
- package/corpus/templates/slides/app/components/design-system/DesignSystemCard.tsx +33 -8
- package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +6 -2
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +114 -128
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +74 -9
- package/corpus/templates/slides/app/components/editor/ImageGenPanel.tsx +1 -1
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +1193 -267
- package/corpus/templates/slides/app/components/editor/SlideStyleInspector.tsx +302 -102
- package/corpus/templates/slides/app/components/editor/rich-text-selection.ts +270 -0
- package/corpus/templates/slides/app/components/editor/slide-escape-arbiter.ts +34 -0
- package/corpus/templates/slides/app/components/editor/slide-object-interactions.ts +368 -0
- package/corpus/templates/slides/app/components/editor/slide-presence.ts +13 -0
- package/corpus/templates/slides/app/components/layout/AgentWorkIndicator.tsx +23 -3
- package/corpus/templates/slides/app/components/layout/Layout.tsx +39 -6
- package/corpus/templates/slides/app/components/layout/Sidebar.tsx +1 -6
- package/corpus/templates/slides/app/components/layout/layout-route-policy.ts +23 -0
- package/corpus/templates/slides/app/components/visual-editor/DrawOverlay.tsx +0 -2
- package/corpus/templates/slides/app/context/DeckContext.tsx +2 -0
- package/corpus/templates/slides/app/global.css +60 -0
- package/corpus/templates/slides/app/hooks/use-agent-generating.ts +76 -5
- package/corpus/templates/slides/app/hooks/use-workspace-defaults.ts +26 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +39 -3
- package/corpus/templates/slides/app/lib/generation-state.ts +13 -3
- package/corpus/templates/slides/app/lib/slide-image-replacement.ts +27 -6
- package/corpus/templates/slides/app/lib/text-tool-shortcut.ts +62 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +197 -83
- package/corpus/templates/slides/app/pages/DesignSystems.tsx +95 -0
- package/corpus/templates/slides/app/pages/Index.tsx +348 -53
- package/corpus/templates/slides/changelog/2026-07-28-pick-an-existing-deck-as-a-style-reference-when-creating-a-n.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-29-ai-editing-status-is-now-quieter-and-only-shows-slide-presen.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-29-design-systems-and-reference-decks-are-easier-to-choose-toge.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-29-generated-decks-now-receive-a-concise-relevant-title-before-.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-29-google-slides-exports-can-connect-your-google-account-direct.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-29-new-deck-prompts-reuse-empty-chats.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-29-new-deck-setup-now-stays-in-the-editor-with-closable-style-a.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-29-selected-words-and-text-runs-can-now-be-styled-independently.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-29-slide-annotation-tools-are-clearer-and-escape-now-exits-anno.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-29-slide-editing-now-opens-with-the-app-navigation-collapsed-fo.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-29-slide-editing-stays-visually-clean-on-hover-without-a-canvas.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-29-slides-now-appear-one-by-one-while-ai-builds-a-deck-with-ste.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-29-text-boxes-now-select-move-duplicate-resize-and-style-like-a.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-29-untitled-decks-now-use-a-clearer-this-deck-label-in-the-agen.md +6 -0
- package/corpus/templates/slides/server/handlers/google-docs-auth.ts +4 -2
- package/corpus/templates/slides/server/handlers/image-providers/builder.ts +305 -0
- package/corpus/templates/slides/server/handlers/image-providers/index.ts +6 -3
- package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -0
- package/corpus/templates/slides/server/workspace-defaults.ts +169 -0
- package/corpus/templates/tasks/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
- package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +31 -8
- package/corpus/templates/tasks/app/i18n/en-US.ts +7 -0
- package/corpus/templates/tasks/app/routes/settings.tsx +25 -2
- package/corpus/templates/tasks/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
- package/corpus/toolkit/CHANGELOG.md +15 -0
- package/corpus/toolkit/README.md +2 -2
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/src/app-shell/sidebar-footer-actions.tsx +0 -3
- package/corpus/toolkit/src/chat-history.css +1 -0
- package/corpus/toolkit/src/composer/ComposerPlusMenu.tsx +35 -14
- package/corpus/toolkit/src/composer/PromptComposer.tsx +6 -0
- package/corpus/toolkit/src/composer/TiptapComposer.tsx +11 -3
- package/corpus/toolkit/src/design-tweaks/visual-style-controls.tsx +64 -11
- package/dist/a2a/types.d.ts +6 -0
- package/dist/a2a/types.d.ts.map +1 -1
- package/dist/a2a/types.js.map +1 -1
- package/dist/action.d.ts +57 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +9 -0
- package/dist/action.js.map +1 -1
- package/dist/agent/engine/builder-engine.js +6 -4
- package/dist/agent/engine/builder-engine.js.map +1 -1
- package/dist/agent/production-agent.d.ts +11 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +131 -90
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/tool-search.d.ts +4 -0
- package/dist/agent/tool-search.d.ts.map +1 -1
- package/dist/agent/tool-search.js +30 -0
- package/dist/agent/tool-search.js.map +1 -1
- package/dist/automations/service.d.ts +85 -0
- package/dist/automations/service.d.ts.map +1 -0
- package/dist/automations/service.js +332 -0
- package/dist/automations/service.js.map +1 -0
- package/dist/browser-context/index.d.ts +579 -0
- package/dist/browser-context/index.d.ts.map +1 -0
- package/dist/browser-context/index.js +331 -0
- package/dist/browser-context/index.js.map +1 -0
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +1 -1
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/FeedbackButton.d.ts +6 -2
- package/dist/client/FeedbackButton.d.ts.map +1 -1
- package/dist/client/FeedbackButton.js +20 -3
- package/dist/client/FeedbackButton.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +19 -2
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/agent-chat.d.ts +15 -0
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js +13 -3
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/agent-page/AgentJobsTab.d.ts +1 -0
- package/dist/client/agent-page/AgentJobsTab.d.ts.map +1 -1
- package/dist/client/agent-page/AgentJobsTab.js +186 -98
- package/dist/client/agent-page/AgentJobsTab.js.map +1 -1
- package/dist/client/agent-page/AgentTabsPage.js +4 -4
- package/dist/client/agent-page/AgentTabsPage.js.map +1 -1
- package/dist/client/agent-page/use-jobs.d.ts +7 -0
- package/dist/client/agent-page/use-jobs.d.ts.map +1 -1
- package/dist/client/agent-page/use-jobs.js.map +1 -1
- package/dist/client/analytics-session.d.ts.map +1 -1
- package/dist/client/analytics-session.js +13 -0
- package/dist/client/analytics-session.js.map +1 -1
- package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
- package/dist/client/extensions/ExtensionViewer.js +29 -6
- package/dist/client/extensions/ExtensionViewer.js.map +1 -1
- package/dist/client/settings/AutomationsSection.d.ts +0 -9
- package/dist/client/settings/AutomationsSection.d.ts.map +1 -1
- package/dist/client/settings/AutomationsSection.js +9 -168
- package/dist/client/settings/AutomationsSection.js.map +1 -1
- package/dist/client/settings/SettingsPanel.d.ts +9 -1
- package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
- package/dist/client/settings/SettingsPanel.js +19 -11
- package/dist/client/settings/SettingsPanel.js.map +1 -1
- package/dist/client/settings/index.d.ts +1 -1
- package/dist/client/settings/index.d.ts.map +1 -1
- package/dist/client/settings/index.js +1 -1
- package/dist/client/settings/index.js.map +1 -1
- package/dist/client/use-agent-chat.d.ts.map +1 -1
- package/dist/client/use-agent-chat.js +20 -3
- package/dist/client/use-agent-chat.js.map +1 -1
- package/dist/client/use-chat-threads.d.ts.map +1 -1
- package/dist/client/use-chat-threads.js +4 -17
- package/dist/client/use-chat-threads.js.map +1 -1
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +2 -1
- package/dist/deploy/build.js.map +1 -1
- package/dist/deploy/workspace-deploy.d.ts.map +1 -1
- package/dist/deploy/workspace-deploy.js +2 -1
- package/dist/deploy/workspace-deploy.js.map +1 -1
- package/dist/extensions/actions.d.ts.map +1 -1
- package/dist/extensions/actions.js +82 -6
- package/dist/extensions/actions.js.map +1 -1
- package/dist/extensions/fetch-tool.d.ts.map +1 -1
- package/dist/extensions/fetch-tool.js +11 -0
- package/dist/extensions/fetch-tool.js.map +1 -1
- package/dist/extensions/routes.d.ts +5 -1
- package/dist/extensions/routes.d.ts.map +1 -1
- package/dist/extensions/routes.js +7 -1
- package/dist/extensions/routes.js.map +1 -1
- package/dist/extensions/web-content.d.ts +1 -0
- package/dist/extensions/web-content.d.ts.map +1 -1
- package/dist/extensions/web-content.js +82 -1
- package/dist/extensions/web-content.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/integrations/computer-supervision.d.ts.map +1 -1
- package/dist/integrations/computer-supervision.js +22 -1
- package/dist/integrations/computer-supervision.js.map +1 -1
- package/dist/integrations/index.d.ts +3 -2
- package/dist/integrations/index.d.ts.map +1 -1
- package/dist/integrations/index.js +3 -2
- package/dist/integrations/index.js.map +1 -1
- package/dist/integrations/integration-durable-dispatch-config.d.ts +2 -0
- package/dist/integrations/integration-durable-dispatch-config.d.ts.map +1 -1
- package/dist/integrations/integration-durable-dispatch-config.js +4 -0
- package/dist/integrations/integration-durable-dispatch-config.js.map +1 -1
- package/dist/integrations/integration-durable-dispatch.d.ts +2 -2
- package/dist/integrations/integration-durable-dispatch.d.ts.map +1 -1
- package/dist/integrations/integration-durable-dispatch.js +2 -2
- package/dist/integrations/integration-durable-dispatch.js.map +1 -1
- package/dist/integrations/plugin.d.ts.map +1 -1
- package/dist/integrations/plugin.js +84 -39
- package/dist/integrations/plugin.js.map +1 -1
- package/dist/integrations/remote-browser-actions.d.ts +7 -0
- package/dist/integrations/remote-browser-actions.d.ts.map +1 -0
- package/dist/integrations/remote-browser-actions.js +482 -0
- package/dist/integrations/remote-browser-actions.js.map +1 -0
- package/dist/jobs/actions/list-recurring-jobs.d.ts.map +1 -1
- package/dist/jobs/actions/list-recurring-jobs.js +4 -6
- package/dist/jobs/actions/list-recurring-jobs.js.map +1 -1
- package/dist/jobs/actions/manage-recurring-job.js +3 -6
- package/dist/jobs/actions/manage-recurring-job.js.map +1 -1
- package/dist/jobs/background-automation-runner.d.ts +76 -0
- package/dist/jobs/background-automation-runner.d.ts.map +1 -0
- package/dist/jobs/background-automation-runner.js +288 -0
- package/dist/jobs/background-automation-runner.js.map +1 -0
- package/dist/jobs/frontmatter.d.ts +63 -0
- package/dist/jobs/frontmatter.d.ts.map +1 -0
- package/dist/jobs/frontmatter.js +216 -0
- package/dist/jobs/frontmatter.js.map +1 -0
- package/dist/jobs/scheduler.d.ts +6 -60
- package/dist/jobs/scheduler.d.ts.map +1 -1
- package/dist/jobs/scheduler.js +67 -484
- package/dist/jobs/scheduler.js.map +1 -1
- package/dist/jobs/tools.d.ts.map +1 -1
- package/dist/jobs/tools.js +24 -3
- package/dist/jobs/tools.js.map +1 -1
- package/dist/localization/default-messages.d.ts +28 -33
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +34 -39
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
- package/dist/mcp-client/index.d.ts.map +1 -1
- package/dist/mcp-client/index.js +4 -0
- package/dist/mcp-client/index.js.map +1 -1
- package/dist/notifications/actions.d.ts.map +1 -1
- package/dist/notifications/actions.js +5 -0
- package/dist/notifications/actions.js.map +1 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/progress/actions.d.ts.map +1 -1
- package/dist/progress/actions.js +5 -0
- package/dist/progress/actions.js.map +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +14 -14
- package/dist/provider-api/actions/github-repo-files.d.ts.map +1 -1
- package/dist/provider-api/actions/github-repo-files.js +9 -0
- package/dist/provider-api/actions/github-repo-files.js.map +1 -1
- package/dist/provider-api/actions/provider-api.d.ts +16 -16
- package/dist/provider-api/actions/provider-api.d.ts.map +1 -1
- package/dist/provider-api/actions/provider-api.js +14 -0
- package/dist/provider-api/actions/provider-api.js.map +1 -1
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/action-discovery.d.ts.map +1 -1
- package/dist/server/action-discovery.js +5 -0
- package/dist/server/action-discovery.js.map +1 -1
- package/dist/server/agent-capabilities.d.ts.map +1 -1
- package/dist/server/agent-capabilities.js +32 -3
- package/dist/server/agent-capabilities.js.map +1 -1
- package/dist/server/agent-chat/action-filters-a2a.d.ts +17 -1
- package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
- package/dist/server/agent-chat/action-filters-a2a.js +50 -1
- package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
- package/dist/server/agent-chat/browser-team-tools.d.ts.map +1 -1
- package/dist/server/agent-chat/browser-team-tools.js +90 -3
- package/dist/server/agent-chat/browser-team-tools.js.map +1 -1
- package/dist/server/agent-chat/framework-prompts.d.ts.map +1 -1
- package/dist/server/agent-chat/framework-prompts.js +7 -11
- package/dist/server/agent-chat/framework-prompts.js.map +1 -1
- package/dist/server/agent-chat/plugin-options.d.ts +3 -3
- package/dist/server/agent-chat/plugin-options.js.map +1 -1
- package/dist/server/agent-chat/script-entries.d.ts.map +1 -1
- package/dist/server/agent-chat/script-entries.js +43 -1
- package/dist/server/agent-chat/script-entries.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +24 -5
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/attribution.d.ts +6 -0
- package/dist/server/attribution.d.ts.map +1 -1
- package/dist/server/attribution.js +14 -14
- package/dist/server/attribution.js.map +1 -1
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +5 -2
- package/dist/server/auth.js.map +1 -1
- package/dist/server/better-auth-instance.d.ts +2 -1
- package/dist/server/better-auth-instance.d.ts.map +1 -1
- package/dist/server/better-auth-instance.js +6 -3
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +7 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +3 -1
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/csrf.d.ts.map +1 -1
- package/dist/server/csrf.js +25 -0
- package/dist/server/csrf.js.map +1 -1
- package/dist/server/google-oauth.d.ts +3 -0
- package/dist/server/google-oauth.d.ts.map +1 -1
- package/dist/server/google-oauth.js +12 -1
- package/dist/server/google-oauth.js.map +1 -1
- package/dist/server/onboarding-html.d.ts.map +1 -1
- package/dist/server/onboarding-html.js +18 -0
- package/dist/server/onboarding-html.js.map +1 -1
- package/dist/server/prompts/framework-core-compact.js +3 -3
- package/dist/server/prompts/framework-core-compact.js.map +1 -1
- package/dist/server/prompts/framework-core.js +3 -3
- package/dist/server/prompts/framework-core.js.map +1 -1
- package/dist/server/prompts/shared-rules.js +3 -3
- package/dist/server/prompts/shared-rules.js.map +1 -1
- package/dist/settings/index.d.ts +1 -1
- package/dist/settings/index.d.ts.map +1 -1
- package/dist/settings/index.js +1 -1
- package/dist/settings/index.js.map +1 -1
- package/dist/shared/analytics-anonymous-id.d.ts +12 -0
- package/dist/shared/analytics-anonymous-id.d.ts.map +1 -0
- package/dist/shared/analytics-anonymous-id.js +29 -0
- package/dist/shared/analytics-anonymous-id.js.map +1 -0
- package/dist/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
- package/dist/templates/chat/app/components/layout/Sidebar.tsx +1 -5
- package/dist/templates/chat/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
- package/dist/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
- package/dist/templates/default/app/i18n/ar-SA.ts +26 -0
- package/dist/templates/default/app/i18n/de-DE.ts +28 -0
- package/dist/templates/default/app/i18n/en-US.ts +27 -0
- package/dist/templates/default/app/i18n/es-ES.ts +28 -0
- package/dist/templates/default/app/i18n/fr-FR.ts +28 -0
- package/dist/templates/default/app/i18n/hi-IN.ts +25 -0
- package/dist/templates/default/app/i18n/ja-JP.ts +27 -0
- package/dist/templates/default/app/i18n/ko-KR.ts +27 -0
- package/dist/templates/default/app/i18n/pt-BR.ts +27 -0
- package/dist/templates/default/app/i18n/zh-CN.ts +24 -0
- package/dist/templates/default/app/i18n/zh-TW.ts +24 -0
- package/dist/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
- package/dist/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
- package/dist/templates/workspace-core/.agents/skills/agent-page/SKILL.md +10 -7
- package/dist/templates/workspace-core/.agents/skills/automations/SKILL.md +53 -8
- package/dist/templates/workspace-core/.agents/skills/browser-sessions/SKILL.md +131 -0
- package/dist/templates/workspace-core/.agents/skills/extension-points/SKILL.md +13 -4
- package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +23 -15
- package/dist/templates/workspace-core/.agents/skills/recurring-jobs/SKILL.md +33 -18
- package/dist/tracking/providers.d.ts.map +1 -1
- package/dist/tracking/providers.js +1 -0
- package/dist/tracking/providers.js.map +1 -1
- package/dist/tracking/registry.d.ts +1 -0
- package/dist/tracking/registry.d.ts.map +1 -1
- package/dist/tracking/registry.js +1 -0
- package/dist/tracking/registry.js.map +1 -1
- package/dist/tracking/types.d.ts +1 -0
- package/dist/tracking/types.d.ts.map +1 -1
- package/dist/tracking/types.js.map +1 -1
- package/dist/triggers/actions/list-automations.d.ts +7 -0
- package/dist/triggers/actions/list-automations.d.ts.map +1 -1
- package/dist/triggers/actions/list-automations.js +29 -41
- package/dist/triggers/actions/list-automations.js.map +1 -1
- package/dist/triggers/actions/manage-automation.js +15 -33
- package/dist/triggers/actions/manage-automation.js.map +1 -1
- package/dist/triggers/actions.d.ts.map +1 -1
- package/dist/triggers/actions.js +184 -95
- package/dist/triggers/actions.js.map +1 -1
- package/dist/triggers/dispatcher.d.ts +3 -9
- package/dist/triggers/dispatcher.d.ts.map +1 -1
- package/dist/triggers/dispatcher.js +134 -304
- package/dist/triggers/dispatcher.js.map +1 -1
- package/dist/triggers/routes.d.ts.map +1 -1
- package/dist/triggers/routes.js +76 -17
- package/dist/triggers/routes.js.map +1 -1
- package/dist/triggers/types.d.ts +4 -23
- package/dist/triggers/types.d.ts.map +1 -1
- package/dist/triggers/types.js.map +1 -1
- package/docs/content/agent-surfaces.mdx +4 -4
- package/docs/content/automations.mdx +61 -12
- package/docs/content/deployment.mdx +11 -4
- package/docs/content/extensions.mdx +39 -33
- package/docs/content/recurring-jobs.mdx +41 -19
- package/docs/content/template-dispatch-operations.mdx +19 -7
- package/package.json +4 -2
- package/src/a2a/types.ts +6 -0
- package/src/action.ts +68 -0
- package/src/agent/engine/builder-engine.ts +8 -4
- package/src/agent/production-agent.ts +175 -100
- package/src/agent/tool-search.ts +36 -0
- package/src/automations/service.ts +506 -0
- package/src/browser-context/index.ts +392 -0
- package/src/client/AgentPanel.tsx +2 -1
- package/src/client/FeedbackButton.tsx +37 -4
- package/src/client/MultiTabAssistantChat.tsx +21 -0
- package/src/client/agent-chat.ts +31 -3
- package/src/client/agent-page/AgentJobsTab.tsx +438 -310
- package/src/client/agent-page/AgentTabsPage.tsx +4 -4
- package/src/client/agent-page/use-jobs.ts +7 -0
- package/src/client/analytics-session.ts +13 -0
- package/src/client/extensions/ExtensionViewer.tsx +67 -0
- package/src/client/settings/AutomationsSection.tsx +27 -499
- package/src/client/settings/SettingsPanel.tsx +32 -11
- package/src/client/settings/index.ts +2 -0
- package/src/client/use-agent-chat.ts +35 -3
- package/src/client/use-chat-threads.ts +8 -25
- package/src/deploy/build.ts +2 -0
- package/src/deploy/workspace-deploy.ts +2 -0
- package/src/extensions/actions.ts +115 -3
- package/src/extensions/fetch-tool.ts +11 -0
- package/src/extensions/routes.ts +16 -1
- package/src/extensions/web-content.ts +96 -1
- package/src/integrations/computer-supervision.ts +33 -1
- package/src/integrations/index.ts +6 -0
- package/src/integrations/integration-durable-dispatch-config.ts +10 -0
- package/src/integrations/integration-durable-dispatch.ts +4 -0
- package/src/integrations/plugin.ts +94 -41
- package/src/integrations/remote-browser-actions.ts +621 -0
- package/src/jobs/actions/list-recurring-jobs.ts +5 -7
- package/src/jobs/actions/manage-recurring-job.ts +3 -7
- package/src/jobs/background-automation-runner.ts +437 -0
- package/src/jobs/frontmatter.ts +308 -0
- package/src/jobs/scheduler.ts +89 -598
- package/src/jobs/tools.ts +22 -3
- package/src/localization/default-messages.ts +45 -45
- package/src/mcp-client/index.ts +6 -0
- package/src/notifications/actions.ts +5 -0
- package/src/progress/actions.ts +5 -0
- package/src/provider-api/actions/github-repo-files.ts +11 -0
- package/src/provider-api/actions/provider-api.ts +13 -0
- package/src/server/__snapshots__/agent-chat-plugin.surface.spec.ts.snap +4 -4
- package/src/server/action-discovery.ts +7 -0
- package/src/server/agent-capabilities.ts +43 -8
- package/src/server/agent-chat/action-filters-a2a.ts +63 -1
- package/src/server/agent-chat/browser-team-tools.ts +126 -3
- package/src/server/agent-chat/framework-prompts.ts +7 -11
- package/src/server/agent-chat/plugin-options.ts +3 -3
- package/src/server/agent-chat/script-entries.ts +52 -1
- package/src/server/agent-chat-plugin.ts +25 -5
- package/src/server/attribution.ts +22 -0
- package/src/server/auth.ts +20 -6
- package/src/server/better-auth-instance.ts +10 -2
- package/src/server/core-routes-plugin.ts +10 -1
- package/src/server/csrf.ts +25 -0
- package/src/server/google-oauth.ts +21 -1
- package/src/server/onboarding-html.ts +18 -0
- package/src/server/prompts/framework-core-compact.ts +3 -3
- package/src/server/prompts/framework-core.ts +3 -3
- package/src/server/prompts/shared-rules.ts +3 -3
- package/src/settings/index.ts +1 -0
- package/src/shared/analytics-anonymous-id.ts +35 -0
- package/src/templates/chat/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
- package/src/templates/chat/app/components/layout/Sidebar.tsx +1 -5
- package/src/templates/chat/changelog/2026-07-29-sidebar-footers-now-keep-feedback-search-and-collapse-togeth.md +6 -0
- package/src/templates/default/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
- package/src/templates/default/app/i18n/ar-SA.ts +26 -0
- package/src/templates/default/app/i18n/de-DE.ts +28 -0
- package/src/templates/default/app/i18n/en-US.ts +27 -0
- package/src/templates/default/app/i18n/es-ES.ts +28 -0
- package/src/templates/default/app/i18n/fr-FR.ts +28 -0
- package/src/templates/default/app/i18n/hi-IN.ts +25 -0
- package/src/templates/default/app/i18n/ja-JP.ts +27 -0
- package/src/templates/default/app/i18n/ko-KR.ts +27 -0
- package/src/templates/default/app/i18n/pt-BR.ts +27 -0
- package/src/templates/default/app/i18n/zh-CN.ts +24 -0
- package/src/templates/default/app/i18n/zh-TW.ts +24 -0
- package/src/templates/headless/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
- package/src/templates/workspace-core/.agents/skills/agent-native-toolkit/SKILL.md +5 -0
- package/src/templates/workspace-core/.agents/skills/agent-page/SKILL.md +10 -7
- package/src/templates/workspace-core/.agents/skills/automations/SKILL.md +53 -8
- package/src/templates/workspace-core/.agents/skills/browser-sessions/SKILL.md +131 -0
- package/src/templates/workspace-core/.agents/skills/extension-points/SKILL.md +13 -4
- package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +23 -15
- package/src/templates/workspace-core/.agents/skills/recurring-jobs/SKILL.md +33 -18
- package/src/tracking/providers.ts +1 -0
- package/src/tracking/registry.ts +2 -1
- package/src/tracking/types.ts +1 -0
- package/src/triggers/actions/list-automations.ts +43 -44
- package/src/triggers/actions/manage-automation.ts +17 -48
- package/src/triggers/actions.ts +218 -127
- package/src/triggers/dispatcher.ts +181 -367
- package/src/triggers/routes.ts +84 -27
- package/src/triggers/types.ts +9 -23
|
@@ -18,8 +18,8 @@ import {
|
|
|
18
18
|
import { appStateKeyForBrowserTab } from "@shared/app-state-tabs";
|
|
19
19
|
import {
|
|
20
20
|
IconAlertTriangle,
|
|
21
|
-
IconArrowsMove,
|
|
22
21
|
IconMaximize,
|
|
22
|
+
IconX,
|
|
23
23
|
IconZoomIn,
|
|
24
24
|
IconZoomOut,
|
|
25
25
|
} from "@tabler/icons-react";
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
useCallback,
|
|
29
29
|
useRef,
|
|
30
30
|
useEffect,
|
|
31
|
+
useLayoutEffect,
|
|
31
32
|
type MouseEvent as ReactMouseEvent,
|
|
32
33
|
type ReactNode,
|
|
33
34
|
} from "react";
|
|
@@ -71,6 +72,37 @@ import { enterSelectionMode } from "@/root";
|
|
|
71
72
|
import type { DesignSystemData } from "../../../shared/api";
|
|
72
73
|
import { BlockBubbleMenu } from "./BlockBubbleMenu";
|
|
73
74
|
import ImageOverlay from "./ImageOverlay";
|
|
75
|
+
import {
|
|
76
|
+
applyInlineTextStyle,
|
|
77
|
+
getInlineTextStyleSnapshot,
|
|
78
|
+
getInlineTextStyleSnapshotForRange,
|
|
79
|
+
restoreEditableTextRange,
|
|
80
|
+
snapshotEditableTextRange,
|
|
81
|
+
type InlineTextStylePatch,
|
|
82
|
+
type InlineTextStyleSnapshot,
|
|
83
|
+
} from "./rich-text-selection";
|
|
84
|
+
import { decideSlideEscape } from "./slide-escape-arbiter";
|
|
85
|
+
import {
|
|
86
|
+
clientPointToSlideCoordinates,
|
|
87
|
+
cloneSlideObject,
|
|
88
|
+
createSlidesSelectionState,
|
|
89
|
+
ensureSlideObjectId,
|
|
90
|
+
escapedEditingSelection,
|
|
91
|
+
findSlideObjectById,
|
|
92
|
+
freezeSlideElementForFreeform,
|
|
93
|
+
getSelectedObjectDragStart,
|
|
94
|
+
getSlideSelectionIdentity,
|
|
95
|
+
getSlideSelectionMode,
|
|
96
|
+
removeSlideObjectAndLayoutSpacer,
|
|
97
|
+
resolveSlideObjectContainingBlock,
|
|
98
|
+
resizeSlideObject,
|
|
99
|
+
type ResizeHandle,
|
|
100
|
+
type SlideObjectGeometry,
|
|
101
|
+
type SlidesSelectionMode,
|
|
102
|
+
type SlidesSelectionState as BaseSlidesSelectionState,
|
|
103
|
+
type SlidesSelectionTool,
|
|
104
|
+
} from "./slide-object-interactions";
|
|
105
|
+
import { getPassiveSlidePresenceUsers } from "./slide-presence";
|
|
74
106
|
import {
|
|
75
107
|
SlideStyleInspector,
|
|
76
108
|
type SlideStylePatch,
|
|
@@ -80,14 +112,52 @@ import { SpeakerNotesPanel } from "./SpeakerNotesPanel";
|
|
|
80
112
|
|
|
81
113
|
let builderIdCounter = 0;
|
|
82
114
|
const CANVAS_ZOOM_PRESETS = [10, 25, 50, 75, 100, 125, 150, 200] as const;
|
|
115
|
+
// Keep the text itself a normal click-to-edit surface. Only the slim perimeter
|
|
116
|
+
// is reserved for object movement, matching the familiar Slides interaction.
|
|
117
|
+
const ELEMENT_EDGE_MOVE_BAND = 8;
|
|
118
|
+
|
|
119
|
+
function isWithinElementEdgeMoveBand(
|
|
120
|
+
rect: DOMRect,
|
|
121
|
+
clientX: number,
|
|
122
|
+
clientY: number,
|
|
123
|
+
): boolean {
|
|
124
|
+
// Tiny labels still need a usable center click target. Cap the move band at
|
|
125
|
+
// one quarter of either dimension so its opposite edges never consume it.
|
|
126
|
+
const edgeBand = Math.min(
|
|
127
|
+
ELEMENT_EDGE_MOVE_BAND,
|
|
128
|
+
rect.width / 4,
|
|
129
|
+
rect.height / 4,
|
|
130
|
+
);
|
|
131
|
+
const outerBand = edgeBand / 2;
|
|
132
|
+
const withinExpandedBounds =
|
|
133
|
+
clientX >= rect.left - outerBand &&
|
|
134
|
+
clientX <= rect.right + outerBand &&
|
|
135
|
+
clientY >= rect.top - outerBand &&
|
|
136
|
+
clientY <= rect.bottom + outerBand;
|
|
137
|
+
if (!withinExpandedBounds) return false;
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
Math.abs(clientX - rect.left) <= edgeBand ||
|
|
141
|
+
Math.abs(clientX - rect.right) <= edgeBand ||
|
|
142
|
+
Math.abs(clientY - rect.top) <= edgeBand ||
|
|
143
|
+
Math.abs(clientY - rect.bottom) <= edgeBand
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function ensureBuilderId(element: HTMLElement): string {
|
|
148
|
+
const existing = element.getAttribute("data-builder-id");
|
|
149
|
+
if (existing) return existing;
|
|
150
|
+
const id = `b-${++builderIdCounter}`;
|
|
151
|
+
element.setAttribute("data-builder-id", id);
|
|
152
|
+
return id;
|
|
153
|
+
}
|
|
83
154
|
|
|
84
155
|
/** Stamp all elements inside a container with unique data-builder-id attributes */
|
|
85
156
|
function stampBuilderIds(container: HTMLElement) {
|
|
86
157
|
const elements = container.querySelectorAll("*");
|
|
87
158
|
elements.forEach((el) => {
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
}
|
|
159
|
+
if ((el as HTMLElement).classList.contains("fmd-layout-spacer")) return;
|
|
160
|
+
ensureBuilderId(el as HTMLElement);
|
|
91
161
|
});
|
|
92
162
|
}
|
|
93
163
|
|
|
@@ -285,6 +355,38 @@ function stylePropertyName(property: string): string {
|
|
|
285
355
|
return property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
286
356
|
}
|
|
287
357
|
|
|
358
|
+
const INLINE_INSPECTOR_STYLE_KEYS = [
|
|
359
|
+
"color",
|
|
360
|
+
"fontSize",
|
|
361
|
+
"fontWeight",
|
|
362
|
+
] as const satisfies readonly (keyof SlideStylePatch)[];
|
|
363
|
+
|
|
364
|
+
function inlineInspectorStylePatch(
|
|
365
|
+
patch: SlideStylePatch,
|
|
366
|
+
): InlineTextStylePatch {
|
|
367
|
+
return Object.fromEntries(
|
|
368
|
+
INLINE_INSPECTOR_STYLE_KEYS.flatMap((key) => {
|
|
369
|
+
const value = patch[key];
|
|
370
|
+
return value === undefined ? [] : [[key, value]];
|
|
371
|
+
}),
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function applyDescendantTextStyle(
|
|
376
|
+
element: HTMLElement,
|
|
377
|
+
patch: InlineTextStylePatch,
|
|
378
|
+
) {
|
|
379
|
+
for (const descendant of Array.from(
|
|
380
|
+
element.querySelectorAll<HTMLElement>("*"),
|
|
381
|
+
)) {
|
|
382
|
+
for (const [property, value] of Object.entries(patch)) {
|
|
383
|
+
if (value !== undefined) {
|
|
384
|
+
descendant.style.setProperty(stylePropertyName(property), value);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
288
390
|
function elementPathFromRoot(
|
|
289
391
|
root: HTMLElement,
|
|
290
392
|
element: HTMLElement,
|
|
@@ -315,20 +417,36 @@ function resolveElementPath(
|
|
|
315
417
|
function buildStyleSnapshot(
|
|
316
418
|
element: HTMLElement,
|
|
317
419
|
selector: string,
|
|
420
|
+
inlineTextStyle?: InlineTextStyleSnapshot,
|
|
318
421
|
): SlideStyleSnapshot {
|
|
319
422
|
const computed = window.getComputedStyle(element);
|
|
423
|
+
const fmdSlide = element.closest(".fmd-slide") as HTMLElement | null;
|
|
424
|
+
const isAbsolute = computed.position === "absolute";
|
|
425
|
+
const slideWidth = fmdSlide?.offsetWidth ?? 0;
|
|
426
|
+
const slideHeight = fmdSlide?.offsetHeight ?? 0;
|
|
427
|
+
const matrix = new DOMMatrixReadOnly(computed.transform);
|
|
428
|
+
const rotation =
|
|
429
|
+
computed.transform === "none"
|
|
430
|
+
? 0
|
|
431
|
+
: Math.round((Math.atan2(matrix.b, matrix.a) * 180) / Math.PI);
|
|
320
432
|
const textPreview = (element.textContent ?? "").trim().slice(0, 80);
|
|
321
|
-
const
|
|
433
|
+
const blockFontSize = cssPx(computed.fontSize);
|
|
322
434
|
const rawLineHeight = cssPx(computed.lineHeight);
|
|
323
435
|
const lineHeight =
|
|
324
|
-
rawLineHeight > 0 &&
|
|
325
|
-
? Number((rawLineHeight /
|
|
436
|
+
rawLineHeight > 0 && blockFontSize > 0
|
|
437
|
+
? Number((rawLineHeight / blockFontSize).toFixed(2))
|
|
326
438
|
: 1.2;
|
|
327
439
|
const paddingLeft = cssPx(computed.paddingLeft);
|
|
328
440
|
const paddingRight = cssPx(computed.paddingRight);
|
|
329
441
|
const paddingTop = cssPx(computed.paddingTop);
|
|
330
442
|
const paddingBottom = cssPx(computed.paddingBottom);
|
|
331
443
|
|
|
444
|
+
const selectedTextStyle =
|
|
445
|
+
inlineTextStyle?.scope === "selection" ? inlineTextStyle : null;
|
|
446
|
+
const selectedColor = selectedTextStyle?.values.color;
|
|
447
|
+
const selectedFontSize = selectedTextStyle?.values.fontSize;
|
|
448
|
+
const selectedFontWeight = selectedTextStyle?.values.fontWeight;
|
|
449
|
+
|
|
332
450
|
return {
|
|
333
451
|
selector,
|
|
334
452
|
label: element.getAttribute("aria-label") || element.tagName.toLowerCase(),
|
|
@@ -338,10 +456,19 @@ function buildStyleSnapshot(
|
|
|
338
456
|
element.tagName !== "IMG" &&
|
|
339
457
|
(!!textPreview || element.classList.contains("fmd-text-box")),
|
|
340
458
|
isImage: element.tagName === "IMG",
|
|
341
|
-
color:
|
|
459
|
+
color:
|
|
460
|
+
selectedColor === null || selectedColor === undefined
|
|
461
|
+
? normalizedColor(computed.color)
|
|
462
|
+
: normalizedColor(selectedColor),
|
|
342
463
|
backgroundColor: normalizedColor(computed.backgroundColor),
|
|
343
|
-
fontSize
|
|
344
|
-
|
|
464
|
+
fontSize:
|
|
465
|
+
selectedFontSize === null || selectedFontSize === undefined
|
|
466
|
+
? blockFontSize
|
|
467
|
+
: cssPx(selectedFontSize),
|
|
468
|
+
fontWeight:
|
|
469
|
+
selectedFontWeight === null || selectedFontWeight === undefined
|
|
470
|
+
? normalizedFontWeight(computed.fontWeight)
|
|
471
|
+
: normalizedFontWeight(selectedFontWeight),
|
|
345
472
|
lineHeight,
|
|
346
473
|
textAlign: normalizedTextAlign(computed.textAlign),
|
|
347
474
|
opacity: Math.round(Number(computed.opacity || 1) * 100),
|
|
@@ -350,11 +477,23 @@ function buildStyleSnapshot(
|
|
|
350
477
|
borderColor: normalizedColor(computed.borderTopColor),
|
|
351
478
|
paddingX: Math.round((paddingLeft + paddingRight) / 2),
|
|
352
479
|
paddingY: Math.round((paddingTop + paddingBottom) / 2),
|
|
480
|
+
textStyleScope: inlineTextStyle?.scope ?? "block",
|
|
481
|
+
mixedTextStyles: selectedTextStyle?.mixed ?? [],
|
|
482
|
+
isAbsolute,
|
|
483
|
+
x: Math.round(element.offsetLeft),
|
|
484
|
+
y: Math.round(element.offsetTop),
|
|
485
|
+
width: Math.round(element.offsetWidth),
|
|
486
|
+
height: Math.round(element.offsetHeight),
|
|
487
|
+
rotation,
|
|
488
|
+
slideWidth,
|
|
489
|
+
slideHeight,
|
|
353
490
|
};
|
|
354
491
|
}
|
|
355
492
|
|
|
356
493
|
interface SlideSelectionItem {
|
|
357
494
|
selector: string;
|
|
495
|
+
runtimeSelector?: string;
|
|
496
|
+
objectId?: string;
|
|
358
497
|
text?: string;
|
|
359
498
|
kind?: string;
|
|
360
499
|
tagName?: string;
|
|
@@ -362,14 +501,30 @@ interface SlideSelectionItem {
|
|
|
362
501
|
style?: Partial<SlideStyleSnapshot>;
|
|
363
502
|
}
|
|
364
503
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
504
|
+
type SlidesSelectionState = BaseSlidesSelectionState<SlideSelectionItem>;
|
|
505
|
+
|
|
506
|
+
function selectionItemForElement(
|
|
507
|
+
element: HTMLElement,
|
|
508
|
+
runtimeSelector: string,
|
|
509
|
+
snapshot?: SlideStyleSnapshot,
|
|
510
|
+
): SlideSelectionItem {
|
|
511
|
+
const identity = getSlideSelectionIdentity(element, runtimeSelector);
|
|
512
|
+
return {
|
|
513
|
+
...identity,
|
|
514
|
+
kind: snapshot?.isImage
|
|
515
|
+
? "image"
|
|
516
|
+
: element.tagName === "IMG"
|
|
517
|
+
? "image"
|
|
518
|
+
: "element",
|
|
519
|
+
tagName: snapshot?.tagName ?? element.tagName.toLowerCase(),
|
|
520
|
+
text:
|
|
521
|
+
snapshot?.textPreview ?? (element.textContent || "").trim().slice(0, 200),
|
|
522
|
+
imageSrc:
|
|
523
|
+
element instanceof HTMLImageElement
|
|
524
|
+
? (element.getAttribute("src") ?? undefined)
|
|
525
|
+
: undefined,
|
|
526
|
+
style: snapshot ? { ...snapshot, selector: identity.selector } : undefined,
|
|
527
|
+
};
|
|
373
528
|
}
|
|
374
529
|
|
|
375
530
|
function syncSelectionToAppState(state: SlidesSelectionState | null) {
|
|
@@ -416,6 +571,14 @@ interface SlideEditorProps {
|
|
|
416
571
|
file: File,
|
|
417
572
|
position?: { x: number; y: number },
|
|
418
573
|
) => void;
|
|
574
|
+
/** Fired when an image is dragged from elsewhere in the app (e.g. a
|
|
575
|
+
* generated-image preview in the agent chat panel) and dropped on the
|
|
576
|
+
* slide canvas, instead of a native OS file drop. */
|
|
577
|
+
onDropImageUrl?: (
|
|
578
|
+
replaceSrc: string | null,
|
|
579
|
+
url: string,
|
|
580
|
+
position?: { x: number; y: number },
|
|
581
|
+
) => void;
|
|
419
582
|
onToggleObjectFit: (imgSrc: string, newFit: string) => void;
|
|
420
583
|
/** Current user display info for cursor caret */
|
|
421
584
|
collabUser?: { name: string; color: string };
|
|
@@ -434,6 +597,10 @@ interface SlideEditorProps {
|
|
|
434
597
|
designSystem?: DesignSystemData;
|
|
435
598
|
/** Deck aspect ratio (defaults to 16:9 when omitted) */
|
|
436
599
|
aspectRatio?: AspectRatio;
|
|
600
|
+
/** Whether the right-side style inspector is visible */
|
|
601
|
+
stylePanelOpen?: boolean;
|
|
602
|
+
/** Close the right-side style inspector */
|
|
603
|
+
onCloseStylePanel?: () => void;
|
|
437
604
|
/** Whether the draw-to-prompt overlay is visible */
|
|
438
605
|
drawMode?: boolean;
|
|
439
606
|
/** Called when the draw overlay should exit (Esc, Send, close button) */
|
|
@@ -588,6 +755,7 @@ function ImageSelectionOutline({
|
|
|
588
755
|
return (
|
|
589
756
|
<SelectionOverlayPortal viewportRect={viewportRect} zIndex={50}>
|
|
590
757
|
<div
|
|
758
|
+
data-slide-selection-outline="true"
|
|
591
759
|
style={{
|
|
592
760
|
position: "absolute",
|
|
593
761
|
top: rect.top - pad,
|
|
@@ -606,20 +774,24 @@ function ImageSelectionOutline({
|
|
|
606
774
|
function ElementSelectionOutline({
|
|
607
775
|
rect,
|
|
608
776
|
viewportRect,
|
|
609
|
-
|
|
777
|
+
onResizeStart,
|
|
610
778
|
}: {
|
|
611
779
|
rect: DOMRect;
|
|
612
780
|
viewportRect: DOMRect | null;
|
|
613
|
-
|
|
781
|
+
onResizeStart?: (handle: ResizeHandle, e: React.PointerEvent) => void;
|
|
614
782
|
}) {
|
|
615
|
-
const t = useT();
|
|
616
783
|
const pad = 2;
|
|
617
784
|
const handle = 7;
|
|
618
785
|
const handleClass =
|
|
619
|
-
"absolute size-[7px] rounded-sm border border-background bg-[#609FF8] shadow-sm";
|
|
786
|
+
"absolute size-[7px] touch-none rounded-sm border border-background bg-[#609FF8] shadow-sm";
|
|
787
|
+
const edgeHandleClass =
|
|
788
|
+
"absolute flex touch-none items-center justify-center bg-transparent p-0";
|
|
789
|
+
const edgeBarClass =
|
|
790
|
+
"rounded-sm border border-background bg-[#609FF8] shadow-sm";
|
|
620
791
|
return (
|
|
621
792
|
<SelectionOverlayPortal viewportRect={viewportRect} zIndex={51}>
|
|
622
793
|
<div
|
|
794
|
+
data-slide-selection-outline="true"
|
|
623
795
|
style={{
|
|
624
796
|
position: "absolute",
|
|
625
797
|
top: rect.top - pad,
|
|
@@ -633,38 +805,121 @@ function ElementSelectionOutline({
|
|
|
633
805
|
}}
|
|
634
806
|
>
|
|
635
807
|
<span
|
|
808
|
+
data-slide-resize-handle="nw"
|
|
809
|
+
onPointerDown={(e) => onResizeStart?.("nw", e)}
|
|
636
810
|
className={handleClass}
|
|
637
|
-
style={{
|
|
811
|
+
style={{
|
|
812
|
+
left: -handle / 2,
|
|
813
|
+
top: -handle / 2,
|
|
814
|
+
pointerEvents: "auto",
|
|
815
|
+
cursor: "nwse-resize",
|
|
816
|
+
zIndex: 2,
|
|
817
|
+
}}
|
|
638
818
|
/>
|
|
639
819
|
<span
|
|
820
|
+
data-slide-resize-handle="ne"
|
|
821
|
+
onPointerDown={(e) => onResizeStart?.("ne", e)}
|
|
640
822
|
className={handleClass}
|
|
641
|
-
style={{
|
|
823
|
+
style={{
|
|
824
|
+
right: -handle / 2,
|
|
825
|
+
top: -handle / 2,
|
|
826
|
+
pointerEvents: "auto",
|
|
827
|
+
cursor: "nesw-resize",
|
|
828
|
+
zIndex: 2,
|
|
829
|
+
}}
|
|
642
830
|
/>
|
|
643
831
|
<span
|
|
832
|
+
data-slide-resize-handle="sw"
|
|
833
|
+
onPointerDown={(e) => onResizeStart?.("sw", e)}
|
|
644
834
|
className={handleClass}
|
|
645
|
-
style={{
|
|
835
|
+
style={{
|
|
836
|
+
left: -handle / 2,
|
|
837
|
+
bottom: -handle / 2,
|
|
838
|
+
pointerEvents: "auto",
|
|
839
|
+
cursor: "nesw-resize",
|
|
840
|
+
zIndex: 2,
|
|
841
|
+
}}
|
|
646
842
|
/>
|
|
647
843
|
<span
|
|
844
|
+
data-slide-resize-handle="se"
|
|
845
|
+
onPointerDown={(e) => onResizeStart?.("se", e)}
|
|
648
846
|
className={handleClass}
|
|
649
|
-
style={{
|
|
847
|
+
style={{
|
|
848
|
+
right: -handle / 2,
|
|
849
|
+
bottom: -handle / 2,
|
|
850
|
+
pointerEvents: "auto",
|
|
851
|
+
cursor: "nwse-resize",
|
|
852
|
+
zIndex: 2,
|
|
853
|
+
}}
|
|
650
854
|
/>
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
855
|
+
<span
|
|
856
|
+
data-slide-resize-handle="n"
|
|
857
|
+
onPointerDown={(e) => onResizeStart?.("n", e)}
|
|
858
|
+
className={edgeHandleClass}
|
|
859
|
+
style={{
|
|
860
|
+
width: 22,
|
|
861
|
+
height: 12,
|
|
862
|
+
left: "50%",
|
|
863
|
+
top: -6,
|
|
864
|
+
transform: "translateX(-50%)",
|
|
865
|
+
pointerEvents: "auto",
|
|
866
|
+
cursor: "ns-resize",
|
|
867
|
+
zIndex: 1,
|
|
868
|
+
}}
|
|
869
|
+
>
|
|
870
|
+
<span className={`${edgeBarClass} h-1 w-3.5`} />
|
|
871
|
+
</span>
|
|
872
|
+
<span
|
|
873
|
+
data-slide-resize-handle="e"
|
|
874
|
+
onPointerDown={(e) => onResizeStart?.("e", e)}
|
|
875
|
+
className={edgeHandleClass}
|
|
876
|
+
style={{
|
|
877
|
+
width: 12,
|
|
878
|
+
height: 22,
|
|
879
|
+
right: -6,
|
|
880
|
+
top: "50%",
|
|
881
|
+
transform: "translateY(-50%)",
|
|
882
|
+
pointerEvents: "auto",
|
|
883
|
+
cursor: "ew-resize",
|
|
884
|
+
zIndex: 1,
|
|
885
|
+
}}
|
|
886
|
+
>
|
|
887
|
+
<span className={`${edgeBarClass} h-3.5 w-1`} />
|
|
888
|
+
</span>
|
|
889
|
+
<span
|
|
890
|
+
data-slide-resize-handle="s"
|
|
891
|
+
onPointerDown={(e) => onResizeStart?.("s", e)}
|
|
892
|
+
className={edgeHandleClass}
|
|
893
|
+
style={{
|
|
894
|
+
width: 22,
|
|
895
|
+
height: 12,
|
|
896
|
+
left: "50%",
|
|
897
|
+
bottom: -6,
|
|
898
|
+
transform: "translateX(-50%)",
|
|
899
|
+
pointerEvents: "auto",
|
|
900
|
+
cursor: "ns-resize",
|
|
901
|
+
zIndex: 1,
|
|
902
|
+
}}
|
|
903
|
+
>
|
|
904
|
+
<span className={`${edgeBarClass} h-1 w-3.5`} />
|
|
905
|
+
</span>
|
|
906
|
+
<span
|
|
907
|
+
data-slide-resize-handle="w"
|
|
908
|
+
onPointerDown={(e) => onResizeStart?.("w", e)}
|
|
909
|
+
className={edgeHandleClass}
|
|
910
|
+
style={{
|
|
911
|
+
width: 12,
|
|
912
|
+
height: 22,
|
|
913
|
+
left: -6,
|
|
914
|
+
top: "50%",
|
|
915
|
+
transform: "translateY(-50%)",
|
|
916
|
+
pointerEvents: "auto",
|
|
917
|
+
cursor: "ew-resize",
|
|
918
|
+
zIndex: 1,
|
|
919
|
+
}}
|
|
920
|
+
>
|
|
921
|
+
<span className={`${edgeBarClass} h-3.5 w-1`} />
|
|
922
|
+
</span>
|
|
668
923
|
</div>
|
|
669
924
|
</SelectionOverlayPortal>
|
|
670
925
|
);
|
|
@@ -800,12 +1055,15 @@ export default function SlideEditor({
|
|
|
800
1055
|
onSearchImage,
|
|
801
1056
|
onLogoSearch,
|
|
802
1057
|
onDropImage,
|
|
1058
|
+
onDropImageUrl,
|
|
803
1059
|
onToggleObjectFit,
|
|
804
1060
|
agentActive,
|
|
805
1061
|
slideIndex = 0,
|
|
806
1062
|
slideCount = 1,
|
|
807
1063
|
designSystem,
|
|
808
1064
|
aspectRatio,
|
|
1065
|
+
stylePanelOpen = false,
|
|
1066
|
+
onCloseStylePanel,
|
|
809
1067
|
drawMode,
|
|
810
1068
|
onExitDrawMode,
|
|
811
1069
|
pinMode,
|
|
@@ -825,7 +1083,6 @@ export default function SlideEditor({
|
|
|
825
1083
|
content.includes('class="fmd-slide"') ||
|
|
826
1084
|
["blank", "section", "statement", "full-image"].includes(slide.layout);
|
|
827
1085
|
|
|
828
|
-
const [isHoveringText, setIsHoveringText] = useState(false);
|
|
829
1086
|
const [canvasZoom, setCanvasZoom] = useState(100);
|
|
830
1087
|
const [imageOverlay, setImageOverlay] = useState<{
|
|
831
1088
|
rect: DOMRect;
|
|
@@ -854,6 +1111,10 @@ export default function SlideEditor({
|
|
|
854
1111
|
d.paths.some((p) => p === `slides.${activeSlideId}`)
|
|
855
1112
|
);
|
|
856
1113
|
});
|
|
1114
|
+
const passivePresentUsers = getPassiveSlidePresenceUsers(
|
|
1115
|
+
presentUsers,
|
|
1116
|
+
agentActive,
|
|
1117
|
+
);
|
|
857
1118
|
const resolveCanvasRect = useCallback(
|
|
858
1119
|
(): DOMRect | null =>
|
|
859
1120
|
slideCanvasRef.current?.getBoundingClientRect() ?? null,
|
|
@@ -872,6 +1133,7 @@ export default function SlideEditor({
|
|
|
872
1133
|
const [selectedElementPath, setSelectedElementPath] = useState<
|
|
873
1134
|
number[] | null
|
|
874
1135
|
>(null);
|
|
1136
|
+
const [selectedObjectId, setSelectedObjectId] = useState<string | null>(null);
|
|
875
1137
|
const [selectedElementSelector, setSelectedElementSelector] = useState<
|
|
876
1138
|
string | null
|
|
877
1139
|
>(null);
|
|
@@ -1079,7 +1341,7 @@ export default function SlideEditor({
|
|
|
1079
1341
|
`3. Reduce slide padding (e.g. 40px top/bottom instead of 60-80px) if the layout is genuinely tight.`,
|
|
1080
1342
|
`4. If the content really can't be compressed without losing meaning, split it across two slides.`,
|
|
1081
1343
|
``,
|
|
1082
|
-
`Do NOT solve this by adding \`transform: scale()
|
|
1344
|
+
`Do NOT solve this by adding \`transform: scale()\` or \`overflow: scroll\`. Preserve existing manually positioned absolute objects, including text boxes; rewrite only the overflowing flow layout so the HTML itself fits ${dimsW}x${dimsH}.`,
|
|
1083
1345
|
]
|
|
1084
1346
|
.filter(Boolean)
|
|
1085
1347
|
.join("\n"),
|
|
@@ -1092,6 +1354,7 @@ export default function SlideEditor({
|
|
|
1092
1354
|
* placing pointerdown doesn't fall through to click-to-select/deselect
|
|
1093
1355
|
* logic and steal focus back off the freshly created box. */
|
|
1094
1356
|
const suppressNextClickRef = useRef(false);
|
|
1357
|
+
const activeGestureCancelRef = useRef<(() => void) | null>(null);
|
|
1095
1358
|
/**
|
|
1096
1359
|
* If the user pressed shift/cmd before starting a marquee, additive mode
|
|
1097
1360
|
* preserves the existing selection on pointerup.
|
|
@@ -1107,6 +1370,7 @@ export default function SlideEditor({
|
|
|
1107
1370
|
* the parent's `onUpdateSlide` would update DeckProvider mid-render.
|
|
1108
1371
|
*/
|
|
1109
1372
|
const editingElRef = useRef<HTMLElement | null>(null);
|
|
1373
|
+
const richTextSelectionRef = useRef<Range | null>(null);
|
|
1110
1374
|
/** Latest onUpdateSlide in a ref so blur handlers always see the current version */
|
|
1111
1375
|
const onUpdateSlideRef = useRef(onUpdateSlide);
|
|
1112
1376
|
useEffect(() => {
|
|
@@ -1144,63 +1408,135 @@ export default function SlideEditor({
|
|
|
1144
1408
|
);
|
|
1145
1409
|
}, []);
|
|
1146
1410
|
|
|
1411
|
+
/**
|
|
1412
|
+
* Turn a normal layout block into a freeform object at its current visual
|
|
1413
|
+
* coordinates. A hidden same-size sibling retains the flex/grid slot, so
|
|
1414
|
+
* the rest of the slide does not reflow when Escape selects the block.
|
|
1415
|
+
*/
|
|
1416
|
+
const freezeElementForFreeformSelection = useCallback(
|
|
1417
|
+
(element: HTMLElement): HTMLElement => {
|
|
1418
|
+
if (window.getComputedStyle(element).position === "absolute") {
|
|
1419
|
+
ensureSlideObjectId(element);
|
|
1420
|
+
return element;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
const fmdSlide = element.closest(".fmd-slide") as HTMLElement | null;
|
|
1424
|
+
const positioningLayer = fmdSlide
|
|
1425
|
+
? (Array.from(fmdSlide.children).find(
|
|
1426
|
+
(child): child is HTMLElement =>
|
|
1427
|
+
child instanceof HTMLElement &&
|
|
1428
|
+
child.hasAttribute("data-fmd-autofit-content"),
|
|
1429
|
+
) ?? fmdSlide)
|
|
1430
|
+
: null;
|
|
1431
|
+
if (!positioningLayer) return element;
|
|
1432
|
+
|
|
1433
|
+
const containingBlock = resolveSlideObjectContainingBlock(
|
|
1434
|
+
element,
|
|
1435
|
+
positioningLayer,
|
|
1436
|
+
);
|
|
1437
|
+
const elementRect = element.getBoundingClientRect();
|
|
1438
|
+
const layerRect = containingBlock.getBoundingClientRect();
|
|
1439
|
+
if (
|
|
1440
|
+
!elementRect.width ||
|
|
1441
|
+
!elementRect.height ||
|
|
1442
|
+
!layerRect.width ||
|
|
1443
|
+
!layerRect.height ||
|
|
1444
|
+
!containingBlock.offsetWidth ||
|
|
1445
|
+
!containingBlock.offsetHeight
|
|
1446
|
+
) {
|
|
1447
|
+
return element;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
const { x, y } = clientPointToSlideCoordinates(
|
|
1451
|
+
elementRect.left,
|
|
1452
|
+
elementRect.top,
|
|
1453
|
+
layerRect,
|
|
1454
|
+
containingBlock.offsetWidth,
|
|
1455
|
+
containingBlock.offsetHeight,
|
|
1456
|
+
);
|
|
1457
|
+
const scaleX = containingBlock.offsetWidth / layerRect.width;
|
|
1458
|
+
const scaleY = containingBlock.offsetHeight / layerRect.height;
|
|
1459
|
+
const computed = window.getComputedStyle(element);
|
|
1460
|
+
freezeSlideElementForFreeform(
|
|
1461
|
+
element,
|
|
1462
|
+
{
|
|
1463
|
+
x,
|
|
1464
|
+
y,
|
|
1465
|
+
width: Math.round(elementRect.width * scaleX),
|
|
1466
|
+
height: Math.round(elementRect.height * scaleY),
|
|
1467
|
+
},
|
|
1468
|
+
{
|
|
1469
|
+
display: computed.display,
|
|
1470
|
+
flexGrow: computed.flexGrow,
|
|
1471
|
+
flexShrink: computed.flexShrink,
|
|
1472
|
+
flexBasis: computed.flexBasis,
|
|
1473
|
+
alignSelf: computed.alignSelf,
|
|
1474
|
+
},
|
|
1475
|
+
);
|
|
1476
|
+
return element;
|
|
1477
|
+
},
|
|
1478
|
+
[],
|
|
1479
|
+
);
|
|
1480
|
+
|
|
1147
1481
|
const resolveSelectedElement = useCallback((): HTMLElement | null => {
|
|
1482
|
+
const slideContent = getSlideContent();
|
|
1483
|
+
if (!slideContent) return null;
|
|
1484
|
+
if (selectedObjectId) {
|
|
1485
|
+
const object = findSlideObjectById(slideContent, selectedObjectId);
|
|
1486
|
+
if (object) return object;
|
|
1487
|
+
}
|
|
1148
1488
|
if (!selectedElementPath) return null;
|
|
1149
|
-
return resolveElementPath(
|
|
1150
|
-
}, [getSlideContent, selectedElementPath]);
|
|
1489
|
+
return resolveElementPath(slideContent, selectedElementPath);
|
|
1490
|
+
}, [getSlideContent, selectedElementPath, selectedObjectId]);
|
|
1151
1491
|
|
|
1152
1492
|
const buildSelectionState = useCallback(
|
|
1153
1493
|
(
|
|
1154
|
-
mode:
|
|
1494
|
+
mode: SlidesSelectionMode,
|
|
1155
1495
|
items: SlideSelectionItem[],
|
|
1156
|
-
activeTool
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
[deckId, drawMode, pinMode, slide.id, slideIndex],
|
|
1496
|
+
activeTool?: SlidesSelectionTool,
|
|
1497
|
+
): SlidesSelectionState =>
|
|
1498
|
+
createSlidesSelectionState({
|
|
1499
|
+
deckId,
|
|
1500
|
+
slideId: slide.id,
|
|
1501
|
+
slideIndex,
|
|
1502
|
+
mode,
|
|
1503
|
+
items,
|
|
1504
|
+
drawMode: Boolean(drawMode),
|
|
1505
|
+
pinMode: Boolean(pinMode),
|
|
1506
|
+
textBoxMode: Boolean(textBoxMode),
|
|
1507
|
+
activeTool,
|
|
1508
|
+
}),
|
|
1509
|
+
[deckId, drawMode, pinMode, slide.id, slideIndex, textBoxMode],
|
|
1171
1510
|
);
|
|
1172
1511
|
|
|
1173
1512
|
const clearSelectedElement = useCallback(() => {
|
|
1513
|
+
richTextSelectionRef.current = null;
|
|
1174
1514
|
setSelectedElementPath(null);
|
|
1515
|
+
setSelectedObjectId(null);
|
|
1175
1516
|
setSelectedElementSelector(null);
|
|
1176
1517
|
setSelectedElementRect(null);
|
|
1177
1518
|
setSelectedStyleSnapshot(null);
|
|
1178
1519
|
}, []);
|
|
1179
1520
|
|
|
1180
1521
|
const selectElementForStyling = useCallback(
|
|
1181
|
-
(
|
|
1522
|
+
(
|
|
1523
|
+
element: HTMLElement,
|
|
1524
|
+
selector: string,
|
|
1525
|
+
selectionMode?: SlidesSelectionMode,
|
|
1526
|
+
) => {
|
|
1182
1527
|
const slideContent = getSlideContent();
|
|
1183
1528
|
if (!slideContent) return;
|
|
1184
1529
|
const path = elementPathFromRoot(slideContent, element);
|
|
1185
1530
|
if (path.length === 0) return;
|
|
1186
1531
|
const snapshot = buildStyleSnapshot(element, selector);
|
|
1187
1532
|
setSelectedElementPath(path);
|
|
1533
|
+
setSelectedObjectId(element.getAttribute("data-slide-object-id"));
|
|
1188
1534
|
setSelectedElementSelector(selector);
|
|
1189
1535
|
setSelectedElementRect(element.getBoundingClientRect());
|
|
1190
1536
|
setSelectedStyleSnapshot(snapshot);
|
|
1191
1537
|
syncSelectionToAppState(
|
|
1192
|
-
buildSelectionState(snapshot
|
|
1193
|
-
|
|
1194
|
-
selector,
|
|
1195
|
-
kind: snapshot.isImage ? "image" : "element",
|
|
1196
|
-
tagName: snapshot.tagName,
|
|
1197
|
-
text: snapshot.textPreview,
|
|
1198
|
-
imageSrc:
|
|
1199
|
-
element instanceof HTMLImageElement
|
|
1200
|
-
? (element.getAttribute("src") ?? undefined)
|
|
1201
|
-
: undefined,
|
|
1202
|
-
style: snapshot,
|
|
1203
|
-
},
|
|
1538
|
+
buildSelectionState(getSlideSelectionMode(snapshot, selectionMode), [
|
|
1539
|
+
selectionItemForElement(element, selector, snapshot),
|
|
1204
1540
|
]),
|
|
1205
1541
|
);
|
|
1206
1542
|
},
|
|
@@ -1212,17 +1548,31 @@ export default function SlideEditor({
|
|
|
1212
1548
|
const el = editingElRef.current;
|
|
1213
1549
|
if (!el) return;
|
|
1214
1550
|
editingElRef.current = null;
|
|
1551
|
+
richTextSelectionRef.current = null;
|
|
1215
1552
|
el.contentEditable = "false";
|
|
1216
1553
|
el.removeAttribute("data-editing-block");
|
|
1217
1554
|
|
|
1555
|
+
const selected = freezeElementForFreeformSelection(el);
|
|
1556
|
+
const selector = getBuilderSelector(selected);
|
|
1557
|
+
|
|
1218
1558
|
const html = readCurrentSlideContentHtml();
|
|
1219
1559
|
if (html !== null) {
|
|
1220
1560
|
onUpdateSlideRef.current({ content: html });
|
|
1221
1561
|
}
|
|
1222
1562
|
inlineEditDraftRef.current = null;
|
|
1223
|
-
|
|
1224
|
-
setEditingEl(
|
|
1225
|
-
|
|
1563
|
+
const next = escapedEditingSelection(selected, resolveSelectedElement());
|
|
1564
|
+
setEditingEl(next.editing);
|
|
1565
|
+
if (next.selected && selector) {
|
|
1566
|
+
selectElementForStyling(next.selected, selector);
|
|
1567
|
+
} else {
|
|
1568
|
+
syncSelectionToAppState(null);
|
|
1569
|
+
}
|
|
1570
|
+
}, [
|
|
1571
|
+
readCurrentSlideContentHtml,
|
|
1572
|
+
resolveSelectedElement,
|
|
1573
|
+
selectElementForStyling,
|
|
1574
|
+
freezeElementForFreeformSelection,
|
|
1575
|
+
]);
|
|
1226
1576
|
|
|
1227
1577
|
/** Enter edit mode on a smart block (text leaf or smart group) */
|
|
1228
1578
|
const enterInlineEdit = useCallback(
|
|
@@ -1248,15 +1598,9 @@ export default function SlideEditor({
|
|
|
1248
1598
|
editingElRef.current = el;
|
|
1249
1599
|
setEditingEl(el);
|
|
1250
1600
|
if (selector) {
|
|
1601
|
+
const item = selectionItemForElement(el, selector);
|
|
1251
1602
|
syncSelectionToAppState(
|
|
1252
|
-
buildSelectionState("editing", [
|
|
1253
|
-
{
|
|
1254
|
-
selector,
|
|
1255
|
-
kind: "text",
|
|
1256
|
-
tagName: el.tagName.toLowerCase(),
|
|
1257
|
-
text: (el.textContent ?? "").trim().slice(0, 200),
|
|
1258
|
-
},
|
|
1259
|
-
]),
|
|
1603
|
+
buildSelectionState("editing", [{ ...item, kind: "text" }]),
|
|
1260
1604
|
);
|
|
1261
1605
|
}
|
|
1262
1606
|
},
|
|
@@ -1276,6 +1620,7 @@ export default function SlideEditor({
|
|
|
1276
1620
|
editing.removeAttribute("data-editing-block");
|
|
1277
1621
|
}
|
|
1278
1622
|
editingElRef.current = null;
|
|
1623
|
+
richTextSelectionRef.current = null;
|
|
1279
1624
|
setEditingEl(null);
|
|
1280
1625
|
|
|
1281
1626
|
if (draft?.slideId === previousSlideId) {
|
|
@@ -1301,6 +1646,44 @@ export default function SlideEditor({
|
|
|
1301
1646
|
return () => editingEl.removeEventListener("input", handleInput);
|
|
1302
1647
|
}, [captureInlineEditDraft, editingEl, slide.id]);
|
|
1303
1648
|
|
|
1649
|
+
useEffect(() => {
|
|
1650
|
+
if (!editingEl) return;
|
|
1651
|
+
|
|
1652
|
+
const updateInspectorTextStyle = () => {
|
|
1653
|
+
const selection = window.getSelection();
|
|
1654
|
+
if (!selection || selection.rangeCount !== 1) return;
|
|
1655
|
+
const range = selection.getRangeAt(0);
|
|
1656
|
+
if (
|
|
1657
|
+
!editingEl.contains(range.startContainer) ||
|
|
1658
|
+
!editingEl.contains(range.endContainer)
|
|
1659
|
+
) {
|
|
1660
|
+
// Inspector and portalled picker interactions move browser focus away
|
|
1661
|
+
// from the slide. Retain the last valid range until the user places a
|
|
1662
|
+
// new caret or selection inside this editable.
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
richTextSelectionRef.current = snapshotEditableTextRange(
|
|
1667
|
+
editingEl,
|
|
1668
|
+
selection,
|
|
1669
|
+
);
|
|
1670
|
+
const selector = selectedElementSelector ?? getBuilderSelector(editingEl);
|
|
1671
|
+
if (!selector) return;
|
|
1672
|
+
setSelectedStyleSnapshot(
|
|
1673
|
+
buildStyleSnapshot(
|
|
1674
|
+
editingEl,
|
|
1675
|
+
selector,
|
|
1676
|
+
getInlineTextStyleSnapshot(editingEl, selection),
|
|
1677
|
+
),
|
|
1678
|
+
);
|
|
1679
|
+
};
|
|
1680
|
+
|
|
1681
|
+
updateInspectorTextStyle();
|
|
1682
|
+
document.addEventListener("selectionchange", updateInspectorTextStyle);
|
|
1683
|
+
return () =>
|
|
1684
|
+
document.removeEventListener("selectionchange", updateInspectorTextStyle);
|
|
1685
|
+
}, [editingEl, selectedElementSelector]);
|
|
1686
|
+
|
|
1304
1687
|
// Global keyboard handling while inline-editing
|
|
1305
1688
|
useEffect(() => {
|
|
1306
1689
|
if (!editingEl) return;
|
|
@@ -1326,12 +1709,6 @@ export default function SlideEditor({
|
|
|
1326
1709
|
) {
|
|
1327
1710
|
return;
|
|
1328
1711
|
}
|
|
1329
|
-
if (e.key === "Escape") {
|
|
1330
|
-
e.preventDefault();
|
|
1331
|
-
e.stopPropagation();
|
|
1332
|
-
exitInlineEdit();
|
|
1333
|
-
return;
|
|
1334
|
-
}
|
|
1335
1712
|
if (e.key === "Enter") {
|
|
1336
1713
|
// Smart Enter:
|
|
1337
1714
|
// - Shift+Enter always inserts a <br>.
|
|
@@ -1385,8 +1762,15 @@ export default function SlideEditor({
|
|
|
1385
1762
|
const onDocMouseDown = (e: MouseEvent) => {
|
|
1386
1763
|
const target = e.target as Node;
|
|
1387
1764
|
if (editingEl.contains(target)) return;
|
|
1388
|
-
//
|
|
1389
|
-
|
|
1765
|
+
// Inspector controls and their popovers deliberately preserve the live
|
|
1766
|
+
// edit session so a saved text range can receive the chosen formatting.
|
|
1767
|
+
if (
|
|
1768
|
+
(target as HTMLElement).closest?.(
|
|
1769
|
+
"[data-block-bubble-menu], [data-slide-style-trigger], [data-slide-style-dock], [data-slide-inline-edit-surface]",
|
|
1770
|
+
)
|
|
1771
|
+
) {
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1390
1774
|
exitInlineEdit();
|
|
1391
1775
|
};
|
|
1392
1776
|
document.addEventListener("mousedown", onDocMouseDown);
|
|
@@ -1409,8 +1793,9 @@ export default function SlideEditor({
|
|
|
1409
1793
|
};
|
|
1410
1794
|
}, [selectedImg]);
|
|
1411
1795
|
|
|
1412
|
-
|
|
1796
|
+
useLayoutEffect(() => {
|
|
1413
1797
|
if (!selectedElementPath || !selectedElementSelector) return;
|
|
1798
|
+
let observedElement: HTMLElement | null = null;
|
|
1414
1799
|
const update = () => {
|
|
1415
1800
|
const element = resolveSelectedElement();
|
|
1416
1801
|
if (!element) {
|
|
@@ -1418,29 +1803,54 @@ export default function SlideEditor({
|
|
|
1418
1803
|
syncSelectionToAppState(null);
|
|
1419
1804
|
return;
|
|
1420
1805
|
}
|
|
1421
|
-
const
|
|
1806
|
+
const inlineTextStyle =
|
|
1807
|
+
editingElRef.current === element
|
|
1808
|
+
? getInlineTextStyleSnapshotForRange(
|
|
1809
|
+
element,
|
|
1810
|
+
richTextSelectionRef.current,
|
|
1811
|
+
)
|
|
1812
|
+
: undefined;
|
|
1813
|
+
const snapshot = buildStyleSnapshot(
|
|
1814
|
+
element,
|
|
1815
|
+
selectedElementSelector,
|
|
1816
|
+
inlineTextStyle,
|
|
1817
|
+
);
|
|
1422
1818
|
setSelectedElementRect(element.getBoundingClientRect());
|
|
1423
1819
|
setSelectedStyleSnapshot(snapshot);
|
|
1424
1820
|
syncSelectionToAppState(
|
|
1425
|
-
buildSelectionState(snapshot
|
|
1426
|
-
|
|
1427
|
-
selector: selectedElementSelector,
|
|
1428
|
-
kind: snapshot.isImage ? "image" : "element",
|
|
1429
|
-
tagName: snapshot.tagName,
|
|
1430
|
-
text: snapshot.textPreview,
|
|
1431
|
-
imageSrc:
|
|
1432
|
-
element instanceof HTMLImageElement
|
|
1433
|
-
? (element.getAttribute("src") ?? undefined)
|
|
1434
|
-
: undefined,
|
|
1435
|
-
style: snapshot,
|
|
1436
|
-
},
|
|
1821
|
+
buildSelectionState(getSlideSelectionMode(snapshot), [
|
|
1822
|
+
selectionItemForElement(element, selectedElementSelector, snapshot),
|
|
1437
1823
|
]),
|
|
1438
1824
|
);
|
|
1439
1825
|
};
|
|
1440
1826
|
update();
|
|
1827
|
+
|
|
1828
|
+
observedElement = resolveSelectedElement();
|
|
1829
|
+
const positioningLayer = observedElement?.closest(
|
|
1830
|
+
"[data-fmd-autofit-content], .fmd-slide",
|
|
1831
|
+
) as HTMLElement | null;
|
|
1832
|
+
const resizeObserver =
|
|
1833
|
+
typeof ResizeObserver === "undefined" ? null : new ResizeObserver(update);
|
|
1834
|
+
if (observedElement) resizeObserver?.observe(observedElement);
|
|
1835
|
+
if (positioningLayer) resizeObserver?.observe(positioningLayer);
|
|
1836
|
+
// Auto-fit writes transform custom properties directly on the layer. Those
|
|
1837
|
+
// mutations do not resize the element's CSS box, but they do change its
|
|
1838
|
+
// viewport rect, so selection chrome must follow the transformed DOM.
|
|
1839
|
+
const mutationObserver =
|
|
1840
|
+
typeof MutationObserver === "undefined" || !positioningLayer
|
|
1841
|
+
? null
|
|
1842
|
+
: new MutationObserver(update);
|
|
1843
|
+
if (mutationObserver && positioningLayer) {
|
|
1844
|
+
mutationObserver.observe(positioningLayer, {
|
|
1845
|
+
attributes: true,
|
|
1846
|
+
attributeFilter: ["class", "style"],
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1441
1849
|
window.addEventListener("resize", update);
|
|
1442
1850
|
window.addEventListener("scroll", update, true);
|
|
1443
1851
|
return () => {
|
|
1852
|
+
resizeObserver?.disconnect();
|
|
1853
|
+
mutationObserver?.disconnect();
|
|
1444
1854
|
window.removeEventListener("resize", update);
|
|
1445
1855
|
window.removeEventListener("scroll", update, true);
|
|
1446
1856
|
};
|
|
@@ -1511,12 +1921,7 @@ export default function SlideEditor({
|
|
|
1511
1921
|
const selector = `[data-builder-id="${id}"]`;
|
|
1512
1922
|
const text = (el.textContent || "").trim().slice(0, 200);
|
|
1513
1923
|
rects.set(id, { rect: el.getBoundingClientRect(), text, selector });
|
|
1514
|
-
items.push(
|
|
1515
|
-
selector,
|
|
1516
|
-
text,
|
|
1517
|
-
kind: el.tagName === "IMG" ? "image" : "element",
|
|
1518
|
-
tagName: el.tagName.toLowerCase(),
|
|
1519
|
-
});
|
|
1924
|
+
items.push(selectionItemForElement(el, selector));
|
|
1520
1925
|
});
|
|
1521
1926
|
}
|
|
1522
1927
|
setMultiSelection(ids);
|
|
@@ -1539,6 +1944,94 @@ export default function SlideEditor({
|
|
|
1539
1944
|
applyMultiSelection(new Set());
|
|
1540
1945
|
}, [applyMultiSelection, multiSelection.size]);
|
|
1541
1946
|
|
|
1947
|
+
// One Escape owner for the HTML editor. Radix dialogs/popovers and native
|
|
1948
|
+
// form controls retain their own Escape behavior before we arbitrate canvas
|
|
1949
|
+
// state. Gesture cancellation is deliberately ahead of selection clearing.
|
|
1950
|
+
useEffect(() => {
|
|
1951
|
+
const onKey = (e: KeyboardEvent) => {
|
|
1952
|
+
if (e.key !== "Escape") return;
|
|
1953
|
+
const target = e.target instanceof Element ? e.target : null;
|
|
1954
|
+
const editing = editingElRef.current;
|
|
1955
|
+
const overlayOwnsEscape = Boolean(
|
|
1956
|
+
document.querySelector(
|
|
1957
|
+
'[role="dialog"]:not([data-state="closed"]), [role="menu"]:not([data-state="closed"]), [role="listbox"]:not([data-state="closed"]), [data-radix-popper-content-wrapper]:not([data-state="closed"])',
|
|
1958
|
+
),
|
|
1959
|
+
);
|
|
1960
|
+
const targetOwnsEscape = Boolean(
|
|
1961
|
+
target?.closest(
|
|
1962
|
+
'[role="dialog"], [data-radix-popper-content-wrapper], [data-radix-menu-content]',
|
|
1963
|
+
) ||
|
|
1964
|
+
((target instanceof HTMLInputElement ||
|
|
1965
|
+
target instanceof HTMLTextAreaElement ||
|
|
1966
|
+
target instanceof HTMLSelectElement ||
|
|
1967
|
+
(target instanceof HTMLElement && target.isContentEditable)) &&
|
|
1968
|
+
!editing?.contains(target)),
|
|
1969
|
+
);
|
|
1970
|
+
const action = decideSlideEscape({
|
|
1971
|
+
editing: Boolean(editing),
|
|
1972
|
+
activeGesture: activeGestureCancelRef.current !== null,
|
|
1973
|
+
activeMode: Boolean(drawMode || pinMode || textBoxMode),
|
|
1974
|
+
multiSelection: multiSelection.size > 0,
|
|
1975
|
+
singleSelection: Boolean(selectedElementSelector),
|
|
1976
|
+
targetOwnsEscape,
|
|
1977
|
+
overlayOwnsEscape,
|
|
1978
|
+
});
|
|
1979
|
+
if (action === "none" || action === "canvas") return;
|
|
1980
|
+
|
|
1981
|
+
e.preventDefault();
|
|
1982
|
+
e.stopImmediatePropagation();
|
|
1983
|
+
if (action === "edit") {
|
|
1984
|
+
exitInlineEdit();
|
|
1985
|
+
} else if (action === "gesture") {
|
|
1986
|
+
activeGestureCancelRef.current?.();
|
|
1987
|
+
} else if (action === "mode") {
|
|
1988
|
+
if (drawMode) onExitDrawMode?.();
|
|
1989
|
+
else if (pinMode) onExitPinMode?.();
|
|
1990
|
+
else onExitTextBoxMode?.();
|
|
1991
|
+
} else if (action === "multi-selection") {
|
|
1992
|
+
clearMultiSelection();
|
|
1993
|
+
} else {
|
|
1994
|
+
clearSelectedElement();
|
|
1995
|
+
syncSelectionToAppState(null);
|
|
1996
|
+
}
|
|
1997
|
+
};
|
|
1998
|
+
window.addEventListener("keydown", onKey, true);
|
|
1999
|
+
return () => window.removeEventListener("keydown", onKey, true);
|
|
2000
|
+
}, [
|
|
2001
|
+
clearMultiSelection,
|
|
2002
|
+
clearSelectedElement,
|
|
2003
|
+
drawMode,
|
|
2004
|
+
exitInlineEdit,
|
|
2005
|
+
multiSelection.size,
|
|
2006
|
+
onExitDrawMode,
|
|
2007
|
+
onExitPinMode,
|
|
2008
|
+
onExitTextBoxMode,
|
|
2009
|
+
pinMode,
|
|
2010
|
+
selectedElementSelector,
|
|
2011
|
+
textBoxMode,
|
|
2012
|
+
]);
|
|
2013
|
+
|
|
2014
|
+
// Tool state is useful even before the user places an object. This keeps
|
|
2015
|
+
// `view-screen` truthful while the text tool is armed and after it exits.
|
|
2016
|
+
useEffect(() => {
|
|
2017
|
+
if (editingEl || multiSelection.size > 0 || selectedElementSelector) {
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
2020
|
+
if (drawMode || pinMode || textBoxMode) {
|
|
2021
|
+
syncSelectionToAppState(buildSelectionState("canvas", []));
|
|
2022
|
+
} else {
|
|
2023
|
+
syncSelectionToAppState(null);
|
|
2024
|
+
}
|
|
2025
|
+
}, [
|
|
2026
|
+
buildSelectionState,
|
|
2027
|
+
drawMode,
|
|
2028
|
+
editingEl,
|
|
2029
|
+
multiSelection.size,
|
|
2030
|
+
pinMode,
|
|
2031
|
+
selectedElementSelector,
|
|
2032
|
+
textBoxMode,
|
|
2033
|
+
]);
|
|
2034
|
+
|
|
1542
2035
|
const getPlaceholderTarget = useCallback(
|
|
1543
2036
|
(placeholder: HTMLElement): string => {
|
|
1544
2037
|
const slideContent = getSlideContent();
|
|
@@ -1613,20 +2106,6 @@ export default function SlideEditor({
|
|
|
1613
2106
|
syncSelectionToAppState(null);
|
|
1614
2107
|
}, [clearSelectedElement, slide.id]);
|
|
1615
2108
|
|
|
1616
|
-
// Escape key clears multi-selection (only when not inline-editing)
|
|
1617
|
-
useEffect(() => {
|
|
1618
|
-
if (multiSelection.size === 0) return;
|
|
1619
|
-
if (editingEl) return; // Esc handler in editing mode owns this key
|
|
1620
|
-
const onKey = (e: KeyboardEvent) => {
|
|
1621
|
-
if (e.key === "Escape") {
|
|
1622
|
-
e.preventDefault();
|
|
1623
|
-
clearMultiSelection();
|
|
1624
|
-
}
|
|
1625
|
-
};
|
|
1626
|
-
window.addEventListener("keydown", onKey);
|
|
1627
|
-
return () => window.removeEventListener("keydown", onKey);
|
|
1628
|
-
}, [multiSelection.size, editingEl, clearMultiSelection]);
|
|
1629
|
-
|
|
1630
2109
|
// Delete/Backspace removes the selected shape/text box (single or
|
|
1631
2110
|
// multi-select) from the slide. Only active when something is selected for
|
|
1632
2111
|
// styling (not while inline-editing text, where Backspace should delete a
|
|
@@ -1647,11 +2126,15 @@ export default function SlideEditor({
|
|
|
1647
2126
|
|
|
1648
2127
|
if (multiSelection.size > 0) {
|
|
1649
2128
|
for (const id of multiSelection) {
|
|
1650
|
-
slideContent.querySelector(
|
|
2129
|
+
const element = slideContent.querySelector(
|
|
2130
|
+
`[data-builder-id="${id}"]`,
|
|
2131
|
+
) as HTMLElement | null;
|
|
2132
|
+
if (element) removeSlideObjectAndLayoutSpacer(element);
|
|
1651
2133
|
}
|
|
1652
2134
|
clearMultiSelection();
|
|
1653
2135
|
} else {
|
|
1654
|
-
resolveSelectedElement()
|
|
2136
|
+
const element = resolveSelectedElement();
|
|
2137
|
+
if (element) removeSlideObjectAndLayoutSpacer(element);
|
|
1655
2138
|
clearSelectedElement();
|
|
1656
2139
|
}
|
|
1657
2140
|
|
|
@@ -1670,6 +2153,7 @@ export default function SlideEditor({
|
|
|
1670
2153
|
resolveSelectedElement,
|
|
1671
2154
|
clearSelectedElement,
|
|
1672
2155
|
readCurrentSlideContentHtml,
|
|
2156
|
+
removeSlideObjectAndLayoutSpacer,
|
|
1673
2157
|
syncSelectionToAppState,
|
|
1674
2158
|
]);
|
|
1675
2159
|
|
|
@@ -1683,6 +2167,7 @@ export default function SlideEditor({
|
|
|
1683
2167
|
(target: HTMLElement, slideContent: HTMLElement): string | null => {
|
|
1684
2168
|
let el: HTMLElement | null = target;
|
|
1685
2169
|
while (el && slideContent.contains(el) && el !== slideContent) {
|
|
2170
|
+
if (el.classList.contains("fmd-layout-spacer")) return null;
|
|
1686
2171
|
const id = el.getAttribute("data-builder-id");
|
|
1687
2172
|
if (id) return id;
|
|
1688
2173
|
el = el.parentElement;
|
|
@@ -1696,6 +2181,7 @@ export default function SlideEditor({
|
|
|
1696
2181
|
(target: HTMLElement, slideContent: HTMLElement): HTMLElement | null => {
|
|
1697
2182
|
let el: HTMLElement | null = target;
|
|
1698
2183
|
while (el && slideContent.contains(el) && el !== slideContent) {
|
|
2184
|
+
if (el.classList.contains("fmd-layout-spacer")) return null;
|
|
1699
2185
|
if (el.getAttribute("data-builder-id")) return el;
|
|
1700
2186
|
el = el.parentElement;
|
|
1701
2187
|
}
|
|
@@ -1723,28 +2209,20 @@ export default function SlideEditor({
|
|
|
1723
2209
|
".fmd-slide",
|
|
1724
2210
|
) as HTMLElement | null;
|
|
1725
2211
|
if (!fmdSlide) return;
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
const
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
: 0;
|
|
1741
|
-
const yPct =
|
|
1742
|
-
rect.height > 0
|
|
1743
|
-
? Math.min(
|
|
1744
|
-
100,
|
|
1745
|
-
Math.max(0, ((clientY - rect.top) / rect.height) * 100),
|
|
1746
|
-
)
|
|
1747
|
-
: 0;
|
|
2212
|
+
const positioningLayer =
|
|
2213
|
+
Array.from(fmdSlide.children).find(
|
|
2214
|
+
(child): child is HTMLElement =>
|
|
2215
|
+
child instanceof HTMLElement &&
|
|
2216
|
+
child.hasAttribute("data-fmd-autofit-content"),
|
|
2217
|
+
) ?? fmdSlide;
|
|
2218
|
+
const rect = positioningLayer.getBoundingClientRect();
|
|
2219
|
+
const { x, y } = clientPointToSlideCoordinates(
|
|
2220
|
+
clientX,
|
|
2221
|
+
clientY,
|
|
2222
|
+
rect,
|
|
2223
|
+
positioningLayer.offsetWidth,
|
|
2224
|
+
positioningLayer.offsetHeight,
|
|
2225
|
+
);
|
|
1748
2226
|
|
|
1749
2227
|
if (getComputedStyle(fmdSlide).position === "static") {
|
|
1750
2228
|
fmdSlide.style.position = "relative";
|
|
@@ -1752,16 +2230,18 @@ export default function SlideEditor({
|
|
|
1752
2230
|
|
|
1753
2231
|
const box = document.createElement("div");
|
|
1754
2232
|
box.className = "fmd-text-box";
|
|
2233
|
+
ensureSlideObjectId(box);
|
|
2234
|
+
ensureBuilderId(box);
|
|
1755
2235
|
box.style.position = "absolute";
|
|
1756
|
-
box.style.left = `${
|
|
1757
|
-
box.style.top = `${
|
|
2236
|
+
box.style.left = `${x}px`;
|
|
2237
|
+
box.style.top = `${y}px`;
|
|
1758
2238
|
box.style.width = "320px";
|
|
1759
2239
|
box.style.fontSize = "24px";
|
|
1760
2240
|
box.style.color = "#fff";
|
|
1761
2241
|
box.style.fontFamily = "'Poppins', sans-serif";
|
|
1762
2242
|
box.style.lineHeight = "1.3";
|
|
1763
2243
|
box.textContent = ZERO_WIDTH_SPACE;
|
|
1764
|
-
|
|
2244
|
+
positioningLayer.appendChild(box);
|
|
1765
2245
|
|
|
1766
2246
|
enterInlineEdit(box);
|
|
1767
2247
|
|
|
@@ -1781,8 +2261,367 @@ export default function SlideEditor({
|
|
|
1781
2261
|
[enterInlineEdit],
|
|
1782
2262
|
);
|
|
1783
2263
|
|
|
2264
|
+
const getObjectGeometry = useCallback(
|
|
2265
|
+
(element: HTMLElement): SlideObjectGeometry => ({
|
|
2266
|
+
x: element.offsetLeft,
|
|
2267
|
+
y: element.offsetTop,
|
|
2268
|
+
width: element.offsetWidth,
|
|
2269
|
+
height: element.offsetHeight,
|
|
2270
|
+
}),
|
|
2271
|
+
[],
|
|
2272
|
+
);
|
|
2273
|
+
|
|
2274
|
+
const applyObjectGeometry = useCallback(
|
|
2275
|
+
(element: HTMLElement, geometry: SlideObjectGeometry) => {
|
|
2276
|
+
element.style.left = `${geometry.x}px`;
|
|
2277
|
+
element.style.top = `${geometry.y}px`;
|
|
2278
|
+
element.style.width = `${geometry.width}px`;
|
|
2279
|
+
element.style.height = `${geometry.height}px`;
|
|
2280
|
+
},
|
|
2281
|
+
[],
|
|
2282
|
+
);
|
|
2283
|
+
|
|
2284
|
+
const startElementDrag = useCallback(
|
|
2285
|
+
(
|
|
2286
|
+
e: React.PointerEvent,
|
|
2287
|
+
element: HTMLElement,
|
|
2288
|
+
{
|
|
2289
|
+
preserveClickWithoutMove = false,
|
|
2290
|
+
}: { preserveClickWithoutMove?: boolean } = {},
|
|
2291
|
+
) => {
|
|
2292
|
+
if (readOnly) return;
|
|
2293
|
+
const fmdSlide = element.closest(".fmd-slide") as HTMLElement | null;
|
|
2294
|
+
if (!fmdSlide || getComputedStyle(element).position !== "absolute") {
|
|
2295
|
+
return;
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
const slideRect = fmdSlide.getBoundingClientRect();
|
|
2299
|
+
const slideWidth = fmdSlide.offsetWidth;
|
|
2300
|
+
const slideHeight = fmdSlide.offsetHeight;
|
|
2301
|
+
if (
|
|
2302
|
+
!slideRect.width ||
|
|
2303
|
+
!slideRect.height ||
|
|
2304
|
+
!slideWidth ||
|
|
2305
|
+
!slideHeight
|
|
2306
|
+
) {
|
|
2307
|
+
return;
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
// Pointer-down on the selection perimeter is a move gesture, never a
|
|
2311
|
+
// text caret placement. A selected object's body, however, has to keep
|
|
2312
|
+
// an unmoved click available for inline editing. In that case wait until
|
|
2313
|
+
// movement crosses the drag threshold before consuming its click.
|
|
2314
|
+
if (!preserveClickWithoutMove) {
|
|
2315
|
+
e.preventDefault();
|
|
2316
|
+
e.stopPropagation();
|
|
2317
|
+
suppressNextClickRef.current = true;
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
const origin = getObjectGeometry(element);
|
|
2321
|
+
const originalObjectId = element.getAttribute("data-slide-object-id");
|
|
2322
|
+
const startClientX = e.clientX;
|
|
2323
|
+
const startClientY = e.clientY;
|
|
2324
|
+
const duplicateRequested = e.altKey;
|
|
2325
|
+
let activeElement = element;
|
|
2326
|
+
let clone: HTMLElement | null = null;
|
|
2327
|
+
let moved = false;
|
|
2328
|
+
|
|
2329
|
+
const stop = () => {
|
|
2330
|
+
window.removeEventListener("pointermove", onMove);
|
|
2331
|
+
window.removeEventListener("pointerup", onUp);
|
|
2332
|
+
window.removeEventListener("pointercancel", onCancel);
|
|
2333
|
+
if (activeGestureCancelRef.current === onCancel) {
|
|
2334
|
+
activeGestureCancelRef.current = null;
|
|
2335
|
+
}
|
|
2336
|
+
};
|
|
2337
|
+
|
|
2338
|
+
const restore = () => {
|
|
2339
|
+
if (clone) {
|
|
2340
|
+
clone.remove();
|
|
2341
|
+
} else {
|
|
2342
|
+
applyObjectGeometry(element, origin);
|
|
2343
|
+
}
|
|
2344
|
+
if (originalObjectId) {
|
|
2345
|
+
element.setAttribute("data-slide-object-id", originalObjectId);
|
|
2346
|
+
} else {
|
|
2347
|
+
element.removeAttribute("data-slide-object-id");
|
|
2348
|
+
}
|
|
2349
|
+
const selector = getBuilderSelector(element);
|
|
2350
|
+
if (selector) selectElementForStyling(element, selector);
|
|
2351
|
+
};
|
|
2352
|
+
|
|
2353
|
+
const onMove = (moveEvent: PointerEvent) => {
|
|
2354
|
+
let dx =
|
|
2355
|
+
((moveEvent.clientX - startClientX) / slideRect.width) * slideWidth;
|
|
2356
|
+
let dy =
|
|
2357
|
+
((moveEvent.clientY - startClientY) / slideRect.height) * slideHeight;
|
|
2358
|
+
if (!moved && Math.hypot(dx, dy) < 2) return;
|
|
2359
|
+
if (moveEvent.shiftKey) {
|
|
2360
|
+
if (Math.abs(dx) >= Math.abs(dy)) dy = 0;
|
|
2361
|
+
else dx = 0;
|
|
2362
|
+
}
|
|
2363
|
+
if (!moved) {
|
|
2364
|
+
moved = true;
|
|
2365
|
+
// React still receives a click after a pointer drag. Suppress that
|
|
2366
|
+
// trailing click so a moved text box does not immediately reopen
|
|
2367
|
+
// inline editing.
|
|
2368
|
+
suppressNextClickRef.current = true;
|
|
2369
|
+
ensureSlideObjectId(element);
|
|
2370
|
+
if (duplicateRequested && moveEvent.altKey) {
|
|
2371
|
+
clone = cloneSlideObject(element);
|
|
2372
|
+
element.after(clone);
|
|
2373
|
+
ensureBuilderId(clone);
|
|
2374
|
+
stampBuilderIds(clone);
|
|
2375
|
+
activeElement = clone;
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
moveEvent.preventDefault();
|
|
2379
|
+
applyObjectGeometry(activeElement, {
|
|
2380
|
+
...origin,
|
|
2381
|
+
x: origin.x + dx,
|
|
2382
|
+
y: origin.y + dy,
|
|
2383
|
+
});
|
|
2384
|
+
const selector = getBuilderSelector(activeElement);
|
|
2385
|
+
if (selector) selectElementForStyling(activeElement, selector);
|
|
2386
|
+
};
|
|
2387
|
+
|
|
2388
|
+
const onUp = (upEvent: PointerEvent) => {
|
|
2389
|
+
stop();
|
|
2390
|
+
if (!moved) {
|
|
2391
|
+
if (preserveClickWithoutMove) return;
|
|
2392
|
+
// Pointer-up can occur outside the canvas, where React will not see
|
|
2393
|
+
// the click that normally clears this flag.
|
|
2394
|
+
window.setTimeout(function clearEdgeClickSuppression() {
|
|
2395
|
+
suppressNextClickRef.current = false;
|
|
2396
|
+
}, 0);
|
|
2397
|
+
return;
|
|
2398
|
+
}
|
|
2399
|
+
// A drag does not always produce a click (for example when released
|
|
2400
|
+
// outside the canvas), so do not leave the one-click suppression
|
|
2401
|
+
// armed for the user's next unrelated action.
|
|
2402
|
+
window.setTimeout(function clearDragClickSuppression() {
|
|
2403
|
+
suppressNextClickRef.current = false;
|
|
2404
|
+
}, 0);
|
|
2405
|
+
// Keeping Option pressed is the explicit duplicate commit. If it was
|
|
2406
|
+
// released before drop, turn the gesture back into a normal move.
|
|
2407
|
+
if (clone && !upEvent.altKey) {
|
|
2408
|
+
applyObjectGeometry(element, getObjectGeometry(clone));
|
|
2409
|
+
clone.remove();
|
|
2410
|
+
activeElement = element;
|
|
2411
|
+
}
|
|
2412
|
+
const html = readCurrentSlideContentHtml();
|
|
2413
|
+
if (html !== null) onUpdateSlideRef.current({ content: html });
|
|
2414
|
+
const selector = getBuilderSelector(activeElement);
|
|
2415
|
+
if (selector) selectElementForStyling(activeElement, selector);
|
|
2416
|
+
};
|
|
2417
|
+
|
|
2418
|
+
const onCancel = () => {
|
|
2419
|
+
stop();
|
|
2420
|
+
suppressNextClickRef.current = false;
|
|
2421
|
+
restore();
|
|
2422
|
+
};
|
|
2423
|
+
|
|
2424
|
+
window.addEventListener("pointermove", onMove);
|
|
2425
|
+
window.addEventListener("pointerup", onUp);
|
|
2426
|
+
window.addEventListener("pointercancel", onCancel);
|
|
2427
|
+
activeGestureCancelRef.current = onCancel;
|
|
2428
|
+
},
|
|
2429
|
+
[
|
|
2430
|
+
applyObjectGeometry,
|
|
2431
|
+
getObjectGeometry,
|
|
2432
|
+
readCurrentSlideContentHtml,
|
|
2433
|
+
readOnly,
|
|
2434
|
+
selectElementForStyling,
|
|
2435
|
+
],
|
|
2436
|
+
);
|
|
2437
|
+
|
|
2438
|
+
const startElementResize = useCallback(
|
|
2439
|
+
(handle: ResizeHandle, e: React.PointerEvent) => {
|
|
2440
|
+
if (readOnly) return;
|
|
2441
|
+
const element = resolveSelectedElement();
|
|
2442
|
+
const fmdSlide = element?.closest(".fmd-slide") as HTMLElement | null;
|
|
2443
|
+
if (
|
|
2444
|
+
!element ||
|
|
2445
|
+
!fmdSlide ||
|
|
2446
|
+
getComputedStyle(element).position !== "absolute"
|
|
2447
|
+
) {
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
e.preventDefault();
|
|
2451
|
+
e.stopPropagation();
|
|
2452
|
+
|
|
2453
|
+
const slideRect = fmdSlide.getBoundingClientRect();
|
|
2454
|
+
const slideWidth = fmdSlide.offsetWidth;
|
|
2455
|
+
const slideHeight = fmdSlide.offsetHeight;
|
|
2456
|
+
if (
|
|
2457
|
+
!slideRect.width ||
|
|
2458
|
+
!slideRect.height ||
|
|
2459
|
+
!slideWidth ||
|
|
2460
|
+
!slideHeight
|
|
2461
|
+
) {
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
const origin = getObjectGeometry(element);
|
|
2466
|
+
const originalObjectId = element.getAttribute("data-slide-object-id");
|
|
2467
|
+
const startClientX = e.clientX;
|
|
2468
|
+
const startClientY = e.clientY;
|
|
2469
|
+
let resized = false;
|
|
2470
|
+
const selector = getBuilderSelector(element);
|
|
2471
|
+
if (selector) selectElementForStyling(element, selector, "resizing");
|
|
2472
|
+
|
|
2473
|
+
const stop = () => {
|
|
2474
|
+
window.removeEventListener("pointermove", onMove);
|
|
2475
|
+
window.removeEventListener("pointerup", onUp);
|
|
2476
|
+
window.removeEventListener("pointercancel", onCancel);
|
|
2477
|
+
if (activeGestureCancelRef.current === onCancel) {
|
|
2478
|
+
activeGestureCancelRef.current = null;
|
|
2479
|
+
}
|
|
2480
|
+
};
|
|
2481
|
+
|
|
2482
|
+
const onMove = (moveEvent: PointerEvent) => {
|
|
2483
|
+
const dx =
|
|
2484
|
+
((moveEvent.clientX - startClientX) / slideRect.width) * slideWidth;
|
|
2485
|
+
const dy =
|
|
2486
|
+
((moveEvent.clientY - startClientY) / slideRect.height) * slideHeight;
|
|
2487
|
+
if (!resized && Math.hypot(dx, dy) < 1) return;
|
|
2488
|
+
resized = true;
|
|
2489
|
+
ensureSlideObjectId(element);
|
|
2490
|
+
applyObjectGeometry(
|
|
2491
|
+
element,
|
|
2492
|
+
resizeSlideObject(origin, {
|
|
2493
|
+
handle,
|
|
2494
|
+
dx,
|
|
2495
|
+
dy,
|
|
2496
|
+
preserveAspectRatio: moveEvent.shiftKey,
|
|
2497
|
+
}),
|
|
2498
|
+
);
|
|
2499
|
+
const currentSelector = getBuilderSelector(element);
|
|
2500
|
+
if (currentSelector) {
|
|
2501
|
+
selectElementForStyling(element, currentSelector, "resizing");
|
|
2502
|
+
}
|
|
2503
|
+
};
|
|
2504
|
+
|
|
2505
|
+
const onUp = () => {
|
|
2506
|
+
stop();
|
|
2507
|
+
const currentSelector = getBuilderSelector(element);
|
|
2508
|
+
if (currentSelector) selectElementForStyling(element, currentSelector);
|
|
2509
|
+
if (!resized) return;
|
|
2510
|
+
const html = readCurrentSlideContentHtml();
|
|
2511
|
+
if (html !== null) onUpdateSlideRef.current({ content: html });
|
|
2512
|
+
};
|
|
2513
|
+
|
|
2514
|
+
const onCancel = () => {
|
|
2515
|
+
stop();
|
|
2516
|
+
applyObjectGeometry(element, origin);
|
|
2517
|
+
if (originalObjectId) {
|
|
2518
|
+
element.setAttribute("data-slide-object-id", originalObjectId);
|
|
2519
|
+
} else {
|
|
2520
|
+
element.removeAttribute("data-slide-object-id");
|
|
2521
|
+
}
|
|
2522
|
+
const selector = getBuilderSelector(element);
|
|
2523
|
+
if (selector) selectElementForStyling(element, selector);
|
|
2524
|
+
};
|
|
2525
|
+
|
|
2526
|
+
window.addEventListener("pointermove", onMove);
|
|
2527
|
+
window.addEventListener("pointerup", onUp);
|
|
2528
|
+
window.addEventListener("pointercancel", onCancel);
|
|
2529
|
+
activeGestureCancelRef.current = onCancel;
|
|
2530
|
+
},
|
|
2531
|
+
[
|
|
2532
|
+
applyObjectGeometry,
|
|
2533
|
+
getObjectGeometry,
|
|
2534
|
+
readCurrentSlideContentHtml,
|
|
2535
|
+
readOnly,
|
|
2536
|
+
resolveSelectedElement,
|
|
2537
|
+
selectElementForStyling,
|
|
2538
|
+
],
|
|
2539
|
+
);
|
|
2540
|
+
|
|
2541
|
+
useEffect(() => {
|
|
2542
|
+
if (
|
|
2543
|
+
readOnly ||
|
|
2544
|
+
editingEl ||
|
|
2545
|
+
multiSelection.size > 0 ||
|
|
2546
|
+
!selectedElementSelector
|
|
2547
|
+
) {
|
|
2548
|
+
return;
|
|
2549
|
+
}
|
|
2550
|
+
const onKey = (e: KeyboardEvent) => {
|
|
2551
|
+
if (!e.key.startsWith("Arrow")) return;
|
|
2552
|
+
const active = document.activeElement;
|
|
2553
|
+
if (
|
|
2554
|
+
active?.tagName === "INPUT" ||
|
|
2555
|
+
active?.tagName === "TEXTAREA" ||
|
|
2556
|
+
active?.tagName === "SELECT" ||
|
|
2557
|
+
(active instanceof HTMLElement && active.isContentEditable)
|
|
2558
|
+
) {
|
|
2559
|
+
return;
|
|
2560
|
+
}
|
|
2561
|
+
const element = resolveSelectedElement();
|
|
2562
|
+
if (!element || getComputedStyle(element).position !== "absolute") {
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2565
|
+
const amount = e.shiftKey ? 10 : 1;
|
|
2566
|
+
const geometry = getObjectGeometry(element);
|
|
2567
|
+
if (e.key === "ArrowLeft") geometry.x -= amount;
|
|
2568
|
+
else if (e.key === "ArrowRight") geometry.x += amount;
|
|
2569
|
+
else if (e.key === "ArrowUp") geometry.y -= amount;
|
|
2570
|
+
else if (e.key === "ArrowDown") geometry.y += amount;
|
|
2571
|
+
else return;
|
|
2572
|
+
|
|
2573
|
+
e.preventDefault();
|
|
2574
|
+
ensureSlideObjectId(element);
|
|
2575
|
+
applyObjectGeometry(element, geometry);
|
|
2576
|
+
const html = readCurrentSlideContentHtml();
|
|
2577
|
+
if (html !== null) onUpdateSlideRef.current({ content: html });
|
|
2578
|
+
const selector = getBuilderSelector(element);
|
|
2579
|
+
if (selector) selectElementForStyling(element, selector);
|
|
2580
|
+
};
|
|
2581
|
+
window.addEventListener("keydown", onKey);
|
|
2582
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
2583
|
+
}, [
|
|
2584
|
+
applyObjectGeometry,
|
|
2585
|
+
editingEl,
|
|
2586
|
+
getObjectGeometry,
|
|
2587
|
+
multiSelection.size,
|
|
2588
|
+
readCurrentSlideContentHtml,
|
|
2589
|
+
readOnly,
|
|
2590
|
+
resolveSelectedElement,
|
|
2591
|
+
selectElementForStyling,
|
|
2592
|
+
selectedElementSelector,
|
|
2593
|
+
]);
|
|
2594
|
+
|
|
1784
2595
|
// --- Marquee drag handlers (attached to slide-content via React props) ---
|
|
1785
2596
|
|
|
2597
|
+
const clearEdgeMoveCursor = useCallback(() => {
|
|
2598
|
+
if (slideCanvasRef.current) slideCanvasRef.current.style.cursor = "";
|
|
2599
|
+
}, []);
|
|
2600
|
+
|
|
2601
|
+
const handleSlidePointerMove = useCallback(
|
|
2602
|
+
(e: React.PointerEvent) => {
|
|
2603
|
+
if (editingEl || readOnly) {
|
|
2604
|
+
clearEdgeMoveCursor();
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2607
|
+
const selected = resolveSelectedElement();
|
|
2608
|
+
const shouldShowMoveCursor =
|
|
2609
|
+
selected !== null &&
|
|
2610
|
+
getComputedStyle(selected).position === "absolute" &&
|
|
2611
|
+
isWithinElementEdgeMoveBand(
|
|
2612
|
+
selected.getBoundingClientRect(),
|
|
2613
|
+
e.clientX,
|
|
2614
|
+
e.clientY,
|
|
2615
|
+
);
|
|
2616
|
+
if (slideCanvasRef.current) {
|
|
2617
|
+
slideCanvasRef.current.style.cursor = shouldShowMoveCursor
|
|
2618
|
+
? "move"
|
|
2619
|
+
: "";
|
|
2620
|
+
}
|
|
2621
|
+
},
|
|
2622
|
+
[clearEdgeMoveCursor, editingEl, readOnly, resolveSelectedElement],
|
|
2623
|
+
);
|
|
2624
|
+
|
|
1786
2625
|
const handleSlidePointerDown = useCallback(
|
|
1787
2626
|
(e: React.PointerEvent) => {
|
|
1788
2627
|
if (e.button !== 0) return; // left click only
|
|
@@ -1806,6 +2645,25 @@ export default function SlideEditor({
|
|
|
1806
2645
|
}
|
|
1807
2646
|
if (editingEl) return; // avoid interfering with an active inline edit
|
|
1808
2647
|
|
|
2648
|
+
const selected = resolveSelectedElement();
|
|
2649
|
+
const dragStart =
|
|
2650
|
+
selected && getComputedStyle(selected).position === "absolute"
|
|
2651
|
+
? getSelectedObjectDragStart({
|
|
2652
|
+
targetWithinSelectedObject: selected.contains(target),
|
|
2653
|
+
pointerWithinMoveBand: isWithinElementEdgeMoveBand(
|
|
2654
|
+
selected.getBoundingClientRect(),
|
|
2655
|
+
e.clientX,
|
|
2656
|
+
e.clientY,
|
|
2657
|
+
),
|
|
2658
|
+
})
|
|
2659
|
+
: null;
|
|
2660
|
+
if (dragStart && selected) {
|
|
2661
|
+
startElementDrag(e, selected, {
|
|
2662
|
+
preserveClickWithoutMove: dragStart === "body",
|
|
2663
|
+
});
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2666
|
+
|
|
1809
2667
|
// Only start a marquee from "whitespace" inside the slide. Clicks on
|
|
1810
2668
|
// an actual element fall through to handleSlideClick (which handles
|
|
1811
2669
|
// shift/cmd-click toggle below).
|
|
@@ -1839,6 +2697,9 @@ export default function SlideEditor({
|
|
|
1839
2697
|
exitInlineEdit,
|
|
1840
2698
|
placeTextBoxAt,
|
|
1841
2699
|
onExitTextBoxMode,
|
|
2700
|
+
getSelectedObjectDragStart,
|
|
2701
|
+
resolveSelectedElement,
|
|
2702
|
+
startElementDrag,
|
|
1842
2703
|
],
|
|
1843
2704
|
);
|
|
1844
2705
|
|
|
@@ -1882,6 +2743,7 @@ export default function SlideEditor({
|
|
|
1882
2743
|
const candidates = slideContent.querySelectorAll("[data-builder-id]");
|
|
1883
2744
|
candidates.forEach((node) => {
|
|
1884
2745
|
const el = node as HTMLElement;
|
|
2746
|
+
if (el.classList.contains("fmd-layout-spacer")) return;
|
|
1885
2747
|
const id = el.getAttribute("data-builder-id");
|
|
1886
2748
|
if (!id) return;
|
|
1887
2749
|
// Skip the slide-content root itself if it ever got stamped
|
|
@@ -1944,6 +2806,32 @@ export default function SlideEditor({
|
|
|
1944
2806
|
[getPlaceholderTarget],
|
|
1945
2807
|
);
|
|
1946
2808
|
|
|
2809
|
+
// Browsers put the dragged element's outerHTML on the "text/html" data
|
|
2810
|
+
// type. Sniffing specifically for an <img> tag there (rather than trusting
|
|
2811
|
+
// any URL on text/uri-list or text/plain) matters because those same types
|
|
2812
|
+
// are also populated when dragging a plain link — e.g. a citation or CTA
|
|
2813
|
+
// button in the agent chat panel — and that URL is not an image. Without
|
|
2814
|
+
// this check, dragging any link onto a slide would replace/insert a broken
|
|
2815
|
+
// image.
|
|
2816
|
+
const extractDraggedImageUrl = useCallback(
|
|
2817
|
+
(dataTransfer: DataTransfer): string | null => {
|
|
2818
|
+
const html = dataTransfer.getData("text/html");
|
|
2819
|
+
if (!html) return null;
|
|
2820
|
+
// Parse instead of regex-matching the raw src attribute text: the
|
|
2821
|
+
// browser HTML-entity-escapes "&" (and other characters) when
|
|
2822
|
+
// serializing outerHTML for the drag payload, so a signed CDN URL like
|
|
2823
|
+
// "...?format=webp&width=800&height=1200" would come through as
|
|
2824
|
+
// "...&width=..." if read verbatim. getAttribute() returns the
|
|
2825
|
+
// already-decoded value.
|
|
2826
|
+
const img = new DOMParser()
|
|
2827
|
+
.parseFromString(html, "text/html")
|
|
2828
|
+
.querySelector("img");
|
|
2829
|
+
const url = img?.getAttribute("src") || null;
|
|
2830
|
+
return url && /^https?:\/\//i.test(url) ? url : null;
|
|
2831
|
+
},
|
|
2832
|
+
[],
|
|
2833
|
+
);
|
|
2834
|
+
|
|
1947
2835
|
const handleSlideDragOver = useCallback((e: React.DragEvent) => {
|
|
1948
2836
|
const files = Array.from(e.dataTransfer.files ?? []);
|
|
1949
2837
|
const items = Array.from(e.dataTransfer.items ?? []);
|
|
@@ -1953,7 +2841,12 @@ export default function SlideEditor({
|
|
|
1953
2841
|
files.some(imageFileLooksSupported) ||
|
|
1954
2842
|
items.some(
|
|
1955
2843
|
(item) => item.kind === "file" && item.type.startsWith("image/"),
|
|
1956
|
-
)
|
|
2844
|
+
) ||
|
|
2845
|
+
// Dragging a rendered <img> (e.g. a generated-image preview in the
|
|
2846
|
+
// agent chat panel) rather than a native OS file. dragover can't read
|
|
2847
|
+
// getData() payloads (only types) in most browsers, so this is a
|
|
2848
|
+
// best-effort signal; the drop handler does the real <img> check.
|
|
2849
|
+
(types.includes("text/html") && types.includes("text/uri-list"));
|
|
1957
2850
|
if (!hasImage) return;
|
|
1958
2851
|
e.preventDefault();
|
|
1959
2852
|
e.dataTransfer.dropEffect = "copy";
|
|
@@ -1963,16 +2856,38 @@ export default function SlideEditor({
|
|
|
1963
2856
|
(e: React.DragEvent) => {
|
|
1964
2857
|
const files = Array.from(e.dataTransfer.files ?? []);
|
|
1965
2858
|
const file = files.find(imageFileLooksSupported);
|
|
1966
|
-
if (files.length
|
|
2859
|
+
if (files.length > 0) {
|
|
2860
|
+
e.preventDefault();
|
|
2861
|
+
e.stopPropagation();
|
|
2862
|
+
if (!file) return;
|
|
2863
|
+
onDropImage?.(
|
|
2864
|
+
getImageReplacementTarget(e.target as HTMLElement),
|
|
2865
|
+
file,
|
|
2866
|
+
{ x: e.clientX, y: e.clientY },
|
|
2867
|
+
);
|
|
2868
|
+
return;
|
|
2869
|
+
}
|
|
2870
|
+
// No native file — check for a dragged <img> instead (e.g. one dragged
|
|
2871
|
+
// out of the agent chat panel's generated-image preview).
|
|
2872
|
+
const url = extractDraggedImageUrl(e.dataTransfer);
|
|
2873
|
+
if (!url) return;
|
|
1967
2874
|
e.preventDefault();
|
|
1968
2875
|
e.stopPropagation();
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
2876
|
+
onDropImageUrl?.(
|
|
2877
|
+
getImageReplacementTarget(e.target as HTMLElement),
|
|
2878
|
+
url,
|
|
2879
|
+
{
|
|
2880
|
+
x: e.clientX,
|
|
2881
|
+
y: e.clientY,
|
|
2882
|
+
},
|
|
2883
|
+
);
|
|
1974
2884
|
},
|
|
1975
|
-
[
|
|
2885
|
+
[
|
|
2886
|
+
extractDraggedImageUrl,
|
|
2887
|
+
getImageReplacementTarget,
|
|
2888
|
+
onDropImage,
|
|
2889
|
+
onDropImageUrl,
|
|
2890
|
+
],
|
|
1976
2891
|
);
|
|
1977
2892
|
|
|
1978
2893
|
const handleSlideClick = useCallback(
|
|
@@ -2078,13 +2993,62 @@ export default function SlideEditor({
|
|
|
2078
2993
|
[showImageOverlay],
|
|
2079
2994
|
);
|
|
2080
2995
|
|
|
2996
|
+
const preserveRichTextSelection = useCallback(() => {
|
|
2997
|
+
const editing = editingElRef.current;
|
|
2998
|
+
const selection = window.getSelection();
|
|
2999
|
+
if (!editing || !selection || selection.rangeCount !== 1) return;
|
|
3000
|
+
const range = selection.getRangeAt(0);
|
|
3001
|
+
if (
|
|
3002
|
+
editing.contains(range.startContainer) &&
|
|
3003
|
+
editing.contains(range.endContainer)
|
|
3004
|
+
) {
|
|
3005
|
+
richTextSelectionRef.current = snapshotEditableTextRange(
|
|
3006
|
+
editing,
|
|
3007
|
+
selection,
|
|
3008
|
+
);
|
|
3009
|
+
}
|
|
3010
|
+
}, []);
|
|
3011
|
+
|
|
2081
3012
|
const applySelectedStylePatch = useCallback(
|
|
2082
3013
|
(patch: SlideStylePatch) => {
|
|
2083
|
-
const
|
|
3014
|
+
const editing = editingElRef.current;
|
|
3015
|
+
const element = editing ?? resolveSelectedElement();
|
|
2084
3016
|
if (!element || !selectedElementSelector) return;
|
|
2085
3017
|
|
|
3018
|
+
const inlinePatch = inlineInspectorStylePatch(patch);
|
|
3019
|
+
const inlineKeys = INLINE_INSPECTOR_STYLE_KEYS.filter(
|
|
3020
|
+
(key) => patch[key] !== undefined,
|
|
3021
|
+
);
|
|
3022
|
+
let styledRange = false;
|
|
3023
|
+
const savedRange =
|
|
3024
|
+
richTextSelectionRef.current ??
|
|
3025
|
+
(editing ? snapshotEditableTextRange(editing) : null);
|
|
3026
|
+
if (
|
|
3027
|
+
editing &&
|
|
3028
|
+
inlineKeys.length > 0 &&
|
|
3029
|
+
restoreEditableTextRange(editing, savedRange)
|
|
3030
|
+
) {
|
|
3031
|
+
const result = applyInlineTextStyle(editing, inlinePatch);
|
|
3032
|
+
if (result.scope === "selection" && result.range) {
|
|
3033
|
+
richTextSelectionRef.current = result.range.cloneRange();
|
|
3034
|
+
styledRange = true;
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
if (!styledRange && inlineKeys.length > 0) {
|
|
3039
|
+
applyDescendantTextStyle(element, inlinePatch);
|
|
3040
|
+
}
|
|
3041
|
+
|
|
2086
3042
|
for (const [property, value] of Object.entries(patch)) {
|
|
2087
3043
|
if (value === undefined) continue;
|
|
3044
|
+
if (
|
|
3045
|
+
styledRange &&
|
|
3046
|
+
INLINE_INSPECTOR_STYLE_KEYS.includes(
|
|
3047
|
+
property as (typeof INLINE_INSPECTOR_STYLE_KEYS)[number],
|
|
3048
|
+
)
|
|
3049
|
+
) {
|
|
3050
|
+
continue;
|
|
3051
|
+
}
|
|
2088
3052
|
element.style.setProperty(stylePropertyName(property), value);
|
|
2089
3053
|
}
|
|
2090
3054
|
|
|
@@ -2097,95 +3061,48 @@ export default function SlideEditor({
|
|
|
2097
3061
|
element.style.borderStyle = "solid";
|
|
2098
3062
|
}
|
|
2099
3063
|
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
3064
|
+
if (editing) {
|
|
3065
|
+
captureInlineEditDraft(slide.id);
|
|
3066
|
+
} else {
|
|
3067
|
+
const html = readCurrentSlideContentHtml();
|
|
3068
|
+
if (html !== null) {
|
|
3069
|
+
onUpdateSlideRef.current({ content: html });
|
|
3070
|
+
}
|
|
2103
3071
|
}
|
|
2104
3072
|
|
|
2105
|
-
const
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
imageSrc:
|
|
2116
|
-
element instanceof HTMLImageElement
|
|
2117
|
-
? (element.getAttribute("src") ?? undefined)
|
|
2118
|
-
: undefined,
|
|
2119
|
-
style: snapshot,
|
|
2120
|
-
},
|
|
2121
|
-
]),
|
|
3073
|
+
const inlineTextStyle = editing
|
|
3074
|
+
? getInlineTextStyleSnapshotForRange(
|
|
3075
|
+
editing,
|
|
3076
|
+
richTextSelectionRef.current,
|
|
3077
|
+
)
|
|
3078
|
+
: undefined;
|
|
3079
|
+
const snapshot = buildStyleSnapshot(
|
|
3080
|
+
element,
|
|
3081
|
+
selectedElementSelector,
|
|
3082
|
+
inlineTextStyle,
|
|
2122
3083
|
);
|
|
3084
|
+
if (!editing) {
|
|
3085
|
+
setSelectedElementRect(element.getBoundingClientRect());
|
|
3086
|
+
}
|
|
3087
|
+
setSelectedStyleSnapshot(snapshot);
|
|
3088
|
+
if (!editing) {
|
|
3089
|
+
syncSelectionToAppState(
|
|
3090
|
+
buildSelectionState(getSlideSelectionMode(snapshot), [
|
|
3091
|
+
selectionItemForElement(element, selectedElementSelector, snapshot),
|
|
3092
|
+
]),
|
|
3093
|
+
);
|
|
3094
|
+
}
|
|
2123
3095
|
},
|
|
2124
3096
|
[
|
|
2125
3097
|
buildSelectionState,
|
|
3098
|
+
captureInlineEditDraft,
|
|
2126
3099
|
readCurrentSlideContentHtml,
|
|
2127
3100
|
resolveSelectedElement,
|
|
2128
3101
|
selectedElementSelector,
|
|
3102
|
+
slide.id,
|
|
2129
3103
|
],
|
|
2130
3104
|
);
|
|
2131
3105
|
|
|
2132
|
-
/**
|
|
2133
|
-
* Drag-to-reposition for the selected element. Only elements that are (or
|
|
2134
|
-
* can safely become) absolutely positioned support this — repositioning an
|
|
2135
|
-
* in-flow element would shift the rest of the slide's layout. Percentages
|
|
2136
|
-
* (not px) are used for left/top so the drag stays accurate under canvas
|
|
2137
|
-
* zoom and the autofit scale transform, matching placeTextBoxAt.
|
|
2138
|
-
*/
|
|
2139
|
-
const startElementDrag = useCallback(
|
|
2140
|
-
(e: React.PointerEvent) => {
|
|
2141
|
-
const element = resolveSelectedElement();
|
|
2142
|
-
if (!element) return;
|
|
2143
|
-
const fmdSlide = element.closest(".fmd-slide") as HTMLElement | null;
|
|
2144
|
-
if (!fmdSlide) return;
|
|
2145
|
-
// left/top only place an element at an absolute coordinate for
|
|
2146
|
-
// position: absolute (or fixed). For position: relative they're an
|
|
2147
|
-
// *offset* from the element's normal flow position instead, so
|
|
2148
|
-
// treating a relative element as draggable here would make it jump by
|
|
2149
|
-
// the wrong amount. Restrict dragging to elements already out of flow.
|
|
2150
|
-
if (getComputedStyle(element).position !== "absolute") return;
|
|
2151
|
-
|
|
2152
|
-
e.preventDefault();
|
|
2153
|
-
e.stopPropagation();
|
|
2154
|
-
|
|
2155
|
-
const slideRect = fmdSlide.getBoundingClientRect();
|
|
2156
|
-
const elRect = element.getBoundingClientRect();
|
|
2157
|
-
const startXPct =
|
|
2158
|
-
((elRect.left - slideRect.left) / slideRect.width) * 100;
|
|
2159
|
-
const startYPct = ((elRect.top - slideRect.top) / slideRect.height) * 100;
|
|
2160
|
-
const startClientX = e.clientX;
|
|
2161
|
-
const startClientY = e.clientY;
|
|
2162
|
-
let moved = false;
|
|
2163
|
-
|
|
2164
|
-
const onMove = (moveEvent: PointerEvent) => {
|
|
2165
|
-
moved = true;
|
|
2166
|
-
const dxPct =
|
|
2167
|
-
((moveEvent.clientX - startClientX) / slideRect.width) * 100;
|
|
2168
|
-
const dyPct =
|
|
2169
|
-
((moveEvent.clientY - startClientY) / slideRect.height) * 100;
|
|
2170
|
-
element.style.left = `${startXPct + dxPct}%`;
|
|
2171
|
-
element.style.top = `${startYPct + dyPct}%`;
|
|
2172
|
-
setSelectedElementRect(element.getBoundingClientRect());
|
|
2173
|
-
};
|
|
2174
|
-
|
|
2175
|
-
const onUp = () => {
|
|
2176
|
-
window.removeEventListener("pointermove", onMove);
|
|
2177
|
-
window.removeEventListener("pointerup", onUp);
|
|
2178
|
-
if (!moved) return;
|
|
2179
|
-
const html = readCurrentSlideContentHtml();
|
|
2180
|
-
if (html !== null) onUpdateSlideRef.current({ content: html });
|
|
2181
|
-
};
|
|
2182
|
-
|
|
2183
|
-
window.addEventListener("pointermove", onMove);
|
|
2184
|
-
window.addEventListener("pointerup", onUp);
|
|
2185
|
-
},
|
|
2186
|
-
[resolveSelectedElement, readCurrentSlideContentHtml],
|
|
2187
|
-
);
|
|
2188
|
-
|
|
2189
3106
|
// --- Pending visual updates ---
|
|
2190
3107
|
const [pendingUpdateCount, setPendingUpdateCount] = useState(0);
|
|
2191
3108
|
|
|
@@ -2344,14 +3261,14 @@ export default function SlideEditor({
|
|
|
2344
3261
|
onContextMenu={handleSlideContextMenu}
|
|
2345
3262
|
onDoubleClick={handleSlideDoubleClick}
|
|
2346
3263
|
onPointerDown={handleSlidePointerDown}
|
|
3264
|
+
onPointerMove={handleSlidePointerMove}
|
|
3265
|
+
onPointerLeave={clearEdgeMoveCursor}
|
|
2347
3266
|
onDragOver={handleSlideDragOver}
|
|
2348
3267
|
onDrop={handleSlideDrop}
|
|
2349
|
-
onMouseEnter={() => setIsHoveringText(true)}
|
|
2350
|
-
onMouseLeave={() => setIsHoveringText(false)}
|
|
2351
3268
|
>
|
|
2352
3269
|
<SlideRenderer
|
|
2353
3270
|
slide={slide}
|
|
2354
|
-
className=
|
|
3271
|
+
className="shadow-2xl shadow-black/40"
|
|
2355
3272
|
designSystem={designSystem}
|
|
2356
3273
|
aspectRatio={aspectRatio}
|
|
2357
3274
|
onOverflowChange={handleOverflowChange}
|
|
@@ -2365,24 +3282,14 @@ export default function SlideEditor({
|
|
|
2365
3282
|
containerRef={slideCanvasRef}
|
|
2366
3283
|
/>
|
|
2367
3284
|
)}
|
|
2368
|
-
{
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
{
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
<AgentPresenceChip active={agentActive} />
|
|
2377
|
-
</div>
|
|
2378
|
-
)}
|
|
2379
|
-
{presentUsers.length > 0 && (
|
|
2380
|
-
<div
|
|
2381
|
-
className={`absolute right-2 z-10 ${
|
|
2382
|
-
agentActive ? "top-11" : "top-2"
|
|
2383
|
-
}`}
|
|
2384
|
-
>
|
|
2385
|
-
<SameSlidePresenceIndicator users={presentUsers} />
|
|
3285
|
+
{(agentActive || passivePresentUsers.length > 0) && (
|
|
3286
|
+
<div className="absolute right-2 top-2 z-10 flex items-center gap-2">
|
|
3287
|
+
<AgentPresenceChip active={Boolean(agentActive)} />
|
|
3288
|
+
{passivePresentUsers.length > 0 && (
|
|
3289
|
+
<SameSlidePresenceIndicator
|
|
3290
|
+
users={passivePresentUsers}
|
|
3291
|
+
/>
|
|
3292
|
+
)}
|
|
2386
3293
|
</div>
|
|
2387
3294
|
)}
|
|
2388
3295
|
{overflowInfo && !readOnly && !agentActive && (
|
|
@@ -2414,10 +3321,11 @@ export default function SlideEditor({
|
|
|
2414
3321
|
)}
|
|
2415
3322
|
</div>
|
|
2416
3323
|
|
|
2417
|
-
{!readOnly && (
|
|
3324
|
+
{!readOnly && stylePanelOpen && (
|
|
2418
3325
|
<div
|
|
2419
3326
|
className="relative z-[70] hidden h-full w-[17rem] shrink-0 border-l border-border/70 bg-background/95 lg:block"
|
|
2420
3327
|
data-slide-style-dock="true"
|
|
3328
|
+
onPointerDownCapture={preserveRichTextSelection}
|
|
2421
3329
|
>
|
|
2422
3330
|
{selectedStyleSnapshot ? (
|
|
2423
3331
|
<SlideStyleInspector
|
|
@@ -2428,13 +3336,29 @@ export default function SlideEditor({
|
|
|
2428
3336
|
onClose={() => {
|
|
2429
3337
|
clearSelectedElement();
|
|
2430
3338
|
syncSelectionToAppState(null);
|
|
3339
|
+
onCloseStylePanel?.();
|
|
2431
3340
|
}}
|
|
2432
3341
|
/>
|
|
2433
3342
|
) : (
|
|
2434
|
-
<div className="flex h-11 items-center border-b border-border/70 px-3">
|
|
3343
|
+
<div className="flex h-11 items-center justify-between border-b border-border/70 px-3">
|
|
2435
3344
|
<span className="text-[10px] font-semibold uppercase tracking-[0.16em] text-muted-foreground/70">
|
|
2436
3345
|
{t("styleInspector.title")}
|
|
2437
3346
|
</span>
|
|
3347
|
+
<Tooltip>
|
|
3348
|
+
<TooltipTrigger asChild>
|
|
3349
|
+
<Button
|
|
3350
|
+
type="button"
|
|
3351
|
+
variant="ghost"
|
|
3352
|
+
size="icon"
|
|
3353
|
+
className="size-7 cursor-pointer text-muted-foreground hover:text-foreground"
|
|
3354
|
+
onClick={onCloseStylePanel}
|
|
3355
|
+
aria-label={t("styleInspector.close")}
|
|
3356
|
+
>
|
|
3357
|
+
<IconX className="size-3.5" />
|
|
3358
|
+
</Button>
|
|
3359
|
+
</TooltipTrigger>
|
|
3360
|
+
<TooltipContent>{t("styleInspector.close")}</TooltipContent>
|
|
3361
|
+
</Tooltip>
|
|
2438
3362
|
</div>
|
|
2439
3363
|
)}
|
|
2440
3364
|
</div>
|
|
@@ -2458,8 +3382,10 @@ export default function SlideEditor({
|
|
|
2458
3382
|
<ElementSelectionOutline
|
|
2459
3383
|
rect={selectedElementRect}
|
|
2460
3384
|
viewportRect={selectionViewportRect}
|
|
2461
|
-
|
|
2462
|
-
|
|
3385
|
+
onResizeStart={
|
|
3386
|
+
!readOnly && isSelectedElementDraggable
|
|
3387
|
+
? startElementResize
|
|
3388
|
+
: undefined
|
|
2463
3389
|
}
|
|
2464
3390
|
/>
|
|
2465
3391
|
)}
|