Files
hyperframes/packages/studio/src/hooks/useConsoleErrorCapture.ts
T
Miguel ÁngelandClaude Opus 4.6 6fe970651d feat(studio): add Layers panel as new inspector tab (#784)
* 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>
2026-05-13 08:22:26 +02:00

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 };
}