diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 57ef673c7..682b217bc 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -33,10 +33,8 @@ export type { PersistQueueModule, PersistQueueOptions } from "./persist-queue.js export type { PersistAdapter, PreviewAdapter, PersistVersionEntry } from "./adapters/types.js"; -// Concrete adapter factories. +// Concrete adapter factories (browser-safe — Node-only fs adapter: @hyperframes/sdk/adapters/fs). export { createMemoryAdapter } from "./adapters/memory.js"; export { createHeadlessAdapter } from "./adapters/headless.js"; -export { createFsAdapter } from "./adapters/fs.js"; -export type { FsAdapterOptions } from "./adapters/fs.js"; export { createHttpAdapter } from "./adapters/http.js"; export type { HttpAdapterOptions } from "./adapters/http.js"; diff --git a/packages/studio/package.json b/packages/studio/package.json index 7173b9a7f..125cb31ca 100644 --- a/packages/studio/package.json +++ b/packages/studio/package.json @@ -38,6 +38,7 @@ "@codemirror/view": "6.40.0", "@hyperframes/core": "workspace:*", "@hyperframes/player": "workspace:*", + "@hyperframes/sdk": "workspace:*", "@phosphor-icons/react": "^2.1.10", "mediabunny": "^1.45.3" }, diff --git a/packages/studio/src/App.tsx b/packages/studio/src/App.tsx index 2f944b228..b884e726b 100644 --- a/packages/studio/src/App.tsx +++ b/packages/studio/src/App.tsx @@ -12,6 +12,8 @@ import { usePreviewPersistence } from "./hooks/usePreviewPersistence"; import { useTimelineEditing } from "./hooks/useTimelineEditing"; import type { BlockPreviewInfo } from "./components/sidebar/BlocksTab"; import { useDomEditSession } from "./hooks/useDomEditSession"; +import { useSdkSession } from "./hooks/useSdkSession"; +import { useSdkSelectionSync } from "./hooks/useSdkSelectionSync"; import { useBlockHandlers } from "./hooks/useBlockHandlers"; import { useAppHotkeys } from "./hooks/useAppHotkeys"; import { useClipboard } from "./hooks/useClipboard"; @@ -145,6 +147,8 @@ export function StudioApp() { setRefreshKey, }); + const sdkSession = useSdkSession(projectId, activeCompPath); + useEffect(() => { if (activeCompPathHydrated) return; if (!fileManager.fileTreeLoaded) return; @@ -314,6 +318,12 @@ export function StudioApp() { if (Number.isFinite(p)) domEditSession.handleGsapRemoveKeyframe(a.id, p); }); }; + useSdkSelectionSync( + sdkSession, + domEditSession.domEditSelection, + domEditSession.domEditGroupSelections, + ); + useCaptionDetection({ projectId, activeCompPath, diff --git a/packages/studio/src/hooks/useSdkSelectionSync.ts b/packages/studio/src/hooks/useSdkSelectionSync.ts new file mode 100644 index 000000000..d36cfdabe --- /dev/null +++ b/packages/studio/src/hooks/useSdkSelectionSync.ts @@ -0,0 +1,25 @@ +import { useEffect } from "react"; +import type { Composition } from "@hyperframes/sdk"; +import type { DomEditSelection } from "../components/editor/domEditing"; + +function toHfIds(group: DomEditSelection[], primary: DomEditSelection | null): string[] { + const source = group.length > 0 ? group : primary ? [primary] : []; + return source.flatMap((s) => (s.hfId ? [s.hfId] : [])); +} + +/** + * Stage 7 Step 2 — mirrors Studio canvas selection into the SDK session. + * + * Calls session.setSelection(hfIds) whenever domEditSelection or + * domEditGroupSelections changes. Pure effect; no existing hook modified. + */ +export function useSdkSelectionSync( + session: Composition | null, + domEditSelection: DomEditSelection | null, + domEditGroupSelections: DomEditSelection[], +): void { + useEffect(() => { + if (!session) return; + session.setSelection(toHfIds(domEditGroupSelections, domEditSelection)); + }, [session, domEditSelection, domEditGroupSelections]); +} diff --git a/packages/studio/src/hooks/useSdkSession.ts b/packages/studio/src/hooks/useSdkSession.ts new file mode 100644 index 000000000..e9cf7256d --- /dev/null +++ b/packages/studio/src/hooks/useSdkSession.ts @@ -0,0 +1,53 @@ +import { useState, useEffect } from "react"; +import { openComposition } from "@hyperframes/sdk"; +import { createHttpAdapter } from "@hyperframes/sdk/adapters/http"; +import type { Composition } from "@hyperframes/sdk"; + +/** + * Stage 7 Step 1 — SDK session wired to the active composition. + * + * Creates an SDK Composition backed by createHttpAdapter on every + * (projectId, activeCompPath) change, disposes the old one on cleanup. + * The session is idle until Step 3 routes dispatch ops through it. + */ +export function useSdkSession( + projectId: string | null, + activeCompPath: string | null, +): Composition | null { + const [session, setSession] = useState(null); + + useEffect(() => { + if (!projectId || !activeCompPath) { + setSession(null); + return; + } + + let cancelled = false; + let comp: Composition | null = null; + + const url = `/api/projects/${projectId}/files/${encodeURIComponent(activeCompPath)}?optional=1`; + fetch(url) + .then((r) => r.json()) + .then(async (data: { content?: string }) => { + if (cancelled || typeof data.content !== "string") return; + const adapter = createHttpAdapter({ + projectFilesUrl: `/api/projects/${projectId}`, + }); + comp = await openComposition(data.content, { persist: adapter }); + comp.on("persist:error", (e) => { + console.warn("[sdk] persist:error", e.error); + }); + if (!cancelled) setSession(comp); + }) + .catch(() => { + if (!cancelled) setSession(null); + }); + + return () => { + cancelled = true; + comp?.dispose(); + }; + }, [projectId, activeCompPath]); + + return session; +}