From b324a43ba0e45f7e9781a64b122ce51067c6c5f9 Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Wed, 8 Jul 2026 20:57:11 -0700 Subject: [PATCH] feat(studio): add FlatGroup accordion and PinnedZoneDivider primitives --- .../propertyPanelFlatPrimitives.test.tsx | 60 ++++++++++- .../editor/propertyPanelFlatPrimitives.tsx | 102 ++++++++++++++++++ 2 files changed, 161 insertions(+), 1 deletion(-) diff --git a/packages/studio/src/components/editor/propertyPanelFlatPrimitives.test.tsx b/packages/studio/src/components/editor/propertyPanelFlatPrimitives.test.tsx index 30ad145c9..33b71d41e 100644 --- a/packages/studio/src/components/editor/propertyPanelFlatPrimitives.test.tsx +++ b/packages/studio/src/components/editor/propertyPanelFlatPrimitives.test.tsx @@ -3,7 +3,12 @@ import React, { act } from "react"; import { createRoot } from "react-dom/client"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { FlatRow, FlatSegmentedRow } from "./propertyPanelFlatPrimitives"; +import { + FlatGroup, + FlatRow, + FlatSegmentedRow, + PinnedZoneDivider, +} from "./propertyPanelFlatPrimitives"; (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; @@ -99,3 +104,56 @@ describe("FlatSegmentedRow", () => { act(() => root.unmount()); }); }); + +describe("FlatGroup", () => { + it("renders the open header (name + pin + caret) and shows children", () => { + const onToggleOpen = vi.fn(); + const onTogglePin = vi.fn(); + const { host, root } = renderInto( + +
body
+
, + ); + expect(host.querySelector('[data-testid="body"]')).not.toBeNull(); + const pin = host.querySelector('[data-flat-group-pin="true"]'); + act(() => pin?.dispatchEvent(new MouseEvent("click", { bubbles: true }))); + expect(onTogglePin).toHaveBeenCalledTimes(1); + act(() => root.unmount()); + }); + + it("renders the collapsed row (name + summary + caret-right) and no children", () => { + const onToggleOpen = vi.fn(); + const { host, root } = renderInto( + +
body
+
, + ); + expect(host.querySelector('[data-testid="body"]')).toBeNull(); + expect(host.textContent).toContain("fill none · 100%"); + const row = host.querySelector('[data-flat-group-collapsed="true"]'); + act(() => row?.dispatchEvent(new MouseEvent("click", { bubbles: true }))); + expect(onToggleOpen).toHaveBeenCalledTimes(1); + act(() => root.unmount()); + }); +}); + +describe("PinnedZoneDivider", () => { + it("renders the 'one open below' label", () => { + const { host, root } = renderInto(); + expect(host.textContent).toContain("one open below"); + act(() => root.unmount()); + }); +}); diff --git a/packages/studio/src/components/editor/propertyPanelFlatPrimitives.tsx b/packages/studio/src/components/editor/propertyPanelFlatPrimitives.tsx index 5acd863ff..067ecb7a7 100644 --- a/packages/studio/src/components/editor/propertyPanelFlatPrimitives.tsx +++ b/packages/studio/src/components/editor/propertyPanelFlatPrimitives.tsx @@ -131,3 +131,105 @@ export function FlatSegmentedRow({ ); } + +/* ------------------------------------------------------------------ */ +/* FlatGroup — one-open-at-a-time accordion group (controlled) */ +/* ------------------------------------------------------------------ */ + +export function FlatGroup({ + title, + isOpen, + isPinned, + onToggleOpen, + onTogglePin, + accessory, + summary, + children, +}: { + title: string; + isOpen: boolean; + isPinned: boolean; + onToggleOpen: () => void; + onTogglePin: () => void; + accessory?: ReactNode; + summary?: string; + children: ReactNode; +}) { + if (!isOpen) { + return ( + + ); + } + + return ( +
+
+ {title} + + {accessory} + + + +
+ {children} +
+ ); +} + +/* ------------------------------------------------------------------ */ +/* PinnedZoneDivider */ +/* ------------------------------------------------------------------ */ + +export function PinnedZoneDivider() { + return ( +
+
+ ); +}