From e2e28bde2f92540f1c8ac84b3da78559ebbd6a3b Mon Sep 17 00:00:00 2001 From: Ernis Date: Tue, 3 Nov 2026 19:59:27 +0600 Subject: [PATCH] Revert "feat(studio): fit and polish promptwise's Timeline tab (#6)" This reverts commit a5d0670801af5782d47f05603b28e6304cf72454. --- .../StudioHeader.embed.dom.test.tsx | 79 ------------------- .../studio/src/components/StudioHeader.tsx | 61 ++++++-------- .../src/components/dock/dockLayout.test.ts | 37 --------- .../studio/src/components/dock/dockLayout.ts | 27 +------ ...PropertyPanelEmptyState.embed.dom.test.tsx | 37 --------- .../editor/PropertyPanelEmptyState.test.tsx | 2 +- .../editor/PropertyPanelEmptyState.tsx | 22 +++--- .../src/components/editor/SnapToolbar.tsx | 22 +++--- .../src/components/nle/NLEPreview.test.ts | 6 +- .../studio/src/components/nle/NLEPreview.tsx | 2 +- .../useStudioUrlState.opening.dom.test.tsx | 56 ------------- .../studio/src/hooks/useStudioUrlState.ts | 24 ++---- .../components/TimelineShortcutHint.tsx | 2 +- packages/studio/src/promptwise/embed.ts | 5 -- packages/studio/src/promptwise/theme.css | 29 +------ .../src/promptwise/usePromptwiseToolbar.ts | 44 ----------- 16 files changed, 64 insertions(+), 391 deletions(-) delete mode 100644 packages/studio/src/components/StudioHeader.embed.dom.test.tsx delete mode 100644 packages/studio/src/components/editor/PropertyPanelEmptyState.embed.dom.test.tsx delete mode 100644 packages/studio/src/hooks/useStudioUrlState.opening.dom.test.tsx delete mode 100644 packages/studio/src/promptwise/usePromptwiseToolbar.ts diff --git a/packages/studio/src/components/StudioHeader.embed.dom.test.tsx b/packages/studio/src/components/StudioHeader.embed.dom.test.tsx deleted file mode 100644 index c6c30e47468..00000000000 --- a/packages/studio/src/components/StudioHeader.embed.dom.test.tsx +++ /dev/null @@ -1,79 +0,0 @@ -// @vitest-environment happy-dom - -/** Embedded in promptwise: no header; the Timeline tab's toolbar drives Inspector and Export by message. */ -import React, { act } from "react"; -import { createRoot } from "react-dom/client"; -import { expect, it, vi } from "vitest"; - -(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; - -const layout = { rightCollapsed: true, setRightCollapsed: vi.fn(), setRightPanelTab: vi.fn() }; - -vi.mock("../promptwise/embed", () => ({ embeddedInPromptwise: true })); -vi.mock("../contexts/StudioContext", () => ({ - useStudioShellContext: () => ({ - projectId: "773d81ab-4ac0-4f9d-acd7-4cd8838312dd", - renderQueue: { isRendering: false, ffmpegMissing: false }, - }), -})); -vi.mock("../contexts/PanelLayoutContext", () => ({ usePanelLayoutContext: () => layout })); -vi.mock("../utils/studioTelemetry", () => ({ trackStudioEvent: vi.fn() })); - -const { StudioHeader } = await import("./StudioHeader"); - -function command(name: string) { - act(() => { - window.dispatchEvent( - new MessageEvent("message", { - data: { type: "promptwise:command", command: name }, - source: window.parent, - }), - ); - }); -} - -it("draws no header, runs the toolbar's presses and reports their state", () => { - const posted: unknown[] = []; - const postMessage = vi.spyOn(window.parent, "postMessage").mockImplementation((message) => { - posted.push(message); - }); - const onExport = vi.fn(); - const host = document.createElement("div"); - document.body.append(host); - const root = createRoot(host); - act(() => - root.render( - , - ), - ); - - expect(host.innerHTML).toBe(""); - expect(posted).toEqual([{ type: "promptwise:state", inspectorOpen: false, rendering: false }]); - - command("inspector"); - expect(layout.setRightPanelTab).toHaveBeenLastCalledWith("design"); - expect(layout.setRightCollapsed).toHaveBeenLastCalledWith(false); - - command("export"); - expect(layout.setRightPanelTab).toHaveBeenLastCalledWith("renders"); - expect(onExport).toHaveBeenCalledOnce(); - - // Only the parent page may drive Studio, and only with the commands it knows. - window.dispatchEvent( - new MessageEvent("message", { data: { type: "promptwise:command", command: "export" } }), - ); - command("delete-everything"); - expect(onExport).toHaveBeenCalledOnce(); - - act(() => root.unmount()); - host.remove(); - postMessage.mockRestore(); -}); diff --git a/packages/studio/src/components/StudioHeader.tsx b/packages/studio/src/components/StudioHeader.tsx index 4347e015756..87a3a019592 100644 --- a/packages/studio/src/components/StudioHeader.tsx +++ b/packages/studio/src/components/StudioHeader.tsx @@ -7,8 +7,6 @@ import { Button, cn, Tooltip } from "./ui"; import { ButtonGroup, ButtonGroupItem } from "../boardui/components/base/buttons/button-group"; import { Dock } from "./dock/Dock"; import { PromptwiseMark } from "../promptwise/PromptwiseMark"; -import { embeddedInPromptwise } from "../promptwise/embed"; -import { usePromptwiseToolbar } from "../promptwise/usePromptwiseToolbar"; import { ShowThemeToggle, ThemeToggle } from "./ThemeToggle"; interface StudioHeaderProps { @@ -57,39 +55,6 @@ export const StudioHeader = memo(function StudioHeader({ const isRendering = renderQueue.isRendering; const ffmpegMissing = renderQueue.ffmpegMissing; - const toggleInspector = () => { - if (shouldOpenInspector(rightCollapsed, inspectorPanelActive)) { - trackStudioEvent("panel_toggle", { panel: "inspector", collapsed: false }); - setRightPanelTab("design"); - setRightCollapsed(false); - return; - } - trackStudioEvent("panel_toggle", { panel: "inspector", collapsed: true }); - // Keep the current selection when collapsing the Inspector — closing - // the panel shouldn't deselect the element. - setRightCollapsed(true); - }; - - const exportVideo = () => { - if (isRendering) return; - setRightPanelTab("renders"); - setRightCollapsed(false); - // Without an encoder this render cannot finish, so the click - // delivers the user to the prompt that fixes it instead of - // queueing a job that exists only to fail. Disabling the button - // would leave them staring at a dead control with no route to - // the explanation. - if (ffmpegMissing) return; - onExport?.(); - }; - - usePromptwiseToolbar( - { inspectorOpen: inspectorButtonActive, rendering: isRendering }, - { inspector: toggleInspector, export: exportVideo }, - ); - // promptwise's Timeline tab has its own toolbar and tab name, so a second header would only repeat them. - if (embeddedInPromptwise) return null; - return (
@@ -139,7 +104,18 @@ export const StudioHeader = memo(function StudioHeader({ aria-pressed={inspectorButtonActive} selected={inspectorButtonActive} leadingIcon={RiLayoutRightLine} - onClick={toggleInspector} + onClick={() => { + if (shouldOpenInspector(rightCollapsed, inspectorPanelActive)) { + trackStudioEvent("panel_toggle", { panel: "inspector", collapsed: false }); + setRightPanelTab("design"); + setRightCollapsed(false); + return; + } + trackStudioEvent("panel_toggle", { panel: "inspector", collapsed: true }); + // Keep the current selection when collapsing the Inspector — closing + // the panel shouldn't deselect the element. + setRightCollapsed(true); + }} > Inspector @@ -161,7 +137,18 @@ export const StudioHeader = memo(function StudioHeader({ variant="primary" data-testid="header-export" disabled={isRendering} - onClick={exportVideo} + onClick={() => { + if (isRendering) return; + setRightPanelTab("renders"); + setRightCollapsed(false); + // Without an encoder this render cannot finish, so the click + // delivers the user to the prompt that fixes it instead of + // queueing a job that exists only to fail. Disabling the button + // would leave them staring at a dead control with no route to + // the explanation. + if (ffmpegMissing) return; + onExport?.(); + }} > {isRendering ? "Rendering…" : "Export"} diff --git a/packages/studio/src/components/dock/dockLayout.test.ts b/packages/studio/src/components/dock/dockLayout.test.ts index 40c3a1271c2..bddf62e8da8 100644 --- a/packages/studio/src/components/dock/dockLayout.test.ts +++ b/packages/studio/src/components/dock/dockLayout.test.ts @@ -132,40 +132,3 @@ describe("applySideMinimums", () => { expect(timeline.group.minimumWidth).toBe(120); }); }); - -describe("buildEditLayout on a narrow dock", () => { - let api: DockviewApi; - beforeEach(() => { - ({ api } = mountBareDockview()); - }); - afterEach(() => { - api.dispose(); - document.body.innerHTML = ""; - }); - - it("puts the preview over the timeline and one hidden side group beside it, as in promptwise's tab", () => { - api.layout(640, 820); - buildEditLayout(api, 640); - const side = api.getPanel("design")!.group; - expect(side.panels.map((panel) => panel.id)).toEqual([ - "design", - "layers", - "renders", - "compositions", - "assets", - ]); - expect(side.api.isVisible).toBe(false); - expect(api.getPanel("preview")!.group.api.isVisible).toBe(true); - expect(api.getPanel("preview")!.group.width).toBe(640); - expect(api.getPanel("timeline")!.group.width).toBe(640); - expect(api.getPanel("timeline")!.group.height).toBe(260); - expect(api.getPanel("code")).toBeUndefined(); - }); - - it("keeps the three columns on a wide one", () => { - api.layout(1200, 700); - buildEditLayout(api, 1200); - expect(api.getPanel("compositions")!.group).not.toBe(api.getPanel("design")!.group); - expect(api.getPanel("design")!.group.api.isVisible).toBe(true); - }); -}); diff --git a/packages/studio/src/components/dock/dockLayout.ts b/packages/studio/src/components/dock/dockLayout.ts index acd9f8c1a35..94886d93aea 100644 --- a/packages/studio/src/components/dock/dockLayout.ts +++ b/packages/studio/src/components/dock/dockLayout.ts @@ -10,11 +10,6 @@ const MIN_SIDE_W_FLOOR = 120; const DEFAULT_TIMELINE_H = 360; const DEFAULT_LEFT = ["compositions", "assets", "code", "catalog"] as const; const DEFAULT_RIGHT = ["design", "layers", "renders", "variables", "motionBlur"] as const; -/** Below this dock width (promptwise's Timeline tab beside its chat) two side columns would crush the preview. */ -const COMPACT_DOCK_W = 1000; -const COMPACT_SIDE = ["design", "layers", "renders", "compositions", "assets"] as const; -/** Room for three tracks under the toolbar and ruler; the preview gets the rest. */ -const COMPACT_TIMELINE_H = 260; /** Preferred side widths; when they overflow the preview's floor the right yields first, then the left. */ export function defaultSideWidths(viewportWidth: number) { @@ -104,20 +99,12 @@ function addSideColumn( return first; } -/** - * The default Edit layout: [library | preview | inspector] over a full-width timeline, or on a narrow - * dock the preview over the timeline with one side group beside the preview, shown when asked for - * (Inspector, a selection) since it would leave the preview too little room. - */ +/** The default Edit layout: [library | preview | inspector] over a full-width timeline. */ export function buildEditLayout( api: DockviewApi, viewportWidth: number, panels: readonly PanelId[] = PANEL_IDS, ) { - if (viewportWidth < COMPACT_DOCK_W) { - buildCompactLayout(api, viewportWidth, panels); - return; - } api.clear(); const widths = defaultSideWidths(viewportWidth); addRegisteredPanel(api, "preview"); @@ -129,15 +116,3 @@ export function buildEditLayout( if (right) api.getPanel(right)?.group.api.setSize({ width: widths.right }); api.getPanel("timeline")?.group.api.setSize({ height: DEFAULT_TIMELINE_H }); } - -function buildCompactLayout(api: DockviewApi, viewportWidth: number, panels: readonly PanelId[]) { - api.clear(); - addRegisteredPanel(api, "preview"); - addRegisteredPanel(api, "timeline", { referencePanel: "preview", direction: "below" }); - const side = addSideColumn(api, COMPACT_SIDE, panels, "right"); - applySideMinimums(api, viewportWidth); - api.getPanel("timeline")?.group.api.setSize({ height: COMPACT_TIMELINE_H }); - const group = side ? api.getPanel(side)?.group : undefined; - group?.api.setSize({ width: Math.max(MIN_SIDE_W, defaultSideWidths(viewportWidth).right) }); - group?.api.setVisible(false); -} diff --git a/packages/studio/src/components/editor/PropertyPanelEmptyState.embed.dom.test.tsx b/packages/studio/src/components/editor/PropertyPanelEmptyState.embed.dom.test.tsx deleted file mode 100644 index 2b21a6f6328..00000000000 --- a/packages/studio/src/components/editor/PropertyPanelEmptyState.embed.dom.test.tsx +++ /dev/null @@ -1,37 +0,0 @@ -// @vitest-environment happy-dom - -/** In promptwise most changes go through the agent, so its button leads the empty Design panel. */ -import React, { act } from "react"; -import { createRoot } from "react-dom/client"; -import { expect, it, vi } from "vitest"; - -(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; - -vi.mock("../../promptwise/embed", async (original) => ({ - ...(await original()), - embeddedInPromptwise: true, -})); - -const { PropertyPanelEmptyState } = await import("./PropertyPanelEmptyState"); - -it("puts the agent first, as the panel's one primary button, and asks promptwise to open its chat", () => { - const posted: unknown[] = []; - const postMessage = vi.spyOn(window.parent, "postMessage").mockImplementation((message) => { - posted.push(message); - }); - const host = document.createElement("div"); - document.body.append(host); - const root = createRoot(host); - act(() => root.render()); - - const text = host.textContent ?? ""; - expect(text.indexOf("Ask the agent")).toBeLessThan(text.indexOf("Record a gesture")); - const agent = host.querySelector("button")!; - expect(agent.textContent).toContain("Ask the agent"); - act(() => agent.click()); - expect(posted).toEqual([{ type: "promptwise:ask-agent" }]); - - act(() => root.unmount()); - host.remove(); - postMessage.mockRestore(); -}); diff --git a/packages/studio/src/components/editor/PropertyPanelEmptyState.test.tsx b/packages/studio/src/components/editor/PropertyPanelEmptyState.test.tsx index 1578a77bb71..2848c1ee0e3 100644 --- a/packages/studio/src/components/editor/PropertyPanelEmptyState.test.tsx +++ b/packages/studio/src/components/editor/PropertyPanelEmptyState.test.tsx @@ -28,7 +28,7 @@ describe("PropertyPanelEmptyState — flat empty", () => { expect(host.textContent).toContain("Nothing selected"); expect(host.textContent).toContain("Record a gesture"); // The agent chip only shows inside promptwise, whose chat it opens. - expect(host.textContent).not.toContain("Ask the agent"); + expect(host.textContent).not.toContain("Describe a change to the agent"); act(() => root.unmount()); }); }); diff --git a/packages/studio/src/components/editor/PropertyPanelEmptyState.tsx b/packages/studio/src/components/editor/PropertyPanelEmptyState.tsx index 8385c190baa..067d9068210 100644 --- a/packages/studio/src/components/editor/PropertyPanelEmptyState.tsx +++ b/packages/studio/src/components/editor/PropertyPanelEmptyState.tsx @@ -3,7 +3,6 @@ import type { DomEditSelection } from "./domEditingTypes"; import { canHideSelections } from "../../utils/timelineInspector"; import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext"; import { askPromptwiseAgent, embeddedInPromptwise } from "../../promptwise/embed"; -import { Button } from "../ui"; function FlatEmptyState() { return ( @@ -25,17 +24,6 @@ function FlatEmptyState() { Click any element on the canvas to edit it, or drag to select several.
- {/* In promptwise the agent is the way most changes are made, so it comes first. */} - {embeddedInPromptwise && ( - - )} ● @@ -45,6 +33,16 @@ function FlatEmptyState() { R + {embeddedInPromptwise && ( + + )}
); diff --git a/packages/studio/src/components/editor/SnapToolbar.tsx b/packages/studio/src/components/editor/SnapToolbar.tsx index 63637d484b5..8d5c72a2d55 100644 --- a/packages/studio/src/components/editor/SnapToolbar.tsx +++ b/packages/studio/src/components/editor/SnapToolbar.tsx @@ -5,10 +5,6 @@ import { usePlayerStore } from "../../player/store/playerStore"; import { usePreviewOverlayContext } from "./PreviewOverlayProvider"; import { ownsPlainKeys } from "../../utils/typingTarget"; -/** A preview overlay button that's off: BoardUI's light surface with a hairline, readable over any frame. */ -const overlayButtonOff = - "bg-background-primary-default/90 text-foreground-icon-secondary shadow-xs ring-1 ring-border-button-default hover:bg-background-primary-hover hover:text-foreground-icon-primary"; - // fallow-ignore-next-line complexity export const SnapToolbar = memo(function SnapToolbar() { const [gridPopoverOpen, setGridPopoverOpen] = useState(false); @@ -91,7 +87,9 @@ export const SnapToolbar = memo(function SnapToolbar() {