mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(studio): render pinned groups first with a persisted, per-element-kind pin zone
This commit is contained in:
@@ -18,6 +18,10 @@ vi.mock("../../contexts/StudioContext", async () => {
|
|||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
document.body.innerHTML = "";
|
document.body.innerHTML = "";
|
||||||
|
// usePersistedPinnedGroups persists to localStorage; clear it so a pinned
|
||||||
|
// group from one test can't leak into the next (which would move a group out
|
||||||
|
// of the accordion and break an unrelated open-by-default assertion).
|
||||||
|
window.localStorage.clear();
|
||||||
vi.doUnmock("./manualEditingAvailability");
|
vi.doUnmock("./manualEditingAvailability");
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
});
|
});
|
||||||
@@ -751,3 +755,44 @@ describe("PropertyPanel — Media group (Plan 4)", () => {
|
|||||||
RENDER_TIMEOUT_MS,
|
RENDER_TIMEOUT_MS,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("PropertyPanel — pinning", () => {
|
||||||
|
it(
|
||||||
|
"renders a pinned group first, always open, above the PinnedZoneDivider",
|
||||||
|
async () => {
|
||||||
|
const { host, root } = await renderPanel(true);
|
||||||
|
// Pin the Text group via its pin button.
|
||||||
|
const pinButton = host.querySelector<HTMLButtonElement>('[data-flat-group-pin="true"]');
|
||||||
|
if (!pinButton) throw new Error("expected a pin button on the open Text group");
|
||||||
|
act(() => pinButton.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
||||||
|
|
||||||
|
const pinnedRow = host.querySelector('[data-pinned-group="true"]');
|
||||||
|
expect(pinnedRow?.textContent).toContain("Text");
|
||||||
|
expect(pinnedRow?.textContent).toContain("Pinned");
|
||||||
|
|
||||||
|
// The divider must appear after the pinned zone.
|
||||||
|
const container = host.querySelector(".flex-1.overflow-y-auto");
|
||||||
|
const children = Array.from(container?.children ?? []);
|
||||||
|
const pinnedIndex = children.indexOf(pinnedRow as Element);
|
||||||
|
const dividerIndex = children.findIndex((el) => el.textContent?.includes("one open below"));
|
||||||
|
expect(pinnedIndex).toBeGreaterThanOrEqual(0);
|
||||||
|
expect(dividerIndex).toBeGreaterThan(pinnedIndex);
|
||||||
|
act(() => root.unmount());
|
||||||
|
},
|
||||||
|
RENDER_TIMEOUT_MS,
|
||||||
|
);
|
||||||
|
|
||||||
|
it(
|
||||||
|
"unpinning returns the group to its normal stack position and it closes",
|
||||||
|
async () => {
|
||||||
|
const { host, root } = await renderPanel(true);
|
||||||
|
const pinButton = host.querySelector<HTMLButtonElement>('[data-flat-group-pin="true"]');
|
||||||
|
act(() => pinButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
||||||
|
const unpinButton = host.querySelector<HTMLButtonElement>('[data-pinned-group-unpin="true"]');
|
||||||
|
act(() => unpinButton?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
|
||||||
|
expect(host.querySelector('[data-pinned-group="true"]')).toBeNull();
|
||||||
|
act(() => root.unmount());
|
||||||
|
},
|
||||||
|
RENDER_TIMEOUT_MS,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { type ReactNode, useState } from "react";
|
||||||
import { resolveEditingSections } from "@hyperframes/core/editing";
|
import { resolveEditingSections } from "@hyperframes/core/editing";
|
||||||
import type { DomEditSelection } from "./domEditing";
|
import type { DomEditSelection } from "./domEditing";
|
||||||
import { isTextEditableSelection } from "./domEditing";
|
import { isTextEditableSelection } from "./domEditing";
|
||||||
@@ -6,7 +6,7 @@ import type { PropertyPanelProps } from "./propertyPanelHelpers";
|
|||||||
import { formatPxMetricValue } from "./propertyPanelHelpers";
|
import { formatPxMetricValue } from "./propertyPanelHelpers";
|
||||||
import { PropertyPanelFlatHeader } from "./PropertyPanelFlatHeader";
|
import { PropertyPanelFlatHeader } from "./PropertyPanelFlatHeader";
|
||||||
import { PropertyPanelFlatFooter } from "./PropertyPanelFlatFooter";
|
import { PropertyPanelFlatFooter } from "./PropertyPanelFlatFooter";
|
||||||
import { FlatGroup } from "./propertyPanelFlatPrimitives";
|
import { FlatGroup, PinnedGroupRow, PinnedZoneDivider } from "./propertyPanelFlatPrimitives";
|
||||||
import { FlatTextSection } from "./propertyPanelFlatTextSection";
|
import { FlatTextSection } from "./propertyPanelFlatTextSection";
|
||||||
import { FlatStyleSection } from "./propertyPanelFlatStyleSections";
|
import { FlatStyleSection } from "./propertyPanelFlatStyleSections";
|
||||||
import { FlatLayoutSection } from "./propertyPanelFlatLayoutSection";
|
import { FlatLayoutSection } from "./propertyPanelFlatLayoutSection";
|
||||||
@@ -18,6 +18,7 @@ import { formatTextFieldPreview, StyleSections } from "./propertyPanelSections";
|
|||||||
import { STUDIO_GSAP_PANEL_ENABLED } from "./manualEditingAvailability";
|
import { STUDIO_GSAP_PANEL_ENABLED } from "./manualEditingAvailability";
|
||||||
import { ColorGradingSection } from "./propertyPanelColorGradingSection";
|
import { ColorGradingSection } from "./propertyPanelColorGradingSection";
|
||||||
import { useColorGradingController } from "./useColorGradingController";
|
import { useColorGradingController } from "./useColorGradingController";
|
||||||
|
import { usePersistedPinnedGroups } from "../../hooks/usePersistedPinnedGroups";
|
||||||
import {
|
import {
|
||||||
FlatColorGradingAccessory,
|
FlatColorGradingAccessory,
|
||||||
FlatColorGradingSection,
|
FlatColorGradingSection,
|
||||||
@@ -25,6 +26,14 @@ import {
|
|||||||
|
|
||||||
type EditingSections = ReturnType<typeof resolveEditingSections>;
|
type EditingSections = ReturnType<typeof resolveEditingSections>;
|
||||||
|
|
||||||
|
type FlatGroupDescriptor = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
summary?: string;
|
||||||
|
accessory?: ReactNode;
|
||||||
|
content: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
// Type-only fallback for the Motion effect-card callbacks. Used solely to
|
// Type-only fallback for the Motion effect-card callbacks. Used solely to
|
||||||
// satisfy FlatMotionSection's required-callback shape when the effect list is
|
// satisfy FlatMotionSection's required-callback shape when the effect list is
|
||||||
// gated off (showEffects === false, so none of these are ever invoked). Keeps
|
// gated off (showEffects === false, so none of these are ever invoked). Keeps
|
||||||
@@ -228,7 +237,6 @@ export function PropertyPanelFlat({
|
|||||||
? "media"
|
? "media"
|
||||||
: "layout",
|
: "layout",
|
||||||
);
|
);
|
||||||
const [pinnedGroupIds, setPinnedGroupIds] = useState<string[]>([]);
|
|
||||||
|
|
||||||
// Grade group state. Called unconditionally (React rules-of-hooks) even when
|
// Grade group state. Called unconditionally (React rules-of-hooks) even when
|
||||||
// sections.colorGrading is false — unlike the legacy ColorGradingSection,
|
// sections.colorGrading is false — unlike the legacy ColorGradingSection,
|
||||||
@@ -246,12 +254,9 @@ export function PropertyPanelFlat({
|
|||||||
|
|
||||||
const isTextEditable = isTextEditableSelection(element);
|
const isTextEditable = isTextEditableSelection(element);
|
||||||
const elementKind = sections.media ? "media" : element.textFields.length > 0 ? "text" : "other";
|
const elementKind = sections.media ? "media" : element.textFields.length > 0 ? "text" : "other";
|
||||||
|
const { pinnedGroupIds, togglePin } = usePersistedPinnedGroups(elementKind);
|
||||||
const toggleOpen = (groupId: string) =>
|
const toggleOpen = (groupId: string) =>
|
||||||
setOpenGroupId((current) => (current === groupId ? "" : groupId));
|
setOpenGroupId((current) => (current === groupId ? "" : groupId));
|
||||||
const togglePin = (groupId: string) =>
|
|
||||||
setPinnedGroupIds((current) =>
|
|
||||||
current.includes(groupId) ? current.filter((id) => id !== groupId) : [...current, groupId],
|
|
||||||
);
|
|
||||||
// Basis for the Layout keyframe gutter (X/Y/W/H/Angle + 3D Transform) —
|
// Basis for the Layout keyframe gutter (X/Y/W/H/Angle + 3D Transform) —
|
||||||
// must agree with Motion's Timing row (FlatTimingRow), which infers the
|
// must agree with Motion's Timing row (FlatTimingRow), which infers the
|
||||||
// range from animations when there's no explicit data-duration. Computed
|
// range from animations when there's no explicit data-duration. Computed
|
||||||
@@ -307,6 +312,152 @@ export function PropertyPanelFlat({
|
|||||||
const showMotionEffects = gsapEffectHandlers !== null;
|
const showMotionEffects = gsapEffectHandlers !== null;
|
||||||
const showMotionGroup = showMotionTiming || showMotionEffects;
|
const showMotionGroup = showMotionTiming || showMotionEffects;
|
||||||
|
|
||||||
|
// Ordered group descriptors — one per FlatGroup this panel renders, gated by
|
||||||
|
// the same conditions the inline JSX used. Partitioned into pinned/unpinned
|
||||||
|
// below so pinned groups render first (always open, no accordion) above the
|
||||||
|
// PinnedZoneDivider, with the rest in the one-open accordion beneath it.
|
||||||
|
const groups: FlatGroupDescriptor[] = [];
|
||||||
|
if (isTextEditable) {
|
||||||
|
groups.push({
|
||||||
|
id: "text",
|
||||||
|
title: "Text",
|
||||||
|
summary: formatTextFieldPreview(element.textFields[0]?.value ?? ""),
|
||||||
|
content: (
|
||||||
|
<FlatTextSection
|
||||||
|
element={element}
|
||||||
|
styles={styles}
|
||||||
|
fontAssets={fontAssets}
|
||||||
|
onImportFonts={onImportFonts}
|
||||||
|
onSetText={onSetText}
|
||||||
|
onSetTextFieldStyle={onSetTextFieldStyle}
|
||||||
|
onAddTextField={onAddTextField}
|
||||||
|
onRemoveTextField={onRemoveTextField}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (showEditableSections) {
|
||||||
|
groups.push({
|
||||||
|
id: "style",
|
||||||
|
title: "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)}%`,
|
||||||
|
content: (
|
||||||
|
<FlatStyleSection
|
||||||
|
projectId={projectId}
|
||||||
|
element={element}
|
||||||
|
styles={styles}
|
||||||
|
assets={assets}
|
||||||
|
onSetStyle={onSetStyle}
|
||||||
|
onImportAssets={onImportAssets}
|
||||||
|
gsapBorderRadius={gsapBorderRadius}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
groups.push({
|
||||||
|
id: "layout",
|
||||||
|
title: "Layout",
|
||||||
|
accessory: <span className="text-[9px] text-panel-text-5">drag values to scrub</span>,
|
||||||
|
summary: `${formatPxMetricValue(displayX)},${formatPxMetricValue(displayY)} · ${Math.round(displayW)}×${Math.round(displayH)}`,
|
||||||
|
content: (
|
||||||
|
<FlatLayoutSection
|
||||||
|
element={element}
|
||||||
|
styles={styles}
|
||||||
|
onSetStyle={onSetStyle}
|
||||||
|
disabled={!element.capabilities.canEditStyles}
|
||||||
|
displayX={displayX}
|
||||||
|
displayY={displayY}
|
||||||
|
displayW={displayW}
|
||||||
|
displayH={displayH}
|
||||||
|
displayR={displayR}
|
||||||
|
manualOffsetEditingDisabled={manualOffsetEditingDisabled}
|
||||||
|
manualSizeEditingDisabled={manualSizeEditingDisabled}
|
||||||
|
manualRotationEditingDisabled={manualRotationEditingDisabled}
|
||||||
|
commitManualOffset={commitManualOffset}
|
||||||
|
commitManualSize={commitManualSize}
|
||||||
|
commitManualRotation={commitManualRotation}
|
||||||
|
gsapAnimId={gsapAnimId}
|
||||||
|
navKeyframes={navKeyframes}
|
||||||
|
currentPct={currentPct}
|
||||||
|
seekFromKfPct={seekFromKfPct}
|
||||||
|
animIdForProp={animIdForProp}
|
||||||
|
resolveAnimIdForProp={animIdForProp}
|
||||||
|
gsapRuntimeValues={gsapRuntimeValues}
|
||||||
|
gsapKeyframes={navKeyframes}
|
||||||
|
elStart={elStart}
|
||||||
|
elDuration={elDuration}
|
||||||
|
onCommitAnimatedProperty={onCommitAnimatedProperty}
|
||||||
|
onCommitAnimatedProperties={onCommitAnimatedProperties}
|
||||||
|
onSeekToTime={onSeekToTime}
|
||||||
|
onRemoveKeyframe={onRemoveKeyframe}
|
||||||
|
onConvertToKeyframes={onConvertToKeyframes}
|
||||||
|
onLivePreviewProps={createGsapLivePreview(previewIframeRef ?? { current: null })}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
if (showMotionGroup) {
|
||||||
|
groups.push({
|
||||||
|
id: "motion",
|
||||||
|
title: "Motion",
|
||||||
|
summary: `${gsapAnimations.length} effect${gsapAnimations.length === 1 ? "" : "s"}`,
|
||||||
|
content: (
|
||||||
|
<FlatMotionSection
|
||||||
|
element={element}
|
||||||
|
animations={gsapAnimations}
|
||||||
|
showTiming={showMotionTiming}
|
||||||
|
showEffects={showMotionEffects}
|
||||||
|
multipleTimelines={gsapMultipleTimelines}
|
||||||
|
unsupportedTimelinePattern={gsapUnsupportedTimelinePattern}
|
||||||
|
onSetAttribute={onSetAttribute}
|
||||||
|
{...(gsapEffectHandlers ?? EMPTY_GSAP_EFFECT_HANDLERS)}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (sections.colorGrading) {
|
||||||
|
groups.push({
|
||||||
|
id: "grade",
|
||||||
|
title: "Grade",
|
||||||
|
accessory: <FlatColorGradingAccessory state={colorGradingController} />,
|
||||||
|
summary: `${colorGradingController.grading.preset ?? "neutral"} · ${Math.round(colorGradingController.grading.intensity * 100)}%`,
|
||||||
|
content: (
|
||||||
|
<FlatColorGradingSection
|
||||||
|
grading={colorGradingController.grading}
|
||||||
|
assets={assets}
|
||||||
|
onImportAssets={onImportAssets}
|
||||||
|
onCommitColorGrading={colorGradingController.commitColorGrading}
|
||||||
|
applyScope={colorGradingController.applyScope}
|
||||||
|
applyBusy={colorGradingController.applyBusy}
|
||||||
|
onSetApplyScope={colorGradingController.setApplyScope}
|
||||||
|
onApplyToScope={() => void colorGradingController.applyToScope()}
|
||||||
|
onApplyScopeAvailable={Boolean(onApplyColorGradingScope)}
|
||||||
|
mediaMetadata={colorGradingController.mediaMetadata}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (sections.media) {
|
||||||
|
groups.push({
|
||||||
|
id: "media",
|
||||||
|
title: "Media",
|
||||||
|
summary: element.tagName,
|
||||||
|
content: (
|
||||||
|
<FlatMediaSection
|
||||||
|
projectDir={projectDir}
|
||||||
|
element={element}
|
||||||
|
styles={styles}
|
||||||
|
onSetStyle={onSetStyle}
|
||||||
|
onSetAttribute={onSetAttribute}
|
||||||
|
onSetHtmlAttribute={onSetHtmlAttribute}
|
||||||
|
onRemoveBackground={onRemoveBackground}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const pinned = groups.filter((g) => pinnedGroupIds.includes(g.id));
|
||||||
|
const unpinned = groups.filter((g) => !pinnedGroupIds.includes(g.id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-0 flex-col overflow-hidden bg-panel-bg text-panel-text-1">
|
<div className="flex h-full min-h-0 flex-col overflow-hidden bg-panel-bg text-panel-text-1">
|
||||||
<PropertyPanelFlatHeader
|
<PropertyPanelFlatHeader
|
||||||
@@ -326,138 +477,31 @@ export function PropertyPanelFlat({
|
|||||||
showUngroup={Boolean(onUngroup && element.dataAttributes["hf-group"] != null)}
|
showUngroup={Boolean(onUngroup && element.dataAttributes["hf-group"] != null)}
|
||||||
/>
|
/>
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
{isTextEditable && (
|
{pinned.map((g) => (
|
||||||
<FlatGroup
|
<PinnedGroupRow
|
||||||
title="Text"
|
key={g.id}
|
||||||
isOpen={openGroupId === "text" || pinnedGroupIds.includes("text")}
|
title={g.title}
|
||||||
isPinned={pinnedGroupIds.includes("text")}
|
accessory={g.accessory}
|
||||||
onToggleOpen={() => toggleOpen("text")}
|
onUnpin={() => togglePin(g.id)}
|
||||||
onTogglePin={() => togglePin("text")}
|
|
||||||
summary={formatTextFieldPreview(element.textFields[0]?.value ?? "")}
|
|
||||||
>
|
>
|
||||||
<FlatTextSection
|
{g.content}
|
||||||
element={element}
|
</PinnedGroupRow>
|
||||||
styles={styles}
|
))}
|
||||||
fontAssets={fontAssets}
|
{pinned.length > 0 && unpinned.length > 0 && <PinnedZoneDivider />}
|
||||||
onImportFonts={onImportFonts}
|
{unpinned.map((g) => (
|
||||||
onSetText={onSetText}
|
|
||||||
onSetTextFieldStyle={onSetTextFieldStyle}
|
|
||||||
onAddTextField={onAddTextField}
|
|
||||||
onRemoveTextField={onRemoveTextField}
|
|
||||||
/>
|
|
||||||
</FlatGroup>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showEditableSections && (
|
|
||||||
<FlatGroup
|
<FlatGroup
|
||||||
title="Style"
|
key={g.id}
|
||||||
isOpen={openGroupId === "style" || pinnedGroupIds.includes("style")}
|
title={g.title}
|
||||||
isPinned={pinnedGroupIds.includes("style")}
|
isOpen={openGroupId === g.id}
|
||||||
onToggleOpen={() => toggleOpen("style")}
|
isPinned={false}
|
||||||
onTogglePin={() => togglePin("style")}
|
onToggleOpen={() => toggleOpen(g.id)}
|
||||||
summary={`fill ${styles["background-image"] && styles["background-image"] !== "none" ? "image/gradient" : styles["background-color"] ? "set" : "none"} · ${Math.round((parseFloat(styles.opacity ?? "1") || 1) * 100)}%`}
|
onTogglePin={() => togglePin(g.id)}
|
||||||
|
summary={g.summary}
|
||||||
|
accessory={g.accessory}
|
||||||
>
|
>
|
||||||
<FlatStyleSection
|
{g.content}
|
||||||
projectId={projectId}
|
|
||||||
element={element}
|
|
||||||
styles={styles}
|
|
||||||
assets={assets}
|
|
||||||
onSetStyle={onSetStyle}
|
|
||||||
onImportAssets={onImportAssets}
|
|
||||||
gsapBorderRadius={gsapBorderRadius}
|
|
||||||
/>
|
|
||||||
</FlatGroup>
|
</FlatGroup>
|
||||||
)}
|
))}
|
||||||
|
|
||||||
<FlatGroup
|
|
||||||
title="Layout"
|
|
||||||
isOpen={openGroupId === "layout" || pinnedGroupIds.includes("layout")}
|
|
||||||
isPinned={pinnedGroupIds.includes("layout")}
|
|
||||||
onToggleOpen={() => toggleOpen("layout")}
|
|
||||||
onTogglePin={() => togglePin("layout")}
|
|
||||||
accessory={<span className="text-[9px] text-panel-text-5">drag values to scrub</span>}
|
|
||||||
summary={`${formatPxMetricValue(displayX)},${formatPxMetricValue(displayY)} · ${Math.round(displayW)}×${Math.round(displayH)}`}
|
|
||||||
>
|
|
||||||
<FlatLayoutSection
|
|
||||||
element={element}
|
|
||||||
styles={styles}
|
|
||||||
onSetStyle={onSetStyle}
|
|
||||||
disabled={!element.capabilities.canEditStyles}
|
|
||||||
displayX={displayX}
|
|
||||||
displayY={displayY}
|
|
||||||
displayW={displayW}
|
|
||||||
displayH={displayH}
|
|
||||||
displayR={displayR}
|
|
||||||
manualOffsetEditingDisabled={manualOffsetEditingDisabled}
|
|
||||||
manualSizeEditingDisabled={manualSizeEditingDisabled}
|
|
||||||
manualRotationEditingDisabled={manualRotationEditingDisabled}
|
|
||||||
commitManualOffset={commitManualOffset}
|
|
||||||
commitManualSize={commitManualSize}
|
|
||||||
commitManualRotation={commitManualRotation}
|
|
||||||
gsapAnimId={gsapAnimId}
|
|
||||||
navKeyframes={navKeyframes}
|
|
||||||
currentPct={currentPct}
|
|
||||||
seekFromKfPct={seekFromKfPct}
|
|
||||||
animIdForProp={animIdForProp}
|
|
||||||
resolveAnimIdForProp={animIdForProp}
|
|
||||||
gsapRuntimeValues={gsapRuntimeValues}
|
|
||||||
gsapKeyframes={navKeyframes}
|
|
||||||
elStart={elStart}
|
|
||||||
elDuration={elDuration}
|
|
||||||
onCommitAnimatedProperty={onCommitAnimatedProperty}
|
|
||||||
onCommitAnimatedProperties={onCommitAnimatedProperties}
|
|
||||||
onSeekToTime={onSeekToTime}
|
|
||||||
onRemoveKeyframe={onRemoveKeyframe}
|
|
||||||
onConvertToKeyframes={onConvertToKeyframes}
|
|
||||||
onLivePreviewProps={createGsapLivePreview(previewIframeRef ?? { current: null })}
|
|
||||||
/>
|
|
||||||
</FlatGroup>
|
|
||||||
|
|
||||||
{showMotionGroup && (
|
|
||||||
<FlatGroup
|
|
||||||
title="Motion"
|
|
||||||
isOpen={openGroupId === "motion" || pinnedGroupIds.includes("motion")}
|
|
||||||
isPinned={pinnedGroupIds.includes("motion")}
|
|
||||||
onToggleOpen={() => toggleOpen("motion")}
|
|
||||||
onTogglePin={() => togglePin("motion")}
|
|
||||||
summary={`${gsapAnimations.length} effect${gsapAnimations.length === 1 ? "" : "s"}`}
|
|
||||||
>
|
|
||||||
<FlatMotionSection
|
|
||||||
element={element}
|
|
||||||
animations={gsapAnimations}
|
|
||||||
showTiming={showMotionTiming}
|
|
||||||
showEffects={showMotionEffects}
|
|
||||||
multipleTimelines={gsapMultipleTimelines}
|
|
||||||
unsupportedTimelinePattern={gsapUnsupportedTimelinePattern}
|
|
||||||
onSetAttribute={onSetAttribute}
|
|
||||||
{...(gsapEffectHandlers ?? EMPTY_GSAP_EFFECT_HANDLERS)}
|
|
||||||
/>
|
|
||||||
</FlatGroup>
|
|
||||||
)}
|
|
||||||
{sections.colorGrading && (
|
|
||||||
<FlatGroup
|
|
||||||
title="Grade"
|
|
||||||
isOpen={openGroupId === "grade" || pinnedGroupIds.includes("grade")}
|
|
||||||
isPinned={pinnedGroupIds.includes("grade")}
|
|
||||||
onToggleOpen={() => toggleOpen("grade")}
|
|
||||||
onTogglePin={() => togglePin("grade")}
|
|
||||||
accessory={<FlatColorGradingAccessory state={colorGradingController} />}
|
|
||||||
summary={`${colorGradingController.grading.preset ?? "neutral"} · ${Math.round(colorGradingController.grading.intensity * 100)}%`}
|
|
||||||
>
|
|
||||||
<FlatColorGradingSection
|
|
||||||
grading={colorGradingController.grading}
|
|
||||||
assets={assets}
|
|
||||||
onImportAssets={onImportAssets}
|
|
||||||
onCommitColorGrading={colorGradingController.commitColorGrading}
|
|
||||||
applyScope={colorGradingController.applyScope}
|
|
||||||
applyBusy={colorGradingController.applyBusy}
|
|
||||||
onSetApplyScope={colorGradingController.setApplyScope}
|
|
||||||
onApplyToScope={() => void colorGradingController.applyToScope()}
|
|
||||||
onApplyScopeAvailable={Boolean(onApplyColorGradingScope)}
|
|
||||||
mediaMetadata={colorGradingController.mediaMetadata}
|
|
||||||
/>
|
|
||||||
</FlatGroup>
|
|
||||||
)}
|
|
||||||
{sections.colorGrading && (
|
{sections.colorGrading && (
|
||||||
<ColorGradingSection
|
<ColorGradingSection
|
||||||
key={[
|
key={[
|
||||||
@@ -475,26 +519,6 @@ export function PropertyPanelFlat({
|
|||||||
onApplyScope={onApplyColorGradingScope}
|
onApplyScope={onApplyColorGradingScope}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{sections.media && (
|
|
||||||
<FlatGroup
|
|
||||||
title="Media"
|
|
||||||
isOpen={openGroupId === "media" || pinnedGroupIds.includes("media")}
|
|
||||||
isPinned={pinnedGroupIds.includes("media")}
|
|
||||||
onToggleOpen={() => toggleOpen("media")}
|
|
||||||
onTogglePin={() => togglePin("media")}
|
|
||||||
summary={element.tagName}
|
|
||||||
>
|
|
||||||
<FlatMediaSection
|
|
||||||
projectDir={projectDir}
|
|
||||||
element={element}
|
|
||||||
styles={styles}
|
|
||||||
onSetStyle={onSetStyle}
|
|
||||||
onSetAttribute={onSetAttribute}
|
|
||||||
onSetHtmlAttribute={onSetHtmlAttribute}
|
|
||||||
onRemoveBackground={onRemoveBackground}
|
|
||||||
/>
|
|
||||||
</FlatGroup>
|
|
||||||
)}
|
|
||||||
{showEditableSections && (
|
{showEditableSections && (
|
||||||
<StyleSections
|
<StyleSections
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
|
|||||||
Reference in New Issue
Block a user