diff options
| author | Maze Winther <mazewinther@gmail.com> | 2026-04-18 15:35:04 +0200 |
|---|---|---|
| committer | Maze Winther <mazewinther@gmail.com> | 2026-04-18 15:35:04 +0200 |
| commit | 0387bd4c80f699cf0272fa0165d3c86dceaf39dc (patch) | |
| tree | 7c72abb6dde074b16ebf84b1d2c72a3bd58b3fff /legacy/components/properties | |
| parent | b1d0b103549b7b707c8b90e8a5cb98235dc385c9 (diff) | |
| download | opencut-0387bd4c80f699cf0272fa0165d3c86dceaf39dc.tar.gz | |
delete legacy folder
Diffstat (limited to 'legacy/components/properties')
| -rw-r--r-- | legacy/components/properties/audio-properties.tsx | 3 | ||||
| -rw-r--r-- | legacy/components/properties/clip-effects-properties.tsx | 215 | ||||
| -rw-r--r-- | legacy/components/properties/effect-param-field.tsx | 152 | ||||
| -rw-r--r-- | legacy/components/properties/effect-properties.tsx | 58 | ||||
| -rw-r--r-- | legacy/components/properties/keyframe-toggle.tsx | 32 | ||||
| -rw-r--r-- | legacy/components/properties/section.tsx | 262 | ||||
| -rw-r--r-- | legacy/components/properties/text-properties.tsx | 732 | ||||
| -rw-r--r-- | legacy/components/properties/video-properties.tsx | 25 |
8 files changed, 0 insertions, 1479 deletions
diff --git a/legacy/components/properties/audio-properties.tsx b/legacy/components/properties/audio-properties.tsx deleted file mode 100644 index 9e2b0b9a..00000000 --- a/legacy/components/properties/audio-properties.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export function AudioProperties() {
- return <div className="space-y-4 p-5">Audio properties</div>;
-}
diff --git a/legacy/components/properties/clip-effects-properties.tsx b/legacy/components/properties/clip-effects-properties.tsx deleted file mode 100644 index e409c22d..00000000 --- a/legacy/components/properties/clip-effects-properties.tsx +++ /dev/null @@ -1,215 +0,0 @@ -"use client";
-
-import { useEffect, useState } from "react";
-import type { Effect } from "@/types/effects";
-import type { VisualElement } from "@/types/timeline";
-import { getEffect } from "@/lib/effects/registry";
-import { useEditor } from "@/hooks/use-editor";
-import { usePropertiesStore } from "@/stores/properties-store";
-import {
- Section,
- SectionContent,
- SectionHeader,
- SectionTitle,
- SectionFields,
-} from "./section";
-import { EffectParamField } from "./effect-param-field";
-import { Button } from "@/components/ui/button";
-import { HugeiconsIcon } from "@hugeicons/react";
-import {
- ArrowLeft01Icon,
- Delete02Icon,
- ViewIcon,
- ViewOffSlashIcon,
-} from "@hugeicons/core-free-icons";
-import { ScrollArea } from "@/components/ui/scroll-area";
-import { cn } from "@/utils/ui";
-export function ClipEffectsProperties({
- element,
- trackId,
-}: {
- element: VisualElement;
- trackId: string;
-}) {
- const closeClipEffects = usePropertiesStore(
- (state) => state.closeClipEffects,
- );
- const editor = useEditor();
- const effects = element.effects ?? [];
-
- useEffect(() => {
- if (effects.length === 0) closeClipEffects();
- }, [effects.length, closeClipEffects]);
-
- const [dragIndex, setDragIndex] = useState<number | null>(null);
- const [dropIndex, setDropIndex] = useState<number | null>(null);
-
- const handleDragStart = ({ index }: { index: number }) => {
- setDragIndex(index);
- };
-
- const handleDragOver = ({ event, index }: { event: React.DragEvent; index: number }) => {
- event.preventDefault();
- if (index !== dropIndex) setDropIndex(index);
- };
-
- const handleDrop = ({ toIndex }: { toIndex: number }) => {
- if (dragIndex !== null && dragIndex !== toIndex) {
- editor.timeline.reorderClipEffects({
- trackId,
- elementId: element.id,
- fromIndex: dragIndex,
- toIndex,
- });
- }
- setDragIndex(null);
- setDropIndex(null);
- };
-
- const handleDragEnd = () => {
- setDragIndex(null);
- setDropIndex(null);
- };
-
- return (
- <div className="flex h-full flex-col">
- <div className="flex h-11 shrink-0 items-center gap-2 border-b px-1.5">
- <Button
- variant="ghost"
- size="icon"
- onClick={closeClipEffects}
- aria-label="Back to properties"
- >
- <HugeiconsIcon icon={ArrowLeft01Icon} />
- </Button>
- <span className="text-sm font-medium">Effects</span>
- </div>
- <ScrollArea className="flex-1 scrollbar-hidden">
- {effects.map((effect, index) => (
- // biome-ignore lint/a11y/noStaticElementInteractions: drag-and-drop list reorder
- <div
- key={effect.id}
- draggable
- onDragStart={() => handleDragStart({ index })}
- onDragOver={(event) => handleDragOver({ event, index })}
- onDrop={() => handleDrop({ toIndex: index })}
- onDragEnd={handleDragEnd}
- className={cn(
- "group",
- dragIndex === index && "opacity-40",
- dropIndex === index &&
- dragIndex !== null &&
- dragIndex !== index &&
- (index < dragIndex
- ? "border-t-2 border-primary"
- : "border-b-2 border-primary"),
- )}
- >
- <ClipEffectSection
- effect={effect}
- element={element}
- trackId={trackId}
- />
- </div>
- ))}
- </ScrollArea>
- </div>
- );
-}
-
-function ClipEffectSection({
- effect,
- element,
- trackId,
-}: {
- effect: Effect;
- element: VisualElement;
- trackId: string;
-}) {
- const editor = useEditor();
- const definition = getEffect({ effectType: effect.type });
-
- const previewParam = ({ key }: { key: string }) => (value: number | string | boolean) => {
- const updatedEffects = (element.effects ?? []).map((existing) =>
- existing.id !== effect.id
- ? existing
- : { ...existing, params: { ...existing.params, [key]: value } },
- );
- editor.timeline.previewElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: { effects: updatedEffects },
- },
- ],
- });
- };
-
- const commitParam = () => editor.timeline.commitPreview();
-
- const toggleEffect = () =>
- editor.timeline.toggleClipEffect({
- trackId,
- elementId: element.id,
- effectId: effect.id,
- });
-
- const removeEffect = () =>
- editor.timeline.removeClipEffect({
- trackId,
- elementId: element.id,
- effectId: effect.id,
- });
-
- return (
- <Section sectionKey={`clip-effect:${effect.id}`} showTopBorder={false}>
- <SectionHeader
- className="cursor-move"
- trailing={
- <div className="flex items-center gap-1">
- <Button
- variant={effect.enabled ? "secondary" : "ghost"}
- size="icon"
- aria-label={`Toggle ${definition.name}`}
- onClick={toggleEffect}
- >
- <HugeiconsIcon
- icon={effect.enabled ? ViewIcon : ViewOffSlashIcon}
- />
- </Button>
- <Button
- variant="ghost"
- size="icon"
- aria-label={`Remove ${definition.name}`}
- onClick={removeEffect}
- >
- <HugeiconsIcon icon={Delete02Icon} />
- </Button>
- </div>
- }
- >
- <SectionTitle
- className={cn(!effect.enabled && "text-muted-foreground")}
- >
- {definition.name}
- </SectionTitle>
- </SectionHeader>
- {effect.enabled && (
- <SectionContent>
- <SectionFields>
- {definition.params.map((param) => (
- <EffectParamField
- key={param.key}
- param={param}
- value={effect.params[param.key] ?? param.default}
- onPreview={previewParam({ key: param.key })}
- onCommit={commitParam}
- />
- ))}
- </SectionFields>
- </SectionContent>
- )}
- </Section>
- );
-}
diff --git a/legacy/components/properties/effect-param-field.tsx b/legacy/components/properties/effect-param-field.tsx deleted file mode 100644 index 3aa87296..00000000 --- a/legacy/components/properties/effect-param-field.tsx +++ /dev/null @@ -1,152 +0,0 @@ -"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>
- );
-}
diff --git a/legacy/components/properties/effect-properties.tsx b/legacy/components/properties/effect-properties.tsx deleted file mode 100644 index 78582961..00000000 --- a/legacy/components/properties/effect-properties.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"use client";
-
-import type { EffectElement } from "@/types/timeline";
-import { getEffect } from "@/lib/effects/registry";
-import { useEditor } from "@/hooks/use-editor";
-import {
- Section,
- SectionContent,
- SectionHeader,
- SectionFields,
- SectionTitle,
-} from "./section";
-import { EffectParamField } from "./effect-param-field";
-
-export function EffectProperties({
- element,
- trackId,
-}: {
- element: EffectElement;
- trackId: string;
-}) {
- const editor = useEditor();
- const definition = getEffect({ effectType: element.effectType });
-
- const previewParam =
- ({ key }: { key: string }) =>
- (value: number | string | boolean) =>
- editor.timeline.previewElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: { params: { ...element.params, [key]: value } },
- },
- ],
- });
-
- return (
- <Section showTopBorder={false}>
- <SectionHeader>
- <SectionTitle>{definition.name}</SectionTitle>
- </SectionHeader>
- <SectionContent>
- <SectionFields>
- {definition.params.map((param) => (
- <EffectParamField
- key={param.key}
- param={param}
- value={element.params[param.key] ?? param.default}
- onPreview={previewParam({ key: param.key })}
- onCommit={() => editor.timeline.commitPreview()}
- />
- ))}
- </SectionFields>
- </SectionContent>
- </Section>
- );
-}
diff --git a/legacy/components/properties/keyframe-toggle.tsx b/legacy/components/properties/keyframe-toggle.tsx deleted file mode 100644 index 00cea82b..00000000 --- a/legacy/components/properties/keyframe-toggle.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Button } from "@/components/ui/button";
-import { HugeiconsIcon } from "@hugeicons/react";
-import { KeyframeIcon } from "@hugeicons/core-free-icons";
-import { cn } from "@/utils/ui";
-
-export function KeyframeToggle({
- isActive,
- isDisabled = false,
- title,
- onToggle,
-}: {
- isActive: boolean;
- isDisabled?: boolean;
- title: string;
- onToggle: () => void;
-}) {
- return (
- <Button
- variant="text"
- aria-pressed={isActive}
- disabled={isDisabled}
- title={title}
- onClick={onToggle}
- className="[&>svg]:size-3.5 mb-0.5"
- >
- <HugeiconsIcon
- icon={KeyframeIcon}
- className={cn(isActive && "text-primary fill-primary")}
- />
- </Button>
- );
-}
diff --git a/legacy/components/properties/section.tsx b/legacy/components/properties/section.tsx deleted file mode 100644 index 07d1c0ca..00000000 --- a/legacy/components/properties/section.tsx +++ /dev/null @@ -1,262 +0,0 @@ -import { createContext, useContext, useEffect, useState } from "react";
-import { cn } from "@/utils/ui";
-import { HugeiconsIcon } from "@hugeicons/react";
-import { ArrowDownIcon } from "@hugeicons/core-free-icons";
-import { Label } from "@/components/ui/label";
-import { Button } from "@/components/ui/button";
-
-const sectionExpandedCache = new Map<string, boolean>();
-const mountedSectionKeys = new Set<string>();
-
-interface SectionContext {
- isOpen: boolean;
- toggle: () => void;
- collapsible: boolean;
-}
-
-const SectionCtx = createContext<SectionContext | null>(null);
-
-function useSectionContext() {
- return useContext(SectionCtx);
-}
-
-interface SectionProps {
- children: React.ReactNode;
- collapsible?: boolean;
- defaultOpen?: boolean;
- sectionKey?: string;
- className?: string;
- showTopBorder?: boolean;
- showBottomBorder?: boolean;
-}
-
-export function Section({
- children,
- collapsible = false,
- defaultOpen = true,
- sectionKey,
- className,
- showTopBorder = true,
- showBottomBorder = true,
-}: SectionProps) {
- const cached = sectionKey ? sectionExpandedCache.get(sectionKey) : undefined;
- const [isOpen, setIsOpen] = useState(cached ?? defaultOpen);
-
- useEffect(() => {
- if (!sectionKey) return;
- if (process.env.NODE_ENV !== "production" && mountedSectionKeys.has(sectionKey)) {
- console.error(`[Section] duplicate sectionKey mounted simultaneously: "${sectionKey}"`);
- }
- mountedSectionKeys.add(sectionKey);
- return () => { mountedSectionKeys.delete(sectionKey); };
- }, [sectionKey]);
-
- const toggle = () => {
- const next = !isOpen;
- setIsOpen(next);
- if (sectionKey) sectionExpandedCache.set(sectionKey, next);
- };
-
- return (
- <SectionCtx.Provider value={{ isOpen, toggle, collapsible }}>
- <div
- className={cn(
- "flex flex-col",
- showTopBorder && "border-t",
- showBottomBorder && "last:border-b",
- className,
- )}
- >
- {children}
- </div>
- </SectionCtx.Provider>
- );
-}
-
-interface SectionHeaderProps {
- children?: React.ReactNode;
- trailing?: React.ReactNode;
- leading?: React.ReactNode;
- actions?: React.ReactNode;
- onClick?: () => void;
- className?: string;
-}
-
-export function SectionHeader({
- children,
- trailing,
- leading,
- actions,
- onClick,
- className,
-}: SectionHeaderProps) {
- const ctx = useSectionContext();
- const isCollapsible = ctx?.collapsible ?? false;
- const isOpen = ctx?.isOpen ?? true;
- const isInteractive = isCollapsible || !!onClick;
- const handleClick = isCollapsible ? ctx?.toggle : onClick;
-
- const chevronIcon = isCollapsible ? (
- <HugeiconsIcon
- icon={ArrowDownIcon}
- className={cn(
- "size-4 shrink-0 transition-transform duration-200 ease-out",
- isOpen ? "rotate-0 text-foreground" : "-rotate-90 text-muted-foreground",
- )}
- />
- ) : null;
-
- const headerContent = (
- <>
- {leading}
- <div className="min-w-0 flex-1">{children}</div>
- {(trailing || chevronIcon) && (
- <div className="flex items-center">
- {trailing}
- {chevronIcon && (
- <Button
- variant="ghost"
- size="icon"
- aria-label={isOpen ? "Collapse section" : "Expand section"}
- onClick={(event) => {
- event.stopPropagation();
- handleClick?.();
- }}
- >
- {chevronIcon}
- </Button>
- )}
- </div>
- )}
- {actions}
- </>
- );
-
- if (!isInteractive) {
- return (
- <div className={cn("flex h-11 w-full items-center gap-2 px-3.5", className)}>
- {headerContent}
- </div>
- );
- }
-
- return (
- // biome-ignore lint/a11y/useSemanticElements: outer div intentionally wraps a nested <Button> (chevron), making <button> invalid HTML here
- <div
- role="button"
- tabIndex={0}
- className={cn(
- "flex h-11 w-full cursor-pointer items-center gap-2 px-3.5",
- className,
- )}
- onClick={handleClick}
- onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") handleClick?.(); }}
- >
- {headerContent}
- </div>
- );
-}
-
-export function SectionTitle({
- children,
- className,
- onClick,
-}: {
- children: React.ReactNode;
- className?: string;
- onClick?: () => void;
-}) {
- const ctx = useSectionContext();
- const isOpen = ctx?.isOpen ?? true;
-
- if (onClick) {
- return (
- <button
- type="button"
- className={cn(
- "cursor-pointer text-sm font-medium",
- isOpen ? "text-foreground" : "text-muted-foreground",
- className,
- )}
- onClick={onClick}
- >
- {children}
- </button>
- );
- }
-
- return (
- <span
- className={cn(
- "text-sm font-medium",
- isOpen ? "text-foreground" : "text-muted-foreground",
- className,
- )}
- >
- {children}
- </span>
- );
-}
-
-export function SectionFields({
- children,
- className,
-}: {
- children: React.ReactNode;
- className?: string;
-}) {
- return (
- <div className={cn("flex flex-col gap-3.5", className)}>{children}</div>
- );
-}
-
-export function SectionField({
- label,
- beforeLabel,
- children,
- className,
-}: {
- label: string;
- beforeLabel?: React.ReactNode;
- children: React.ReactNode;
- className?: string;
-}) {
- return (
- <div className={cn("flex flex-col gap-2", className)}>
- <div className="flex h-4 items-center gap-1.5">
- {beforeLabel}
- <Label>{label}</Label>
- </div>
- {children}
- </div>
- );
-}
-
-export function SectionContent({
- children,
- className,
-}: {
- children: React.ReactNode;
- className?: string;
-}) {
- const ctx = useSectionContext();
- const isCollapsible = ctx?.collapsible ?? false;
- const isOpen = ctx?.isOpen ?? true;
-
- if (isCollapsible) {
- return (
- <div
- className={cn(
- "grid transition-[grid-template-rows] duration-100 ease-out",
- isOpen ? "grid-rows-[1fr]" : "grid-rows-[0fr]",
- )}
- >
- <div className="overflow-hidden">
- <div className={cn("p-4 pt-0", className)}>{children}</div>
- </div>
- </div>
- );
- }
-
- return <div className={cn("p-4 pt-0", className)}>{children}</div>;
-}
diff --git a/legacy/components/properties/text-properties.tsx b/legacy/components/properties/text-properties.tsx deleted file mode 100644 index 7af95450..00000000 --- a/legacy/components/properties/text-properties.tsx +++ /dev/null @@ -1,732 +0,0 @@ -import { Textarea } from "@/components/ui/textarea";
-import { FontPicker } from "@/components/ui/font-picker";
-import type { TextElement } from "@/types/timeline";
-import { NumberField } from "@/components/ui/number-field";
-import { useRef } from "react";
-import {
- Section,
- SectionContent,
- SectionField,
- SectionFields,
- SectionHeader,
- SectionTitle,
-} from "./section";
-import { ColorPicker } from "@/components/ui/color-picker";
-import { Button } from "@/components/ui/button";
-import { uppercase } from "@/utils/string";
-import { clamp } from "@/utils/math";
-import { useEditor } from "@/hooks/use-editor";
-import { DEFAULT_COLOR } from "@/constants/project-constants";
-import {
- CORNER_RADIUS_MAX,
- CORNER_RADIUS_MIN,
- DEFAULT_LETTER_SPACING,
- DEFAULT_LINE_HEIGHT,
- DEFAULT_TEXT_BACKGROUND,
- DEFAULT_TEXT_ELEMENT,
- MAX_FONT_SIZE,
- MIN_FONT_SIZE,
-} from "@/constants/text-constants";
-import { usePropertyDraft } from "./hooks/use-property-draft";
-import { useKeyframedColorProperty } from "./hooks/use-keyframed-color-property";
-import { useKeyframedNumberProperty } from "./hooks/use-keyframed-number-property";
-import { useElementPlayhead } from "./hooks/use-element-playhead";
-import { TransformSection, BlendingSection } from "./sections";
-import { KeyframeToggle } from "./keyframe-toggle";
-import { isPropertyAtDefault } from "./sections/transform";
-import { resolveColorAtTime, resolveNumberAtTime } from "@/lib/animation";
-import { HugeiconsIcon } from "@hugeicons/react";
-import {
- TextFontIcon,
- ViewIcon,
- ViewOffSlashIcon,
-} from "@hugeicons/core-free-icons";
-import { OcTextHeightIcon, OcTextWidthIcon } from "@opencut/ui/icons";
-import { cn } from "@/utils/ui";
-
-export function TextProperties({
- element,
- trackId,
-}: {
- element: TextElement;
- trackId: string;
-}) {
- return (
- <div className="flex h-full flex-col">
- <ContentSection element={element} trackId={trackId} />
- <TransformSection element={element} trackId={trackId} />
- <BlendingSection element={element} trackId={trackId} />
- <TypographySection element={element} trackId={trackId} />
- <SpacingSection element={element} trackId={trackId} />
- <BackgroundSection element={element} trackId={trackId} />
- </div>
- );
-}
-
-function ContentSection({
- element,
- trackId,
-}: {
- element: TextElement;
- trackId: string;
-}) {
- const editor = useEditor();
-
- const content = usePropertyDraft({
- displayValue: element.content,
- parse: (input) => input,
- onPreview: (value) =>
- editor.timeline.previewElements({
- updates: [
- { trackId, elementId: element.id, updates: { content: value } },
- ],
- }),
- onCommit: () => editor.timeline.commitPreview(),
- });
-
- return (
- <Section collapsible sectionKey="text:content" showTopBorder={false}>
- <SectionHeader>
- <SectionTitle>Content</SectionTitle>
- </SectionHeader>
- <SectionContent>
- <Textarea
- placeholder="Name"
- value={content.displayValue}
- className="min-h-20"
- onFocus={content.onFocus}
- onChange={content.onChange}
- onBlur={content.onBlur}
- />
- </SectionContent>
- </Section>
- );
-}
-
-function TypographySection({
- element,
- trackId,
-}: {
- element: TextElement;
- trackId: string;
-}) {
- const editor = useEditor();
- const { localTime, isPlayheadWithinElementRange } = useElementPlayhead({
- startTime: element.startTime,
- duration: element.duration,
- });
- const resolvedTextColor = resolveColorAtTime({
- baseColor: element.color,
- animations: element.animations,
- propertyPath: "color",
- localTime,
- });
-
- const textColor = useKeyframedColorProperty({
- trackId,
- elementId: element.id,
- animations: element.animations,
- propertyPath: "color",
- localTime,
- isPlayheadWithinElementRange,
- resolvedColor: resolvedTextColor,
- buildBaseUpdates: ({ value }) => ({ color: value }),
- });
-
- const fontSize = usePropertyDraft({
- displayValue: element.fontSize.toString(),
- parse: (input) => {
- const parsed = parseFloat(input);
- if (Number.isNaN(parsed)) return null;
- return clamp({ value: parsed, min: MIN_FONT_SIZE, max: MAX_FONT_SIZE });
- },
- onPreview: (value) =>
- editor.timeline.previewElements({
- updates: [
- { trackId, elementId: element.id, updates: { fontSize: value } },
- ],
- }),
- onCommit: () => editor.timeline.commitPreview(),
- });
-
- return (
- <Section collapsible sectionKey="text:typography">
- <SectionHeader>
- <SectionTitle>Typography</SectionTitle>
- </SectionHeader>
- <SectionContent>
- <SectionFields>
- <SectionField label="Font">
- <FontPicker
- defaultValue={element.fontFamily}
- onValueChange={(value) =>
- editor.timeline.updateElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: { fontFamily: value },
- },
- ],
- })
- }
- />
- </SectionField>
- <SectionField label="Size">
- <NumberField
- value={fontSize.displayValue}
- min={MIN_FONT_SIZE}
- max={MAX_FONT_SIZE}
- onFocus={fontSize.onFocus}
- onChange={fontSize.onChange}
- onBlur={fontSize.onBlur}
- onScrub={fontSize.scrubTo}
- onScrubEnd={fontSize.commitScrub}
- onReset={() =>
- editor.timeline.updateElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: { fontSize: DEFAULT_TEXT_ELEMENT.fontSize },
- },
- ],
- })
- }
- isDefault={element.fontSize === DEFAULT_TEXT_ELEMENT.fontSize}
- icon={<HugeiconsIcon icon={TextFontIcon} />}
- />
- </SectionField>
- <SectionField
- label="Color"
- beforeLabel={
- <KeyframeToggle
- isActive={textColor.isKeyframedAtTime}
- isDisabled={!isPlayheadWithinElementRange}
- title="Toggle text color keyframe"
- onToggle={textColor.toggleKeyframe}
- />
- }
- >
- <ColorPicker
- value={uppercase({
- string: resolvedTextColor.replace("#", ""),
- })}
- onChange={(color) => textColor.onChange({ color: `#${color}` })}
- onChangeEnd={textColor.onChangeEnd}
- />
- </SectionField>
- </SectionFields>
- </SectionContent>
- </Section>
- );
-}
-
-function SpacingSection({
- element,
- trackId,
-}: {
- element: TextElement;
- trackId: string;
-}) {
- const editor = useEditor();
-
- const letterSpacing = usePropertyDraft({
- displayValue: Math.round(
- element.letterSpacing ?? DEFAULT_LETTER_SPACING,
- ).toString(),
- parse: (input) => {
- const parsed = parseFloat(input);
- return Number.isNaN(parsed) ? null : Math.round(parsed);
- },
- onPreview: (value) =>
- editor.timeline.previewElements({
- updates: [
- { trackId, elementId: element.id, updates: { letterSpacing: value } },
- ],
- }),
- onCommit: () => editor.timeline.commitPreview(),
- });
-
- const lineHeight = usePropertyDraft({
- displayValue: (element.lineHeight ?? DEFAULT_LINE_HEIGHT).toFixed(1),
- parse: (input) => {
- const parsed = parseFloat(input);
- return Number.isNaN(parsed)
- ? null
- : Math.max(0.1, Math.round(parsed * 10) / 10);
- },
- onPreview: (value) =>
- editor.timeline.previewElements({
- updates: [
- { trackId, elementId: element.id, updates: { lineHeight: value } },
- ],
- }),
- onCommit: () => editor.timeline.commitPreview(),
- });
-
- return (
- <Section collapsible sectionKey="text:spacing" showBottomBorder={false}>
- <SectionHeader>
- <SectionTitle>Spacing</SectionTitle>
- </SectionHeader>
- <SectionContent>
- <div className="flex items-start gap-2">
- <SectionField label="Letter spacing" className="w-1/2">
- <NumberField
- value={letterSpacing.displayValue}
- onFocus={letterSpacing.onFocus}
- onChange={letterSpacing.onChange}
- onBlur={letterSpacing.onBlur}
- onScrub={letterSpacing.scrubTo}
- onScrubEnd={letterSpacing.commitScrub}
- onReset={() =>
- editor.timeline.updateElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: { letterSpacing: DEFAULT_LETTER_SPACING },
- },
- ],
- })
- }
- isDefault={
- (element.letterSpacing ?? DEFAULT_LETTER_SPACING) ===
- DEFAULT_LETTER_SPACING
- }
- icon={<OcTextWidthIcon size={14} />}
- />
- </SectionField>
- <SectionField label="Line height" className="w-1/2">
- <NumberField
- value={lineHeight.displayValue}
- onFocus={lineHeight.onFocus}
- onChange={lineHeight.onChange}
- onBlur={lineHeight.onBlur}
- onScrub={lineHeight.scrubTo}
- onScrubEnd={lineHeight.commitScrub}
- onReset={() =>
- editor.timeline.updateElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: { lineHeight: DEFAULT_LINE_HEIGHT },
- },
- ],
- })
- }
- isDefault={
- (element.lineHeight ?? DEFAULT_LINE_HEIGHT) ===
- DEFAULT_LINE_HEIGHT
- }
- icon={<OcTextHeightIcon size={14} />}
- />
- </SectionField>
- </div>
- </SectionContent>
- </Section>
- );
-}
-
-function BackgroundSection({
- element,
- trackId,
-}: {
- element: TextElement;
- trackId: string;
-}) {
- const editor = useEditor();
- const lastSelectedColor = useRef(DEFAULT_COLOR);
- const { localTime, isPlayheadWithinElementRange } = useElementPlayhead({
- startTime: element.startTime,
- duration: element.duration,
- });
- const resolvedBgColor = resolveColorAtTime({
- baseColor: element.background.color,
- animations: element.animations,
- propertyPath: "background.color",
- localTime,
- });
-
- const bgColor = useKeyframedColorProperty({
- trackId,
- elementId: element.id,
- animations: element.animations,
- propertyPath: "background.color",
- localTime,
- isPlayheadWithinElementRange,
- resolvedColor: resolvedBgColor,
- buildBaseUpdates: ({ value }) => ({
- background: { ...element.background, color: value },
- }),
- });
-
- const bg = element.background;
-
- const resolvedPaddingX = resolveNumberAtTime({
- baseValue: bg.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX,
- animations: element.animations,
- propertyPath: "background.paddingX",
- localTime,
- });
- const resolvedPaddingY = resolveNumberAtTime({
- baseValue: bg.paddingY ?? DEFAULT_TEXT_BACKGROUND.paddingY,
- animations: element.animations,
- propertyPath: "background.paddingY",
- localTime,
- });
- const resolvedOffsetX = resolveNumberAtTime({
- baseValue: bg.offsetX ?? DEFAULT_TEXT_BACKGROUND.offsetX,
- animations: element.animations,
- propertyPath: "background.offsetX",
- localTime,
- });
- const resolvedOffsetY = resolveNumberAtTime({
- baseValue: bg.offsetY ?? DEFAULT_TEXT_BACKGROUND.offsetY,
- animations: element.animations,
- propertyPath: "background.offsetY",
- localTime,
- });
- const resolvedCornerRadius = resolveNumberAtTime({
- baseValue: bg.cornerRadius ?? CORNER_RADIUS_MIN,
- animations: element.animations,
- propertyPath: "background.cornerRadius",
- localTime,
- });
-
- const paddingX = useKeyframedNumberProperty({
- trackId,
- elementId: element.id,
- animations: element.animations,
- propertyPath: "background.paddingX",
- localTime,
- isPlayheadWithinElementRange,
- displayValue: Math.round(resolvedPaddingX).toString(),
- parse: (input) => {
- const parsed = parseFloat(input);
- return Number.isNaN(parsed) ? null : Math.max(0, Math.round(parsed));
- },
- valueAtPlayhead: resolvedPaddingX,
- buildBaseUpdates: ({ value }) => ({
- background: { ...bg, paddingX: value },
- }),
- });
-
- const paddingY = useKeyframedNumberProperty({
- trackId,
- elementId: element.id,
- animations: element.animations,
- propertyPath: "background.paddingY",
- localTime,
- isPlayheadWithinElementRange,
- displayValue: Math.round(resolvedPaddingY).toString(),
- parse: (input) => {
- const parsed = parseFloat(input);
- return Number.isNaN(parsed) ? null : Math.max(0, Math.round(parsed));
- },
- valueAtPlayhead: resolvedPaddingY,
- buildBaseUpdates: ({ value }) => ({
- background: { ...bg, paddingY: value },
- }),
- });
-
- const offsetX = useKeyframedNumberProperty({
- trackId,
- elementId: element.id,
- animations: element.animations,
- propertyPath: "background.offsetX",
- localTime,
- isPlayheadWithinElementRange,
- displayValue: Math.round(resolvedOffsetX).toString(),
- parse: (input) => {
- const parsed = parseFloat(input);
- return Number.isNaN(parsed) ? null : Math.round(parsed);
- },
- valueAtPlayhead: resolvedOffsetX,
- buildBaseUpdates: ({ value }) => ({
- background: { ...bg, offsetX: value },
- }),
- });
-
- const offsetY = useKeyframedNumberProperty({
- trackId,
- elementId: element.id,
- animations: element.animations,
- propertyPath: "background.offsetY",
- localTime,
- isPlayheadWithinElementRange,
- displayValue: Math.round(resolvedOffsetY).toString(),
- parse: (input) => {
- const parsed = parseFloat(input);
- return Number.isNaN(parsed) ? null : Math.round(parsed);
- },
- valueAtPlayhead: resolvedOffsetY,
- buildBaseUpdates: ({ value }) => ({
- background: { ...bg, offsetY: value },
- }),
- });
-
- const cornerRadius = useKeyframedNumberProperty({
- trackId,
- elementId: element.id,
- animations: element.animations,
- propertyPath: "background.cornerRadius",
- localTime,
- isPlayheadWithinElementRange,
- displayValue: Math.round(resolvedCornerRadius).toString(),
- parse: (input) => {
- const parsed = parseFloat(input);
- if (Number.isNaN(parsed)) return null;
- return clamp({ value: Math.round(parsed), min: CORNER_RADIUS_MIN, max: CORNER_RADIUS_MAX });
- },
- valueAtPlayhead: resolvedCornerRadius,
- buildBaseUpdates: ({ value }) => ({
- background: { ...bg, cornerRadius: value },
- }),
- });
-
- const toggleBackgroundEnabled = () => {
- const enabled = !element.background.enabled;
- const color =
- enabled && element.background.color === "transparent"
- ? lastSelectedColor.current
- : element.background.color;
- editor.timeline.updateElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: {
- background: {
- ...element.background,
- enabled,
- color,
- },
- },
- },
- ],
- });
- };
-
- return (
- <Section
- collapsible
- defaultOpen={element.background.enabled}
- sectionKey="text:background"
- >
- <SectionHeader
- trailing={
- <Button
- variant="ghost"
- size="icon"
- onClick={(event) => {
- event.stopPropagation();
- toggleBackgroundEnabled();
- }}
- >
- <HugeiconsIcon
- icon={element.background.enabled ? ViewIcon : ViewOffSlashIcon}
- />
- </Button>
- }
- >
- <SectionTitle>Background</SectionTitle>
- </SectionHeader>
- <SectionContent
- className={cn(
- !element.background.enabled && "pointer-events-none opacity-50",
- )}
- >
- <SectionFields>
- <SectionField
- label="Color"
- beforeLabel={
- <KeyframeToggle
- isActive={bgColor.isKeyframedAtTime}
- isDisabled={!isPlayheadWithinElementRange}
- title="Toggle background color keyframe"
- onToggle={bgColor.toggleKeyframe}
- />
- }
- >
- <ColorPicker
- value={
- !element.background.enabled ||
- element.background.color === "transparent"
- ? lastSelectedColor.current.replace("#", "")
- : resolvedBgColor.replace("#", "")
- }
- onChange={(color) => {
- const hexColor = `#${color}`;
- if (color !== "transparent") {
- lastSelectedColor.current = hexColor;
- }
- bgColor.onChange({ color: hexColor });
- }}
- onChangeEnd={bgColor.onChangeEnd}
- />
- </SectionField>
- <div className="flex items-start gap-2">
- <SectionField
- label="Width"
- className="w-1/2"
- beforeLabel={
- <KeyframeToggle
- isActive={paddingX.isKeyframedAtTime}
- isDisabled={!isPlayheadWithinElementRange}
- title="Toggle background width keyframe"
- onToggle={paddingX.toggleKeyframe}
- />
- }
- >
- <NumberField
- icon="W"
- value={paddingX.displayValue}
- min={0}
- onFocus={paddingX.onFocus}
- onChange={paddingX.onChange}
- onBlur={paddingX.onBlur}
- onScrub={paddingX.scrubTo}
- onScrubEnd={paddingX.commitScrub}
- onReset={() => paddingX.commitValue({ value: DEFAULT_TEXT_BACKGROUND.paddingX })}
- isDefault={isPropertyAtDefault({
- hasAnimatedKeyframes: paddingX.hasAnimatedKeyframes,
- isPlayheadWithinElementRange,
- resolvedValue: resolvedPaddingX,
- staticValue: bg.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX,
- defaultValue: DEFAULT_TEXT_BACKGROUND.paddingX,
- })}
- />
- </SectionField>
- <SectionField
- label="Height"
- className="w-1/2"
- beforeLabel={
- <KeyframeToggle
- isActive={paddingY.isKeyframedAtTime}
- isDisabled={!isPlayheadWithinElementRange}
- title="Toggle background height keyframe"
- onToggle={paddingY.toggleKeyframe}
- />
- }
- >
- <NumberField
- icon="H"
- value={paddingY.displayValue}
- min={0}
- onFocus={paddingY.onFocus}
- onChange={paddingY.onChange}
- onBlur={paddingY.onBlur}
- onScrub={paddingY.scrubTo}
- onScrubEnd={paddingY.commitScrub}
- onReset={() => paddingY.commitValue({ value: DEFAULT_TEXT_BACKGROUND.paddingY })}
- isDefault={isPropertyAtDefault({
- hasAnimatedKeyframes: paddingY.hasAnimatedKeyframes,
- isPlayheadWithinElementRange,
- resolvedValue: resolvedPaddingY,
- staticValue: bg.paddingY ?? DEFAULT_TEXT_BACKGROUND.paddingY,
- defaultValue: DEFAULT_TEXT_BACKGROUND.paddingY,
- })}
- />
- </SectionField>
- </div>
- <div className="flex items-start gap-2">
- <SectionField
- label="X-offset"
- className="w-1/2"
- beforeLabel={
- <KeyframeToggle
- isActive={offsetX.isKeyframedAtTime}
- isDisabled={!isPlayheadWithinElementRange}
- title="Toggle x-offset keyframe"
- onToggle={offsetX.toggleKeyframe}
- />
- }
- >
- <NumberField
- icon="X"
- value={offsetX.displayValue}
- onFocus={offsetX.onFocus}
- onChange={offsetX.onChange}
- onBlur={offsetX.onBlur}
- onScrub={offsetX.scrubTo}
- onScrubEnd={offsetX.commitScrub}
- onReset={() => offsetX.commitValue({ value: DEFAULT_TEXT_BACKGROUND.offsetX })}
- isDefault={isPropertyAtDefault({
- hasAnimatedKeyframes: offsetX.hasAnimatedKeyframes,
- isPlayheadWithinElementRange,
- resolvedValue: resolvedOffsetX,
- staticValue: bg.offsetX ?? DEFAULT_TEXT_BACKGROUND.offsetX,
- defaultValue: DEFAULT_TEXT_BACKGROUND.offsetX,
- })}
- />
- </SectionField>
- <SectionField
- label="Y-offset"
- className="w-1/2"
- beforeLabel={
- <KeyframeToggle
- isActive={offsetY.isKeyframedAtTime}
- isDisabled={!isPlayheadWithinElementRange}
- title="Toggle y-offset keyframe"
- onToggle={offsetY.toggleKeyframe}
- />
- }
- >
- <NumberField
- icon="Y"
- value={offsetY.displayValue}
- onFocus={offsetY.onFocus}
- onChange={offsetY.onChange}
- onBlur={offsetY.onBlur}
- onScrub={offsetY.scrubTo}
- onScrubEnd={offsetY.commitScrub}
- onReset={() => offsetY.commitValue({ value: DEFAULT_TEXT_BACKGROUND.offsetY })}
- isDefault={isPropertyAtDefault({
- hasAnimatedKeyframes: offsetY.hasAnimatedKeyframes,
- isPlayheadWithinElementRange,
- resolvedValue: resolvedOffsetY,
- staticValue: bg.offsetY ?? DEFAULT_TEXT_BACKGROUND.offsetY,
- defaultValue: DEFAULT_TEXT_BACKGROUND.offsetY,
- })}
- />
- </SectionField>
- </div>
- <SectionField
- label="Corner radius"
- beforeLabel={
- <KeyframeToggle
- isActive={cornerRadius.isKeyframedAtTime}
- isDisabled={!isPlayheadWithinElementRange}
- title="Toggle corner radius keyframe"
- onToggle={cornerRadius.toggleKeyframe}
- />
- }
- >
- <NumberField
- icon="R"
- value={cornerRadius.displayValue}
- min={CORNER_RADIUS_MIN}
- max={CORNER_RADIUS_MAX}
- onFocus={cornerRadius.onFocus}
- onChange={cornerRadius.onChange}
- onBlur={cornerRadius.onBlur}
- onScrub={cornerRadius.scrubTo}
- onScrubEnd={cornerRadius.commitScrub}
- onReset={() => cornerRadius.commitValue({ value: CORNER_RADIUS_MIN })}
- isDefault={isPropertyAtDefault({
- hasAnimatedKeyframes: cornerRadius.hasAnimatedKeyframes,
- isPlayheadWithinElementRange,
- resolvedValue: resolvedCornerRadius,
- staticValue: bg.cornerRadius ?? CORNER_RADIUS_MIN,
- defaultValue: CORNER_RADIUS_MIN,
- })}
- />
- </SectionField>
- </SectionFields>
- </SectionContent>
- </Section>
- );
-}
diff --git a/legacy/components/properties/video-properties.tsx b/legacy/components/properties/video-properties.tsx deleted file mode 100644 index b12203d1..00000000 --- a/legacy/components/properties/video-properties.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import type {
- ImageElement,
- StickerElement,
- VideoElement,
-} from "@/types/timeline";
-import { BlendingSection, TransformSection } from "./sections";
-
-export function VideoProperties({
- element,
- trackId,
-}: {
- element: VideoElement | ImageElement | StickerElement;
- trackId: string;
-}) {
- return (
- <div className="flex h-full flex-col">
- <TransformSection
- element={element}
- trackId={trackId}
- showTopBorder={false}
- />
- <BlendingSection element={element} trackId={trackId} />
- </div>
- );
-}
|
