From d868debe2c55f183ac8a80355c9912f531d45dcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Mon, 11 May 2026 08:54:44 -0700 Subject: [PATCH] fix(studio): delete key removes preview-selected elements The consolidated keyboard handler only checked selectedElementId (timeline clips). When a user selected a child element in the preview via the inspector, selectedElementId was null because the element didn't correspond to a top-level timeline clip, so Delete/Backspace did nothing. Add handleDomEditElementDelete that removes the element referenced by the current domEditSelection via the remove-element mutation API. The Delete key handler now falls through from timeline selection to DOM edit selection. --- packages/studio/src/App.tsx | 91 ++++++++++++++++++++++++++++++++++--- 1 file changed, 85 insertions(+), 6 deletions(-) diff --git a/packages/studio/src/App.tsx b/packages/studio/src/App.tsx index a33b72d7d4..1191740d2f 100644 --- a/packages/studio/src/App.tsx +++ b/packages/studio/src/App.tsx @@ -1858,6 +1858,75 @@ export function StudioApp() { [activeCompPath, editHistory.recordEdit, showToast, timelineElements, writeProjectFile], ); + const handleDomEditElementDelete = useCallback( + async (selection: DomEditSelection) => { + const pid = projectIdRef.current; + if (!pid) return; + + const targetPath = selection.sourceFile || activeCompPath || "index.html"; + try { + const response = await fetch( + `/api/projects/${pid}/files/${encodeURIComponent(targetPath)}`, + ); + if (!response.ok) throw new Error(`Failed to read ${targetPath}`); + + const data = (await response.json()) as { content?: string }; + const originalContent = data.content; + if (typeof originalContent !== "string") + throw new Error(`Missing file contents for ${targetPath}`); + + const patchTarget: { id?: string; selector?: string; selectorIndex?: number } = selection.id + ? { + id: selection.id, + selector: selection.selector, + selectorIndex: selection.selectorIndex, + } + : selection.selector + ? { selector: selection.selector, selectorIndex: selection.selectorIndex } + : ({} as never); + if (!patchTarget.id && !patchTarget.selector) { + throw new Error("Selected element has no patchable target"); + } + + const removeResponse = await fetch( + `/api/projects/${pid}/file-mutations/remove-element/${encodeURIComponent(targetPath)}`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ target: patchTarget }), + }, + ); + if (!removeResponse.ok) throw new Error(`Failed to delete element from ${targetPath}`); + + const removeData = (await removeResponse.json()) as { changed?: boolean; content?: string }; + const patchedContent = + typeof removeData.content === "string" ? removeData.content : originalContent; + + domEditSaveTimestampRef.current = Date.now(); + await saveProjectFilesWithHistory({ + projectId: pid, + label: "Delete element", + kind: "timeline", + files: { [targetPath]: patchedContent }, + readFile: async () => originalContent, + writeFile: writeProjectFile, + recordEdit: editHistory.recordEdit, + }); + + domEditSelectionRef.current = null; + domEditGroupSelectionsRef.current = []; + setDomEditSelection(null); + setDomEditGroupSelections([]); + usePlayerStore.getState().setSelectedElementId(null); + setRefreshKey((k) => k + 1); + } catch (error) { + const message = error instanceof Error ? error.message : "Failed to delete element"; + showToast(message); + } + }, + [activeCompPath, editHistory.recordEdit, showToast, writeProjectFile], + ); + // ── Consolidated keyboard shortcuts ──────────────────────────────── // All app-level window keydown handlers live here. // Component-scoped shortcuts (playback J/K/L/Space, caption nudge) @@ -1866,6 +1935,8 @@ export function StudioApp() { handleToggleRef.current = handleTimelineToggleHotkey; const handleDeleteRef = useRef(handleTimelineElementDelete); handleDeleteRef.current = handleTimelineElementDelete; + const handleDomEditDeleteRef = useRef(handleDomEditElementDelete); + handleDomEditDeleteRef.current = handleDomEditElementDelete; // eslint-disable-next-line no-restricted-syntax useEffect(() => { @@ -1904,7 +1975,7 @@ export function StudioApp() { } } - // Delete / Backspace — remove selected timeline element + // Delete / Backspace — remove selected element (timeline clip or preview selection) if ( (event.key === "Delete" || event.key === "Backspace") && !event.metaKey && @@ -1913,11 +1984,19 @@ export function StudioApp() { !isEditableTarget(event.target) ) { const { selectedElementId, elements } = usePlayerStore.getState(); - if (!selectedElementId) return; - const element = elements.find((el) => (el.key ?? el.id) === selectedElementId); - if (!element) return; - event.preventDefault(); - void handleDeleteRef.current(element); + if (selectedElementId) { + const element = elements.find((el) => (el.key ?? el.id) === selectedElementId); + if (element) { + event.preventDefault(); + void handleDeleteRef.current(element); + return; + } + } + const domSelection = domEditSelectionRef.current; + if (domSelection) { + event.preventDefault(); + void handleDomEditDeleteRef.current(domSelection); + } } }