feat(studio): auto-reveal source when selecting elements in Code tab

When the Code tab is active and a user clicks an element in the preview,
the code editor now auto-scrolls to the corresponding HTML source. This
removes the need for Alt+click — the existing click-to-source mechanism
fires automatically when the Code tab is already open.

- Add getTab() to LeftSidebarHandle for reading the active tab
- Add getSidebarTab getter to useDomEditSession
- Add effect that calls openSourceForSelection on selection change
  when Code tab is active
This commit is contained in:
Miguel Ángel
2026-05-21 15:25:51 -04:00
parent afa6530a10
commit e2de547a28
4 changed files with 40 additions and 3 deletions
+11 -1
View File
@@ -295,6 +295,15 @@ export function StudioApp() {
handleCut,
});
const selectSidebarTabStable = useCallback(
(tab: SidebarTab) => leftSidebarRef.current?.selectTab(tab),
[],
);
const getSidebarTabStable = useCallback(
() => leftSidebarRef.current?.getTab() ?? "compositions",
[],
);
const domEditSession = useDomEditSession({
projectId,
activeCompPath,
@@ -327,7 +336,8 @@ export function StudioApp() {
reloadPreview,
setRefreshKey,
openSourceForSelection: fileManager.openSourceForSelection,
selectSidebarTab: (tab: SidebarTab) => leftSidebarRef.current?.selectTab(tab),
selectSidebarTab: selectSidebarTabStable,
getSidebarTab: getSidebarTabStable,
});
domEditSelectionBridgeRef.current = domEditSession.domEditSelection;
@@ -3,6 +3,7 @@ import {
useState,
useCallback,
useImperativeHandle,
useRef,
forwardRef,
type ReactNode,
} from "react";
@@ -17,6 +18,7 @@ export type SidebarTab = "compositions" | "assets" | "code" | "blocks";
export interface LeftSidebarHandle {
selectTab: (tab: SidebarTab) => void;
getTab: () => SidebarTab;
}
const STORAGE_KEY = "hf-studio-sidebar-tab";
@@ -87,6 +89,8 @@ export const LeftSidebar = memo(
ref,
) {
const [tab, setTab] = useState<SidebarTab>(getPersistedTab);
const tabRef = useRef(tab);
tabRef.current = tab;
const selectTab = useCallback((t: SidebarTab) => {
setTab(t);
@@ -94,7 +98,9 @@ export const LeftSidebar = memo(
trackStudioEvent("tab_switch", { panel: "left_sidebar", tab: t });
}, []);
useImperativeHandle(ref, () => ({ selectTab }), [selectTab]);
const getTab = useCallback(() => tabRef.current, []);
useImperativeHandle(ref, () => ({ selectTab, getTab }), [selectTab, getTab]);
return (
<div
+19 -1
View File
@@ -1,4 +1,4 @@
import { useCallback, useEffect } from "react";
import { useCallback, useEffect, useRef } from "react";
import type { TimelineElement } from "../player";
import { STUDIO_INSPECTOR_PANELS_ENABLED } from "../components/editor/manualEditingAvailability";
import { findElementForSelection, type DomEditSelection } from "../components/editor/domEditing";
@@ -56,6 +56,7 @@ export interface UseDomEditSessionParams {
setRefreshKey: React.Dispatch<React.SetStateAction<number>>;
openSourceForSelection?: (sourceFile: string, target: PatchTarget) => void;
selectSidebarTab?: (tab: SidebarTab) => void;
getSidebarTab?: () => SidebarTab;
}
// ── Hook ──
@@ -93,6 +94,7 @@ export function useDomEditSession({
setRefreshKey: _setRefreshKey,
openSourceForSelection,
selectSidebarTab,
getSidebarTab,
}: UseDomEditSessionParams) {
void _setRefreshKey;
@@ -281,6 +283,22 @@ export function useDomEditSession({
applyStudioManualEditsToPreviewRef,
]);
// Auto-reveal source when an element is selected while the Code tab is active.
// Use a ref for the callback so the effect only fires on selection changes,
// not when openSourceForSelection is recreated due to editingFile content updates.
const openSourceRef = useRef(openSourceForSelection);
openSourceRef.current = openSourceForSelection;
useEffect(() => {
if (!domEditSelection || !openSourceRef.current || !getSidebarTab) return;
if (!domEditSelection.sourceFile) return;
if (getSidebarTab() !== "code") return;
openSourceRef.current(domEditSelection.sourceFile, {
id: domEditSelection.id,
selector: domEditSelection.selector,
selectorIndex: domEditSelection.selectorIndex,
});
}, [domEditSelection, getSidebarTab]);
return {
// State
domEditSelection,
@@ -122,6 +122,9 @@ export function useFileManager({
const handleFileSelect = useCallback((path: string) => {
const pid = projectIdRef.current;
if (!pid) return;
revealAbortRef.current?.abort();
revealAbortRef.current = null;
revealRequestIdRef.current++;
// Skip fetching binary content for media files — just set the path for preview
if (isMediaFile(path)) {
setEditingFile({ path, content: null });