From 3bb943e96a26ab64ed6506e367d0723fbc647f6c Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Wed, 8 Jul 2026 21:07:19 -0700 Subject: [PATCH] feat(studio): add flat footer (ask agent + record) for the inspector --- .../editor/PropertyPanelFlatFooter.test.tsx | 55 ++++++++++++++++++ .../editor/PropertyPanelFlatFooter.tsx | 58 +++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 packages/studio/src/components/editor/PropertyPanelFlatFooter.test.tsx create mode 100644 packages/studio/src/components/editor/PropertyPanelFlatFooter.tsx diff --git a/packages/studio/src/components/editor/PropertyPanelFlatFooter.test.tsx b/packages/studio/src/components/editor/PropertyPanelFlatFooter.test.tsx new file mode 100644 index 000000000..0ae07968f --- /dev/null +++ b/packages/studio/src/components/editor/PropertyPanelFlatFooter.test.tsx @@ -0,0 +1,55 @@ +// @vitest-environment happy-dom + +import React, { act } from "react"; +import { createRoot } from "react-dom/client"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { PropertyPanelFlatFooter } from "./PropertyPanelFlatFooter"; + +(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + +afterEach(() => { + document.body.innerHTML = ""; +}); + +function renderFooter(overrides: Partial[0]> = {}) { + const host = document.createElement("div"); + document.body.append(host); + const root = createRoot(host); + act(() => { + root.render(); + }); + return { host, root }; +} + +describe("PropertyPanelFlatFooter", () => { + it("renders the ask-agent affordance and fires onAskAgent on click", () => { + const onAskAgent = vi.fn(); + const { host, root } = renderFooter({ onAskAgent }); + expect(host.textContent).toContain("Ask agent about this element"); + const askButton = host.querySelector('[data-flat-footer-ask="true"]'); + act(() => askButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }))); + expect(onAskAgent).toHaveBeenCalledTimes(1); + act(() => root.unmount()); + }); + + it("shows the idle record affordance and toggles recording on click", () => { + const onToggleRecording = vi.fn(); + const { host, root } = renderFooter({ recordingState: "idle", onToggleRecording }); + const recordButton = host.querySelector('[data-flat-footer-record="true"]'); + expect(recordButton?.title).toBe("Record gesture (R)"); + act(() => recordButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }))); + expect(onToggleRecording).toHaveBeenCalledTimes(1); + act(() => root.unmount()); + }); + + it("shows the recording duration while recording", () => { + const { host, root } = renderFooter({ + recordingState: "recording", + recordingDuration: 2.4, + onToggleRecording: vi.fn(), + }); + const recordButton = host.querySelector('[data-flat-footer-record="true"]'); + expect(recordButton?.title).toBe("Stop recording 2.4s"); + act(() => root.unmount()); + }); +}); diff --git a/packages/studio/src/components/editor/PropertyPanelFlatFooter.tsx b/packages/studio/src/components/editor/PropertyPanelFlatFooter.tsx new file mode 100644 index 000000000..90c898f86 --- /dev/null +++ b/packages/studio/src/components/editor/PropertyPanelFlatFooter.tsx @@ -0,0 +1,58 @@ +export function PropertyPanelFlatFooter({ + onAskAgent, + recordingState, + recordingDuration, + onToggleRecording, +}: { + onAskAgent?: () => void; + recordingState?: "idle" | "recording" | "preview"; + recordingDuration?: number; + onToggleRecording?: () => void; +}) { + const recording = recordingState === "recording"; + const recordTitle = recording + ? `Stop recording ${(recordingDuration ?? 0).toFixed(1)}s` + : "Record gesture (R)"; + + return ( +
+ + {onToggleRecording && ( + + )} +
+ ); +}