summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor/panels/properties
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/components/editor/panels/properties')
-rw-r--r--apps/web/src/components/editor/panels/properties/effect-properties.tsx103
-rw-r--r--apps/web/src/components/editor/panels/properties/index.tsx65
2 files changed, 142 insertions, 26 deletions
diff --git a/apps/web/src/components/editor/panels/properties/effect-properties.tsx b/apps/web/src/components/editor/panels/properties/effect-properties.tsx
new file mode 100644
index 00000000..bf137291
--- /dev/null
+++ b/apps/web/src/components/editor/panels/properties/effect-properties.tsx
@@ -0,0 +1,103 @@
+"use client";
+
+import type { EffectElement } from "@/types/timeline";
+import type { EffectParamDefinition } from "@/types/effects";
+import { getEffect } from "@/lib/effects/registry";
+import { useEditor } from "@/hooks/use-editor";
+import { clamp } from "@/utils/math";
+import { Section, SectionContent, SectionHeader, SectionField, SectionFields } from "./section";
+import { Slider } from "@/components/ui/slider";
+import { NumberField } from "@/components/ui/number-field";
+import { usePropertyDraft } from "./hooks/use-property-draft";
+
+function EffectParamField({
+ param,
+ element,
+ trackId,
+}: {
+ param: EffectParamDefinition;
+ element: EffectElement;
+ trackId: string;
+}) {
+ const editor = useEditor();
+
+ const currentValue = Number(element.params[param.key] ?? param.default);
+ const min = param.min ?? 0;
+ const max = param.max ?? 100;
+ const step = param.step ?? 1;
+
+ const updateParam = (value: number) =>
+ editor.timeline.previewElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: { params: { ...element.params, [param.key]: value } },
+ },
+ ],
+ });
+
+ const commitParam = () => editor.timeline.commitPreview();
+
+ const draft = usePropertyDraft({
+ displayValue: String(currentValue),
+ parse: (input) => {
+ const parsed = parseFloat(input);
+ if (Number.isNaN(parsed)) return null;
+ return clamp({ value: parsed, min, max });
+ },
+ onPreview: updateParam,
+ onCommit: commitParam,
+ });
+
+ return (
+ <SectionField label={param.label}>
+ <div className="flex items-center gap-3">
+ <Slider
+ className="flex-1"
+ min={min}
+ max={max}
+ step={step}
+ value={[currentValue]}
+ onValueChange={([value]) => updateParam(value)}
+ onValueCommit={commitParam}
+ />
+ <NumberField
+ className="w-16 shrink-0"
+ value={draft.displayValue}
+ onFocus={draft.onFocus}
+ onChange={draft.onChange}
+ onBlur={draft.onBlur}
+ />
+ </div>
+ </SectionField>
+ );
+}
+
+export function EffectProperties({
+ element,
+ trackId,
+}: {
+ element: EffectElement;
+ trackId: string;
+}) {
+ const definition = getEffect({ effectType: element.effectType });
+
+ return (
+ <Section hasBorderTop={false}>
+ <SectionHeader title={definition.name} />
+ <SectionContent>
+ <SectionFields>
+ {definition.params.map((param) => (
+ <EffectParamField
+ key={param.key}
+ param={param}
+ element={element}
+ trackId={trackId}
+ />
+ ))}
+ </SectionFields>
+ </SectionContent>
+ </Section>
+ );
+}
diff --git a/apps/web/src/components/editor/panels/properties/index.tsx b/apps/web/src/components/editor/panels/properties/index.tsx
index 4f285f84..75dd81a9 100644
--- a/apps/web/src/components/editor/panels/properties/index.tsx
+++ b/apps/web/src/components/editor/panels/properties/index.tsx
@@ -4,9 +4,37 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import { AudioProperties } from "./audio-properties";
import { VideoProperties } from "./video-properties";
import { TextProperties } from "./text-properties";
+import { EffectProperties } from "./effect-properties";
import { EmptyView } from "./empty-view";
import { useEditor } from "@/hooks/use-editor";
import { useElementSelection } from "@/hooks/timeline/element/use-element-selection";
+import type { TimelineElement, TimelineTrack } from "@/types/timeline";
+
+function ElementProperties({
+ track,
+ element,
+}: {
+ track: TimelineTrack;
+ element: TimelineElement;
+}) {
+ if (element.type === "text") {
+ return <TextProperties element={element} trackId={track.id} />;
+ }
+ if (element.type === "audio") {
+ return <AudioProperties _element={element} />;
+ }
+ if (
+ element.type === "video" ||
+ element.type === "image" ||
+ element.type === "sticker"
+ ) {
+ return <VideoProperties element={element} trackId={track.id} />;
+ }
+ if (element.type === "effect") {
+ return <EffectProperties element={element} trackId={track.id} />;
+ }
+ return null;
+}
export function PropertiesPanel() {
const editor = useEditor();
@@ -16,34 +44,19 @@ export function PropertiesPanel() {
elements: selectedElements,
});
+ const hasSelection = selectedElements.length > 0;
+
return (
- <div className="panel bg-background h-full rounded-sm border border-t-0 overflow-hidden">
- {selectedElements.length > 0 ? (
+ <div className="panel bg-background h-full rounded-sm border overflow-hidden">
+ {hasSelection ? (
<ScrollArea className="h-full scrollbar-hidden">
- {elementsWithTracks.map(({ track, element }) => {
- if (element.type === "text") {
- return (
- <div key={element.id}>
- <TextProperties element={element} trackId={track.id} />
- </div>
- );
- }
- if (element.type === "audio") {
- return <AudioProperties key={element.id} _element={element} />;
- }
- if (
- element.type === "video" ||
- element.type === "image" ||
- element.type === "sticker"
- ) {
- return (
- <div key={element.id}>
- <VideoProperties element={element} trackId={track.id} />
- </div>
- );
- }
- return null;
- })}
+ {elementsWithTracks.map(({ track, element }) => (
+ <ElementProperties
+ key={element.id}
+ track={track}
+ element={element}
+ />
+ ))}
</ScrollArea>
) : (
<EmptyView />