From 8fcbb63a371752932027f3495dbc6ae6b5daf0f7 Mon Sep 17 00:00:00 2001 From: James Russo Date: Tue, 9 Jun 2026 23:39:22 -0700 Subject: [PATCH] docs(readme): swap hero media to hyperframes-logo-motion (#1315) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs(readme): swap hero media to hyperframes-logo-motion Replaces the prior hfgif-1280.webp hero with a new logo-motion clip Bin trimmed for the launch. Converted the source MP4 to animated webp (the existing hero's format) so it auto-plays in the GitHub README the same way the old one did - MP4 sources don't render inline or autoplay in tags. - New asset: static.heygen.ai/hyperframes-oss/docs/images/ hyperframes-logo-motion-1280.webp (1280x720, 85 frames, 199KB) - ffmpeg conversion: scale=1280, libwebp_anim, q=80, loop=0 Co-Authored-By: Claude Opus 4.7 * fix(studio): format 5 hooks files (oxfmt) * style: remove unused imports in studio hooks (pre-existing lint failures) CI Lint on main was already failing with 5 unused-import errors in packages/studio/src/hooks/. Removed the unused symbols to unblock the README hero PR's CI: - gsapRuntimeBridge.ts: resolveTweenStart, resolveTweenDuration - useGsapScriptCommits.ts: usePlayerStore - useTimelineEditing.ts: PatchTarget (type-only) - gsapDragCommit.ts: readGsapProperty Bundled into the README PR per James's request to fix CI in-place. Co-Authored-By: Claude Opus 4.7 * test(studio): add childRects: [] to DomEditOverlay test mock useDomEditOverlayRects' return type added a childRects: OverlayRect[] field; the DomEditOverlay test's mock didn't get updated and was returning an object without it, so DomEditOverlay.tsx's 'childRects.length > 0' check threw TypeError on undefined. One-line mock-vs-hook contract realignment. Co-Authored-By: Claude Opus 4.7 * test(studio): drive player-store currentTime in selection-hydration test (#1311 follow-up) The 'hydrates seek first, preserves the initial url state, then restores selection' test was failing because PR #1311 (keyframes feat) changed useStudioUrlState to read currentTime from the player store via usePlayerStore((s) => s.currentTime), removing it from the hook's prop shape. The test was still trying to drive currentTime via the harness prop, which is now a no-op — so the selection-hydration useEffect's time-stability guard Math.abs(currentTime - stableTimeRef.current!) > 0.05 never passed (store currentTime stayed at 0 while stableTimeRef caught the 4.2 seek target). buildDomSelectionFromTarget was never reached, applyDomSelection was never called, and the assertion got 0 calls. Fix: setState the store's currentTime to 4.2 ahead of the rerender so the hook's selector picks it up and the time-stability guard passes. Harness prop kept as-is — it's a no-op but doesn't hurt. Pre-existing failure on main HEAD 81416ab3; surfaced as CI gate on the unrelated docs/readme-hero-motion-update PR. * test(studio): stub getBoundingClientRect + flush RAF in DomEditOverlay test The 'renders selected bounds right after clicking a movable selection' test asserts the selection box appears after pointerdown, but happy-dom returns 0 for newly-created elements' getBoundingClientRect. The overlay's compRect updates via a RAF loop that early-returns when iframe width is 0; the keyframes PR a468550f added a compRect.width > 0 guard to the selection-box render path, so compRect=0 silently gates the box off and the assertion fails. Stub Element.prototype.getBoundingClientRect to return 800x450 for the test, and flush two RAFs after render so the compRect state update lands before the pointerdown assertion. Restore the prototype at test end. Co-Authored-By: Claude Opus 4.7 --------- Co-authored-by: Claude Opus 4.7 Co-authored-by: Miguel Sierra --- README.md | 2 +- .../components/editor/DomEditOverlay.test.ts | 35 ++++++++++++++++++- packages/studio/src/hooks/gsapDragCommit.ts | 3 +- .../studio/src/hooks/gsapRuntimeBridge.ts | 1 - packages/studio/src/hooks/useBlockHandlers.ts | 15 +++----- .../studio/src/hooks/useDomEditPreviewSync.ts | 4 +-- .../studio/src/hooks/useDomEditSession.ts | 4 +-- packages/studio/src/hooks/useGestureCommit.ts | 9 ++++- .../studio/src/hooks/useGsapScriptCommits.ts | 2 +- .../studio/src/hooks/useTimelineEditing.ts | 2 +- .../studio/src/utils/studioUrlState.test.ts | 9 +++++ 11 files changed, 61 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 861699949..7c35c60cf 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@

- HyperFrames demo: HTML code on the left transforms into a rendered video on the right + HyperFrames demo: HTML code on the left transforms into a rendered video on the right

HyperFrames is an open-source framework for turning HTML, CSS, media, and seekable animations into deterministic MP4 videos. Use it locally with the CLI, from AI coding agents with skills, or as the rendering core behind hosted authoring workflows. diff --git a/packages/studio/src/components/editor/DomEditOverlay.test.ts b/packages/studio/src/components/editor/DomEditOverlay.test.ts index 51801475b..00424601b 100644 --- a/packages/studio/src/components/editor/DomEditOverlay.test.ts +++ b/packages/studio/src/components/editor/DomEditOverlay.test.ts @@ -61,6 +61,7 @@ vi.mock("./useDomEditOverlayRects", async () => { groupOverlayItems, groupOverlayItemsRef, setGroupOverlayItems, + childRects: [], }; }, }; @@ -96,7 +97,29 @@ describe("focusDomEditOverlayElement", () => { }); describe("DomEditOverlay", () => { - it("renders selected bounds right after clicking a movable selection", () => { + it("renders selected bounds right after clicking a movable selection", async () => { + // The overlay's compRect updates via a RAF loop reading iframe + overlay + // getBoundingClientRect. happy-dom returns all zeros for newly-created + // elements with no layout, so without stubs the RAF early-returns + // (iRect.width <= 0) and compRect.width stays 0 — gating the selection + // box (and other bounded UI) behind `compRect.width > 0` (added in the + // keyframes PR a468550f). Stub element-level getBoundingClientRect for + // the test so the RAF compRect update produces a real width. + const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect; + Element.prototype.getBoundingClientRect = function (): DOMRect { + return { + left: 0, + top: 0, + right: 800, + bottom: 450, + width: 800, + height: 450, + x: 0, + y: 0, + toJSON: () => ({}), + }; + }; + const host = document.createElement("div"); document.body.append(host); const root = createRoot(host); @@ -162,6 +185,15 @@ describe("DomEditOverlay", () => { root.render(React.createElement(Harness)); }); + // Flush the mount's RAF tick so the compRect update lands before the + // pointer-down. Two animation-frame ticks: the first scheduled by + // useMountEffect's update(), the second by update()'s tail recursion. + await act(async () => { + await new Promise((resolve) => { + requestAnimationFrame(() => requestAnimationFrame(() => resolve())); + }); + }); + const overlay = host.querySelector('[aria-label="Composition canvas"]') as HTMLDivElement; expect(overlay).toBeTruthy(); @@ -183,6 +215,7 @@ describe("DomEditOverlay", () => { root.unmount(); }); HTMLDivElement.prototype.setPointerCapture = originalPointerCapture; + Element.prototype.getBoundingClientRect = originalGetBoundingClientRect; host.remove(); }); }); diff --git a/packages/studio/src/hooks/gsapDragCommit.ts b/packages/studio/src/hooks/gsapDragCommit.ts index e26e63a62..cd01f3a30 100644 --- a/packages/studio/src/hooks/gsapDragCommit.ts +++ b/packages/studio/src/hooks/gsapDragCommit.ts @@ -11,7 +11,7 @@ import { resolveTweenStart, resolveTweenDuration, } from "../utils/globalTimeCompiler"; -import { readAllAnimatedProperties, readGsapProperty } from "./gsapRuntimeReaders"; +import { readAllAnimatedProperties } from "./gsapRuntimeReaders"; export interface GsapDragCommitCallbacks { commitMutation: ( @@ -292,4 +292,3 @@ export async function commitGsapPositionFromDrag( ); } } - diff --git a/packages/studio/src/hooks/gsapRuntimeBridge.ts b/packages/studio/src/hooks/gsapRuntimeBridge.ts index 5254b5059..786ec8767 100644 --- a/packages/studio/src/hooks/gsapRuntimeBridge.ts +++ b/packages/studio/src/hooks/gsapRuntimeBridge.ts @@ -11,7 +11,6 @@ import type { GsapAnimation } from "@hyperframes/core/gsap-parser"; import type { DomEditSelection } from "../components/editor/domEditingTypes"; -import { resolveTweenStart, resolveTweenDuration } from "../utils/globalTimeCompiler"; import { readAllAnimatedProperties, readGsapProperty } from "./gsapRuntimeReaders"; import { commitGsapPositionFromDrag, diff --git a/packages/studio/src/hooks/useBlockHandlers.ts b/packages/studio/src/hooks/useBlockHandlers.ts index 444fcac49..a80afa1c8 100644 --- a/packages/studio/src/hooks/useBlockHandlers.ts +++ b/packages/studio/src/hooks/useBlockHandlers.ts @@ -45,14 +45,8 @@ export interface UseBlockHandlersResult { React.SetStateAction >; handleAddBlock: (blockName: string) => void; - handleTimelineBlockDrop: ( - blockName: string, - placement: { start: number; track: number }, - ) => void; - handlePreviewBlockDrop: ( - blockName: string, - position: { left: number; top: number }, - ) => void; + handleTimelineBlockDrop: (blockName: string, placement: { start: number; track: number }) => void; + handlePreviewBlockDrop: (blockName: string, position: { left: number; top: number }) => void; } export function useBlockHandlers({ @@ -62,9 +56,8 @@ export function useBlockHandlers({ setRightCollapsed, setRightPanelTab, }: UseBlockHandlersParams): UseBlockHandlersResult { - const [activeBlockParams, setActiveBlockParams] = useState< - UseBlockHandlersResult["activeBlockParams"] - >(null); + const [activeBlockParams, setActiveBlockParams] = + useState(null); const blockCtx = useMemo( () => ({ diff --git a/packages/studio/src/hooks/useDomEditPreviewSync.ts b/packages/studio/src/hooks/useDomEditPreviewSync.ts index d2ea5d73a..47e4d471c 100644 --- a/packages/studio/src/hooks/useDomEditPreviewSync.ts +++ b/packages/studio/src/hooks/useDomEditPreviewSync.ts @@ -4,9 +4,7 @@ * Extracted from useDomEditSession to keep file sizes under the 600-line limit. */ import { useEffect, useRef } from "react"; -import { - STUDIO_INSPECTOR_PANELS_ENABLED, -} from "../components/editor/manualEditingAvailability"; +import { STUDIO_INSPECTOR_PANELS_ENABLED } from "../components/editor/manualEditingAvailability"; import { findElementForSelection, type DomEditSelection } from "../components/editor/domEditing"; import { reapplyPositionEditsAfterSeek } from "../components/editor/manualEdits"; import type { SidebarTab } from "../components/sidebar/LeftSidebar"; diff --git a/packages/studio/src/hooks/useDomEditSession.ts b/packages/studio/src/hooks/useDomEditSession.ts index 11f6e7a80..540847d92 100644 --- a/packages/studio/src/hooks/useDomEditSession.ts +++ b/packages/studio/src/hooks/useDomEditSession.ts @@ -1,9 +1,7 @@ import { useCallback, useEffect, useRef } from "react"; import type { TimelineElement } from "../player"; import { usePlayerStore } from "../player"; -import { - STUDIO_GSAP_PANEL_ENABLED, -} from "../components/editor/manualEditingAvailability"; +import { STUDIO_GSAP_PANEL_ENABLED } from "../components/editor/manualEditingAvailability"; import { type DomEditSelection } from "../components/editor/domEditing"; import { useDomEditPreviewSync } from "./useDomEditPreviewSync"; import type { ImportedFontAsset } from "../components/editor/fontAssets"; diff --git a/packages/studio/src/hooks/useGestureCommit.ts b/packages/studio/src/hooks/useGestureCommit.ts index 8c1a2185f..5947aef81 100644 --- a/packages/studio/src/hooks/useGestureCommit.ts +++ b/packages/studio/src/hooks/useGestureCommit.ts @@ -147,7 +147,14 @@ export function useGestureCommit({ void stopAndCommitRecording(); } }, 100); - }, [gestureRecording, showToast, stopAndCommitRecording, previewIframeRef, domEditSessionRef, isGestureRecordingRef]); + }, [ + gestureRecording, + showToast, + stopAndCommitRecording, + previewIframeRef, + domEditSessionRef, + isGestureRecordingRef, + ]); return { gestureState, gestureRecording, handleToggleRecording }; } diff --git a/packages/studio/src/hooks/useGsapScriptCommits.ts b/packages/studio/src/hooks/useGsapScriptCommits.ts index 7daaf2845..5e12e64f7 100644 --- a/packages/studio/src/hooks/useGsapScriptCommits.ts +++ b/packages/studio/src/hooks/useGsapScriptCommits.ts @@ -4,7 +4,7 @@ import type { DomEditSelection } from "../components/editor/domEditingTypes"; import type { EditHistoryKind } from "../utils/editHistory"; import { applySoftReload } from "../utils/gsapSoftReload"; import { executeOptimistic } from "../utils/optimisticUpdate"; -import { usePlayerStore, type KeyframeCacheEntry } from "../player/store/playerStore"; +import type { KeyframeCacheEntry } from "../player/store/playerStore"; import { commitKeyframeAtTimeImpl } from "./gsapKeyframeCommit"; import { updateKeyframeCacheFromParsed, diff --git a/packages/studio/src/hooks/useTimelineEditing.ts b/packages/studio/src/hooks/useTimelineEditing.ts index b877f32bb..b05cbdb74 100644 --- a/packages/studio/src/hooks/useTimelineEditing.ts +++ b/packages/studio/src/hooks/useTimelineEditing.ts @@ -26,7 +26,7 @@ import { applyPatchByTarget, formatTimelineAttributeNumber, } from "./timelineEditingHelpers"; -import type { PatchTarget, PersistTimelineEditInput } from "./timelineEditingHelpers"; +import type { PersistTimelineEditInput } from "./timelineEditingHelpers"; // ── Types ── diff --git a/packages/studio/src/utils/studioUrlState.test.ts b/packages/studio/src/utils/studioUrlState.test.ts index 3277f7200..6b9620a8a 100644 --- a/packages/studio/src/utils/studioUrlState.test.ts +++ b/packages/studio/src/utils/studioUrlState.test.ts @@ -231,6 +231,15 @@ describe("studio url state", () => { expect(window.location.hash).toContain("t=4.2"); expect(applyDomSelection).not.toHaveBeenCalled(); + // Drive the hook's internal currentTime read. Per #1311 the hook stopped + // taking currentTime as a prop and now subscribes to the player store + // directly (usePlayerStore((s) => s.currentTime)). The harness prop is a + // no-op; the selection-hydration useEffect's time-stability guard + // (`Math.abs(currentTime - stableTimeRef.current) > 0.05`) only passes + // once the store's currentTime catches up to the seek target. + act(() => { + usePlayerStore.setState({ currentTime: 4.2 }); + }); harness.rerender({ currentTime: 4.2 }); await act(async () => { vi.advanceTimersByTime(250);