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
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:
@@ -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>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user