@agent-native/core 0.163.4 → 0.164.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/app/components/agents/FeatureFlagsFleetPanel.tsx +78 -19
- package/corpus/templates/analytics/app/hooks/use-dashboard-chat-context.ts +29 -0
- package/corpus/templates/analytics/app/lib/use-auto-focus-select.ts +4 -4
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +3 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +6 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +1 -2
- package/corpus/templates/analytics/app/routes/_index.tsx +3 -2
- package/corpus/templates/analytics/app/routes/adhoc.$id.tsx +7 -2
- package/corpus/templates/analytics/app/routes/dashboard.tsx +3 -2
- package/corpus/templates/analytics/app/routes/overview.tsx +3 -2
- package/corpus/templates/analytics/app/routes/traffic.tsx +3 -2
- package/corpus/templates/analytics/changelog/2026-08-18-verify-cross-app-feature-flag-changes.md +6 -0
- package/corpus/templates/analytics/netlify.toml +2 -0
- package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +196 -48
- package/corpus/templates/analytics/server/plugins/feature-flags.ts +5 -0
- package/corpus/templates/analytics/shared/feature-flags.ts +10 -0
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +5 -1
- package/corpus/templates/assets/app/routes/brand-kits._index.tsx +4 -1
- package/corpus/templates/assets/app/routes/libraries.tsx +4 -1
- package/corpus/templates/assets/app/routes/picker.tsx +4 -1
- package/corpus/templates/assets/netlify.toml +2 -0
- package/corpus/templates/brain/netlify.toml +2 -0
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +2 -1
- package/corpus/templates/calendar/changelog/2026-08-19-calendar-page-loads-stay-fast-after-idle-periods.md +6 -0
- package/corpus/templates/calendar/netlify.toml +2 -0
- package/corpus/templates/chat/netlify.toml +2 -0
- package/corpus/templates/clips/app/components/player/player-controls.tsx +3 -3
- package/corpus/templates/clips/app/components/player/reaction-emojis.ts +12 -0
- package/corpus/templates/clips/app/components/player/reactions-tray.tsx +90 -9
- package/corpus/templates/clips/app/components/player/scrubber.tsx +43 -10
- package/corpus/templates/clips/app/components/player/video-player.tsx +105 -15
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +1 -2
- package/corpus/templates/clips/app/routes/_app.wispr.tsx +2 -1
- package/corpus/templates/clips/app/routes/_index.tsx +7 -3
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +13 -8
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +15 -8
- package/corpus/templates/clips/netlify.toml +2 -0
- package/corpus/templates/clips/server/routes/[...page].head.ts +5 -0
- package/corpus/templates/content/netlify.toml +2 -0
- package/corpus/templates/design/.agents/skills/design-editor-architecture/SKILL.md +103 -0
- package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +2 -1
- package/corpus/templates/design/AGENTS.md +3 -1
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +3 -2
- package/corpus/templates/design/app/components/design/editor/AgentNativeMenuMark.tsx +25 -0
- package/corpus/templates/design/app/components/design/editor/DesignBottomToolbar.tsx +411 -0
- package/corpus/templates/design/app/components/design/editor/DesignCollaborators.tsx +153 -0
- package/corpus/templates/design/app/components/design/editor/DesignWorkspaceRail.tsx +231 -0
- package/corpus/templates/design/app/components/design/editor/MakeRealDialog.tsx +223 -0
- package/corpus/templates/design/app/components/design/editor/PendingScreenDeletionDialog.tsx +58 -0
- package/corpus/templates/design/app/components/design/editor/PendingVisualStyleWarningDialog.tsx +56 -0
- package/corpus/templates/design/app/components/design/editor/ReadOnlyEditorPanel.tsx +23 -0
- package/corpus/templates/design/app/components/design/editor/SaveTemplateDialog.tsx +161 -0
- package/corpus/templates/design/app/components/design/editor/toolbar-controls.tsx +185 -0
- package/corpus/templates/design/app/pages/DesignEditor.tsx +20913 -0
- package/corpus/templates/design/app/pages/design-editor/command-types.ts +120 -0
- package/corpus/templates/design/app/pages/design-editor/commands/README.md +193 -0
- package/corpus/templates/design/app/pages/design-editor/commands/add-auto-layout.ts +416 -0
- package/corpus/templates/design/app/pages/design-editor/commands/add-screen.ts +112 -0
- package/corpus/templates/design/app/pages/design-editor/commands/align-selection.ts +183 -0
- package/corpus/templates/design/app/pages/design-editor/commands/apply-design-editor-command.ts +243 -0
- package/corpus/templates/design/app/pages/design-editor/commands/apply-file-content-update.ts +236 -0
- package/corpus/templates/design/app/pages/design-editor/commands/apply-local-content-update.ts +304 -0
- package/corpus/templates/design/app/pages/design-editor/commands/apply-pending-visual-styles-with-agent.ts +376 -0
- package/corpus/templates/design/app/pages/design-editor/commands/apply-to-source.ts +132 -0
- package/corpus/templates/design/app/pages/design-editor/commands/can-move-layer.ts +117 -0
- package/corpus/templates/design/app/pages/design-editor/commands/change-selected-z-index.ts +212 -0
- package/corpus/templates/design/app/pages/design-editor/commands/commit-relative-style-delta-to-selected-layers.ts +176 -0
- package/corpus/templates/design/app/pages/design-editor/commands/commit-styles-to-selected-layers.ts +175 -0
- package/corpus/templates/design/app/pages/design-editor/commands/commit-visual-styles.ts +710 -0
- package/corpus/templates/design/app/pages/design-editor/commands/confirm-make-real.ts +99 -0
- package/corpus/templates/design/app/pages/design-editor/commands/copy-as-figma-svg.ts +95 -0
- package/corpus/templates/design/app/pages/design-editor/commands/copy-selection.ts +156 -0
- package/corpus/templates/design/app/pages/design-editor/commands/create-primitive.ts +291 -0
- package/corpus/templates/design/app/pages/design-editor/commands/create-screen-frame.ts +131 -0
- package/corpus/templates/design/app/pages/design-editor/commands/cross-screen-element-drop.ts +577 -0
- package/corpus/templates/design/app/pages/design-editor/commands/delete-files.ts +364 -0
- package/corpus/templates/design/app/pages/design-editor/commands/delete-selection.ts +521 -0
- package/corpus/templates/design/app/pages/design-editor/commands/detach-instance-menu-action.ts +88 -0
- package/corpus/templates/design/app/pages/design-editor/commands/distribute-selection.ts +114 -0
- package/corpus/templates/design/app/pages/design-editor/commands/download-all-screens-pdf.ts +116 -0
- package/corpus/templates/design/app/pages/design-editor/commands/download-pdf.ts +103 -0
- package/corpus/templates/design/app/pages/design-editor/commands/download-svg.ts +159 -0
- package/corpus/templates/design/app/pages/design-editor/commands/duplicate-screen.ts +201 -0
- package/corpus/templates/design/app/pages/design-editor/commands/duplicate-selection.ts +279 -0
- package/corpus/templates/design/app/pages/design-editor/commands/editor-paste.ts +81 -0
- package/corpus/templates/design/app/pages/design-editor/commands/enter-hotkey.ts +155 -0
- package/corpus/templates/design/app/pages/design-editor/commands/enter-single-screen.ts +168 -0
- package/corpus/templates/design/app/pages/design-editor/commands/escape-hotkey.ts +212 -0
- package/corpus/templates/design/app/pages/design-editor/commands/frame-selection.ts +119 -0
- package/corpus/templates/design/app/pages/design-editor/commands/geometry-commit.ts +169 -0
- package/corpus/templates/design/app/pages/design-editor/commands/get-selected-layer-snapshots.ts +177 -0
- package/corpus/templates/design/app/pages/design-editor/commands/group-selection.ts +127 -0
- package/corpus/templates/design/app/pages/design-editor/commands/iframe-context-menu.ts +121 -0
- package/corpus/templates/design/app/pages/design-editor/commands/import-figma-clipboard-into-design.ts +119 -0
- package/corpus/templates/design/app/pages/design-editor/commands/layer-marquee-selection-change.ts +163 -0
- package/corpus/templates/design/app/pages/design-editor/commands/layer-move-to-screen.ts +331 -0
- package/corpus/templates/design/app/pages/design-editor/commands/layer-move.ts +517 -0
- package/corpus/templates/design/app/pages/design-editor/commands/layer-rename.ts +247 -0
- package/corpus/templates/design/app/pages/design-editor/commands/layer-selection-change.ts +161 -0
- package/corpus/templates/design/app/pages/design-editor/commands/mode-change.ts +122 -0
- package/corpus/templates/design/app/pages/design-editor/commands/nudge-selection.ts +213 -0
- package/corpus/templates/design/app/pages/design-editor/commands/overview-primitive-reparent.ts +318 -0
- package/corpus/templates/design/app/pages/design-editor/commands/paste-copied-screens.ts +114 -0
- package/corpus/templates/design/app/pages/design-editor/commands/paste-over-selection.ts +70 -0
- package/corpus/templates/design/app/pages/design-editor/commands/paste-selection.ts +436 -0
- package/corpus/templates/design/app/pages/design-editor/commands/paste-to-replace.ts +135 -0
- package/corpus/templates/design/app/pages/design-editor/commands/pasted-image-files.ts +209 -0
- package/corpus/templates/design/app/pages/design-editor/commands/persist-frame-geometry-save.ts +113 -0
- package/corpus/templates/design/app/pages/design-editor/commands/persist-tweak-save.ts +160 -0
- package/corpus/templates/design/app/pages/design-editor/commands/primitive-created.ts +168 -0
- package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-layer-state-edit.ts +149 -0
- package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-structure-edit.ts +200 -0
- package/corpus/templates/design/app/pages/design-editor/commands/record-pending-live-text-edit.ts +152 -0
- package/corpus/templates/design/app/pages/design-editor/commands/record-pending-visual-style-edit.ts +247 -0
- package/corpus/templates/design/app/pages/design-editor/commands/redo.ts +1086 -0
- package/corpus/templates/design/app/pages/design-editor/commands/render-png-blob.ts +200 -0
- package/corpus/templates/design/app/pages/design-editor/commands/save-file-content.ts +207 -0
- package/corpus/templates/design/app/pages/design-editor/commands/screen-element-select.ts +262 -0
- package/corpus/templates/design/app/pages/design-editor/commands/screen-text-content-change.ts +216 -0
- package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-duplicate-change.ts +108 -0
- package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-structure-change.ts +280 -0
- package/corpus/templates/design/app/pages/design-editor/commands/screen-visual-style-change.ts +151 -0
- package/corpus/templates/design/app/pages/design-editor/commands/send-overview-annotations.ts +88 -0
- package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-move-semantic-handoff.ts +122 -0
- package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-semantic-handoff.ts +143 -0
- package/corpus/templates/design/app/pages/design-editor/commands/send-runtime-layer-state-semantic-handoff.ts +115 -0
- package/corpus/templates/design/app/pages/design-editor/commands/start-retry-generation.ts +152 -0
- package/corpus/templates/design/app/pages/design-editor/commands/start-sidebar-resize.ts +99 -0
- package/corpus/templates/design/app/pages/design-editor/commands/style-change.ts +152 -0
- package/corpus/templates/design/app/pages/design-editor/commands/styles-change.ts +155 -0
- package/corpus/templates/design/app/pages/design-editor/commands/suggest-auto-layout.ts +125 -0
- package/corpus/templates/design/app/pages/design-editor/commands/text-content-change.ts +201 -0
- package/corpus/templates/design/app/pages/design-editor/commands/tidy-up.ts +123 -0
- package/corpus/templates/design/app/pages/design-editor/commands/toggle-layer-hidden.ts +167 -0
- package/corpus/templates/design/app/pages/design-editor/commands/toggle-layer-locked.ts +175 -0
- package/corpus/templates/design/app/pages/design-editor/commands/toggle-motion-keyframe.ts +99 -0
- package/corpus/templates/design/app/pages/design-editor/commands/tweak-prompt-submit.ts +84 -0
- package/corpus/templates/design/app/pages/design-editor/commands/undo.ts +906 -0
- package/corpus/templates/design/app/pages/design-editor/commands/ungroup-selection.ts +170 -0
- package/corpus/templates/design/app/pages/design-editor/commands/visual-duplicate-change.ts +119 -0
- package/corpus/templates/design/app/pages/design-editor/commands/visual-structure-change.ts +274 -0
- package/corpus/templates/design/app/pages/design-editor/commands/write-frame-geometry-snapshot.ts +99 -0
- package/corpus/templates/design/app/pages/design-editor/derive/design-breakpoints.ts +56 -0
- package/corpus/templates/design/app/pages/design-editor/derive/overview-screens.ts +146 -0
- package/corpus/templates/design/app/pages/design-editor/domains/use-tweaks.ts +326 -0
- package/corpus/templates/design/app/pages/design-editor/editor-constants.ts +54 -0
- package/corpus/templates/design/app/pages/design-editor/editor-helpers.ts +183 -0
- package/corpus/templates/design/app/pages/design-editor/effects/adopt-db-file-content.ts +270 -0
- package/corpus/templates/design/app/pages/design-editor/effects/mirror-selection-to-agent-chat.ts +134 -0
- package/corpus/templates/design/app/pages/design-editor/effects/motion-autosave.ts +335 -0
- package/corpus/templates/design/app/pages/design-editor/effects/observe-collab-text.ts +183 -0
- package/corpus/templates/design/app/pages/design-editor/effects/publish-agent-selection-context.ts +252 -0
- package/corpus/templates/design/app/pages/design-editor/effects/resume-pending-generation.ts +178 -0
- package/corpus/templates/design/app/pages/design-editor/effects/seed-collab-content.ts +157 -0
- package/corpus/templates/design/app/pages/design-editor/preview-html.ts +19 -0
- package/corpus/templates/design/app/routes/examples.tsx +2 -1
- package/corpus/templates/design/netlify.toml +2 -0
- package/corpus/templates/design/shared/board-file.ts +2 -1
- package/corpus/templates/dispatch/netlify.toml +2 -0
- package/corpus/templates/forms/app/routes/_index.tsx +3 -2
- package/corpus/templates/forms/netlify.toml +2 -0
- package/corpus/templates/mail/app/routes/_index.tsx +3 -2
- package/corpus/templates/mail/netlify.toml +2 -0
- package/corpus/templates/plan/netlify.toml +2 -0
- package/corpus/templates/slides/netlify.toml +2 -0
- package/corpus/templates/tasks/app/routes/_index.tsx +2 -1
- package/dist/agent/engine/builder-engine.d.ts +6 -4
- package/dist/agent/engine/builder-engine.js +91 -26
- package/dist/agent/engine/registry.js +16 -0
- package/dist/cli/code-agent-agent-tools.d.ts +2 -0
- package/dist/cli/code-agent-agent-tools.js +188 -0
- package/dist/cli/code-agent-collaboration.d.ts +34 -0
- package/dist/cli/code-agent-collaboration.js +104 -0
- package/dist/cli/code-agent-executor.js +6 -0
- package/dist/cli/code-agent-schedules.d.ts +54 -0
- package/dist/cli/code-agent-schedules.js +198 -0
- package/dist/client/AgentPanel.js +1 -1
- package/dist/client/ConnectBuilderCard.js +5 -2
- package/dist/client/EnvironmentBadge.js +6 -5
- package/dist/client/chat/run-recovery.js +3 -2
- package/dist/client/chat/tool-call-display.js +2 -3
- package/dist/client/components/CodeRequiredDialog.js +4 -2
- package/dist/client/i18n.js +5 -4
- package/dist/client/settings/SettingsPanel.js +1 -1
- package/dist/client/settings/VoiceTranscriptionSection.js +1 -1
- package/dist/client/settings/useBuilderStatus.d.ts +6 -1
- package/dist/client/settings/useBuilderStatus.js +22 -33
- package/dist/client/transcription/BuilderTranscriptionCta.js +1 -1
- package/dist/code-agents/index.d.ts +2 -0
- package/dist/code-agents/index.js +2 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/routes.d.ts +1 -1
- package/dist/deploy/build.js +10 -4
- package/dist/localization/shared.d.ts +2 -0
- package/dist/localization/shared.js +5 -0
- package/dist/mcp-client/oauth-client.d.ts +1 -0
- package/dist/mcp-client/oauth-client.js +50 -1
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/routes.d.ts +1 -1
- package/dist/provider-api/actions/provider-api.d.ts +2 -2
- package/dist/server/auth-marketing-locales.d.ts +7 -0
- package/dist/server/auth-marketing-locales.js +1322 -0
- package/dist/server/auth-marketing.d.ts +1 -0
- package/dist/server/auth-marketing.js +35 -6
- package/dist/server/auth.js +4 -17
- package/dist/server/builder-browser.d.ts +22 -0
- package/dist/server/builder-browser.js +77 -1
- package/dist/server/builder-oauth.d.ts +50 -0
- package/dist/server/builder-oauth.js +263 -0
- package/dist/server/core-routes-plugin.d.ts +11 -0
- package/dist/server/core-routes-plugin.js +291 -415
- package/dist/server/credential-provider.d.ts +1 -1
- package/dist/server/credential-provider.js +1 -1
- package/dist/server/csrf.js +2 -2
- package/dist/server/onboarding-html.js +52 -120
- package/dist/server/ssr-handler.js +13 -4
- package/dist/settings/store.d.ts +9 -0
- package/dist/settings/store.js +19 -0
- package/dist/shared/cache-control.d.ts +22 -0
- package/dist/shared/cache-control.js +25 -0
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/templates/chat/netlify.toml +2 -0
- package/docs/content/deployment.mdx +2 -0
- package/docs/content/multi-app-workspace.mdx +1 -1
- package/package.json +2 -2
- package/src/templates/chat/netlify.toml +2 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { useActionMutation } from "@agent-native/core/client/hooks";
|
|
2
|
+
import {
|
|
3
|
+
compile as compileMotionTimeline,
|
|
4
|
+
injectManagedMotionCss,
|
|
5
|
+
} from "@shared/motion-compiler";
|
|
6
|
+
import { sortMotionKeyframes } from "@shared/motion-timeline";
|
|
7
|
+
import type { QueryClient } from "@tanstack/react-query";
|
|
8
|
+
import type { Dispatch, RefObject, SetStateAction } from "react";
|
|
9
|
+
import { toast } from "sonner";
|
|
10
|
+
|
|
11
|
+
import type { MotionDockTrack } from "@/components/design/MotionDock";
|
|
12
|
+
import type { ClipboardContentMutationPublication } from "@/lib/clipboard-content-lineage";
|
|
13
|
+
import { MOTION_AUTOSAVE_DELAY_MS } from "@/pages/design-editor/editor-constants";
|
|
14
|
+
import { getFreshActiveFileContent } from "@/pages/design-editor/editor-state";
|
|
15
|
+
import { motionTimelineFingerprint } from "@/pages/design-editor/motion-state";
|
|
16
|
+
import type { DesignFile } from "@/pages/design-editor/types";
|
|
17
|
+
|
|
18
|
+
export interface MotionAutosaveArgs {
|
|
19
|
+
activeContent: string;
|
|
20
|
+
activeFile: DesignFile;
|
|
21
|
+
applyFileContentUpdate: (
|
|
22
|
+
fileId: string,
|
|
23
|
+
nextContent: string,
|
|
24
|
+
options?: {
|
|
25
|
+
refreshPreview?: boolean;
|
|
26
|
+
skipPreview?: boolean;
|
|
27
|
+
forcePreviewFullDocument?: boolean;
|
|
28
|
+
persist?: boolean;
|
|
29
|
+
recordHistory?: boolean;
|
|
30
|
+
updatedAt?: string;
|
|
31
|
+
clipboardMutation?: ClipboardContentMutationPublication;
|
|
32
|
+
},
|
|
33
|
+
) => void;
|
|
34
|
+
applyMotionEdit: ReturnType<
|
|
35
|
+
typeof useActionMutation<undefined, undefined, "apply-motion-edit">
|
|
36
|
+
>["mutate"];
|
|
37
|
+
clearMotionAutosaveTimer: () => void;
|
|
38
|
+
getScreenContent: (screenId: string) => string;
|
|
39
|
+
id: string | undefined;
|
|
40
|
+
lastLocalContentRef: RefObject<string | null>;
|
|
41
|
+
lastScheduledMotionAutosaveRevisionRef: RefObject<number>;
|
|
42
|
+
latestActiveContentRef: RefObject<string | null>;
|
|
43
|
+
motionAutosaveFailedRevisionRef: RefObject<number | null>;
|
|
44
|
+
motionAutosaveFlushRef: RefObject<(() => void) | null>;
|
|
45
|
+
motionAutosavePending: ReturnType<
|
|
46
|
+
typeof useActionMutation<undefined, undefined, "apply-motion-edit">
|
|
47
|
+
>["isPending"];
|
|
48
|
+
motionAutosaveRevision: number;
|
|
49
|
+
motionAutosaveRevisionRef: RefObject<number>;
|
|
50
|
+
motionAutosaveTimerRef: RefObject<number | null>;
|
|
51
|
+
motionDefaultEase: string;
|
|
52
|
+
motionDurationMs: number;
|
|
53
|
+
motionTimelineId: string | null;
|
|
54
|
+
motionTracks: MotionDockTrack[];
|
|
55
|
+
motionTracksDirty: boolean;
|
|
56
|
+
previousMotionFileIdRef: RefObject<string | null>;
|
|
57
|
+
queryClient: QueryClient;
|
|
58
|
+
removeMotionTimeline: ReturnType<
|
|
59
|
+
typeof useActionMutation<undefined, undefined, "remove-motion-timeline">
|
|
60
|
+
>["mutate"];
|
|
61
|
+
removeMotionTimelineMutation: ReturnType<
|
|
62
|
+
typeof useActionMutation<undefined, undefined, "remove-motion-timeline">
|
|
63
|
+
>;
|
|
64
|
+
setMotionHydrationFingerprint: Dispatch<SetStateAction<string | null>>;
|
|
65
|
+
setMotionTimelineId: Dispatch<SetStateAction<string | null>>;
|
|
66
|
+
setMotionTracksDirty: Dispatch<SetStateAction<boolean>>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function runMotionAutosave({
|
|
70
|
+
activeContent,
|
|
71
|
+
activeFile,
|
|
72
|
+
applyFileContentUpdate,
|
|
73
|
+
applyMotionEdit,
|
|
74
|
+
clearMotionAutosaveTimer,
|
|
75
|
+
getScreenContent,
|
|
76
|
+
id,
|
|
77
|
+
lastLocalContentRef,
|
|
78
|
+
lastScheduledMotionAutosaveRevisionRef,
|
|
79
|
+
latestActiveContentRef,
|
|
80
|
+
motionAutosaveFailedRevisionRef,
|
|
81
|
+
motionAutosaveFlushRef,
|
|
82
|
+
motionAutosavePending,
|
|
83
|
+
motionAutosaveRevision,
|
|
84
|
+
motionAutosaveRevisionRef,
|
|
85
|
+
motionAutosaveTimerRef,
|
|
86
|
+
motionDefaultEase,
|
|
87
|
+
motionDurationMs,
|
|
88
|
+
motionTimelineId,
|
|
89
|
+
motionTracks,
|
|
90
|
+
motionTracksDirty,
|
|
91
|
+
previousMotionFileIdRef,
|
|
92
|
+
queryClient,
|
|
93
|
+
removeMotionTimeline,
|
|
94
|
+
removeMotionTimelineMutation,
|
|
95
|
+
setMotionHydrationFingerprint,
|
|
96
|
+
setMotionTimelineId,
|
|
97
|
+
setMotionTracksDirty,
|
|
98
|
+
}: MotionAutosaveArgs) {
|
|
99
|
+
if (!id || !activeFile?.id || !motionTracksDirty) return;
|
|
100
|
+
if (motionAutosavePending) return;
|
|
101
|
+
if (motionAutosaveFailedRevisionRef.current === motionAutosaveRevision)
|
|
102
|
+
return;
|
|
103
|
+
if (
|
|
104
|
+
lastScheduledMotionAutosaveRevisionRef.current === motionAutosaveRevision &&
|
|
105
|
+
motionAutosaveTimerRef.current !== null
|
|
106
|
+
) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// U-motion-empty: the user deleted every track (or the last keyframe of
|
|
110
|
+
// the last track, which MotionDock collapses into removing the track).
|
|
111
|
+
// apply-motion-edit's schema rejects an empty tracks array, so this can't
|
|
112
|
+
// go through the normal autosave — it must go through remove-motion-timeline
|
|
113
|
+
// instead, which deletes the motion_timeline row AND strips the managed
|
|
114
|
+
// <style data-agent-native-motion> block so a reload doesn't restore the
|
|
115
|
+
// old animation. Nothing to remove if there was never a persisted
|
|
116
|
+
// timeline for this file (motionTimelineId is null) — just clear dirty.
|
|
117
|
+
if (motionTracks.length === 0) {
|
|
118
|
+
if (!motionTimelineId) {
|
|
119
|
+
setMotionTracksDirty(false);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (removeMotionTimelineMutation.isPending) return;
|
|
123
|
+
const revisionAtSchedule = motionAutosaveRevision;
|
|
124
|
+
const timelineIdAtSchedule = motionTimelineId;
|
|
125
|
+
removeMotionTimeline(
|
|
126
|
+
{ designId: id, timelineId: timelineIdAtSchedule },
|
|
127
|
+
{
|
|
128
|
+
onSuccess: () => {
|
|
129
|
+
if (motionAutosaveRevisionRef.current !== revisionAtSchedule) return;
|
|
130
|
+
setMotionTracksDirty(false);
|
|
131
|
+
setMotionTimelineId(null);
|
|
132
|
+
setMotionHydrationFingerprint(null);
|
|
133
|
+
void queryClient.invalidateQueries({
|
|
134
|
+
queryKey: ["action", "get-motion-timeline"],
|
|
135
|
+
});
|
|
136
|
+
},
|
|
137
|
+
onError: (error: unknown) => {
|
|
138
|
+
if (motionAutosaveRevisionRef.current === revisionAtSchedule) {
|
|
139
|
+
motionAutosaveFailedRevisionRef.current = revisionAtSchedule;
|
|
140
|
+
}
|
|
141
|
+
toast.error(
|
|
142
|
+
error instanceof Error
|
|
143
|
+
? error.message
|
|
144
|
+
: // i18n-ignore: fallback toast for motion timeline removal failure.
|
|
145
|
+
"Motion changes could not be saved.",
|
|
146
|
+
);
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const revisionAtSchedule = motionAutosaveRevision;
|
|
154
|
+
lastScheduledMotionAutosaveRevisionRef.current = revisionAtSchedule;
|
|
155
|
+
clearMotionAutosaveTimer();
|
|
156
|
+
const fileIdAtSchedule = activeFile.id;
|
|
157
|
+
const fileRevisionAtSchedule = activeFile.updatedAt;
|
|
158
|
+
const fire = () => {
|
|
159
|
+
if (motionAutosaveRevisionRef.current !== revisionAtSchedule) return;
|
|
160
|
+
// Drop empty tracks defensively (a 0-keyframe track fails the action
|
|
161
|
+
// schema and would brick every subsequent autosave) and canonicalise
|
|
162
|
+
// keyframe order so the persisted JSON is always time-sorted. Full
|
|
163
|
+
// emptiness (0 tracks) is handled above via remove-motion-timeline
|
|
164
|
+
// before this closure is ever scheduled; this guards the case where
|
|
165
|
+
// every remaining track individually has 0 keyframes (shouldn't
|
|
166
|
+
// normally happen — MotionDock removes a track once its keyframes hit
|
|
167
|
+
// 0 — but stays a defensive no-op rather than sending an invalid
|
|
168
|
+
// payload that would brick the next autosave).
|
|
169
|
+
const tracksForSave = motionTracks
|
|
170
|
+
.filter((track) => track.keyframes.length > 0)
|
|
171
|
+
.map(({ label: _label, ...track }) => ({
|
|
172
|
+
...track,
|
|
173
|
+
keyframes: sortMotionKeyframes(track.keyframes),
|
|
174
|
+
}));
|
|
175
|
+
if (tracksForSave.length === 0) return;
|
|
176
|
+
// When flushed after a file switch, the schedule-time file is no longer
|
|
177
|
+
// active; resolve its content from the per-screen cache instead of the
|
|
178
|
+
// active-file refs (which may already point at the NEW file).
|
|
179
|
+
const isActiveFileNow =
|
|
180
|
+
previousMotionFileIdRef.current === fileIdAtSchedule;
|
|
181
|
+
const currentContent = isActiveFileNow
|
|
182
|
+
? getFreshActiveFileContent({
|
|
183
|
+
activeContent,
|
|
184
|
+
latestContent: latestActiveContentRef.current,
|
|
185
|
+
lastLocalContent: lastLocalContentRef.current,
|
|
186
|
+
})
|
|
187
|
+
: getScreenContent(fileIdAtSchedule) || activeContent;
|
|
188
|
+
const localMotionCss = compileMotionTimeline({
|
|
189
|
+
id: motionTimelineId ?? "",
|
|
190
|
+
designId: id,
|
|
191
|
+
sourceRef: fileIdAtSchedule,
|
|
192
|
+
filePath: null,
|
|
193
|
+
tracks: tracksForSave,
|
|
194
|
+
durationMs: motionDurationMs,
|
|
195
|
+
defaultEase: motionDefaultEase,
|
|
196
|
+
compiledHash: null,
|
|
197
|
+
createdAt: "",
|
|
198
|
+
updatedAt: "",
|
|
199
|
+
}).css;
|
|
200
|
+
applyMotionEdit(
|
|
201
|
+
{
|
|
202
|
+
designId: id,
|
|
203
|
+
fileId: fileIdAtSchedule,
|
|
204
|
+
timelineId: motionTimelineId ?? undefined,
|
|
205
|
+
sourceRef: fileIdAtSchedule,
|
|
206
|
+
tracks: tracksForSave,
|
|
207
|
+
durationMs: motionDurationMs,
|
|
208
|
+
defaultEase: motionDefaultEase,
|
|
209
|
+
currentContent,
|
|
210
|
+
revision: fileRevisionAtSchedule,
|
|
211
|
+
includeContent: false,
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
onSuccess: (result) => {
|
|
215
|
+
const response = result as {
|
|
216
|
+
fileId?: unknown;
|
|
217
|
+
timelineId?: unknown;
|
|
218
|
+
updatedAt?: unknown;
|
|
219
|
+
compiledHash?: unknown;
|
|
220
|
+
contentPatched?: unknown;
|
|
221
|
+
};
|
|
222
|
+
const isStillActiveFile =
|
|
223
|
+
previousMotionFileIdRef.current === fileIdAtSchedule;
|
|
224
|
+
if (typeof response.timelineId === "string" && isStillActiveFile) {
|
|
225
|
+
setMotionTimelineId(response.timelineId);
|
|
226
|
+
}
|
|
227
|
+
if (
|
|
228
|
+
motionAutosaveRevisionRef.current === revisionAtSchedule &&
|
|
229
|
+
isStillActiveFile
|
|
230
|
+
) {
|
|
231
|
+
setMotionTracksDirty(false);
|
|
232
|
+
// Seed the hydration fingerprint with the EXPECTED post-save
|
|
233
|
+
// identity instead of null: a null fingerprint lets the
|
|
234
|
+
// still-stale get-motion-timeline cache re-hydrate the
|
|
235
|
+
// pre-save tracks (UI reverts, then jumps back on refetch,
|
|
236
|
+
// and edits made in that window lock in the reverted state).
|
|
237
|
+
const contentPatched = response.contentPatched !== false;
|
|
238
|
+
const expectedHash =
|
|
239
|
+
typeof response.compiledHash === "string"
|
|
240
|
+
? response.compiledHash
|
|
241
|
+
: null;
|
|
242
|
+
setMotionHydrationFingerprint(
|
|
243
|
+
motionTimelineFingerprint(fileIdAtSchedule, {
|
|
244
|
+
id:
|
|
245
|
+
typeof response.timelineId === "string"
|
|
246
|
+
? response.timelineId
|
|
247
|
+
: (motionTimelineId ?? null),
|
|
248
|
+
designId: id,
|
|
249
|
+
sourceRef: fileIdAtSchedule,
|
|
250
|
+
filePath: null,
|
|
251
|
+
tracks: tracksForSave,
|
|
252
|
+
durationMs: motionDurationMs,
|
|
253
|
+
defaultEase: motionDefaultEase,
|
|
254
|
+
compiledHash: expectedHash,
|
|
255
|
+
cssHash: contentPatched ? expectedHash : null,
|
|
256
|
+
source: "stored",
|
|
257
|
+
createdAt: null,
|
|
258
|
+
updatedAt:
|
|
259
|
+
typeof response.updatedAt === "string"
|
|
260
|
+
? response.updatedAt
|
|
261
|
+
: null,
|
|
262
|
+
}),
|
|
263
|
+
);
|
|
264
|
+
lastScheduledMotionAutosaveRevisionRef.current = 0;
|
|
265
|
+
}
|
|
266
|
+
if (
|
|
267
|
+
typeof response.fileId === "string" &&
|
|
268
|
+
response.fileId === fileIdAtSchedule &&
|
|
269
|
+
response.contentPatched !== false
|
|
270
|
+
) {
|
|
271
|
+
// Re-inject the managed CSS into the FRESHEST content at
|
|
272
|
+
// success time — replaying the pre-flight snapshot would
|
|
273
|
+
// clobber document edits made while the save was in flight.
|
|
274
|
+
const freshContent = isStillActiveFile
|
|
275
|
+
? getFreshActiveFileContent({
|
|
276
|
+
activeContent: currentContent,
|
|
277
|
+
latestContent: latestActiveContentRef.current,
|
|
278
|
+
lastLocalContent: lastLocalContentRef.current,
|
|
279
|
+
})
|
|
280
|
+
: currentContent;
|
|
281
|
+
const hasConcurrentEdits = freshContent !== currentContent;
|
|
282
|
+
const patchedContent = injectManagedMotionCss(
|
|
283
|
+
freshContent,
|
|
284
|
+
localMotionCss,
|
|
285
|
+
);
|
|
286
|
+
applyFileContentUpdate(response.fileId, patchedContent, {
|
|
287
|
+
refreshPreview: false,
|
|
288
|
+
forcePreviewFullDocument: true,
|
|
289
|
+
recordHistory: false,
|
|
290
|
+
// Without concurrent edits, adopt the server's write as-is
|
|
291
|
+
// (no re-save). With concurrent edits the merged content must
|
|
292
|
+
// flow through the normal save path and stay marked pending.
|
|
293
|
+
...(hasConcurrentEdits
|
|
294
|
+
? {}
|
|
295
|
+
: {
|
|
296
|
+
persist: false,
|
|
297
|
+
...(typeof response.updatedAt === "string"
|
|
298
|
+
? { updatedAt: response.updatedAt }
|
|
299
|
+
: {}),
|
|
300
|
+
}),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
void queryClient.invalidateQueries({
|
|
304
|
+
queryKey: ["action", "get-motion-timeline"],
|
|
305
|
+
});
|
|
306
|
+
},
|
|
307
|
+
onError: (error) => {
|
|
308
|
+
if (motionAutosaveRevisionRef.current === revisionAtSchedule) {
|
|
309
|
+
motionAutosaveFailedRevisionRef.current = revisionAtSchedule;
|
|
310
|
+
lastScheduledMotionAutosaveRevisionRef.current = 0;
|
|
311
|
+
}
|
|
312
|
+
toast.error(
|
|
313
|
+
error instanceof Error
|
|
314
|
+
? error.message
|
|
315
|
+
: // i18n-ignore: fallback toast for motion autosave failure.
|
|
316
|
+
"Motion changes could not be saved.",
|
|
317
|
+
);
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
);
|
|
321
|
+
};
|
|
322
|
+
motionAutosaveTimerRef.current = window.setTimeout(() => {
|
|
323
|
+
motionAutosaveTimerRef.current = null;
|
|
324
|
+
motionAutosaveFlushRef.current = null;
|
|
325
|
+
fire();
|
|
326
|
+
}, MOTION_AUTOSAVE_DELAY_MS);
|
|
327
|
+
motionAutosaveFlushRef.current = () => {
|
|
328
|
+
if (motionAutosaveTimerRef.current !== null) {
|
|
329
|
+
window.clearTimeout(motionAutosaveTimerRef.current);
|
|
330
|
+
motionAutosaveTimerRef.current = null;
|
|
331
|
+
}
|
|
332
|
+
motionAutosaveFlushRef.current = null;
|
|
333
|
+
fire();
|
|
334
|
+
};
|
|
335
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { Dispatch, RefObject, SetStateAction } from "react";
|
|
2
|
+
import * as Y from "yjs";
|
|
3
|
+
|
|
4
|
+
import type { ElementInfo } from "@/components/design/types";
|
|
5
|
+
import { refreshElementInfoFromContent } from "@/pages/design-editor/code-layer-state";
|
|
6
|
+
import { shouldApplyRemotePreviewContent } from "@/pages/design-editor/collab-sync";
|
|
7
|
+
import {
|
|
8
|
+
LOCAL_EDIT_ORIGIN,
|
|
9
|
+
TAB_ID,
|
|
10
|
+
shouldCheckpointAgentContent,
|
|
11
|
+
} from "@/pages/design-editor/editor-session";
|
|
12
|
+
import type { PreviewContentReplaceResult } from "@/pages/design-editor/editor-state";
|
|
13
|
+
import { previewContentReplaceNeedsRenderFallback } from "@/pages/design-editor/editor-state";
|
|
14
|
+
import type { ContentHistoryChange } from "@/pages/design-editor/history";
|
|
15
|
+
|
|
16
|
+
export interface ObserveCollabTextArgs {
|
|
17
|
+
activeFileId: string | null;
|
|
18
|
+
agentActive: boolean;
|
|
19
|
+
documentFileContentRef: RefObject<string | null>;
|
|
20
|
+
documentFileUpdatedAtRef: RefObject<string | null>;
|
|
21
|
+
isSynced: boolean;
|
|
22
|
+
lastAppliedFileUpdatedAtRef: RefObject<string | null>;
|
|
23
|
+
lastLocalContentRef: RefObject<string | null>;
|
|
24
|
+
latestActiveContentRef: RefObject<string | null>;
|
|
25
|
+
pendingLocalFileContentsRef: RefObject<
|
|
26
|
+
Map<
|
|
27
|
+
string,
|
|
28
|
+
{ content: string; startedAt: number; baseUpdatedAt?: string | null }
|
|
29
|
+
>
|
|
30
|
+
>;
|
|
31
|
+
recordExternalContentHistoryCheckpoint: (
|
|
32
|
+
change: ContentHistoryChange,
|
|
33
|
+
) => void;
|
|
34
|
+
replacePreviewContent: (
|
|
35
|
+
nextContent: string,
|
|
36
|
+
selector?: string | null,
|
|
37
|
+
options?: { forceFullDocument?: boolean },
|
|
38
|
+
) => PreviewContentReplaceResult;
|
|
39
|
+
setCollabContent: Dispatch<SetStateAction<string | null>>;
|
|
40
|
+
setCollabContentFileId: Dispatch<SetStateAction<string | null>>;
|
|
41
|
+
setContentRenderRevision: Dispatch<SetStateAction<number>>;
|
|
42
|
+
setHoveredElement: Dispatch<SetStateAction<ElementInfo | null>>;
|
|
43
|
+
setSelectedElement: Dispatch<SetStateAction<ElementInfo | null>>;
|
|
44
|
+
undoManagerRef: RefObject<Y.UndoManager | null>;
|
|
45
|
+
ydoc: Y.Doc | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function runObserveCollabText({
|
|
49
|
+
activeFileId,
|
|
50
|
+
agentActive,
|
|
51
|
+
documentFileContentRef,
|
|
52
|
+
documentFileUpdatedAtRef,
|
|
53
|
+
isSynced,
|
|
54
|
+
lastAppliedFileUpdatedAtRef,
|
|
55
|
+
lastLocalContentRef,
|
|
56
|
+
latestActiveContentRef,
|
|
57
|
+
pendingLocalFileContentsRef,
|
|
58
|
+
recordExternalContentHistoryCheckpoint,
|
|
59
|
+
replacePreviewContent,
|
|
60
|
+
setCollabContent,
|
|
61
|
+
setCollabContentFileId,
|
|
62
|
+
setContentRenderRevision,
|
|
63
|
+
setHoveredElement,
|
|
64
|
+
setSelectedElement,
|
|
65
|
+
undoManagerRef,
|
|
66
|
+
ydoc,
|
|
67
|
+
}: ObserveCollabTextArgs) {
|
|
68
|
+
if (!ydoc || !isSynced || !activeFileId) return;
|
|
69
|
+
const fileId = activeFileId;
|
|
70
|
+
const ytext = ydoc.getText("content");
|
|
71
|
+
const handler = (_event: unknown, transaction?: { origin?: unknown }) => {
|
|
72
|
+
const next = ytext.toString();
|
|
73
|
+
// Item 5 (edit-flash): capture what the preview already reflects BEFORE
|
|
74
|
+
// this observe fires, so a remote-origin transaction that merely ECHOES
|
|
75
|
+
// content we already rendered (e.g. update-file's own applyText/
|
|
76
|
+
// seedFromText round-tripping our own just-saved commit back through
|
|
77
|
+
// the collab sync channel) can be recognized as a no-op instead of
|
|
78
|
+
// unconditionally forcing a full srcdoc rebuild below. Every commit
|
|
79
|
+
// path already sets latestActiveContentRef.current synchronously
|
|
80
|
+
// before the network round trip lands, so this ref reliably holds the
|
|
81
|
+
// pre-update value at the moment a same-content echo arrives.
|
|
82
|
+
const previousActiveContent = latestActiveContentRef.current;
|
|
83
|
+
// UndoManager fires with itself as the origin; treat those as local too
|
|
84
|
+
// so the reconcile watermark and stale-selection fix are consistent.
|
|
85
|
+
const isLocalEdit =
|
|
86
|
+
transaction?.origin === TAB_ID ||
|
|
87
|
+
transaction?.origin === LOCAL_EDIT_ORIGIN ||
|
|
88
|
+
transaction?.origin === undoManagerRef.current;
|
|
89
|
+
if (
|
|
90
|
+
shouldCheckpointAgentContent({
|
|
91
|
+
agentActive,
|
|
92
|
+
isLocalEdit,
|
|
93
|
+
previousContent: previousActiveContent,
|
|
94
|
+
nextContent: next,
|
|
95
|
+
})
|
|
96
|
+
) {
|
|
97
|
+
// An agent/chat edit is remote at the CRDT layer but local in the UX:
|
|
98
|
+
// Cmd+Z should restore the attachment/design state from before the run.
|
|
99
|
+
// Record the replacement in the history owned by the current view
|
|
100
|
+
// mode so it remains undoable from either canvas.
|
|
101
|
+
recordExternalContentHistoryCheckpoint({
|
|
102
|
+
fileId,
|
|
103
|
+
before: previousActiveContent!,
|
|
104
|
+
after: next,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
const pendingLocalContent =
|
|
108
|
+
pendingLocalFileContentsRef.current.get(fileId)?.content;
|
|
109
|
+
if (pendingLocalContent && next !== pendingLocalContent && !isLocalEdit) {
|
|
110
|
+
setCollabContent(pendingLocalContent);
|
|
111
|
+
setCollabContentFileId(fileId);
|
|
112
|
+
lastLocalContentRef.current = pendingLocalContent;
|
|
113
|
+
latestActiveContentRef.current = pendingLocalContent;
|
|
114
|
+
if (
|
|
115
|
+
previewContentReplaceNeedsRenderFallback(
|
|
116
|
+
replacePreviewContent(pendingLocalContent, null, {
|
|
117
|
+
forceFullDocument: true,
|
|
118
|
+
}),
|
|
119
|
+
)
|
|
120
|
+
) {
|
|
121
|
+
setContentRenderRevision((revision) => revision + 1);
|
|
122
|
+
}
|
|
123
|
+
// Untracked full rewrite — see clear() note in the seed effect above.
|
|
124
|
+
undoManagerRef.current?.clear(true, false);
|
|
125
|
+
ydoc.transact(() => {
|
|
126
|
+
ytext.delete(0, ytext.length);
|
|
127
|
+
ytext.insert(0, pendingLocalContent);
|
|
128
|
+
}, TAB_ID);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
setCollabContent(next);
|
|
132
|
+
setCollabContentFileId(fileId);
|
|
133
|
+
latestActiveContentRef.current = next;
|
|
134
|
+
if (isLocalEdit) {
|
|
135
|
+
lastLocalContentRef.current = next;
|
|
136
|
+
} else if (
|
|
137
|
+
shouldApplyRemotePreviewContent({
|
|
138
|
+
isLocalEdit,
|
|
139
|
+
previousContent: previousActiveContent,
|
|
140
|
+
nextContent: next,
|
|
141
|
+
})
|
|
142
|
+
) {
|
|
143
|
+
// Holistic flash pipeline: a remote (peer/agent) edit arriving mid-
|
|
144
|
+
// session is exactly the "remote adoption" case that should apply
|
|
145
|
+
// in-place — this is not a file switch or initial mount. Try the
|
|
146
|
+
// bridge's live in-place full-document replace (same live iframe, no
|
|
147
|
+
// navigation) first; only fall back to an actual srcdoc rebuild when
|
|
148
|
+
// the bridge can't apply it (e.g. this screen's iframe isn't mounted/
|
|
149
|
+
// registered right now).
|
|
150
|
+
if (
|
|
151
|
+
previewContentReplaceNeedsRenderFallback(
|
|
152
|
+
replacePreviewContent(next, null, { forceFullDocument: true }),
|
|
153
|
+
)
|
|
154
|
+
) {
|
|
155
|
+
setContentRenderRevision((revision) => revision + 1);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Only advance the DB reconcile watermark when the live CRDT text
|
|
159
|
+
// actually matches the current SQL snapshot. Otherwise an intermediate
|
|
160
|
+
// or malformed Yjs update can shadow valid saved HTML until reload.
|
|
161
|
+
if (next === documentFileContentRef.current) {
|
|
162
|
+
lastAppliedFileUpdatedAtRef.current =
|
|
163
|
+
documentFileUpdatedAtRef.current ?? lastAppliedFileUpdatedAtRef.current;
|
|
164
|
+
}
|
|
165
|
+
// Stale-selection fix: when a remote/agent edit changes the document,
|
|
166
|
+
// verify the selected element still exists in the new DOM. If not, clear
|
|
167
|
+
// selection and hover so the Edit panel doesn't operate on a ghost element.
|
|
168
|
+
if (!isLocalEdit) {
|
|
169
|
+
setSelectedElement((prev) => {
|
|
170
|
+
if (!prev) return prev;
|
|
171
|
+
return refreshElementInfoFromContent(next, prev);
|
|
172
|
+
});
|
|
173
|
+
setHoveredElement((prev) => {
|
|
174
|
+
if (!prev) return prev;
|
|
175
|
+
return refreshElementInfoFromContent(next, prev);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
ytext.observe(handler);
|
|
180
|
+
return () => {
|
|
181
|
+
ytext.unobserve(handler);
|
|
182
|
+
};
|
|
183
|
+
}
|