Skip to content
Merged
59 changes: 11 additions & 48 deletions packages/studio/src/components/StudioRightPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
type MutableRefObject,
type PointerEvent as ReactPointerEvent,
} from "react";
import { useCallback, useEffect, useMemo, useRef, type MutableRefObject } from "react";
import { PropertyPanel } from "./editor/PropertyPanel";
import { LayersPanel } from "./editor/LayersPanel";
import { CaptionPropertyPanel } from "../captions/components/CaptionPropertyPanel";
Expand Down Expand Up @@ -39,9 +31,7 @@ import {
} from "./studioColorGradingScope";
import type { BackgroundRemovalProgress } from "./editor/propertyPanelTypes";
import { timelineKeysForSelections, type ToggleHiddenHandler } from "../utils/studioHelpers";

const MIN_INSPECTOR_SPLIT_PERCENT = 20;
const MAX_INSPECTOR_SPLIT_PERCENT = 75;
import { useInspectorSplitResize } from "../hooks/useInspectorSplitResize";

export interface StudioRightPanelProps {
designPanelActive: boolean;
Expand Down Expand Up @@ -115,6 +105,7 @@ export function StudioRightPanel({
handleDomAttributeCommit,
handleDomAttributeLiveCommit,
handleDomHtmlAttributeCommit,
handleDomAttributesCommit,
handleDomPathOffsetCommit,
handleDomBoxSizeCommit,
handleDomRotationCommit,
Expand Down Expand Up @@ -184,13 +175,13 @@ export function StudioRightPanel({
coalesceKey: activeCompPath ? `slideshow-notes:${activeCompPath}` : "slideshow-notes",
});

const [layersPanePercent, setLayersPanePercent] = useState(40);
const splitContainerRef = useRef<HTMLDivElement>(null);
const splitDragRef = useRef<{
startY: number;
startPercent: number;
height: number;
} | null>(null);
const {
layersPanePercent,
splitContainerRef,
handleInspectorSplitResizeStart,
handleInspectorSplitResizeMove,
handleInspectorSplitResizeEnd,
} = useInspectorSplitResize();
const backgroundRemovalAbortRef = useRef<AbortController | null>(null);

useEffect(
Expand Down Expand Up @@ -231,35 +222,6 @@ export function StudioRightPanel({
toggleRightInspectorPane(pane);
};

const handleInspectorSplitResizeStart = useCallback(
(event: ReactPointerEvent<HTMLDivElement>) => {
event.preventDefault();
event.currentTarget.setPointerCapture(event.pointerId);
const height = splitContainerRef.current?.getBoundingClientRect().height ?? 0;
splitDragRef.current = {
startY: event.clientY,
startPercent: layersPanePercent,
height,
};
},
[layersPanePercent],
);

const handleInspectorSplitResizeMove = useCallback((event: ReactPointerEvent<HTMLDivElement>) => {
const drag = splitDragRef.current;
if (!drag || drag.height <= 0) return;
const deltaPercent = ((event.clientY - drag.startY) / drag.height) * 100;
const next = Math.min(
MAX_INSPECTOR_SPLIT_PERCENT,
Math.max(MIN_INSPECTOR_SPLIT_PERCENT, drag.startPercent + deltaPercent),
);
setLayersPanePercent(next);
}, []);

const handleInspectorSplitResizeEnd = useCallback(() => {
splitDragRef.current = null;
}, []);

const handleApplyColorGradingScope = useCallback(
async (scope: ColorGradingScope, value: string | null) =>
applyColorGradingScopeUpdate({
Expand Down Expand Up @@ -375,6 +337,7 @@ export function StudioRightPanel({
onUngroup={handleUngroupSelection}
onSetStyle={handleDomStyleCommit}
onSetAttribute={handleDomAttributeCommit}
onSetAttributes={handleDomAttributesCommit}
onSetAttributeLive={handleDomAttributeLiveCommit}
onApplyColorGradingScope={handleApplyColorGradingScope}
onSetHtmlAttribute={handleDomHtmlAttributeCommit}
Expand Down
5 changes: 4 additions & 1 deletion packages/studio/src/components/editor/PropertyPanelFlat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export function PropertyPanelFlat({
onUngroup,
onSetStyle,
onSetAttribute,
onSetAttributes,
onSetAttributeLive,
onApplyColorGradingScope,
onSetHtmlAttribute,
Expand Down Expand Up @@ -145,6 +146,7 @@ export function PropertyPanelFlat({
| "onUngroup"
| "onSetStyle"
| "onSetAttribute"
| "onSetAttributes"
| "onSetAttributeLive"
| "onApplyColorGradingScope"
| "onSetHtmlAttribute"
Expand Down Expand Up @@ -440,6 +442,7 @@ export function PropertyPanelFlat({
multipleTimelines={gsapMultipleTimelines}
unsupportedTimelinePattern={gsapUnsupportedTimelinePattern}
onSetAttribute={onSetAttribute}
onSetAttributes={onSetAttributes}
{...(gsapEffectHandlers ?? EMPTY_GSAP_EFFECT_HANDLERS)}
/>
),
Expand Down Expand Up @@ -516,7 +519,7 @@ export function PropertyPanelFlat({
onUngroup={onUngroup}
showUngroup={Boolean(onUngroup && element.dataAttributes["hf-group"] != null)}
/>
<div data-flat-panel-body="true" className="flex min-h-0 flex-1 flex-col overflow-hidden">
<div data-flat-panel-body="true" className="flex min-h-0 flex-1 flex-col overflow-y-auto">
{beforeOpen.map((g) => (
<FlatGroupHeader
key={g.id}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,11 @@ export function ColorGradingSection({
assets: string[];
previewIframeRef?: RefObject<HTMLIFrameElement | null>;
onImportAssets?: (files: FileList, dir?: string) => Promise<string[]>;
onSetAttributeLive: (attr: string, value: string | null) => void | Promise<void>;
onSetAttributeLive: (
attr: string,
value: string | null,
onSettled?: (ok: boolean) => void,
) => void | Promise<void>;
onApplyScope?: (
scope: "source-file" | "project",
value: string | null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,10 @@ describe("FlatColorGradingSection — Preset + LUT", () => {
);
if (!presetSelect) throw new Error("expected a preset select");
expect(presetSelect.value).toBe("neutral");
// The visible "Preset" label is a sibling span outside FlatSelectRow
// (label="" there, to avoid rendering it twice) — the select still
// needs its own accessible name via the dedicated ariaLabel prop.
expect(presetSelect.getAttribute("aria-label")).toBe("Preset");
act(() => {
presetSelect.value = "fresh-pop";
presetSelect.dispatchEvent(new Event("change", { bubbles: true }));
Expand Down Expand Up @@ -274,6 +278,7 @@ describe("FlatColorGradingSection — Preset + LUT", () => {
act(() => lutToggle?.dispatchEvent(new MouseEvent("click", { bubbles: true })));
const lutSelect = host.querySelector<HTMLSelectElement>('[data-flat-grade-lut-select="true"]');
if (!lutSelect) throw new Error("expected a LUT catalog select");
expect(lutSelect.getAttribute("aria-label")).toBe("Custom LUT");
act(() => {
lutSelect.value = "assets/luts/cool.cube";
lutSelect.dispatchEvent(new Event("change", { bubbles: true }));
Expand Down Expand Up @@ -607,4 +612,11 @@ describe("FlatColorGradingSection — HDR banner and Apply scope", () => {
expect(onApplyToScope).toHaveBeenCalledTimes(1);
act(() => root.unmount());
});

it("gives the Copy-grade-to scope select an accessible name — the visible text sits in a sibling span, not a <label>", () => {
const { host, root } = renderInto(<FlatColorGradingSection {...neutralPropsBase()} />);
const scopeSelect = host.querySelector<HTMLSelectElement>('[aria-label="Copy grade to"]');
expect(scopeSelect).not.toBeNull();
act(() => root.unmount());
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useMemo, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import {
HF_COLOR_GRADING_PRESETS,
isHfColorGradingActive,
Expand Down Expand Up @@ -32,6 +32,18 @@ export function FlatColorGradingAccessory({
}) {
const { grading, compareEnabled, runtimeStatus, commitCompare, resetGrading } = state;
const gradingActive = isHfColorGradingActive(grading);
// Tracks the active hold's cleanup so it can be torn down on unmount too —
// without this, switching selection away mid-hold (unmounting this
// accessory) leaves the pointerup/pointercancel/blur listeners registered
// on `window` forever, each holding a closure over the old commitCompare.
const releaseRef = useRef<(() => void) | null>(null);
useEffect(
() => () => {
releaseRef.current?.();
releaseRef.current = null;
},
[],
);

return (
<span className="flex items-center gap-2.5">
Expand All @@ -50,7 +62,9 @@ export function FlatColorGradingAccessory({
window.removeEventListener("pointerup", release);
window.removeEventListener("pointercancel", release);
window.removeEventListener("blur", release);
releaseRef.current = null;
};
releaseRef.current = release;
window.addEventListener("pointerup", release);
window.addEventListener("pointercancel", release);
window.addEventListener("blur", release);
Expand Down Expand Up @@ -298,6 +312,7 @@ export function FlatColorGradingSection({
<span className="text-[11px] text-panel-text-2">Preset</span>
<FlatSelectRow
label=""
ariaLabel="Preset"
value={grading.preset ?? "neutral"}
options={PRESET_OPTIONS}
tier={resolveValueTier(
Expand Down Expand Up @@ -344,6 +359,7 @@ export function FlatColorGradingSection({
</span>
<select
data-flat-grade-lut-select="true"
aria-label="Custom LUT"
value={lut?.src ?? ""}
onChange={(e) => {
const src = e.target.value;
Expand Down Expand Up @@ -510,6 +526,7 @@ export function FlatColorGradingSection({
<span className="flex items-center gap-1.5 text-[11px] text-panel-text-2">
Copy grade to
<select
aria-label="Copy grade to"
value={applyScope}
onChange={(e) => onSetApplyScope(e.target.value as "source-file" | "project")}
disabled={applyBusy}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,8 @@ export function LayoutFlexBlock({
<FlatSegmentedRow
label="Direction"
options={[
{ key: "row", node: "→ Row", active: direction === "row" },
{ key: "column", node: "Column", active: direction === "column" },
{ 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)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import {
buildInsetClipPathSides,
buildInsetClipPathValue,
formatNumericValue,
formatPxMetricValue,
getClipPathInsetPx,
inferClipPathPreset,
parseInsetClipPathSides,
parsePxMetricValue,
type ClipPathInsetSides,
} from "./propertyPanelHelpers";
import { FlatSlider } from "./propertyPanelFlatPrimitives";
import { MetricField } from "./propertyPanelPrimitives";

/* ------------------------------------------------------------------ */
/* Flat Mask inset — uniform slider + per-side fields */
/* (split out of propertyPanelFlatStyleSections.tsx to stay under the */
/* 600-line file-size gate) */
/* ------------------------------------------------------------------ */

export function FlatMaskInsetRows({
clipPathValue,
radiusValue,
disabled,
onSetStyle,
}: {
clipPathValue: string;
radiusValue: number;
disabled: boolean;
onSetStyle: (prop: string, value: string) => void | Promise<void>;
}) {
const clipPathPreset = inferClipPathPreset(clipPathValue);
const parsedClipInsets = parseInsetClipPathSides(clipPathValue);
const clipInsetValue = getClipPathInsetPx(clipPathValue);
const clipInsetSides = parsedClipInsets ?? {
top: clipInsetValue,
right: clipInsetValue,
bottom: clipInsetValue,
left: clipInsetValue,
radius: radiusValue,
};
const showClipInsetSides = clipPathPreset === "inset" || parsedClipInsets != null;

const commitClipInsetSide = (side: keyof ClipPathInsetSides, nextValue: string) => {
const next = parsePxMetricValue(nextValue);
if (next == null) return;
const sides: ClipPathInsetSides = {
top: clipInsetSides.top,
right: clipInsetSides.right,
bottom: clipInsetSides.bottom,
left: clipInsetSides.left,
};
sides[side] = next;
void onSetStyle("clip-path", buildInsetClipPathSides(sides, clipInsetSides.radius));
};

return (
<>
<FlatSlider
label="Mask inset"
value={clipInsetValue}
min={0}
max={Math.max(120, Math.ceil(clipInsetValue))}
step={1}
tier={clipInsetValue > 0 ? "explicitCustom" : "default"}
displayValue={`${formatNumericValue(clipInsetValue)}px`}
disabled={disabled}
onCommit={(next) =>
void onSetStyle("clip-path", buildInsetClipPathValue(next, radiusValue))
}
/>
{showClipInsetSides && (
<div className="grid grid-cols-4 gap-2">
<MetricField
label="T"
value={formatPxMetricValue(clipInsetSides.top)}
disabled={disabled}
onCommit={(next) => commitClipInsetSide("top", next)}
/>
<MetricField
label="R"
value={formatPxMetricValue(clipInsetSides.right)}
disabled={disabled}
onCommit={(next) => commitClipInsetSide("right", next)}
/>
<MetricField
label="B"
value={formatPxMetricValue(clipInsetSides.bottom)}
disabled={disabled}
onCommit={(next) => commitClipInsetSide("bottom", next)}
/>
<MetricField
label="L"
value={formatPxMetricValue(clipInsetSides.left)}
disabled={disabled}
onCommit={(next) => commitClipInsetSide("left", next)}
/>
</div>
)}
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
parseNumericValue,
stripQueryAndHash,
} from "./propertyPanelHelpers";
import { FlatSelectRow, FlatSlider, FlatToggle } from "./propertyPanelFlatPrimitives";
import { FlatSelectRow, FlatSlider } from "./propertyPanelFlatPrimitives";
import { FlatToggle } from "./propertyPanelFlatToggle";

// fallow-ignore-next-line complexity
export function FlatMediaSection({
Expand Down
Loading
Loading