docs(readme): swap hero media to hyperframes-logo-motion (#1315)

* 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 <img> 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Miguel Sierra <miguel.sierra@heygen.com>
This commit is contained in:
James Russo
2026-06-09 23:39:22 -07:00
committed by GitHub
co-authored by Claude Opus 4.7 Miguel Sierra
parent 81416ab3c9
commit 8fcbb63a37
11 changed files with 61 additions and 25 deletions
+1 -1
View File
@@ -26,7 +26,7 @@
</p>
<p align="center">
<img src="https://static.heygen.ai/hyperframes-oss/docs/images/hfgif-1280.webp" alt="HyperFrames demo: HTML code on the left transforms into a rendered video on the right" width="800">
<img src="https://static.heygen.ai/hyperframes-oss/docs/images/hyperframes-logo-motion-1280.webp" alt="HyperFrames demo: HTML code on the left transforms into a rendered video on the right" width="800">
</p>
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.
@@ -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<void>((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();
});
});
+1 -2
View File
@@ -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(
);
}
}
@@ -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,
+4 -11
View File
@@ -45,14 +45,8 @@ export interface UseBlockHandlersResult {
React.SetStateAction<UseBlockHandlersResult["activeBlockParams"]>
>;
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<UseBlockHandlersResult["activeBlockParams"]>(null);
const blockCtx = useMemo(
() => ({
@@ -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";
@@ -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";
@@ -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 };
}
@@ -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,
@@ -26,7 +26,7 @@ import {
applyPatchByTarget,
formatTimelineAttributeNumber,
} from "./timelineEditingHelpers";
import type { PatchTarget, PersistTimelineEditInput } from "./timelineEditingHelpers";
import type { PersistTimelineEditInput } from "./timelineEditingHelpers";
// ── Types ──
@@ -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);