feat(studio): add flat Layout Z-index row and Flex sub-block

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-07-14 15:50:41 -07:00
co-authored by Claude Sonnet 5
parent 588895f317
commit 3b958aeb77
2 changed files with 137 additions and 2 deletions
@@ -3,7 +3,11 @@
import React, { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, describe, expect, it, vi } from "vitest";
import { LayoutGeometryRows } from "./propertyPanelFlatLayoutSection";
import {
LayoutFlexBlock,
LayoutGeometryRows,
LayoutZIndexRow,
} from "./propertyPanelFlatLayoutSection";
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
@@ -107,3 +111,55 @@ describe("LayoutGeometryRows", () => {
act(() => root.unmount());
});
});
describe("LayoutZIndexRow", () => {
it("renders the current z-index at the default tier and commits edits", () => {
const onSetStyle = vi.fn();
const { host, root } = renderInto(
<LayoutZIndexRow styles={{ "z-index": "3" }} onSetStyle={onSetStyle} />,
);
expect(host.textContent).toContain("Z-index");
const input = host.querySelector("input");
if (!input) throw new Error("expected an input");
expect(input.value).toBe("3");
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")!.set!;
act(() => {
setter.call(input, "5");
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("focusout", { bubbles: true }));
});
expect(onSetStyle).toHaveBeenCalledWith("z-index", "5");
act(() => root.unmount());
});
});
describe("LayoutFlexBlock", () => {
it("renders nothing when the element is not flex", () => {
const { host, root } = renderInto(
<LayoutFlexBlock styles={{ display: "block" }} onSetStyle={vi.fn()} disabled={false} />,
);
expect(host.textContent).toBe("");
act(() => root.unmount());
});
it("renders direction/justify/align/gap and commits a direction change", () => {
const onSetStyle = vi.fn();
const { host, root } = renderInto(
<LayoutFlexBlock
styles={{ display: "flex", "flex-direction": "row", gap: "8px" }}
onSetStyle={onSetStyle}
disabled={false}
/>,
);
expect(host.textContent).toContain("Flex");
const columnOption = Array.from(host.querySelectorAll('[data-flat-segment="true"]')).find(
(el) => el.textContent === "Column",
);
if (!columnOption) throw new Error("expected a Column segment option");
act(() =>
(columnOption as HTMLElement).dispatchEvent(new MouseEvent("click", { bubbles: true })),
);
expect(onSetStyle).toHaveBeenCalledWith("flex-direction", "column");
act(() => root.unmount());
});
});
@@ -1,7 +1,8 @@
import { FlatRow } from "./propertyPanelFlatPrimitives";
import { FlatRow, FlatSegmentedRow, FlatSelectRow } from "./propertyPanelFlatPrimitives";
import { KeyframeNavigation } from "./KeyframeNavigation";
import { formatPxMetricValue } from "./propertyPanelHelpers";
import { STUDIO_KEYFRAMES_ENABLED } from "./manualEditingAvailability";
import { resolveValueTier } from "./propertyPanelValueTier";
type KeyframeEntry = Array<{
percentage: number;
@@ -156,3 +157,81 @@ export function LayoutGeometryRows({
</>
);
}
export function LayoutZIndexRow({
styles,
onSetStyle,
}: {
styles: Record<string, string>;
onSetStyle: (prop: string, value: string) => void | Promise<void>;
}) {
const zIndex = String(parseInt(styles["z-index"] || "auto", 10) || 0);
return (
<FlatRow
label="Z-index"
value={zIndex}
tier="default"
onCommit={(next) => void onSetStyle("z-index", next)}
/>
);
}
export function LayoutFlexBlock({
styles,
onSetStyle,
disabled,
}: {
styles: Record<string, string>;
onSetStyle: (prop: string, value: string) => void | Promise<void>;
disabled: boolean;
}) {
const isFlex = styles.display === "flex" || styles.display === "inline-flex";
if (!isFlex) return null;
const direction = styles["flex-direction"] || "row";
return (
<div className="border-l-2 border-panel-border-input py-0.5 pl-[10px]">
<div className="mb-[3px] text-[9px] font-semibold uppercase tracking-[0.12em] text-panel-text-5">
Flex
</div>
<FlatSegmentedRow
label="Direction"
options={[
{ key: "row", node: "→ Row", active: direction === "row" },
{ key: "column", node: "Column", active: direction === "column" },
]}
disabled={disabled}
onChange={(next) => void onSetStyle("flex-direction", next)}
/>
<FlatSelectRow
label="Justify"
value={styles["justify-content"] || "flex-start"}
tier={resolveValueTier(styles["justify-content"], "flex-start")}
disabled={disabled}
options={[
"flex-start",
"center",
"space-between",
"space-around",
"space-evenly",
"flex-end",
]}
onChange={(next) => void onSetStyle("justify-content", next)}
/>
<FlatSelectRow
label="Align"
value={styles["align-items"] || "stretch"}
tier={resolveValueTier(styles["align-items"], "stretch")}
disabled={disabled}
options={["stretch", "flex-start", "center", "flex-end", "baseline"]}
onChange={(next) => void onSetStyle("align-items", next)}
/>
<FlatRow
label="Gap"
value={styles.gap ?? "0px"}
tier={resolveValueTier(styles.gap, "0px")}
disabled={disabled}
onCommit={(next) => void onSetStyle("gap", next.endsWith("px") ? next : `${next}px`)}
/>
</div>
);
}