Skip to content

Commit 1fea656

Browse files
committed
refactor(studio): remove section pinning from the flat inspector
1 parent 5d24fc1 commit 1fea656

11 files changed

Lines changed: 22 additions & 368 deletions

‎packages/studio/src/components/editor/PropertyPanel.test.tsx‎

Lines changed: 1 addition & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,6 @@ vi.mock("../../contexts/StudioContext", async () => {
1818

1919
afterEach(() => {
2020
document.body.innerHTML = "";
21-
// usePersistedPinnedGroups persists to localStorage; clear it so a pinned
22-
// group from one test can't leak into the next (which would move a group out
23-
// of the accordion and break an unrelated open-by-default assertion).
24-
window.localStorage.clear();
2521
vi.doUnmock("./manualEditingAvailability");
2622
vi.resetModules();
2723
});
@@ -813,47 +809,6 @@ describe("PropertyPanel — Media group (Plan 4)", () => {
813809
);
814810
});
815811

816-
describe("PropertyPanel — pinning", () => {
817-
it(
818-
"renders a pinned group first, always open, above the PinnedZoneDivider",
819-
async () => {
820-
const { host, root } = await renderPanel(true);
821-
// Pin the Text group via its pin button.
822-
const pinButton = host.querySelector<HTMLButtonElement>('[data-flat-group-pin="true"]');
823-
if (!pinButton) throw new Error("expected a pin button on the open Text group");
824-
act(() => pinButton.dispatchEvent(new MouseEvent("click", { bubbles: true })));
825-
826-
const pinnedRow = host.querySelector('[data-pinned-group="true"]');
827-
expect(pinnedRow?.textContent).toContain("Text");
828-
expect(pinnedRow?.textContent).toContain("Pinned");
829-
830-
// The divider must appear after the pinned zone.
831-
const container = host.querySelector('[data-flat-panel-body="true"]');
832-
const children = Array.from(container?.children ?? []);
833-
const pinnedIndex = children.indexOf(pinnedRow as Element);
834-
const dividerIndex = children.findIndex((el) => el.textContent?.includes("one open below"));
835-
expect(pinnedIndex).toBeGreaterThanOrEqual(0);
836-
expect(dividerIndex).toBeGreaterThan(pinnedIndex);
837-
act(() => root.unmount());
838-
},
839-
RENDER_TIMEOUT_MS,
840-
);
841-
842-
it(
843-
"unpinning returns the group to its normal accordion stack position",
844-
async () => {
845-
const { host, root } = await renderPanel(true);
846-
const pinButton = host.querySelector<HTMLButtonElement>('[data-flat-group-pin="true"]');
847-
act(() => pinButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
848-
const unpinButton = host.querySelector<HTMLButtonElement>('[data-pinned-group-unpin="true"]');
849-
act(() => unpinButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
850-
expect(host.querySelector('[data-pinned-group="true"]')).toBeNull();
851-
act(() => root.unmount());
852-
},
853-
RENDER_TIMEOUT_MS,
854-
);
855-
});
856-
857812
// design_handoff scrollable-open-section: collapsed headers before/after the
858813
// open group render in normal document flow and never move (no sticky, no
859814
// stacking offsets) — only the open group's own body content scrolls, in a
@@ -882,8 +837,7 @@ describe("PropertyPanel — fixed headers + scrollable open section (Plan 11)",
882837
if (child.matches('[data-flat-group-open="true"]')) return child.textContent ?? "";
883838
return null;
884839
});
885-
// Filter to just the group entries (drop nulls from any divider, none
886-
// expected here since no groups are pinned).
840+
// Filter to just the group entries (drop any non-group nulls).
887841
const groupTitles = titles.filter((t): t is string => t !== null);
888842
expect(groupTitles).toHaveLength(6);
889843
expect(groupTitles[0]).toContain("Text");

‎packages/studio/src/components/editor/PropertyPanel.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
265265
if (STUDIO_FLAT_INSPECTOR_ENABLED) {
266266
// Forward the raw props (handlers, ids, assets, recording, fonts, etc.) and
267267
// the values the legacy path already computed above (so they aren't derived
268-
// twice). PropertyPanelFlat owns the one-open/pin group state.
268+
// twice). PropertyPanelFlat owns the one-open group state.
269269
return (
270270
<PropertyPanelFlat
271271
{...props}

‎packages/studio/src/components/editor/PropertyPanelFlat.tsx‎

Lines changed: 8 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { PropertyPanelProps } from "./propertyPanelHelpers";
66
import { formatPxMetricValue } from "./propertyPanelHelpers";
77
import { PropertyPanelFlatHeader } from "./PropertyPanelFlatHeader";
88
import { PropertyPanelFlatFooter } from "./PropertyPanelFlatFooter";
9-
import { FlatGroupHeader, PinnedGroupRow, PinnedZoneDivider } from "./propertyPanelFlatPrimitives";
9+
import { FlatGroupHeader } from "./propertyPanelFlatPrimitives";
1010
import { FlatTextSection } from "./propertyPanelFlatTextSection";
1111
import { FlatStyleSection } from "./propertyPanelFlatStyleSections";
1212
import { FlatLayoutSection } from "./propertyPanelFlatLayoutSection";
@@ -17,7 +17,6 @@ import { createGsapLivePreview } from "./gsapLivePreview";
1717
import { formatTextFieldPreview } from "./propertyPanelSections";
1818
import { STUDIO_GSAP_PANEL_ENABLED } from "./manualEditingAvailability";
1919
import { useColorGradingController } from "./useColorGradingController";
20-
import { usePersistedPinnedGroups } from "../../hooks/usePersistedPinnedGroups";
2120
import {
2221
FlatColorGradingAccessory,
2322
FlatColorGradingSection,
@@ -52,7 +51,7 @@ const EMPTY_GSAP_EFFECT_HANDLERS = {
5251
*
5352
* Extracted from PropertyPanel so that file stays under the 600-LOC gate
5453
* (same one-directional-import precedent as FlatTextSection). Rendered only
55-
* when STUDIO_FLAT_INSPECTOR_ENABLED is on; owns the one-open/pin group state.
54+
* when STUDIO_FLAT_INSPECTOR_ENABLED is on; owns the one-open group state.
5655
*
5756
* The Text/Style/Layout/Motion/Media/Grade groups share the one-open accordion.
5857
*/
@@ -252,7 +251,6 @@ export function PropertyPanelFlat({
252251

253252
const isTextEditable = isTextEditableSelection(element);
254253
const elementKind = sections.media ? "media" : element.textFields.length > 0 ? "text" : "other";
255-
const { pinnedGroupIds, togglePin } = usePersistedPinnedGroups(elementKind);
256254
const toggleOpen = (groupId: string) =>
257255
setOpenGroupId((current) => (current === groupId ? "" : groupId));
258256
// Basis for the Layout keyframe gutter (X/Y/W/H/Angle + 3D Transform) —
@@ -311,9 +309,8 @@ export function PropertyPanelFlat({
311309
const showMotionGroup = showMotionTiming || showMotionEffects;
312310

313311
// Ordered group descriptors — one per FlatGroup this panel renders, gated by
314-
// the same conditions the inline JSX used. Partitioned into pinned/unpinned
315-
// below so pinned groups render first (always open, no accordion) above the
316-
// PinnedZoneDivider, with the rest in the one-open accordion beneath it.
312+
// the same conditions the inline JSX used. Split below into before-open/
313+
// open/after-open regions for the one-open accordion.
317314
const groups: FlatGroupDescriptor[] = [];
318315
if (isTextEditable) {
319316
groups.push({
@@ -453,20 +450,17 @@ export function PropertyPanelFlat({
453450
});
454451
}
455452

456-
const pinned = groups.filter((g) => pinnedGroupIds.includes(g.id));
457-
const unpinned = groups.filter((g) => !pinnedGroupIds.includes(g.id));
458-
459453
// Fixed-headers + scrollable-open-section layout (design_handoff
460454
// scrollable-open-section, replaces the prior sticky-stacking mechanism):
461455
// collapsed headers before/after the open group render in normal document
462456
// flow and never move. Only the open group's own body content scrolls, in
463457
// a dedicated region between the two fixed header stacks. When no group is
464458
// open, every group is just a collapsed header — there's no scrollable
465459
// middle region at all, since nothing is expanded.
466-
const openIndex = unpinned.findIndex((g) => g.id === openGroupId);
467-
const beforeOpen = openIndex === -1 ? unpinned : unpinned.slice(0, openIndex);
468-
const openGroup = openIndex === -1 ? null : unpinned[openIndex];
469-
const afterOpen = openIndex === -1 ? [] : unpinned.slice(openIndex + 1);
460+
const openIndex = groups.findIndex((g) => g.id === openGroupId);
461+
const beforeOpen = openIndex === -1 ? groups : groups.slice(0, openIndex);
462+
const openGroup = openIndex === -1 ? null : groups[openIndex];
463+
const afterOpen = openIndex === -1 ? [] : groups.slice(openIndex + 1);
470464

471465
return (
472466
<div className="flex h-full min-h-0 flex-col overflow-hidden bg-panel-bg text-panel-text-1">
@@ -487,25 +481,12 @@ export function PropertyPanelFlat({
487481
showUngroup={Boolean(onUngroup && element.dataAttributes["hf-group"] != null)}
488482
/>
489483
<div data-flat-panel-body="true" className="flex min-h-0 flex-1 flex-col overflow-hidden">
490-
{pinned.map((g) => (
491-
<PinnedGroupRow
492-
key={g.id}
493-
title={g.title}
494-
accessory={g.accessory}
495-
onUnpin={() => togglePin(g.id)}
496-
>
497-
{g.content}
498-
</PinnedGroupRow>
499-
))}
500-
{pinned.length > 0 && unpinned.length > 0 && <PinnedZoneDivider />}
501484
{beforeOpen.map((g) => (
502485
<FlatGroupHeader
503486
key={g.id}
504487
title={g.title}
505488
isOpen={false}
506-
isPinned={false}
507489
onToggleOpen={() => toggleOpen(g.id)}
508-
onTogglePin={() => togglePin(g.id)}
509490
summary={g.summary}
510491
/>
511492
))}
@@ -514,9 +495,7 @@ export function PropertyPanelFlat({
514495
<FlatGroupHeader
515496
title={openGroup.title}
516497
isOpen
517-
isPinned={false}
518498
onToggleOpen={() => toggleOpen(openGroup.id)}
519-
onTogglePin={() => togglePin(openGroup.id)}
520499
accessory={openGroup.accessory}
521500
/>
522501
<div className="min-h-0 flex-1 overflow-y-auto border-b border-panel-hairline px-4 py-3">
@@ -529,9 +508,7 @@ export function PropertyPanelFlat({
529508
key={g.id}
530509
title={g.title}
531510
isOpen={false}
532-
isPinned={false}
533511
onToggleOpen={() => toggleOpen(g.id)}
534-
onTogglePin={() => togglePin(g.id)}
535512
summary={g.summary}
536513
/>
537514
))}

‎packages/studio/src/components/editor/PropertyPanelFlatFooter.test.tsx‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,9 @@ describe("PropertyPanelFlatFooter", () => {
6969
// rounding gap) — is gone now that nothing above the footer is
7070
// `position: sticky`. Live browser verification (p11 report) confirmed the
7171
// boundary renders as a single clean hairline without it: whatever
72-
// immediately precedes the footer (a collapsed FlatGroupHeader, the open
73-
// group's scrollable body wrapper, or a PinnedGroupRow) already draws its
74-
// own border-b in normal document flow, so the footer needs no border or
75-
// seal of its own.
72+
// immediately precedes the footer (a collapsed FlatGroupHeader, or the open
73+
// group's scrollable body wrapper) already draws its own border-b in normal
74+
// document flow, so the footer needs no border or seal of its own.
7675
it("renders no seal overlay and no border of its own — the boundary line comes from whatever precedes it", () => {
7776
const { host, root } = renderFooter({ onAskAgent: vi.fn() });
7877
const footerRoot = host.firstElementChild as HTMLElement;

‎packages/studio/src/components/editor/PropertyPanelFlatFooter.tsx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ export function PropertyPanelFlatFooter({
1717
return (
1818
// No border-t here: every possible element immediately above this footer
1919
// in the new fixed-headers + scrollable-open-section layout (a collapsed
20-
// FlatGroupHeader, the open group's scrollable body wrapper, or a
21-
// PinnedGroupRow) already draws its own border-b in normal document flow
22-
// — nothing here is `position: sticky` anymore, so there's no rounding
23-
// seam to seal (see p11-scrollable-open-section-report.md).
20+
// FlatGroupHeader, or the open group's scrollable body wrapper) already
21+
// draws its own border-b in normal document flow — nothing here is
22+
// `position: sticky` anymore, so there's no rounding seam to seal (see
23+
// p11-scrollable-open-section-report.md).
2424
<div className="flex items-center justify-between bg-panel-bg px-4 py-[11px]">
2525
<button
2626
type="button"

‎packages/studio/src/components/editor/propertyPanelFlatPrimitives.test.tsx‎

Lines changed: 4 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import {
1010
FlatSelectRow,
1111
FlatSlider,
1212
FlatToggle,
13-
PinnedGroupRow,
14-
PinnedZoneDivider,
1513
} from "./propertyPanelFlatPrimitives";
1614

1715
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
@@ -110,22 +108,12 @@ describe("FlatSegmentedRow", () => {
110108
});
111109

112110
describe("FlatGroupHeader", () => {
113-
it("renders the open header (name + pin + caret), with no sticky-related props required", () => {
111+
it("renders the open header (name + caret), with no sticky-related props required", () => {
114112
const onToggleOpen = vi.fn();
115-
const onTogglePin = vi.fn();
116113
const { host, root } = renderInto(
117-
<FlatGroupHeader
118-
title="Text"
119-
isOpen
120-
isPinned={false}
121-
onToggleOpen={onToggleOpen}
122-
onTogglePin={onTogglePin}
123-
/>,
114+
<FlatGroupHeader title="Text" isOpen onToggleOpen={onToggleOpen} />,
124115
);
125116
expect(host.textContent).toContain("Text");
126-
const pin = host.querySelector<HTMLButtonElement>('[data-flat-group-pin="true"]');
127-
act(() => pin?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
128-
expect(onTogglePin).toHaveBeenCalledTimes(1);
129117
const collapse = host.querySelector<HTMLButtonElement>('button[title="Collapse"]');
130118
act(() => collapse?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
131119
expect(onToggleOpen).toHaveBeenCalledTimes(1);
@@ -138,9 +126,7 @@ describe("FlatGroupHeader", () => {
138126
<FlatGroupHeader
139127
title="Style"
140128
isOpen={false}
141-
isPinned={false}
142129
onToggleOpen={onToggleOpen}
143-
onTogglePin={vi.fn()}
144130
summary="fill none · 100%"
145131
/>,
146132
);
@@ -154,13 +140,7 @@ describe("FlatGroupHeader", () => {
154140

155141
it("renders no inline position styling in either state (collapsed headers never move)", () => {
156142
const { host: collapsedHost, root: collapsedRoot } = renderInto(
157-
<FlatGroupHeader
158-
title="Layout"
159-
isOpen={false}
160-
isPinned={false}
161-
onToggleOpen={vi.fn()}
162-
onTogglePin={vi.fn()}
163-
/>,
143+
<FlatGroupHeader title="Layout" isOpen={false} onToggleOpen={vi.fn()} />,
164144
);
165145
const row = collapsedHost.querySelector<HTMLButtonElement>(
166146
'[data-flat-group-collapsed="true"]',
@@ -169,28 +149,14 @@ describe("FlatGroupHeader", () => {
169149
act(() => collapsedRoot.unmount());
170150

171151
const { host: openHost, root: openRoot } = renderInto(
172-
<FlatGroupHeader
173-
title="Motion"
174-
isOpen
175-
isPinned={false}
176-
onToggleOpen={vi.fn()}
177-
onTogglePin={vi.fn()}
178-
/>,
152+
<FlatGroupHeader title="Motion" isOpen onToggleOpen={vi.fn()} />,
179153
);
180154
expect(openHost.textContent).toContain("Motion");
181155
expect(openHost.querySelector("[style]")).toBeNull();
182156
act(() => openRoot.unmount());
183157
});
184158
});
185159

186-
describe("PinnedZoneDivider", () => {
187-
it("renders the 'one open below' label", () => {
188-
const { host, root } = renderInto(<PinnedZoneDivider />);
189-
expect(host.textContent).toContain("one open below");
190-
act(() => root.unmount());
191-
});
192-
});
193-
194160
describe("FlatSlider", () => {
195161
it("renders the default tier with a dim knob at the correct position", () => {
196162
const { host, root } = renderInto(
@@ -486,21 +452,3 @@ describe("FlatToggle", () => {
486452
act(() => root.unmount());
487453
});
488454
});
489-
490-
describe("PinnedGroupRow", () => {
491-
it("renders a 'Pinned' badge, filled pin icon, and always shows children", () => {
492-
const onUnpin = vi.fn();
493-
const { host, root } = renderInto(
494-
<PinnedGroupRow title="Motion" onUnpin={onUnpin}>
495-
<div data-testid="body">body</div>
496-
</PinnedGroupRow>,
497-
);
498-
expect(host.textContent).toContain("Pinned");
499-
expect(host.textContent).toContain("Motion");
500-
expect(host.querySelector('[data-testid="body"]')).not.toBeNull();
501-
const unpin = host.querySelector<HTMLButtonElement>('[data-pinned-group-unpin="true"]');
502-
act(() => unpin?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
503-
expect(onUnpin).toHaveBeenCalledTimes(1);
504-
act(() => root.unmount());
505-
});
506-
});

0 commit comments

Comments
 (0)