chore: import upstream snapshot with attribution
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:35 +08:00
commit 85453da49f
4031 changed files with 710987 additions and 0 deletions
@@ -0,0 +1,590 @@
import { memo, useEffect, useMemo, useRef, useState } from "react";
import { Move } from "../../icons/SystemIcons";
import { Eye, EyeSlash } from "@phosphor-icons/react";
import { InspectorHeaderActions } from "./InspectorHeaderActions";
import { useStudioShellContext } from "../../contexts/StudioContext";
import { readStudioBoxSize, readStudioPathOffset, readStudioRotation } from "./manualEdits";
import {
EMPTY_STYLES,
formatPxMetricValue,
parsePxMetricValue,
RESPONSIVE_GRID,
readGsapRuntimeValuesForPanel,
readGsapBorderRadiusForPanel,
isSelectedElementHidden,
} from "./propertyPanelHelpers";
import { MetricField, Section } from "./propertyPanelPrimitives";
import { createTransformCommitHandlers } from "./propertyPanelTransformCommit";
import { classifyPropertyGroup } from "@hyperframes/core/gsap-parser";
import { resolveEditingSections } from "@hyperframes/core/editing";
import { MediaSection } from "./propertyPanelMediaSection";
import { ColorGradingSection } from "./propertyPanelColorGradingSection";
import { domEditSelectionToFacts } from "./domEditingLayers";
import { TextSection, StyleSections } from "./propertyPanelSections";
import { GsapAnimationSection } from "./GsapAnimationSection";
import { PropertyPanel3dTransform } from "./propertyPanel3dTransform";
import { KeyframeNavigation } from "./KeyframeNavigation";
import { STUDIO_GSAP_PANEL_ENABLED, STUDIO_KEYFRAMES_ENABLED } from "./manualEditingAvailability";
import { usePlayerStore, liveTime } from "../../player";
import { TimingSection } from "./propertyPanelTimingSection";
import { type PropertyPanelProps } from "./propertyPanelHelpers";
import { GestureRecordPanelButton } from "./GestureRecordControl";
import { PropertyPanelEmptyState } from "./PropertyPanelEmptyState";
// Re-export helpers that external consumers import from this module
export {
buildInsetClipPathSides,
buildStrokeStyleUpdates,
buildStrokeWidthStyleUpdates,
getCssFilterFunctionPx,
getClipPathInsetPx,
inferBoxShadowPreset,
inferClipPathPreset,
normalizePanelPxValue,
parseInsetClipPathSides,
setCssFilterFunctionPx,
} from "./propertyPanelHelpers";
// fallow-ignore-next-line complexity
export const PropertyPanel = memo(function PropertyPanel({
projectId,
projectDir,
assets,
element,
multiSelectCount = 0,
copiedAgentPrompt: _copiedAgentPrompt,
onClearSelection,
onUngroup,
onSetStyle,
onSetAttribute,
onSetAttributeLive,
onApplyColorGradingScope,
onSetHtmlAttribute,
onRemoveBackground,
onSetManualOffset,
onSetManualSize,
onSetManualRotation,
onSetText,
onSetTextFieldStyle,
onAddTextField,
onRemoveTextField,
onAskAgent: _onAskAgent,
onToggleElementHidden,
onImportAssets,
fontAssets = [],
onImportFonts,
previewIframeRef,
gsapAnimations = [],
gsapMultipleTimelines,
gsapUnsupportedTimelinePattern,
onUpdateGsapProperty,
onUpdateGsapMeta,
onDeleteGsapAnimation,
onAddGsapProperty,
onRemoveGsapProperty,
onUpdateGsapFromProperty,
onAddGsapFromProperty,
onRemoveGsapFromProperty,
onAddGsapAnimation,
onSetArcPath,
onUpdateArcSegment,
onUnroll,
onUpdateKeyframeEase,
onSetAllKeyframeEases,
onAddKeyframe,
onRemoveKeyframe,
onConvertToKeyframes,
onCommitAnimatedProperty,
onCommitAnimatedProperties,
onSeekToTime,
recordingState,
recordingDuration,
onToggleRecording,
}: PropertyPanelProps) {
const styles = element?.computedStyles ?? EMPTY_STYLES;
const { showToast } = useStudioShellContext();
const [clipboardCopied, setClipboardCopied] = useState(false);
const clipboardTimerRef = useRef<ReturnType<typeof setTimeout>>(undefined);
const storeTime = usePlayerStore((s) => s.currentTime);
const isPlaying = usePlayerStore((s) => s.isPlaying);
const timelineElements = usePlayerStore((s) => s.elements);
const selectedElementId = usePlayerStore((s) => s.selectedElementId);
const selectedElementHidden = isSelectedElementHidden(timelineElements, selectedElementId);
const visibilityToggleLabel = selectedElementHidden ? "Show element" : "Hide element";
const liveTimeRef = useRef(storeTime);
const [, forceRender] = useState(0);
useEffect(() => {
if (!isPlaying) return;
let timerId: ReturnType<typeof setTimeout> | 0 = 0;
const unsub = liveTime.subscribe((t) => {
liveTimeRef.current = t;
if (!timerId)
timerId = setTimeout(() => {
timerId = 0;
forceRender((v) => v + 1);
}, 33);
});
return () => {
unsub();
if (timerId) clearTimeout(timerId);
};
}, [isPlaying]);
const currentTime = isPlaying ? liveTimeRef.current : storeTime;
const cacheElementKey = element?.id ?? element?.selector ?? "";
const cacheEntry = usePlayerStore((s) => s.keyframeCache.get(cacheElementKey));
const iframeRef = previewIframeRef ?? { current: null };
const gsapAnimIdForMemo = element
? (gsapAnimations?.find((a: { keyframes?: unknown }) => a.keyframes)?.id ??
gsapAnimations?.[0]?.id ??
null)
: null;
const gsapRuntimeValues = useMemo(
() =>
element
? readGsapRuntimeValuesForPanel(gsapAnimIdForMemo, gsapAnimations, element, iframeRef)
: null,
// eslint-disable-next-line react-hooks/exhaustive-deps -- iframeRef is stable; currentTime drives re-reads during playback
[gsapAnimIdForMemo, gsapAnimations, element, currentTime],
);
const gsapBorderRadius = useMemo(
() =>
element
? readGsapBorderRadiusForPanel(gsapRuntimeValues, gsapAnimations, element, iframeRef)
: null,
// eslint-disable-next-line react-hooks/exhaustive-deps
[gsapRuntimeValues, gsapAnimations, element, currentTime],
);
// The 3D Transform panel should be reachable on ANY element, not only ones GSAP is
// already animating — otherwise you can't add depth/rotation to a fresh static
// element (the panel never appears, the classic chicken-and-egg). Default to
// identity when there are no runtime values yet; the first edit creates the
// gsap.set via commitStaticSet, after which real runtime values flow in.
const gsap3dValues: Record<string, number> = gsapRuntimeValues ?? {
rotationX: 0,
rotationY: 0,
rotationZ: 0,
z: 0,
scale: 1,
transformPerspective: 0,
};
if (!element) {
return <PropertyPanelEmptyState multiSelectCount={multiSelectCount} />;
}
const manualOffsetEditingDisabled = !element.capabilities.canApplyManualOffset;
const manualSizeEditingDisabled = !element.capabilities.canApplyManualSize;
const manualRotationEditingDisabled = !element.capabilities.canApplyManualRotation;
const sourceLabel = element.id ? `#${element.id}` : element.selector;
const showEditableSections = element.capabilities.canEditStyles;
// Capabilities are already resolved on the selection; recompute only sections,
// feeding the live GSAP tween count (arrives on the gsapAnimations prop, not the
// selection) so the Timing section shows for pure-GSAP elements with no data-start.
const sections = resolveEditingSections(domEditSelectionToFacts(element, gsapAnimations.length));
const manualOffset = readStudioPathOffset(element.element);
const manualSize = readStudioBoxSize(element.element);
const resolvedWidth =
manualSize.width > 0
? manualSize.width
: (parsePxMetricValue(styles.width ?? "") ?? element.boundingBox.width);
const resolvedHeight =
manualSize.height > 0
? manualSize.height
: (parsePxMetricValue(styles.height ?? "") ?? element.boundingBox.height);
const manualRotation = readStudioRotation(element.element);
const elStart = Number.parseFloat(element?.dataAttributes?.start ?? "0") || 0;
const elDuration = Number.parseFloat(element?.dataAttributes?.duration ?? "1") || 0;
const currentPct = elDuration > 0 ? ((currentTime - elStart) / elDuration) * 100 : 0;
const gsapKfAnim = gsapAnimations?.find((a) => a.keyframes) ?? null;
const gsapKeyframes = gsapKfAnim?.keyframes?.keyframes ?? null;
const gsapAnimId = gsapKfAnim?.id ?? gsapAnimations?.[0]?.id ?? null;
const hasGsapAnimation = !!(gsapAnimId || gsapAnimations.length > 0);
const { commitManualOffset, commitManualSize, commitManualRotation } =
createTransformCommitHandlers({
element,
styles,
hasGsapAnimation,
gsapAnimId,
gsapKeyframes,
currentPct,
onCommitAnimatedProperty,
onAddKeyframe,
onSetManualOffset,
onSetManualSize,
onSetManualRotation,
showToast,
});
const navKeyframes = cacheEntry?.keyframes ?? gsapKeyframes;
const seekFromKfPct = (pct: number) => onSeekToTime?.(elStart + (pct / 100) * elDuration);
const animIdForProp = (prop: string): string => {
const group = classifyPropertyGroup(prop);
const groupAnim = gsapAnimations?.find((a) => a.propertyGroup === group);
if (groupAnim) return groupAnim.id;
return gsapAnimId ?? "";
};
const displayX = gsapRuntimeValues?.x ?? manualOffset.x;
const displayY = gsapRuntimeValues?.y ?? manualOffset.y;
const displayW = gsapRuntimeValues?.width ?? resolvedWidth;
const displayH = gsapRuntimeValues?.height ?? resolvedHeight;
const displayR = gsapRuntimeValues?.rotation ?? manualRotation.angle;
// fallow-ignore-next-line complexity
const handleCopyElementInfo = () => {
const file = element.sourceFile ?? "index.html";
let lineNum: number | null = null;
try {
const src = previewIframeRef?.current?.contentDocument?.documentElement?.outerHTML ?? "";
if (src && element.id) {
const idx = src.indexOf(`id="${element.id}"`);
if (idx > -1) lineNum = src.slice(0, idx).split("\n").length;
}
if (!lineNum && element.selector) {
const tag = element.tagName.toLowerCase();
const cls = element.selector.startsWith(".")
? element.selector.slice(1).split(".")[0]
: null;
const search = cls ? `class="${cls}` : `<${tag}`;
const idx = src.indexOf(search);
if (idx > -1) lineNum = src.slice(0, idx).split("\n").length;
}
} catch {}
const fileLoc = lineNum ? `${file}:${lineNum}` : file;
const lines = [
`Element: ${element.label} (${sourceLabel})`,
`File: ${fileLoc}`,
`Position: x=${Math.round(element.boundingBox.x)}, y=${Math.round(element.boundingBox.y)}`,
`Size: ${Math.round(element.boundingBox.width)}×${Math.round(element.boundingBox.height)}`,
`Tag: <${element.tagName}>`,
];
if (element.computedStyles["z-index"] && element.computedStyles["z-index"] !== "auto") {
lines.push(`Z-index: ${element.computedStyles["z-index"]}`);
}
if (gsapAnimations.length > 0) {
const anim = gsapAnimations[0];
lines.push(
`Animation: ${anim.method}() ${anim.duration}s at ${anim.position}s, ease: ${anim.ease ?? "default"}`,
);
const props = Object.entries(anim.properties)
.map(([k, v]) => `${k}: ${v}`)
.join(", ");
if (props) lines.push(`Properties: ${props}`);
}
const text = lines.join("\n");
void navigator.clipboard.writeText(text);
showToast(`Copied element info for ${element.label} — paste into any AI agent`, "info");
setClipboardCopied(true);
clearTimeout(clipboardTimerRef.current);
clipboardTimerRef.current = setTimeout(() => setClipboardCopied(false), 1500);
};
return (
<div className="flex h-full min-h-0 flex-col overflow-hidden bg-panel-bg text-panel-text-1">
<div className="px-4 py-3">
<div className="flex items-center justify-between gap-4">
<div className="min-w-0">
<div className="truncate text-[13px] font-semibold text-neutral-100">
{element.label}
</div>
<div className="mt-0.5 truncate text-[11px] text-neutral-500">{sourceLabel}</div>
</div>
<div className="flex items-center gap-1">
{selectedElementId && onToggleElementHidden && (
<button
type="button"
aria-label={visibilityToggleLabel}
title={visibilityToggleLabel}
onClick={() => {
void onToggleElementHidden(selectedElementId, !selectedElementHidden);
}}
className="flex h-6 w-6 items-center justify-center rounded text-neutral-500 transition-colors hover:bg-neutral-800 hover:text-neutral-300"
>
{selectedElementHidden ? (
<EyeSlash size={13} weight="bold" aria-hidden="true" />
) : (
<Eye size={13} weight="bold" aria-hidden="true" />
)}
</button>
)}
<InspectorHeaderActions
element={element}
copied={clipboardCopied}
onCopy={handleCopyElementInfo}
onClear={onClearSelection}
onUngroup={onUngroup}
/>
</div>
</div>
</div>
<div className="flex-1 overflow-y-auto">
{onToggleRecording && (
<GestureRecordPanelButton
recordingState={recordingState}
recordingDuration={recordingDuration}
onToggleRecording={onToggleRecording}
/>
)}
<TextSection
element={element}
styles={styles}
fontAssets={fontAssets}
onImportFonts={onImportFonts}
onSetText={onSetText}
onSetTextFieldStyle={onSetTextFieldStyle}
onAddTextField={onAddTextField}
onRemoveTextField={onRemoveTextField}
/>
{sections.timing && (
// Render whenever there's an authored clip range OR animations to infer
// one from — a pure-GSAP element with no data-start still gets a Timing
// range (TimingSection derives it from its tweens).
<TimingSection
element={element}
animations={gsapAnimations}
onSetAttribute={onSetAttribute}
/>
)}
{sections.colorGrading && (
<ColorGradingSection
key={[
element.id ?? "",
element.hfId ?? "",
element.selector ?? "",
String(element.selectorIndex ?? ""),
].join("|")}
projectId={projectId}
element={element}
assets={assets}
previewIframeRef={previewIframeRef}
onImportAssets={onImportAssets}
onSetAttributeLive={onSetAttributeLive}
onApplyScope={onApplyColorGradingScope}
/>
)}
{sections.media && (
<MediaSection
projectDir={projectDir}
element={element}
styles={styles}
onSetStyle={onSetStyle}
onSetAttribute={onSetAttribute}
onSetHtmlAttribute={onSetHtmlAttribute}
onRemoveBackground={onRemoveBackground}
/>
)}
<Section title="Layout" icon={<Move size={15} />}>
<div className={RESPONSIVE_GRID}>
<div className="flex items-center gap-1">
<div className="flex-1">
<MetricField
label="X"
value={formatPxMetricValue(displayX)}
disabled={manualOffsetEditingDisabled}
scrub
onCommit={(next) => commitManualOffset("x", next)}
/>
</div>
{STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
<KeyframeNavigation
property="x"
keyframes={navKeyframes}
currentPercentage={currentPct}
onSeek={seekFromKfPct}
onAddKeyframe={() =>
onCommitAnimatedProperty &&
void onCommitAnimatedProperty(element, "x", displayX)
}
onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("x"), pct)}
onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("x"))}
/>
)}
</div>
<div className="flex items-center gap-1">
<div className="flex-1">
<MetricField
label="Y"
value={formatPxMetricValue(displayY)}
disabled={manualOffsetEditingDisabled}
scrub
onCommit={(next) => commitManualOffset("y", next)}
/>
</div>
{STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
<KeyframeNavigation
property="y"
keyframes={navKeyframes}
currentPercentage={currentPct}
onSeek={seekFromKfPct}
onAddKeyframe={() =>
onCommitAnimatedProperty &&
void onCommitAnimatedProperty(element, "y", displayY)
}
onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("y"), pct)}
onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("y"))}
/>
)}
</div>
<div className="flex items-center gap-1">
<div className="flex-1">
<MetricField
label="W"
value={formatPxMetricValue(displayW)}
disabled={manualSizeEditingDisabled}
scrub
onCommit={(next) => commitManualSize("width", next)}
/>
</div>
{STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
<KeyframeNavigation
property="width"
keyframes={navKeyframes}
currentPercentage={currentPct}
onSeek={seekFromKfPct}
onAddKeyframe={() =>
onCommitAnimatedProperty &&
void onCommitAnimatedProperty(element, "width", displayW)
}
onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("width"), pct)}
onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("width"))}
/>
)}
</div>
<div className="flex items-center gap-1">
<div className="flex-1">
<MetricField
label="H"
value={formatPxMetricValue(displayH)}
disabled={manualSizeEditingDisabled}
scrub
onCommit={(next) => commitManualSize("height", next)}
/>
</div>
{STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
<KeyframeNavigation
property="height"
keyframes={navKeyframes}
currentPercentage={currentPct}
onSeek={seekFromKfPct}
onAddKeyframe={() =>
onCommitAnimatedProperty &&
void onCommitAnimatedProperty(element, "height", displayH)
}
onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("height"), pct)}
onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("height"))}
/>
)}
</div>
<div className="flex items-center gap-1">
<div className="flex-1">
<MetricField
label="R"
value={`${displayR}°`}
disabled={manualRotationEditingDisabled}
onCommit={(next) => commitManualRotation(next.replace("°", ""))}
/>
</div>
{STUDIO_KEYFRAMES_ENABLED && gsapAnimId && (
<KeyframeNavigation
property="rotation"
keyframes={navKeyframes}
currentPercentage={currentPct}
onSeek={seekFromKfPct}
onAddKeyframe={() =>
onCommitAnimatedProperty &&
void onCommitAnimatedProperty(element, "rotation", displayR)
}
onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp("rotation"), pct)}
onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp("rotation"))}
/>
)}
</div>
</div>
<PropertyPanel3dTransform
gsapRuntimeValues={gsap3dValues}
gsapAnimId={gsapAnimId}
resolveAnimIdForProp={animIdForProp}
gsapKeyframes={navKeyframes}
currentPct={currentPct}
elStart={elStart}
elDuration={elDuration}
element={element}
onCommitAnimatedProperty={onCommitAnimatedProperty}
onCommitAnimatedProperties={onCommitAnimatedProperties}
onSeekToTime={onSeekToTime}
onRemoveKeyframe={onRemoveKeyframe}
onConvertToKeyframes={onConvertToKeyframes}
onLivePreviewProps={(el, props) => {
const iframe = iframeRef.current;
const win = iframe?.contentWindow as
| { gsap?: { set: (t: Element, v: Record<string, number>) => void } }
| null
| undefined;
const sel = el.id ? `#${el.id}` : el.selector;
const node = sel ? iframe?.contentDocument?.querySelector(sel) : null;
if (win?.gsap && node) win.gsap.set(node, props);
}}
/>
<div className="mt-3">
<div className="mb-2 text-[10px] font-medium uppercase tracking-wider text-neutral-600">
Stacking
</div>
<MetricField
label="Z-index"
value={String(parseInt(styles["z-index"] || "auto", 10) || 0)}
scrub
onCommit={(next) => onSetStyle("z-index", next)}
/>
</div>
</Section>
{STUDIO_GSAP_PANEL_ENABLED &&
onUpdateGsapProperty &&
onUpdateGsapMeta &&
onDeleteGsapAnimation &&
onAddGsapProperty &&
onAddGsapAnimation && (
<GsapAnimationSection
animations={gsapAnimations}
multipleTimelines={gsapMultipleTimelines}
unsupportedTimelinePattern={gsapUnsupportedTimelinePattern}
onUpdateProperty={onUpdateGsapProperty}
onUpdateMeta={onUpdateGsapMeta}
onDeleteAnimation={onDeleteGsapAnimation}
onAddProperty={onAddGsapProperty}
onRemoveProperty={onRemoveGsapProperty ?? (() => {})}
onUpdateFromProperty={onUpdateGsapFromProperty}
onAddFromProperty={onAddGsapFromProperty}
onRemoveFromProperty={onRemoveGsapFromProperty}
onAddAnimation={onAddGsapAnimation}
onSetArcPath={onSetArcPath}
onUpdateArcSegment={onUpdateArcSegment}
onUnroll={onUnroll}
onUpdateKeyframeEase={onUpdateKeyframeEase}
onSetAllKeyframeEases={onSetAllKeyframeEases}
/>
)}
{showEditableSections && (
<StyleSections
projectId={projectId}
element={element}
styles={styles}
assets={assets}
onSetStyle={onSetStyle}
onImportAssets={onImportAssets}
gsapBorderRadius={gsapBorderRadius}
/>
)}
</div>
</div>
);
});