mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 01:56:04 +00:00
Fixes issues raised in the Deepwork re-review of #2120-#2190 that weren't covered by #2225's earlier fix pass: - useColorGradingController: reset grading/compare/mediaMetadata state (and cancel pending persist/status timers) when selection changes to a different element — this hook is called unconditionally on every render (unlike legacy ColorGradingSection, remounted via a selectionIdentityKey React key), so switching selection reused the previous element's state. - useColorGradingController: stop permanently caching a non-OK /media/metadata response as null — a transient server error poisoned the HDR banner for that asset for the whole page lifetime. - FlatSelectRow: preserve a valid authored value outside the preset list (e.g. mix-blend-mode: difference, an arbitrary object-position) instead of silently misrepresenting it as the first preset — touching the control would overwrite real persisted state. - FlatSlider: the throttled trailing commit now reads onCommit through a ref updated every render instead of closing over it at schedule time — a caller whose onCommit spreads other current state (Grade's per-detail commits) could otherwise have a delayed commit revert whatever the user changed on a different control in the same 40ms window. - FlatSlider: flush a still-queued trailing commit on unmount instead of dropping it, and disable the reset button when the slider itself is disabled. - FlatSlider: add touch-action: none to the track so touch drags don't compete with page scroll. - FlatColorGradingAccessory: clean up the compare-hold's window listeners on unmount, not only on release — switching selection mid-hold used to leak them. - Align (flat Text): re-clicking the option already visually active for a logical start/end value no longer rewrites it to the physical left/right, preserving RTL semantics. - FlatSegmentedRow: give every option an accessible name and aria-pressed state — two visually-identical glyph buttons (upright/italic "A") had no way to be told apart by assistive tech. - PropertyPanelFlat: the panel body falls back to its own scroll when the collapsed group headers alone exceed the available height, so groups can't become permanently unreachable in a short pane. New regression tests for all of the above; full studio suite at the known pre-existing baseline (55 failures unrelated to this stack).
372 lines
11 KiB
TypeScript
372 lines
11 KiB
TypeScript
import { FlatRow, FlatSegmentedRow, FlatSelectRow } from "./propertyPanelFlatPrimitives";
|
|
import { KeyframeNavigation } from "./KeyframeNavigation";
|
|
import { formatPxMetricValue } from "./propertyPanelHelpers";
|
|
import { STUDIO_KEYFRAMES_ENABLED } from "./manualEditingAvailability";
|
|
import { resolveValueTier } from "./propertyPanelValueTier";
|
|
import { PropertyPanel3dTransform } from "./propertyPanel3dTransform";
|
|
import type { DomEditSelection } from "./domEditingTypes";
|
|
|
|
type KeyframeEntry = Array<{
|
|
percentage: number;
|
|
tweenPercentage?: number;
|
|
properties: Record<string, number | string>;
|
|
ease?: string;
|
|
}> | null;
|
|
|
|
interface GeometryRowsProps {
|
|
element: DomEditSelection;
|
|
displayX: number;
|
|
displayY: number;
|
|
displayW: number;
|
|
displayH: number;
|
|
displayR: number;
|
|
manualOffsetEditingDisabled: boolean;
|
|
manualSizeEditingDisabled: boolean;
|
|
manualRotationEditingDisabled: boolean;
|
|
commitManualOffset: (axis: "x" | "y", value: string) => void;
|
|
commitManualSize: (dimension: "width" | "height", value: string) => void;
|
|
commitManualRotation: (value: string) => void;
|
|
gsapAnimId: string | null;
|
|
navKeyframes: KeyframeEntry;
|
|
currentPct: number;
|
|
seekFromKfPct: (pct: number) => void;
|
|
animIdForProp: (prop: string) => string;
|
|
onCommitAnimatedProperty?: (
|
|
element: DomEditSelection,
|
|
property: string,
|
|
value: number,
|
|
) => Promise<void>;
|
|
onRemoveKeyframe?: (animId: string, pct: number) => void;
|
|
onConvertToKeyframes?: (animId: string) => void;
|
|
}
|
|
|
|
function KeyframeGutter({
|
|
element,
|
|
property,
|
|
displayValue,
|
|
gsapAnimId,
|
|
navKeyframes,
|
|
currentPct,
|
|
seekFromKfPct,
|
|
animIdForProp,
|
|
onCommitAnimatedProperty,
|
|
onRemoveKeyframe,
|
|
onConvertToKeyframes,
|
|
}: {
|
|
property: string;
|
|
displayValue: number;
|
|
} & Pick<
|
|
GeometryRowsProps,
|
|
| "element"
|
|
| "gsapAnimId"
|
|
| "navKeyframes"
|
|
| "currentPct"
|
|
| "seekFromKfPct"
|
|
| "animIdForProp"
|
|
| "onCommitAnimatedProperty"
|
|
| "onRemoveKeyframe"
|
|
| "onConvertToKeyframes"
|
|
>) {
|
|
if (!STUDIO_KEYFRAMES_ENABLED || !gsapAnimId) return null;
|
|
const hasKeyframesOnProp = Boolean(navKeyframes?.some((kf) => property in kf.properties));
|
|
return (
|
|
<span data-flat-kf-gutter="true" style={{ opacity: hasKeyframesOnProp ? 1 : 0.3 }}>
|
|
<KeyframeNavigation
|
|
property={property}
|
|
keyframes={navKeyframes}
|
|
currentPercentage={currentPct}
|
|
onSeek={seekFromKfPct}
|
|
onAddKeyframe={() =>
|
|
onCommitAnimatedProperty && void onCommitAnimatedProperty(element, property, displayValue)
|
|
}
|
|
onRemoveKeyframe={(pct) => onRemoveKeyframe?.(animIdForProp(property), pct)}
|
|
onConvertToKeyframes={() => onConvertToKeyframes?.(animIdForProp(property))}
|
|
/>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
export function LayoutGeometryRows({
|
|
element,
|
|
displayX,
|
|
displayY,
|
|
displayW,
|
|
displayH,
|
|
displayR,
|
|
manualOffsetEditingDisabled,
|
|
manualSizeEditingDisabled,
|
|
manualRotationEditingDisabled,
|
|
commitManualOffset,
|
|
commitManualSize,
|
|
commitManualRotation,
|
|
gsapAnimId,
|
|
navKeyframes,
|
|
currentPct,
|
|
seekFromKfPct,
|
|
animIdForProp,
|
|
onCommitAnimatedProperty,
|
|
onRemoveKeyframe,
|
|
onConvertToKeyframes,
|
|
}: GeometryRowsProps) {
|
|
const gutterProps = {
|
|
element,
|
|
gsapAnimId,
|
|
navKeyframes,
|
|
currentPct,
|
|
seekFromKfPct,
|
|
animIdForProp,
|
|
onCommitAnimatedProperty,
|
|
onRemoveKeyframe,
|
|
onConvertToKeyframes,
|
|
};
|
|
return (
|
|
<>
|
|
<FlatRow
|
|
label="X"
|
|
value={formatPxMetricValue(displayX)}
|
|
tier={displayX === 0 ? "default" : "explicitCustom"}
|
|
disabled={manualOffsetEditingDisabled}
|
|
onCommit={(next) => commitManualOffset("x", next)}
|
|
suffix={<KeyframeGutter property="x" displayValue={displayX} {...gutterProps} />}
|
|
/>
|
|
<FlatRow
|
|
label="Y"
|
|
value={formatPxMetricValue(displayY)}
|
|
tier={displayY === 0 ? "default" : "explicitCustom"}
|
|
disabled={manualOffsetEditingDisabled}
|
|
onCommit={(next) => commitManualOffset("y", next)}
|
|
suffix={<KeyframeGutter property="y" displayValue={displayY} {...gutterProps} />}
|
|
/>
|
|
<FlatRow
|
|
label="W"
|
|
value={formatPxMetricValue(displayW)}
|
|
tier="default"
|
|
disabled={manualSizeEditingDisabled}
|
|
onCommit={(next) => commitManualSize("width", next)}
|
|
suffix={<KeyframeGutter property="width" displayValue={displayW} {...gutterProps} />}
|
|
/>
|
|
<FlatRow
|
|
label="H"
|
|
value={formatPxMetricValue(displayH)}
|
|
tier="default"
|
|
disabled={manualSizeEditingDisabled}
|
|
onCommit={(next) => commitManualSize("height", next)}
|
|
suffix={<KeyframeGutter property="height" displayValue={displayH} {...gutterProps} />}
|
|
/>
|
|
<FlatRow
|
|
label="Angle"
|
|
value={`${displayR}°`}
|
|
tier="default"
|
|
disabled={manualRotationEditingDisabled}
|
|
onCommit={(next) => commitManualRotation(next.replace("°", ""))}
|
|
suffix={<KeyframeGutter property="rotation" displayValue={displayR} {...gutterProps} />}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
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", label: "Row", active: direction === "row" },
|
|
{ key: "column", node: "Column", label: "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>
|
|
);
|
|
}
|
|
|
|
export function LayoutTransform3DBlock({
|
|
gsapRuntimeValues,
|
|
gsapAnimId,
|
|
resolveAnimIdForProp,
|
|
gsapKeyframes,
|
|
currentPct,
|
|
elStart,
|
|
elDuration,
|
|
element,
|
|
onCommitAnimatedProperty,
|
|
onCommitAnimatedProperties,
|
|
onSeekToTime,
|
|
onRemoveKeyframe,
|
|
onConvertToKeyframes,
|
|
onLivePreviewProps,
|
|
}: {
|
|
gsapRuntimeValues: Record<string, number>;
|
|
gsapAnimId: string | null;
|
|
resolveAnimIdForProp?: (prop: string) => string | null;
|
|
gsapKeyframes: Array<{
|
|
percentage: number;
|
|
properties: Record<string, number | string>;
|
|
ease?: string;
|
|
}> | null;
|
|
currentPct: number;
|
|
elStart: number;
|
|
elDuration: number;
|
|
element: DomEditSelection;
|
|
onCommitAnimatedProperty?: (
|
|
element: DomEditSelection,
|
|
property: string,
|
|
value: number,
|
|
) => Promise<void>;
|
|
onCommitAnimatedProperties?: (
|
|
element: DomEditSelection,
|
|
props: Record<string, number | string>,
|
|
) => Promise<void>;
|
|
onSeekToTime?: (time: number) => void;
|
|
onRemoveKeyframe?: (animId: string, pct: number) => void;
|
|
onConvertToKeyframes?: (animId: string, duration?: number) => void;
|
|
onLivePreviewProps?: (element: DomEditSelection, props: Record<string, number>) => void;
|
|
}) {
|
|
return (
|
|
<div className="border-t border-panel-hairline pt-2.5">
|
|
<div className="mb-[3px] text-[9px] font-semibold uppercase tracking-[0.12em] text-panel-text-5">
|
|
3D Transform
|
|
</div>
|
|
<PropertyPanel3dTransform
|
|
gsapRuntimeValues={gsapRuntimeValues}
|
|
gsapAnimId={gsapAnimId}
|
|
resolveAnimIdForProp={resolveAnimIdForProp}
|
|
gsapKeyframes={gsapKeyframes}
|
|
currentPct={currentPct}
|
|
elStart={elStart}
|
|
elDuration={elDuration}
|
|
element={element}
|
|
onCommitAnimatedProperty={onCommitAnimatedProperty}
|
|
onCommitAnimatedProperties={onCommitAnimatedProperties}
|
|
onSeekToTime={onSeekToTime}
|
|
onRemoveKeyframe={onRemoveKeyframe}
|
|
onConvertToKeyframes={onConvertToKeyframes}
|
|
onLivePreviewProps={onLivePreviewProps}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface FlatLayoutSectionProps
|
|
extends
|
|
Omit<GeometryRowsProps, never>,
|
|
Pick<
|
|
Parameters<typeof LayoutTransform3DBlock>[0],
|
|
| "gsapRuntimeValues"
|
|
| "resolveAnimIdForProp"
|
|
| "gsapKeyframes"
|
|
| "elStart"
|
|
| "elDuration"
|
|
| "onCommitAnimatedProperties"
|
|
| "onSeekToTime"
|
|
| "onLivePreviewProps"
|
|
> {
|
|
element: DomEditSelection;
|
|
styles: Record<string, string>;
|
|
onSetStyle: (prop: string, value: string) => void | Promise<void>;
|
|
disabled: boolean;
|
|
}
|
|
|
|
export function FlatLayoutSection({
|
|
element,
|
|
styles,
|
|
onSetStyle,
|
|
disabled,
|
|
gsapRuntimeValues,
|
|
resolveAnimIdForProp,
|
|
gsapKeyframes,
|
|
elStart,
|
|
elDuration,
|
|
onCommitAnimatedProperties,
|
|
onSeekToTime,
|
|
onLivePreviewProps,
|
|
...geometry
|
|
}: FlatLayoutSectionProps) {
|
|
return (
|
|
<div className="space-y-1.5">
|
|
<LayoutGeometryRows element={element} {...geometry} />
|
|
<LayoutZIndexRow styles={styles} onSetStyle={onSetStyle} />
|
|
<LayoutFlexBlock styles={styles} onSetStyle={onSetStyle} disabled={disabled} />
|
|
<LayoutTransform3DBlock
|
|
gsapRuntimeValues={gsapRuntimeValues}
|
|
gsapAnimId={geometry.gsapAnimId}
|
|
resolveAnimIdForProp={resolveAnimIdForProp}
|
|
gsapKeyframes={gsapKeyframes}
|
|
currentPct={geometry.currentPct}
|
|
elStart={elStart}
|
|
elDuration={elDuration}
|
|
element={element}
|
|
onCommitAnimatedProperty={geometry.onCommitAnimatedProperty}
|
|
onCommitAnimatedProperties={onCommitAnimatedProperties}
|
|
onSeekToTime={onSeekToTime}
|
|
onRemoveKeyframe={geometry.onRemoveKeyframe}
|
|
onConvertToKeyframes={geometry.onConvertToKeyframes}
|
|
onLivePreviewProps={onLivePreviewProps}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|