Files
OpenCut/apps/web/src/components/editor/panels/properties/effect-param-field.tsx
T
Maze Winther e7dcb586c0 feat: Clip effects, asset sorting, and timeline improvements
Major features and improvements:

* **Clip Effects**:
  * Added UI in Properties Panel to manage effects on video/image clips (add, remove, toggle, reorder).
  * Implemented dynamic parameter fields for effects.
  * Added support for keyframing effect parameters.

* **Assets Panel**:
  * Added sorting options: Name, Type, Duration, and File Size.
  * Persisted view preferences (grid/list mode, sort order) to local storage.
  * Refactored media item rendering and drag interactions.

* **Timeline & Interaction**:
  * **Keyframe Dragging**: Added ability to drag keyframes directly on the timeline element.
  * **Resizing**: Improved resize logic to respect neighboring clips (prevents overlaps).
  * **Visuals**: Implemented tiled background rendering for video/image clips on the timeline.
  * **Shortcuts**: Added "Deselect All" action bound to the `Escape` key.
  * **Fixes**: Corrected drag-and-drop coordinate calculations when the timeline track area is scrolled.

* **Text Elements**:
  * Refactored text background storage to use an explicit `enabled` flag.
  * Added `V8toV9` storage migration to update existing projects.

* **Architecture**:
  * Moved export state management to `ProjectManager` for better lifecycle handling.
  * Refactored `PropertiesPanel` sections to be more composable (custom headers, borders).
2026-03-02 13:13:07 +01:00

153 lines
3.1 KiB
TypeScript

"use client";
import type { EffectParamDefinition, NumberEffectParamDefinition } from "@/types/effects";
import { clamp } from "@/utils/math";
import { SectionField } from "./section";
import { Slider } from "@/components/ui/slider";
import { NumberField } from "@/components/ui/number-field";
import { Switch } from "@/components/ui/switch";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { usePropertyDraft } from "./hooks/use-property-draft";
export function EffectParamField({
param,
value,
onPreview,
onCommit,
}: {
param: EffectParamDefinition;
value: number | string | boolean;
onPreview: (value: number | string | boolean) => void;
onCommit: () => void;
}) {
return (
<SectionField label={param.label}>
<EffectParamInput param={param} value={value} onPreview={onPreview} onCommit={onCommit} />
</SectionField>
);
}
function EffectParamInput({
param,
value,
onPreview,
onCommit,
}: {
param: EffectParamDefinition;
value: number | string | boolean;
onPreview: (value: number | string | boolean) => void;
onCommit: () => void;
}) {
if (param.type === "number") {
return (
<NumberParamField
param={param}
value={typeof value === "number" ? value : Number(value)}
onPreview={onPreview}
onCommit={onCommit}
/>
);
}
if (param.type === "boolean") {
return (
<Switch
checked={Boolean(value)}
onCheckedChange={(checked) => {
onPreview(checked);
onCommit();
}}
/>
);
}
if (param.type === "select") {
return (
<Select
value={String(value)}
onValueChange={(selected) => {
onPreview(selected);
onCommit();
}}
>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{param.options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
);
}
if (param.type === "color") {
return (
<input
type="color"
className="h-8 w-full cursor-pointer rounded border"
value={String(value)}
onChange={(event) => onPreview(event.target.value)}
onBlur={onCommit}
/>
);
}
return null;
}
function NumberParamField({
param,
value,
onPreview,
onCommit,
}: {
param: NumberEffectParamDefinition;
value: number;
onPreview: (value: number) => void;
onCommit: () => void;
}) {
const { min, max, step } = param;
const draft = usePropertyDraft({
displayValue: String(value),
parse: (input) => {
const parsed = parseFloat(input);
if (Number.isNaN(parsed)) return null;
return clamp({ value: parsed, min, max });
},
onPreview,
onCommit,
});
return (
<div className="flex items-center gap-3">
<Slider
className="flex-1"
min={min}
max={max}
step={step}
value={[value]}
onValueChange={([newValue]) => onPreview(newValue)}
onValueCommit={onCommit}
/>
<NumberField
className="w-16 shrink-0"
value={draft.displayValue}
onFocus={draft.onFocus}
onChange={draft.onChange}
onBlur={draft.onBlur}
/>
</div>
);
}