mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
* feat(studio): add Layers panel as new inspector tab Adds a dedicated Layers tab alongside Design and Renders in the right panel inspector. The panel shows the full composition element tree with collapsible hierarchy — clicking a layer selects it without navigating away from the tree view. Closes #783 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(studio): add visual element previews to Layers panel Each layer row now shows a small color/content preview thumbnail: - Text elements show a snippet of their content in the actual font color - Container elements show their background color as a colored swatch - Image elements show a tiny thumbnail of the image - Media elements show an icon indicator Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(studio): add hover-to-highlight and auto-seek to Layers panel Replace tiny preview thumbnails with hover highlighting — hovering a layer row highlights the element in the preview canvas. Clicking a layer auto-seeks the playhead to that element's start time in the timeline. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(studio): layers panel tab order, autoseek, and renders toolbar overflow - Reorder inspector tabs to Design → Layers → Renders - Fix autoseek: walk DOM ancestors when selected element has no direct timeline match, so clicking a child like S2 Heading correctly seeks to the start of its parent scene - Fix Renders toolbar overflow: add flex-wrap to the export controls header so selects and the Export button wrap instead of clipping * fix(studio): seek to midpoint of element duration in layers panel autoseek * fix(studio): layers panel seek now drives adapter.seek via requestSeek signal setCurrentTime() only updated the store — adapter.seek() and liveTime.notify() were never called so the iframe never moved. Add requestedSeekTime to the player store; useTimelinePlayer subscribes and calls the real seek() path when it fires. * feat(studio): hover over a layer auto-seeks to element midpoint (300ms debounce) * feat(studio): add collapsible sections to Design panel Section component now supports collapse/expand with a chevron toggle. Text, Layout, and Fill sections stay expanded by default. Less-used sections (Flex, Radius, Stroke, Effects, Clip, Transparency) start collapsed to reduce scrolling. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(studio): remove stale selectedTimelineElement usages dropped in rebase * fix(studio): stabilize resetErrors in useConsoleErrorCapture to break render loop resetErrors was a new function object on every render. handlePreviewIframeRef had it as a dep, so it also changed every render. NLELayout's useEffect watching onIframeRef would re-fire, calling setPreviewIframe again, which re-ran useConsoleErrorCapture with the new iframe — infinite loop. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
61 lines
2.3 KiB
TypeScript
61 lines
2.3 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from "react";
|
|
import type { LintFinding } from "../components/LintModal";
|
|
|
|
/**
|
|
* Captures `console.error` and `window.onerror` events from a preview iframe
|
|
* and exposes them as LintFinding[] for the console errors modal.
|
|
*/
|
|
export function useConsoleErrorCapture(previewIframe: HTMLIFrameElement | null) {
|
|
const [consoleErrors, setConsoleErrors] = useState<LintFinding[] | null>(null);
|
|
const consoleErrorsRef = useRef<LintFinding[]>([]);
|
|
|
|
const resetErrors = useCallback(() => {
|
|
consoleErrorsRef.current = [];
|
|
setConsoleErrors(null);
|
|
}, []);
|
|
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
useEffect(() => {
|
|
if (!previewIframe) return;
|
|
const attachErrorCapture = () => {
|
|
try {
|
|
const win = previewIframe.contentWindow as (Window & typeof globalThis) | null;
|
|
if (!win) return;
|
|
if ((win as unknown as Record<string, unknown>).__hfErrorCapture) return;
|
|
(win as unknown as Record<string, unknown>).__hfErrorCapture = true;
|
|
const origError = win.console.error.bind(win.console);
|
|
win.console.error = function (...args: unknown[]) {
|
|
origError(...args);
|
|
const text = args.map((a) => (a instanceof Error ? a.message : String(a))).join(" ");
|
|
if (text.includes("favicon")) return;
|
|
consoleErrorsRef.current = [
|
|
...consoleErrorsRef.current,
|
|
{ severity: "error", message: text },
|
|
];
|
|
setConsoleErrors([...consoleErrorsRef.current]);
|
|
};
|
|
win.addEventListener("error", (e: ErrorEvent) => {
|
|
const text = e.message || String(e);
|
|
consoleErrorsRef.current = [
|
|
...consoleErrorsRef.current,
|
|
{ severity: "error", message: text },
|
|
];
|
|
setConsoleErrors([...consoleErrorsRef.current]);
|
|
});
|
|
} catch {
|
|
/* same-origin only */
|
|
}
|
|
};
|
|
attachErrorCapture();
|
|
const handleLoad = () => {
|
|
consoleErrorsRef.current = [];
|
|
setConsoleErrors(null);
|
|
attachErrorCapture();
|
|
};
|
|
previewIframe.addEventListener("load", handleLoad);
|
|
return () => previewIframe.removeEventListener("load", handleLoad);
|
|
}, [previewIframe]);
|
|
|
|
return { consoleErrors, setConsoleErrors, resetErrors };
|
|
}
|