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>
This commit is contained in:
Miguel Ángel
2026-05-13 08:22:26 +02:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 51f968ec87
commit 6fe970651d
14 changed files with 411 additions and 26 deletions
+27 -6
View File
@@ -50,7 +50,11 @@ export interface UseDomSelectionReturn {
// Callbacks
applyDomSelection: (
selection: DomEditSelection | null,
options?: { revealPanel?: boolean; additive?: boolean; preserveGroup?: boolean },
options?: {
revealPanel?: boolean;
additive?: boolean;
preserveGroup?: boolean;
},
) => void;
clearDomSelection: () => void;
buildDomSelectionFromTarget: (
@@ -108,7 +112,11 @@ export function useDomSelection({
const applyDomSelection = useCallback(
(
selection: DomEditSelection | null,
options?: { revealPanel?: boolean; additive?: boolean; preserveGroup?: boolean },
options?: {
revealPanel?: boolean;
additive?: boolean;
preserveGroup?: boolean;
},
) => {
if (!selection) {
domEditSelectionRef.current = null;
@@ -157,7 +165,9 @@ export function useDomSelection({
if (nextSelection) {
if (options?.revealPanel !== false) {
setRightCollapsed(false);
setRightPanelTab("design");
if (rightPanelTab !== "layers") {
setRightPanelTab("design");
}
}
const nextSelectedTimelineId = findMatchingTimelineElementId(
nextSelection,
@@ -169,7 +179,13 @@ export function useDomSelection({
setSelectedTimelineElementId(null);
},
[setSelectedTimelineElementId, timelineElements, setRightCollapsed, setRightPanelTab],
[
setSelectedTimelineElementId,
timelineElements,
setRightCollapsed,
setRightPanelTab,
rightPanelTab,
],
);
const clearDomSelection = useCallback(() => {
@@ -223,7 +239,9 @@ export function useDomSelection({
isMasterView,
});
return targetElement
? buildDomSelectionFromTarget(targetElement, { preferClipAncestor: false })
? buildDomSelectionFromTarget(targetElement, {
preferClipAncestor: false,
})
: null;
},
[activeCompPath, buildDomSelectionFromTarget, compIdToSrc, isMasterView, previewIframeRef],
@@ -259,7 +277,10 @@ export function useDomSelection({
const nextSelection = buildDomSelectionFromTarget(element);
if (nextSelection) {
applyDomSelection(nextSelection, { revealPanel: false, preserveGroup: true });
applyDomSelection(nextSelection, {
revealPanel: false,
preserveGroup: true,
});
}
},
[activeCompPath, applyDomSelection, buildDomSelectionFromTarget, previewIframeRef],