From 220ec757d42b54f498d031436e5b4b03931b165c Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Sun, 29 Mar 2026 14:15:21 +0200 Subject: refactor: move packages sub-folders into web app --- .../editor/panels/properties/registry.tsx | 606 ++++---- .../editor/panels/properties/tabs/blending-tab.tsx | 460 +++--- .../editor/panels/properties/tabs/masks-tab.tsx | 2 +- .../editor/panels/properties/tabs/text-tab.tsx | 1530 ++++++++++---------- 4 files changed, 1299 insertions(+), 1299 deletions(-) (limited to 'apps/web/src/components/editor/panels/properties') diff --git a/apps/web/src/components/editor/panels/properties/registry.tsx b/apps/web/src/components/editor/panels/properties/registry.tsx index 38d0a189..df6c0e52 100644 --- a/apps/web/src/components/editor/panels/properties/registry.tsx +++ b/apps/web/src/components/editor/panels/properties/registry.tsx @@ -1,303 +1,303 @@ -import type { ReactNode } from "react"; -import type { - EffectElement, - GraphicElement, - ImageElement, - MaskableElement, - RetimableElement, - StickerElement, - TextElement, - VisualElement, - VideoElement, - AudioElement, - TimelineElement, -} from "@/lib/timeline"; -import type { MediaAsset } from "@/lib/media/types"; -import { HugeiconsIcon } from "@hugeicons/react"; -import { - TextFontIcon, - ArrowExpandIcon, - RainDropIcon, - MusicNote03Icon, - MagicWand05Icon, - DashboardSpeed02Icon, -} from "@hugeicons/core-free-icons"; -import { TransformTab } from "./tabs/transform-tab"; -import { BlendingTab } from "./tabs/blending-tab"; -import { AudioTab } from "./tabs/audio-tab"; -import { TextTab } from "./tabs/text-tab"; -import { ClipEffectsTab, StandaloneEffectTab } from "./tabs/effects-tab"; -import { MasksTab } from "./tabs/masks-tab"; -import { SpeedTab } from "./tabs/speed-tab"; -import { GraphicTab } from "./tabs/graphic-tab"; -import { OcShapesIcon } from "@opencut/ui/icons"; - -export type TabContentProps = { - trackId: string; -}; - -export type PropertiesTabDef = { - id: string; - label: string; - icon: ReactNode; - content: (props: TabContentProps) => ReactNode; -}; - -export type ElementPropertiesConfig = { - defaultTab: string; - tabs: PropertiesTabDef[]; -}; - -function buildTransformTab({ - element, -}: { - element: VisualElement; -}): PropertiesTabDef { - return { - id: "transform", - label: "Transform", - icon: , - content: ({ trackId }) => ( - - ), - }; -} - -function buildBlendingTab({ - element, -}: { - element: VisualElement; -}): PropertiesTabDef { - return { - id: "blending", - label: "Blending", - icon: , - content: ({ trackId }) => ( - - ), - }; -} - -function buildAudioTab({ - element, -}: { - element: AudioElement | VideoElement; -}): PropertiesTabDef { - return { - id: "audio", - label: "Audio", - icon: , - content: ({ trackId }) => , - }; -} - -function buildSpeedTab({ - element, -}: { - element: RetimableElement; -}): PropertiesTabDef { - return { - id: "speed", - label: "Speed", - icon: , - content: ({ trackId }) => , - }; -} - -function buildMasksTab({ - element, -}: { - element: MaskableElement; -}): PropertiesTabDef { - return { - id: "masks", - label: "Masks", - icon: , - content: ({ trackId }) => , - }; -} - -function buildClipEffectsTab({ - element, -}: { - element: VisualElement; -}): PropertiesTabDef { - return { - id: "effects", - label: "Effects", - icon: , - content: ({ trackId }) => ( - - ), - }; -} - -function buildTextTab({ element }: { element: TextElement }): PropertiesTabDef { - return { - id: "text", - label: "Text", - icon: , - content: ({ trackId }) => , - }; -} - -function buildGraphicTab({ - element, -}: { - element: GraphicElement; -}): PropertiesTabDef { - return { - id: "graphic", - label: "Graphic", - icon: , - content: ({ trackId }) => , - }; -} - -function buildStandaloneEffectTab({ - element, -}: { - element: EffectElement; -}): PropertiesTabDef { - return { - id: "effects", - label: "Effects", - icon: , - content: ({ trackId }) => ( - - ), - }; -} - -function getTextConfig({ - element, -}: { - element: TextElement; -}): ElementPropertiesConfig { - return { - defaultTab: "text", - tabs: [ - buildTextTab({ element }), - buildTransformTab({ element }), - buildBlendingTab({ element }), - ], - }; -} - -function getVideoConfig({ - element, - mediaAsset, -}: { - element: VideoElement; - mediaAsset: MediaAsset | undefined; -}): ElementPropertiesConfig { - const showAudioTab = mediaAsset?.hasAudio !== false; - return { - defaultTab: "transform", - tabs: [ - buildTransformTab({ element }), - ...(showAudioTab ? [buildAudioTab({ element })] : []), - buildSpeedTab({ element }), - buildBlendingTab({ element }), - buildMasksTab({ element }), - buildClipEffectsTab({ element }), - ], - }; -} - -function getImageConfig({ - element, -}: { - element: ImageElement; -}): ElementPropertiesConfig { - return { - defaultTab: "transform", - tabs: [ - buildTransformTab({ element }), - buildBlendingTab({ element }), - buildMasksTab({ element }), - buildClipEffectsTab({ element }), - ], - }; -} - -function getStickerConfig({ - element, -}: { - element: StickerElement; -}): ElementPropertiesConfig { - return { - defaultTab: "transform", - tabs: [ - buildTransformTab({ element }), - buildBlendingTab({ element }), - buildClipEffectsTab({ element }), - ], - }; -} - -function getGraphicConfig({ - element, -}: { - element: GraphicElement; -}): ElementPropertiesConfig { - return { - defaultTab: "graphic", - tabs: [ - buildGraphicTab({ element }), - buildTransformTab({ element }), - buildBlendingTab({ element }), - buildMasksTab({ element }), - buildClipEffectsTab({ element }), - ], - }; -} - -function getAudioConfig({ - element, -}: { - element: AudioElement; -}): ElementPropertiesConfig { - return { - defaultTab: "audio", - tabs: [buildAudioTab({ element }), buildSpeedTab({ element })], - }; -} - -function getEffectConfig({ - element, -}: { - element: EffectElement; -}): ElementPropertiesConfig { - return { - defaultTab: "effects", - tabs: [buildStandaloneEffectTab({ element })], - }; -} - -export function getPropertiesConfig({ - element, - mediaAssets, -}: { - element: TimelineElement; - mediaAssets: MediaAsset[]; -}): ElementPropertiesConfig { - switch (element.type) { - case "text": - return getTextConfig({ element }); - case "video": { - const mediaAsset = mediaAssets.find((a) => a.id === element.mediaId); - return getVideoConfig({ element, mediaAsset }); - } - case "image": - return getImageConfig({ element }); - case "sticker": - return getStickerConfig({ element }); - case "graphic": - return getGraphicConfig({ element }); - case "audio": - return getAudioConfig({ element }); - case "effect": - return getEffectConfig({ element }); - } -} +import type { ReactNode } from "react"; +import type { + EffectElement, + GraphicElement, + ImageElement, + MaskableElement, + RetimableElement, + StickerElement, + TextElement, + VisualElement, + VideoElement, + AudioElement, + TimelineElement, +} from "@/lib/timeline"; +import type { MediaAsset } from "@/lib/media/types"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { + TextFontIcon, + ArrowExpandIcon, + RainDropIcon, + MusicNote03Icon, + MagicWand05Icon, + DashboardSpeed02Icon, +} from "@hugeicons/core-free-icons"; +import { TransformTab } from "./tabs/transform-tab"; +import { BlendingTab } from "./tabs/blending-tab"; +import { AudioTab } from "./tabs/audio-tab"; +import { TextTab } from "./tabs/text-tab"; +import { ClipEffectsTab, StandaloneEffectTab } from "./tabs/effects-tab"; +import { MasksTab } from "./tabs/masks-tab"; +import { SpeedTab } from "./tabs/speed-tab"; +import { GraphicTab } from "./tabs/graphic-tab"; +import { OcShapesIcon } from "@/components/icons"; + +export type TabContentProps = { + trackId: string; +}; + +export type PropertiesTabDef = { + id: string; + label: string; + icon: ReactNode; + content: (props: TabContentProps) => ReactNode; +}; + +export type ElementPropertiesConfig = { + defaultTab: string; + tabs: PropertiesTabDef[]; +}; + +function buildTransformTab({ + element, +}: { + element: VisualElement; +}): PropertiesTabDef { + return { + id: "transform", + label: "Transform", + icon: , + content: ({ trackId }) => ( + + ), + }; +} + +function buildBlendingTab({ + element, +}: { + element: VisualElement; +}): PropertiesTabDef { + return { + id: "blending", + label: "Blending", + icon: , + content: ({ trackId }) => ( + + ), + }; +} + +function buildAudioTab({ + element, +}: { + element: AudioElement | VideoElement; +}): PropertiesTabDef { + return { + id: "audio", + label: "Audio", + icon: , + content: ({ trackId }) => , + }; +} + +function buildSpeedTab({ + element, +}: { + element: RetimableElement; +}): PropertiesTabDef { + return { + id: "speed", + label: "Speed", + icon: , + content: ({ trackId }) => , + }; +} + +function buildMasksTab({ + element, +}: { + element: MaskableElement; +}): PropertiesTabDef { + return { + id: "masks", + label: "Masks", + icon: , + content: ({ trackId }) => , + }; +} + +function buildClipEffectsTab({ + element, +}: { + element: VisualElement; +}): PropertiesTabDef { + return { + id: "effects", + label: "Effects", + icon: , + content: ({ trackId }) => ( + + ), + }; +} + +function buildTextTab({ element }: { element: TextElement }): PropertiesTabDef { + return { + id: "text", + label: "Text", + icon: , + content: ({ trackId }) => , + }; +} + +function buildGraphicTab({ + element, +}: { + element: GraphicElement; +}): PropertiesTabDef { + return { + id: "graphic", + label: "Graphic", + icon: , + content: ({ trackId }) => , + }; +} + +function buildStandaloneEffectTab({ + element, +}: { + element: EffectElement; +}): PropertiesTabDef { + return { + id: "effects", + label: "Effects", + icon: , + content: ({ trackId }) => ( + + ), + }; +} + +function getTextConfig({ + element, +}: { + element: TextElement; +}): ElementPropertiesConfig { + return { + defaultTab: "text", + tabs: [ + buildTextTab({ element }), + buildTransformTab({ element }), + buildBlendingTab({ element }), + ], + }; +} + +function getVideoConfig({ + element, + mediaAsset, +}: { + element: VideoElement; + mediaAsset: MediaAsset | undefined; +}): ElementPropertiesConfig { + const showAudioTab = mediaAsset?.hasAudio !== false; + return { + defaultTab: "transform", + tabs: [ + buildTransformTab({ element }), + ...(showAudioTab ? [buildAudioTab({ element })] : []), + buildSpeedTab({ element }), + buildBlendingTab({ element }), + buildMasksTab({ element }), + buildClipEffectsTab({ element }), + ], + }; +} + +function getImageConfig({ + element, +}: { + element: ImageElement; +}): ElementPropertiesConfig { + return { + defaultTab: "transform", + tabs: [ + buildTransformTab({ element }), + buildBlendingTab({ element }), + buildMasksTab({ element }), + buildClipEffectsTab({ element }), + ], + }; +} + +function getStickerConfig({ + element, +}: { + element: StickerElement; +}): ElementPropertiesConfig { + return { + defaultTab: "transform", + tabs: [ + buildTransformTab({ element }), + buildBlendingTab({ element }), + buildClipEffectsTab({ element }), + ], + }; +} + +function getGraphicConfig({ + element, +}: { + element: GraphicElement; +}): ElementPropertiesConfig { + return { + defaultTab: "graphic", + tabs: [ + buildGraphicTab({ element }), + buildTransformTab({ element }), + buildBlendingTab({ element }), + buildMasksTab({ element }), + buildClipEffectsTab({ element }), + ], + }; +} + +function getAudioConfig({ + element, +}: { + element: AudioElement; +}): ElementPropertiesConfig { + return { + defaultTab: "audio", + tabs: [buildAudioTab({ element }), buildSpeedTab({ element })], + }; +} + +function getEffectConfig({ + element, +}: { + element: EffectElement; +}): ElementPropertiesConfig { + return { + defaultTab: "effects", + tabs: [buildStandaloneEffectTab({ element })], + }; +} + +export function getPropertiesConfig({ + element, + mediaAssets, +}: { + element: TimelineElement; + mediaAssets: MediaAsset[]; +}): ElementPropertiesConfig { + switch (element.type) { + case "text": + return getTextConfig({ element }); + case "video": { + const mediaAsset = mediaAssets.find((a) => a.id === element.mediaId); + return getVideoConfig({ element, mediaAsset }); + } + case "image": + return getImageConfig({ element }); + case "sticker": + return getStickerConfig({ element }); + case "graphic": + return getGraphicConfig({ element }); + case "audio": + return getAudioConfig({ element }); + case "effect": + return getEffectConfig({ element }); + } +} diff --git a/apps/web/src/components/editor/panels/properties/tabs/blending-tab.tsx b/apps/web/src/components/editor/panels/properties/tabs/blending-tab.tsx index fdcde148..a69acebf 100644 --- a/apps/web/src/components/editor/panels/properties/tabs/blending-tab.tsx +++ b/apps/web/src/components/editor/panels/properties/tabs/blending-tab.tsx @@ -1,230 +1,230 @@ -import { useEditor } from "@/hooks/use-editor"; -import { clamp } from "@/utils/math"; -import { NumberField } from "@/components/ui/number-field"; -import { OcCheckerboardIcon } from "@opencut/ui/icons"; -import { Fragment, useRef } from "react"; -import { useMenuPreview } from "@/hooks/use-menu-preview"; -import { - Section, - SectionContent, - SectionField, - SectionHeader, - SectionTitle, -} from "@/components/section"; -import { - Select, - SelectContent, - SelectItem, - SelectSeparator, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import type { BlendMode } from "@/lib/rendering"; -import type { ElementType } from "@/lib/timeline"; -import type { ElementAnimations } from "@/lib/animation/types"; -import { HugeiconsIcon } from "@hugeicons/react"; -import { RainDropIcon } from "@hugeicons/core-free-icons"; -import { KeyframeToggle } from "../components/keyframe-toggle"; -import { useKeyframedNumberProperty } from "../hooks/use-keyframed-number-property"; -import { useElementPlayhead } from "../hooks/use-element-playhead"; -import { resolveOpacityAtTime } from "@/lib/animation"; -import { DEFAULTS } from "@/lib/timeline/defaults"; -import { isPropertyAtDefault } from "./transform-tab"; - -type BlendingElement = { - id: string; - opacity: number; - type: ElementType; - blendMode?: BlendMode; - startTime: number; - duration: number; - animations?: ElementAnimations; -}; - -const BLEND_MODE_GROUPS = [ - [{ value: "normal", label: "Normal" }], - [ - { value: "darken", label: "Darken" }, - { value: "multiply", label: "Multiply" }, - { value: "color-burn", label: "Color Burn" }, - ], - [ - { value: "lighten", label: "Lighten" }, - { value: "screen", label: "Screen" }, - { value: "plus-lighter", label: "Plus Lighter" }, - { value: "color-dodge", label: "Color Dodge" }, - ], - [ - { value: "overlay", label: "Overlay" }, - { value: "soft-light", label: "Soft Light" }, - { value: "hard-light", label: "Hard Light" }, - ], - [ - { value: "difference", label: "Difference" }, - { value: "exclusion", label: "Exclusion" }, - ], - [ - { value: "hue", label: "Hue" }, - { value: "saturation", label: "Saturation" }, - { value: "color", label: "Color" }, - { value: "luminosity", label: "Luminosity" }, - ], -]; - -export function BlendingTab({ - element, - trackId, -}: { - element: BlendingElement; - trackId: string; -}) { - const editor = useEditor(); - const isPreviewActive = useEditor((e) => e.timeline.isPreviewActive()); - const blendMode = element.blendMode ?? DEFAULTS.element.blendMode; - const committedBlendModeRef = useRef(blendMode); - if (!isPreviewActive) { - committedBlendModeRef.current = blendMode; - } - - const { - onPointerLeave, - onOpenChange: handleBlendModeOpenChange, - markCommitted, - } = useMenuPreview(); - - const previewBlendMode = ({ value }: { value: BlendMode }) => - editor.timeline.previewElements({ - updates: [ - { trackId, elementId: element.id, updates: { blendMode: value } }, - ], - }); - - const commitBlendMode = (value: string) => { - if (editor.timeline.isPreviewActive()) { - editor.timeline.commitPreview(); - } else { - editor.timeline.updateElements({ - updates: [ - { - trackId, - elementId: element.id, - updates: { blendMode: value as BlendMode }, - }, - ], - }); - } - markCommitted(); - }; - - const { localTime, isPlayheadWithinElementRange } = useElementPlayhead({ - startTime: element.startTime, - duration: element.duration, - }); - const resolvedOpacity = resolveOpacityAtTime({ - baseOpacity: element.opacity, - animations: element.animations, - localTime, - }); - - const opacity = useKeyframedNumberProperty({ - trackId, - elementId: element.id, - animations: element.animations, - propertyPath: "opacity", - localTime, - isPlayheadWithinElementRange, - displayValue: Math.round(resolvedOpacity * 100).toString(), - parse: (input) => { - const parsed = parseFloat(input); - if (Number.isNaN(parsed)) return null; - return clamp({ value: parsed, min: 0, max: 100 }) / 100; - }, - valueAtPlayhead: resolvedOpacity, - step: 0.01, - buildBaseUpdates: ({ value }) => ({ opacity: value }), - }); - - return ( -
- - Blending - - -
- - } - > - - } - value={opacity.displayValue} - min={0} - max={100} - onFocus={opacity.onFocus} - onChange={opacity.onChange} - onBlur={opacity.onBlur} - onScrub={opacity.scrubTo} - onScrubEnd={opacity.commitScrub} - onReset={() => - opacity.commitValue({ value: DEFAULTS.element.opacity }) - } - isDefault={isPropertyAtDefault({ - hasAnimatedKeyframes: opacity.hasAnimatedKeyframes, - isPlayheadWithinElementRange, - resolvedValue: resolvedOpacity, - staticValue: element.opacity, - defaultValue: DEFAULTS.element.opacity, - })} - dragSensitivity="slow" - /> - - - - -
-
-
- ); -} +import { useEditor } from "@/hooks/use-editor"; +import { clamp } from "@/utils/math"; +import { NumberField } from "@/components/ui/number-field"; +import { OcCheckerboardIcon } from "@/components/icons"; +import { Fragment, useRef } from "react"; +import { useMenuPreview } from "@/hooks/use-menu-preview"; +import { + Section, + SectionContent, + SectionField, + SectionHeader, + SectionTitle, +} from "@/components/section"; +import { + Select, + SelectContent, + SelectItem, + SelectSeparator, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import type { BlendMode } from "@/lib/rendering"; +import type { ElementType } from "@/lib/timeline"; +import type { ElementAnimations } from "@/lib/animation/types"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { RainDropIcon } from "@hugeicons/core-free-icons"; +import { KeyframeToggle } from "../components/keyframe-toggle"; +import { useKeyframedNumberProperty } from "../hooks/use-keyframed-number-property"; +import { useElementPlayhead } from "../hooks/use-element-playhead"; +import { resolveOpacityAtTime } from "@/lib/animation"; +import { DEFAULTS } from "@/lib/timeline/defaults"; +import { isPropertyAtDefault } from "./transform-tab"; + +type BlendingElement = { + id: string; + opacity: number; + type: ElementType; + blendMode?: BlendMode; + startTime: number; + duration: number; + animations?: ElementAnimations; +}; + +const BLEND_MODE_GROUPS = [ + [{ value: "normal", label: "Normal" }], + [ + { value: "darken", label: "Darken" }, + { value: "multiply", label: "Multiply" }, + { value: "color-burn", label: "Color Burn" }, + ], + [ + { value: "lighten", label: "Lighten" }, + { value: "screen", label: "Screen" }, + { value: "plus-lighter", label: "Plus Lighter" }, + { value: "color-dodge", label: "Color Dodge" }, + ], + [ + { value: "overlay", label: "Overlay" }, + { value: "soft-light", label: "Soft Light" }, + { value: "hard-light", label: "Hard Light" }, + ], + [ + { value: "difference", label: "Difference" }, + { value: "exclusion", label: "Exclusion" }, + ], + [ + { value: "hue", label: "Hue" }, + { value: "saturation", label: "Saturation" }, + { value: "color", label: "Color" }, + { value: "luminosity", label: "Luminosity" }, + ], +]; + +export function BlendingTab({ + element, + trackId, +}: { + element: BlendingElement; + trackId: string; +}) { + const editor = useEditor(); + const isPreviewActive = useEditor((e) => e.timeline.isPreviewActive()); + const blendMode = element.blendMode ?? DEFAULTS.element.blendMode; + const committedBlendModeRef = useRef(blendMode); + if (!isPreviewActive) { + committedBlendModeRef.current = blendMode; + } + + const { + onPointerLeave, + onOpenChange: handleBlendModeOpenChange, + markCommitted, + } = useMenuPreview(); + + const previewBlendMode = ({ value }: { value: BlendMode }) => + editor.timeline.previewElements({ + updates: [ + { trackId, elementId: element.id, updates: { blendMode: value } }, + ], + }); + + const commitBlendMode = (value: string) => { + if (editor.timeline.isPreviewActive()) { + editor.timeline.commitPreview(); + } else { + editor.timeline.updateElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { blendMode: value as BlendMode }, + }, + ], + }); + } + markCommitted(); + }; + + const { localTime, isPlayheadWithinElementRange } = useElementPlayhead({ + startTime: element.startTime, + duration: element.duration, + }); + const resolvedOpacity = resolveOpacityAtTime({ + baseOpacity: element.opacity, + animations: element.animations, + localTime, + }); + + const opacity = useKeyframedNumberProperty({ + trackId, + elementId: element.id, + animations: element.animations, + propertyPath: "opacity", + localTime, + isPlayheadWithinElementRange, + displayValue: Math.round(resolvedOpacity * 100).toString(), + parse: (input) => { + const parsed = parseFloat(input); + if (Number.isNaN(parsed)) return null; + return clamp({ value: parsed, min: 0, max: 100 }) / 100; + }, + valueAtPlayhead: resolvedOpacity, + step: 0.01, + buildBaseUpdates: ({ value }) => ({ opacity: value }), + }); + + return ( +
+ + Blending + + +
+ + } + > + + } + value={opacity.displayValue} + min={0} + max={100} + onFocus={opacity.onFocus} + onChange={opacity.onChange} + onBlur={opacity.onBlur} + onScrub={opacity.scrubTo} + onScrubEnd={opacity.commitScrub} + onReset={() => + opacity.commitValue({ value: DEFAULTS.element.opacity }) + } + isDefault={isPropertyAtDefault({ + hasAnimatedKeyframes: opacity.hasAnimatedKeyframes, + isPlayheadWithinElementRange, + resolvedValue: resolvedOpacity, + staticValue: element.opacity, + defaultValue: DEFAULTS.element.opacity, + })} + dragSensitivity="slow" + /> + + + + +
+
+
+ ); +} diff --git a/apps/web/src/components/editor/panels/properties/tabs/masks-tab.tsx b/apps/web/src/components/editor/panels/properties/tabs/masks-tab.tsx index db8bd728..d416dc28 100644 --- a/apps/web/src/components/editor/panels/properties/tabs/masks-tab.tsx +++ b/apps/web/src/components/editor/panels/properties/tabs/masks-tab.tsx @@ -53,7 +53,7 @@ import { SectionTitle, } from "@/components/section"; import { usePropertyDraft } from "../hooks/use-property-draft"; -import { OcMirrorIcon, OcShapesIcon } from "@opencut/ui/icons"; +import { OcMirrorIcon, OcShapesIcon } from "@/components/icons"; import { cn } from "@/utils/ui"; type MasksTabProps = { diff --git a/apps/web/src/components/editor/panels/properties/tabs/text-tab.tsx b/apps/web/src/components/editor/panels/properties/tabs/text-tab.tsx index be85bede..4c58023b 100644 --- a/apps/web/src/components/editor/panels/properties/tabs/text-tab.tsx +++ b/apps/web/src/components/editor/panels/properties/tabs/text-tab.tsx @@ -1,765 +1,765 @@ -"use client"; - -import { Textarea } from "@/components/ui/textarea"; -import { FontPicker } from "@/components/ui/font-picker"; -import type { TextElement } from "@/lib/timeline"; -import { NumberField } from "@/components/ui/number-field"; -import { useRef } from "react"; -import { - Section, - SectionContent, - SectionField, - SectionFields, - SectionHeader, - SectionTitle, -} from "@/components/section"; -import { ColorPicker } from "@/components/ui/color-picker"; -import { Button } from "@/components/ui/button"; -import { uppercase } from "@/utils/string"; -import { clamp, formatNumberForDisplay } from "@/utils/math"; -import { useEditor } from "@/hooks/use-editor"; -import { DEFAULT_COLOR } from "@/constants/project-constants"; -import { - CORNER_RADIUS_MAX, - CORNER_RADIUS_MIN, - 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 { KeyframeToggle } from "../components/keyframe-toggle"; -import { isPropertyAtDefault } from "./transform-tab"; -import { resolveColorAtTime, resolveNumberAtTime } from "@/lib/animation"; -import { HugeiconsIcon } from "@hugeicons/react"; -import { - MinusSignIcon, - PlusSignIcon, - TextFontIcon, -} from "@hugeicons/core-free-icons"; -import { OcTextHeightIcon, OcTextWidthIcon } from "@opencut/ui/icons"; -import { DEFAULTS } from "@/lib/timeline/defaults"; -import { cn } from "@/utils/ui"; - -export function TextTab({ - element, - trackId, -}: { - element: TextElement; - trackId: string; -}) { - return ( -
- - - - -
- ); -} - -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 ( -
- - Content - - -