mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
feat(studio): wire the flat Style group into the one-open accordion
This commit is contained in:
@@ -100,6 +100,20 @@ function multiFieldTextElement() {
|
||||
};
|
||||
}
|
||||
|
||||
// Style-only fixture: no text fields (Text group must not render), but
|
||||
// canEditStyles stays true (inherited from baseElement()) so the Style group
|
||||
// is gated in.
|
||||
function styleOnlyElement() {
|
||||
return {
|
||||
...baseElement(),
|
||||
id: "stat-card",
|
||||
selector: ".stat-card",
|
||||
label: "Stat Card",
|
||||
textFields: [],
|
||||
inlineStyles: { "background-color": "#0D0C09" },
|
||||
};
|
||||
}
|
||||
|
||||
async function renderPanel(
|
||||
flatEnabled: boolean,
|
||||
elementOverride: ReturnType<typeof baseElement> = baseElement(),
|
||||
@@ -185,9 +199,19 @@ describe("PropertyPanel — STUDIO_FLAT_INSPECTOR_ENABLED on", () => {
|
||||
it(
|
||||
"renders no Text group at all for a non-text element (bug 1)",
|
||||
async () => {
|
||||
// nonTextElement() inherits canEditStyles: true from baseElement(), so
|
||||
// the Style group (Task 10) renders and opens by default here — the
|
||||
// invariant under test is narrower than "no flat group at all": no
|
||||
// group titled "Text" may appear, open or collapsed.
|
||||
const { host, root } = await renderPanel(true, nonTextElement());
|
||||
expect(host.querySelector('[data-flat-group-open="true"]')).toBeNull();
|
||||
expect(host.querySelector('[data-flat-group-collapsed="true"]')).toBeNull();
|
||||
const openTitle = host.querySelector(
|
||||
'[data-flat-group-open="true"] .text-panel-text-0',
|
||||
)?.textContent;
|
||||
const collapsedTitles = Array.from(
|
||||
host.querySelectorAll('[data-flat-group-collapsed="true"] .text-panel-text-2'),
|
||||
).map((el) => el.textContent);
|
||||
expect(openTitle).not.toBe("Text");
|
||||
expect(collapsedTitles).not.toContain("Text");
|
||||
act(() => root.unmount());
|
||||
},
|
||||
RENDER_TIMEOUT_MS,
|
||||
@@ -209,3 +233,36 @@ describe("PropertyPanel — STUDIO_FLAT_INSPECTOR_ENABLED on", () => {
|
||||
RENDER_TIMEOUT_MS,
|
||||
);
|
||||
});
|
||||
|
||||
describe("PropertyPanel — Style group (flag on)", () => {
|
||||
it(
|
||||
"renders the Style group for a style-editable, non-text element",
|
||||
async () => {
|
||||
const { host, root } = await renderPanel(true, styleOnlyElement());
|
||||
expect(host.textContent).toContain("Style");
|
||||
expect(host.textContent).toContain("Fill");
|
||||
act(() => root.unmount());
|
||||
},
|
||||
RENDER_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
it(
|
||||
"one-open accordion: opening Style closes Text",
|
||||
async () => {
|
||||
// baseElement() is text-editable and has capabilities.canEditStyles:
|
||||
// true, so both the Text and Style groups render for it.
|
||||
const { host, root } = await renderPanel(true);
|
||||
const textGroup = () => host.querySelector('[data-flat-group-open="true"]');
|
||||
expect(textGroup()?.textContent).toContain("Text");
|
||||
const styleCollapsedRow = Array.from(
|
||||
host.querySelectorAll('[data-flat-group-collapsed="true"]'),
|
||||
).find((el) => el.textContent?.includes("Style"));
|
||||
if (!styleCollapsedRow) throw new Error("expected a collapsed Style row");
|
||||
act(() => styleCollapsedRow.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
||||
expect(textGroup()?.textContent).not.toContain("Text");
|
||||
expect(host.querySelector('[data-flat-group-open="true"]')?.textContent).toContain("Style");
|
||||
act(() => root.unmount());
|
||||
},
|
||||
RENDER_TIMEOUT_MS,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -268,6 +268,7 @@ export const PropertyPanel = memo(function PropertyPanel(props: PropertyPanelPro
|
||||
return (
|
||||
<PropertyPanelFlat
|
||||
{...props}
|
||||
key={element.id ?? element.selector}
|
||||
element={element}
|
||||
styles={styles}
|
||||
sections={sections}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { PropertyPanelFlatHeader } from "./PropertyPanelFlatHeader";
|
||||
import { PropertyPanelFlatFooter } from "./PropertyPanelFlatFooter";
|
||||
import { FlatGroup } from "./propertyPanelFlatPrimitives";
|
||||
import { FlatTextSection } from "./propertyPanelFlatTextSection";
|
||||
import { FlatStyleSection } from "./propertyPanelFlatStyleSections";
|
||||
import { formatTextFieldPreview, StyleSections } from "./propertyPanelSections";
|
||||
import { TimingSection } from "./propertyPanelTimingSection";
|
||||
import { ColorGradingSection } from "./propertyPanelColorGradingSection";
|
||||
@@ -102,15 +103,25 @@ export function PropertyPanelFlat({
|
||||
clipboardCopied: boolean;
|
||||
onCopyElementInfo: () => void;
|
||||
}) {
|
||||
// Defaulting to "text" is harmless for a non-text element even though the
|
||||
// Text FlatGroup won't render (nothing else reads openGroupId yet) — this
|
||||
// only matters once a second FlatGroup exists (Plan 2+), at which point a
|
||||
// non-text element should default-open that group instead.
|
||||
const [openGroupId, setOpenGroupId] = useState<string>("text");
|
||||
// Lazy initializer: pick whichever group actually renders for this element
|
||||
// (Text if text-editable, else Style if style-editable, else none open) so a
|
||||
// style-only element doesn't start with everything collapsed. Only runs on
|
||||
// mount — PropertyPanel.tsx keys <PropertyPanelFlat> by element identity so
|
||||
// switching the selection re-mounts this component and re-derives the
|
||||
// default instead of preserving stale state across unrelated elements.
|
||||
const [openGroupId, setOpenGroupId] = useState<string>(() =>
|
||||
isTextEditableSelection(element) ? "text" : showEditableSections ? "style" : "",
|
||||
);
|
||||
const [pinnedGroupIds, setPinnedGroupIds] = useState<string[]>([]);
|
||||
|
||||
const isTextEditable = isTextEditableSelection(element);
|
||||
const elementKind = sections.media ? "media" : element.textFields.length > 0 ? "text" : "other";
|
||||
const toggleOpen = (groupId: string) =>
|
||||
setOpenGroupId((current) => (current === groupId ? "" : groupId));
|
||||
const togglePin = (groupId: string) =>
|
||||
setPinnedGroupIds((current) =>
|
||||
current.includes(groupId) ? current.filter((id) => id !== groupId) : [...current, groupId],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col overflow-hidden bg-panel-bg text-panel-text-1">
|
||||
@@ -136,14 +147,8 @@ export function PropertyPanelFlat({
|
||||
title="Text"
|
||||
isOpen={openGroupId === "text" || pinnedGroupIds.includes("text")}
|
||||
isPinned={pinnedGroupIds.includes("text")}
|
||||
onToggleOpen={() => setOpenGroupId((current) => (current === "text" ? "" : "text"))}
|
||||
onTogglePin={() =>
|
||||
setPinnedGroupIds((current) =>
|
||||
current.includes("text")
|
||||
? current.filter((id) => id !== "text")
|
||||
: [...current, "text"],
|
||||
)
|
||||
}
|
||||
onToggleOpen={() => toggleOpen("text")}
|
||||
onTogglePin={() => togglePin("text")}
|
||||
summary={formatTextFieldPreview(element.textFields[0]?.value ?? "")}
|
||||
>
|
||||
<FlatTextSection
|
||||
@@ -159,6 +164,27 @@ export function PropertyPanelFlat({
|
||||
</FlatGroup>
|
||||
)}
|
||||
|
||||
{showEditableSections && (
|
||||
<FlatGroup
|
||||
title="Style"
|
||||
isOpen={openGroupId === "style" || pinnedGroupIds.includes("style")}
|
||||
isPinned={pinnedGroupIds.includes("style")}
|
||||
onToggleOpen={() => toggleOpen("style")}
|
||||
onTogglePin={() => togglePin("style")}
|
||||
summary={`fill ${styles["background-image"] && styles["background-image"] !== "none" ? "image/gradient" : styles["background-color"] ? "set" : "none"} · ${Math.round((parseFloat(styles.opacity ?? "1") || 1) * 100)}%`}
|
||||
>
|
||||
<FlatStyleSection
|
||||
projectId={projectId}
|
||||
element={element}
|
||||
styles={styles}
|
||||
assets={assets}
|
||||
onSetStyle={onSetStyle}
|
||||
onImportAssets={onImportAssets}
|
||||
gsapBorderRadius={gsapBorderRadius}
|
||||
/>
|
||||
</FlatGroup>
|
||||
)}
|
||||
|
||||
{sections.timing && (
|
||||
<TimingSection
|
||||
element={element}
|
||||
|
||||
Reference in New Issue
Block a user