diff options
Diffstat (limited to 'apps/web/src/components/editor')
6 files changed, 629 insertions, 283 deletions
diff --git a/apps/web/src/components/editor/panels/preview/text-edit-overlay.tsx b/apps/web/src/components/editor/panels/preview/text-edit-overlay.tsx index 265e972d..fdd4055d 100644 --- a/apps/web/src/components/editor/panels/preview/text-edit-overlay.tsx +++ b/apps/web/src/components/editor/panels/preview/text-edit-overlay.tsx @@ -99,9 +99,9 @@ export function TextEditOverlay({ const fontStyle = element.fontStyle === "italic" ? "italic" : "normal"; const letterSpacing = element.letterSpacing ?? 0; const backgroundColor = - element.backgroundColor === "transparent" + element.background.color === "transparent" ? "transparent" - : element.backgroundColor; + : element.background.color; return ( <div diff --git a/apps/web/src/components/editor/panels/properties/index.tsx b/apps/web/src/components/editor/panels/properties/index.tsx index 05ad5396..790acdda 100644 --- a/apps/web/src/components/editor/panels/properties/index.tsx +++ b/apps/web/src/components/editor/panels/properties/index.tsx @@ -19,7 +19,7 @@ export function PropertiesPanel() { return ( <div className="panel bg-background h-full rounded-sm border overflow-hidden"> {selectedElements.length > 0 ? ( - <ScrollArea className="h-full"> + <ScrollArea className="h-full scrollbar-hidden"> {elementsWithTracks.map(({ track, element }) => { if (element.type === "text") { return ( diff --git a/apps/web/src/components/editor/panels/properties/section.tsx b/apps/web/src/components/editor/panels/properties/section.tsx index 0b45065f..72d78d80 100644 --- a/apps/web/src/components/editor/panels/properties/section.tsx +++ b/apps/web/src/components/editor/panels/properties/section.tsx @@ -2,6 +2,7 @@ import { createContext, useContext, 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"; const sectionExpandedCache = new Map<string, boolean>(); @@ -131,6 +132,33 @@ export function SectionHeader({ return <div className={baseClassName}>{content}</div>; } +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, + children, + className, +}: { + label: string; + children: React.ReactNode; + className?: string; +}) { + return ( + <div className={cn("flex flex-col gap-2", className)}> + <Label>{label}</Label> + {children} + </div> + ); +} + export function SectionContent({ children, className, diff --git a/apps/web/src/components/editor/panels/properties/sections/blending.tsx b/apps/web/src/components/editor/panels/properties/sections/blending.tsx index 430efa25..4343c692 100644 --- a/apps/web/src/components/editor/panels/properties/sections/blending.tsx +++ b/apps/web/src/components/editor/panels/properties/sections/blending.tsx @@ -8,7 +8,7 @@ import { } from "@/constants/timeline-constants"; import { OcCheckerboardIcon } from "@opencut/ui/icons"; import { Fragment, useRef } from "react"; -import { Section, SectionContent, SectionHeader } from "../section"; +import { Section, SectionContent, SectionField, SectionHeader } from "../section"; import { Select, SelectContent, @@ -124,73 +124,73 @@ export function BlendingSection({ return ( <Section collapsible sectionKey={`${element.type}:blending`}> <SectionHeader title="Blending" /> - <SectionContent> - <div className="flex items-start gap-2"> - <div className="w-1/2 space-y-1.5"> - <NumberField + <SectionContent> + <div className="flex items-start gap-2"> + <SectionField label="Opacity" className="w-1/2"> + <NumberField + className="w-full" + icon={ + <OcCheckerboardIcon className="size-3.5 text-muted-foreground" /> + } + value={opacity.displayValue} + min={0} + max={100} + onFocus={opacity.onFocus} + onChange={opacity.onChange} + onBlur={opacity.onBlur} + onScrub={opacity.scrubTo} + onScrubEnd={opacity.commitScrub} + onReset={() => + editor.timeline.updateElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { opacity: DEFAULT_OPACITY }, + }, + ], + }) + } + isDefault={element.opacity === DEFAULT_OPACITY} + dragSensitivity="slow" + /> + </SectionField> + <SectionField label="Blend mode" className="w-1/2"> + <Select + value={committedBlendModeRef.current} + onOpenChange={handleBlendModeOpenChange} + onValueChange={commitBlendMode} + > + <SelectTrigger + icon={<HugeiconsIcon icon={RainDropIcon} />} className="w-full" - icon={ - <OcCheckerboardIcon className="size-3.5 text-muted-foreground" /> - } - value={opacity.displayValue} - min={0} - max={100} - onFocus={opacity.onFocus} - onChange={opacity.onChange} - onBlur={opacity.onBlur} - onScrub={opacity.scrubTo} - onScrubEnd={opacity.commitScrub} - onReset={() => - editor.timeline.updateElements({ - updates: [ - { - trackId, - elementId: element.id, - updates: { opacity: DEFAULT_OPACITY }, - }, - ], - }) - } - isDefault={element.opacity === DEFAULT_OPACITY} - dragSensitivity="slow" - /> - </div> - <div className="w-1/2 space-y-1.5"> - <Select - value={committedBlendModeRef.current} - onOpenChange={handleBlendModeOpenChange} - onValueChange={commitBlendMode} > - <SelectTrigger - icon={<HugeiconsIcon icon={RainDropIcon} />} - className="w-full" - > - <SelectValue placeholder="Select blend mode" /> - </SelectTrigger> - <SelectContent className="w-36"> - {BLEND_MODE_GROUPS.map((group, groupIndex) => ( - <Fragment key={group[0]?.value ?? `group-${groupIndex}`}> - {group.map((option) => ( - <SelectItem - key={option.value} - value={option.value} - onPointerEnter={() => - previewBlendMode(option.value as BlendMode) - } - > - {option.label} - </SelectItem> - ))} - {groupIndex < BLEND_MODE_GROUPS.length - 1 ? ( - <SelectSeparator /> - ) : null} - </Fragment> - ))} - </SelectContent> - </Select> - </div> - </div> - </SectionContent> + <SelectValue placeholder="Select blend mode" /> + </SelectTrigger> + <SelectContent className="w-36"> + {BLEND_MODE_GROUPS.map((group, groupIndex) => ( + <Fragment key={group[0]?.value ?? `group-${groupIndex}`}> + {group.map((option) => ( + <SelectItem + key={option.value} + value={option.value} + onPointerEnter={() => + previewBlendMode(option.value as BlendMode) + } + > + {option.label} + </SelectItem> + ))} + {groupIndex < BLEND_MODE_GROUPS.length - 1 ? ( + <SelectSeparator /> + ) : null} + </Fragment> + ))} + </SelectContent> + </Select> + </SectionField> + </div> + </SectionContent> </Section> ); } diff --git a/apps/web/src/components/editor/panels/properties/sections/transform.tsx b/apps/web/src/components/editor/panels/properties/sections/transform.tsx index 17a54b4c..21da36e9 100644 --- a/apps/web/src/components/editor/panels/properties/sections/transform.tsx +++ b/apps/web/src/components/editor/panels/properties/sections/transform.tsx @@ -3,7 +3,7 @@ import { useEditor } from "@/hooks/use-editor"; import { usePropertyDraft } from "../hooks/use-property-draft"; import { clamp } from "@/utils/math"; import type { ElementType, Transform } from "@/types/timeline"; -import { Section, SectionContent, SectionHeader } from "../section"; +import { Section, SectionContent, SectionField, SectionFields, SectionHeader } from "../section"; import { Button } from "@/components/ui/button"; import { HugeiconsIcon } from "@hugeicons/react"; import { @@ -120,8 +120,9 @@ export function TransformSection({ return ( <Section collapsible sectionKey={`${element.type}:transform`}> <SectionHeader title="Transform" /> - <SectionContent> - <div className="flex flex-col gap-2"> + <SectionContent> + <SectionFields> + <SectionField label="Scale"> <div className="flex items-center gap-2"> {isScaleLocked ? ( <> @@ -144,105 +145,108 @@ export function TransformSection({ <HugeiconsIcon icon={Link05Icon} /> </Button> </div> + </SectionField> + <SectionField label="Position"> <div className="flex items-center gap-2"> - <NumberField - icon="X" - className="flex-1" - value={positionX.displayValue} - onFocus={positionX.onFocus} - onChange={positionX.onChange} - onBlur={positionX.onBlur} - onScrub={positionX.scrubTo} - onScrubEnd={positionX.commitScrub} - onReset={() => - editor.timeline.updateElements({ - updates: [ - { - trackId, - elementId: element.id, - updates: { - transform: { - ...element.transform, - position: { - ...element.transform.position, - x: DEFAULT_TRANSFORM.position.x, + <NumberField + icon="X" + className="flex-1" + value={positionX.displayValue} + onFocus={positionX.onFocus} + onChange={positionX.onChange} + onBlur={positionX.onBlur} + onScrub={positionX.scrubTo} + onScrubEnd={positionX.commitScrub} + onReset={() => + editor.timeline.updateElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { + transform: { + ...element.transform, + position: { + ...element.transform.position, + x: DEFAULT_TRANSFORM.position.x, + }, }, }, }, - }, - ], - }) - } - isDefault={ - element.transform.position.x === DEFAULT_TRANSFORM.position.x - } - /> - <NumberField - icon="Y" - className="flex-1" - value={positionY.displayValue} - onFocus={positionY.onFocus} - onChange={positionY.onChange} - onBlur={positionY.onBlur} - onScrub={positionY.scrubTo} - onScrubEnd={positionY.commitScrub} - onReset={() => - editor.timeline.updateElements({ - updates: [ - { - trackId, - elementId: element.id, - updates: { - transform: { - ...element.transform, - position: { - ...element.transform.position, - y: DEFAULT_TRANSFORM.position.y, + ], + }) + } + isDefault={ + element.transform.position.x === DEFAULT_TRANSFORM.position.x + } + /> + <NumberField + icon="Y" + className="flex-1" + value={positionY.displayValue} + onFocus={positionY.onFocus} + onChange={positionY.onChange} + onBlur={positionY.onBlur} + onScrub={positionY.scrubTo} + onScrubEnd={positionY.commitScrub} + onReset={() => + editor.timeline.updateElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { + transform: { + ...element.transform, + position: { + ...element.transform.position, + y: DEFAULT_TRANSFORM.position.y, + }, }, }, }, - }, - ], - }) - } - isDefault={ - element.transform.position.y === DEFAULT_TRANSFORM.position.y - } - /> + ], + }) + } + isDefault={ + element.transform.position.y === DEFAULT_TRANSFORM.position.y + } + /> </div> + </SectionField> - <div className="flex items-center gap-2"> - <NumberField - icon={<HugeiconsIcon icon={RotateClockwiseIcon} />} - className="flex-1" - value={rotation.displayValue} - onFocus={rotation.onFocus} - onChange={rotation.onChange} - onBlur={rotation.onBlur} - dragSensitivity="slow" - onScrub={rotation.scrubTo} - onScrubEnd={rotation.commitScrub} - onReset={() => - editor.timeline.updateElements({ - updates: [ - { - trackId, - elementId: element.id, - updates: { - transform: { - ...element.transform, - rotate: DEFAULT_TRANSFORM.rotate, - }, + <SectionField label="Rotation"> + <NumberField + icon={<HugeiconsIcon icon={RotateClockwiseIcon} />} + className="flex-none" + value={rotation.displayValue} + onFocus={rotation.onFocus} + onChange={rotation.onChange} + onBlur={rotation.onBlur} + dragSensitivity="slow" + onScrub={rotation.scrubTo} + onScrubEnd={rotation.commitScrub} + onReset={() => + editor.timeline.updateElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { + transform: { + ...element.transform, + rotate: DEFAULT_TRANSFORM.rotate, }, }, - ], - }) - } - isDefault={element.transform.rotate === DEFAULT_TRANSFORM.rotate} - /> - </div> - </div> - </SectionContent> + }, + ], + }) + } + isDefault={element.transform.rotate === DEFAULT_TRANSFORM.rotate} + /> + </SectionField> + </SectionFields> + </SectionContent> </Section> ); } diff --git a/apps/web/src/components/editor/panels/properties/text-properties.tsx b/apps/web/src/components/editor/panels/properties/text-properties.tsx index cfca52dc..1a17c26a 100644 --- a/apps/web/src/components/editor/panels/properties/text-properties.tsx +++ b/apps/web/src/components/editor/panels/properties/text-properties.tsx @@ -1,31 +1,49 @@ import { Textarea } from "@/components/ui/textarea"; import { FontPicker } from "@/components/ui/font-picker"; import type { TextElement } from "@/types/timeline"; -import { Slider } from "@/components/ui/slider"; import { NumberField } from "@/components/ui/number-field"; import { useRef } from "react"; -import { Section, SectionContent, SectionHeader } from "./section"; +import { Section, SectionContent, SectionField, SectionFields, SectionHeader } from "./section"; import { ColorPicker } from "@/components/ui/color-picker"; import { uppercase } from "@/utils/string"; import { clamp } from "@/utils/math"; import { useEditor } from "@/hooks/use-editor"; import { DEFAULT_COLOR } from "@/constants/project-constants"; import { + 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 { TransformSection, BlendingSection } from "./sections"; -import { - Select, - SelectTrigger, - SelectValue, - SelectContent, - SelectItem, -} from "@/components/ui/select"; -import { OcFontWeightIcon } from "@opencut/ui/icons"; -import { Label } from "@/components/ui/label"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { TextFontIcon } from "@hugeicons/core-free-icons"; +import { OcTextHeightIcon, OcTextWidthIcon } from "@opencut/ui/icons"; + +function createOffsetConverter({ + defaultValue, + scale = 1, + min, +}: { + defaultValue: number; + scale?: number; + min?: number; +}) { + return { + toDisplay: (value: number) => Math.round((value - defaultValue) * scale), + fromDisplay: (display: number) => { + const stored = defaultValue + display / scale; + return min !== undefined ? Math.max(min, stored) : stored; + }, + }; +} + +const lineHeightConverter = createOffsetConverter({ defaultValue: DEFAULT_LINE_HEIGHT, scale: 10 }); +const paddingXConverter = createOffsetConverter({ defaultValue: DEFAULT_TEXT_BACKGROUND.paddingX, min: 0 }); +const paddingYConverter = createOffsetConverter({ defaultValue: DEFAULT_TEXT_BACKGROUND.paddingY, min: 0 }); export function TextProperties({ element, @@ -39,8 +57,9 @@ export function TextProperties({ <ContentSection element={element} trackId={trackId} /> <TransformSection element={element} trackId={trackId} /> <BlendingSection element={element} trackId={trackId} /> - <FontSection element={element} trackId={trackId} /> - <ColorSection element={element} trackId={trackId} /> + <TypographySection element={element} trackId={trackId} /> + <SpacingSection element={element} trackId={trackId} /> + <BackgroundSection element={element} trackId={trackId} /> </div> ); } @@ -83,7 +102,7 @@ function ContentSection({ ); } -function FontSection({ +function TypographySection({ element, trackId, }: { @@ -109,10 +128,11 @@ function FontSection({ }); return ( - <Section collapsible sectionKey="text:font"> - <SectionHeader title="Font" /> - <SectionContent> - <div className="flex flex-col gap-2"> + <Section collapsible sectionKey="text:typography"> + <SectionHeader title="Typography" /> + <SectionContent> + <SectionFields> + <SectionField label="Font"> <FontPicker defaultValue={element.fontFamily} onValueChange={(value) => @@ -127,52 +147,409 @@ function FontSection({ }) } /> - <Select value={element.fontWeight}> - <SelectTrigger className="w-full" icon={<OcFontWeightIcon />}> - <SelectValue placeholder="Select weight" /> - </SelectTrigger> - <SelectContent> - <SelectItem value="100">Thin</SelectItem> - <SelectItem value="200">Extra Light</SelectItem> - <SelectItem value="300">Light</SelectItem> - <SelectItem value="400">Normal</SelectItem> - <SelectItem value="500">Medium</SelectItem> - <SelectItem value="600">Semi Bold</SelectItem> - <SelectItem value="700">Bold</SelectItem> - <SelectItem value="800">Extra Bold</SelectItem> - <SelectItem value="900">Black</SelectItem> - </SelectContent> - </Select> + </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"> + <ColorPicker + value={uppercase({ + string: (element.color || "FFFFFF").replace("#", ""), + })} + onChange={(color) => + editor.timeline.previewElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { color: `#${color}` }, + }, + ], + }) + } + onChangeEnd={() => editor.timeline.commitPreview()} + /> + </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: lineHeightConverter.toDisplay(element.lineHeight ?? DEFAULT_LINE_HEIGHT).toString(), + parse: (input) => { + const parsed = parseFloat(input); + return Number.isNaN(parsed) ? null : lineHeightConverter.fromDisplay(Math.round(parsed)); + }, + onPreview: (value) => + editor.timeline.previewElements({ + updates: [{ trackId, elementId: element.id, updates: { lineHeight: value } }], + }), + onCommit: () => editor.timeline.commitPreview(), + }); + + return ( + <Section collapsible sectionKey="text:spacing" hasBorderBottom={false}> + <SectionHeader title="Spacing" /> + <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> + ); +} - <Label>Font size</Label> - <div className="flex items-center gap-2"> - <Slider - value={[element.fontSize]} - min={MIN_FONT_SIZE} - max={MAX_FONT_SIZE} - step={1} - onValueChange={([value]) => +function BackgroundSection({ + element, + trackId, +}: { + element: TextElement; + trackId: string; +}) { + const editor = useEditor(); + const lastSelectedColor = useRef(DEFAULT_COLOR); + + const cornerRadius = usePropertyDraft({ + displayValue: Math.round(element.background.cornerRadius ?? 0).toString(), + parse: (input) => { + const parsed = parseFloat(input); + return Number.isNaN(parsed) ? null : Math.max(0, Math.round(parsed)); + }, + onPreview: (value) => + editor.timeline.previewElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { + background: { ...element.background, cornerRadius: value }, + }, + }, + ], + }), + onCommit: () => editor.timeline.commitPreview(), + }); + + const paddingX = usePropertyDraft({ + displayValue: paddingXConverter.toDisplay(element.background.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX).toString(), + parse: (input) => { + const parsed = parseFloat(input); + return Number.isNaN(parsed) ? null : paddingXConverter.fromDisplay(Math.round(parsed)); + }, + onPreview: (value) => + editor.timeline.previewElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { background: { ...element.background, paddingX: value } }, + }, + ], + }), + onCommit: () => editor.timeline.commitPreview(), + }); + + const paddingY = usePropertyDraft({ + displayValue: paddingYConverter.toDisplay(element.background.paddingY ?? DEFAULT_TEXT_BACKGROUND.paddingY).toString(), + parse: (input) => { + const parsed = parseFloat(input); + return Number.isNaN(parsed) ? null : paddingYConverter.fromDisplay(Math.round(parsed)); + }, + onPreview: (value) => + editor.timeline.previewElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { background: { ...element.background, paddingY: value } }, + }, + ], + }), + onCommit: () => editor.timeline.commitPreview(), + }); + + const offsetX = usePropertyDraft({ + displayValue: Math.round(element.background.offsetX ?? 0).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: { background: { ...element.background, offsetX: value } }, + }, + ], + }), + onCommit: () => editor.timeline.commitPreview(), + }); + + const offsetY = usePropertyDraft({ + displayValue: Math.round(element.background.offsetY ?? 0).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: { background: { ...element.background, offsetY: value } }, + }, + ], + }), + onCommit: () => editor.timeline.commitPreview(), + }); + + return ( + <Section collapsible sectionKey="text:background"> + <SectionHeader title="Background" /> + <SectionContent> + <SectionFields> + <SectionField label="Color"> + <ColorPicker + value={ + element.background.color === "transparent" + ? lastSelectedColor.current.replace("#", "") + : element.background.color.replace("#", "") + } + onChange={(color) => { + const hexColor = `#${color}`; + if (color !== "transparent") { + lastSelectedColor.current = hexColor; + } editor.timeline.previewElements({ updates: [ { trackId, elementId: element.id, - updates: { fontSize: value }, + updates: { + background: { ...element.background, color: hexColor }, + }, }, ], - }) + }); + }} + onChangeEnd={() => editor.timeline.commitPreview()} + className={ + element.background.color === "transparent" + ? "pointer-events-none opacity-50" + : "" } - onValueCommit={() => editor.timeline.commitPreview()} - className="w-full" /> + </SectionField> + <div className="flex items-start gap-2"> + <SectionField label="Width" className="w-1/2"> <NumberField - className="w-18 shrink-0" - value={fontSize.displayValue} - min={MIN_FONT_SIZE} - max={MAX_FONT_SIZE} - onFocus={fontSize.onFocus} - onChange={fontSize.onChange} - onBlur={fontSize.onBlur} + icon="W" + value={paddingX.displayValue} + min={0} + onFocus={paddingX.onFocus} + onChange={paddingX.onChange} + onBlur={paddingX.onBlur} + onScrub={paddingX.scrubTo} + onScrubEnd={paddingX.commitScrub} + onReset={() => + editor.timeline.updateElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { + background: { + ...element.background, + paddingX: DEFAULT_TEXT_BACKGROUND.paddingX, + }, + }, + }, + ], + }) + } + isDefault={ + (element.background.paddingX ?? DEFAULT_TEXT_BACKGROUND.paddingX) === + DEFAULT_TEXT_BACKGROUND.paddingX + } + /> + </SectionField> + <SectionField label="Height" className="w-1/2"> + <NumberField + icon="H" + value={paddingY.displayValue} + min={0} + onFocus={paddingY.onFocus} + onChange={paddingY.onChange} + onBlur={paddingY.onBlur} + onScrub={paddingY.scrubTo} + onScrubEnd={paddingY.commitScrub} + onReset={() => + editor.timeline.updateElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { + background: { + ...element.background, + paddingY: DEFAULT_TEXT_BACKGROUND.paddingY, + }, + }, + }, + ], + }) + } + isDefault={ + (element.background.paddingY ?? DEFAULT_TEXT_BACKGROUND.paddingY) === + DEFAULT_TEXT_BACKGROUND.paddingY + } + /> + </SectionField> + </div> + <div className="flex items-start gap-2"> + <SectionField label="X-offset" className="w-1/2"> + <NumberField + icon="X" + value={offsetX.displayValue} + onFocus={offsetX.onFocus} + onChange={offsetX.onChange} + onBlur={offsetX.onBlur} + onScrub={offsetX.scrubTo} + onScrubEnd={offsetX.commitScrub} + onReset={() => + editor.timeline.updateElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { + background: { ...element.background, offsetX: 0 }, + }, + }, + ], + }) + } + isDefault={(element.background.offsetX ?? 0) === 0} + /> + </SectionField> + <SectionField label="Y-offset" className="w-1/2"> + <NumberField + icon="Y" + value={offsetY.displayValue} + onFocus={offsetY.onFocus} + onChange={offsetY.onChange} + onBlur={offsetY.onBlur} + onScrub={offsetY.scrubTo} + onScrubEnd={offsetY.commitScrub} + onReset={() => + editor.timeline.updateElements({ + updates: [ + { + trackId, + elementId: element.id, + updates: { + background: { ...element.background, offsetY: 0 }, + }, + }, + ], + }) + } + isDefault={(element.background.offsetY ?? 0) === 0} + /> + </SectionField> + </div> + <SectionField label="Corner Radius"> + <NumberField + icon="R" + value={cornerRadius.displayValue} + min={0} + onFocus={cornerRadius.onFocus} + onChange={cornerRadius.onChange} + onBlur={cornerRadius.onBlur} + onScrub={cornerRadius.scrubTo} + onScrubEnd={cornerRadius.commitScrub} onReset={() => editor.timeline.updateElements({ updates: [ @@ -180,82 +557,19 @@ function FontSection({ trackId, elementId: element.id, updates: { - fontSize: DEFAULT_TEXT_ELEMENT.fontSize, + background: { + ...element.background, + cornerRadius: 0, + }, }, }, ], }) } - isDefault={element.fontSize === DEFAULT_TEXT_ELEMENT.fontSize} + isDefault={(element.background.cornerRadius ?? 0) === 0} /> - </div> - </div> - </SectionContent> - </Section> - ); -} - -function ColorSection({ - element, - trackId, -}: { - element: TextElement; - trackId: string; -}) { - const editor = useEditor(); - const lastSelectedColor = useRef(DEFAULT_COLOR); - - return ( - <Section collapsible sectionKey="text:color" hasBorderBottom={false}> - <SectionHeader title="Color" /> - <SectionContent> - <div className="flex flex-col gap-6"> - <ColorPicker - value={uppercase({ - string: (element.color || "FFFFFF").replace("#", ""), - })} - onChange={(color) => - editor.timeline.previewElements({ - updates: [ - { - trackId, - elementId: element.id, - updates: { color: `#${color}` }, - }, - ], - }) - } - onChangeEnd={() => editor.timeline.commitPreview()} - /> - <ColorPicker - value={ - element.backgroundColor === "transparent" - ? lastSelectedColor.current.replace("#", "") - : element.backgroundColor.replace("#", "") - } - onChange={(color) => { - const hexColor = `#${color}`; - if (color !== "transparent") { - lastSelectedColor.current = hexColor; - } - editor.timeline.previewElements({ - updates: [ - { - trackId, - elementId: element.id, - updates: { backgroundColor: hexColor }, - }, - ], - }); - }} - onChangeEnd={() => editor.timeline.commitPreview()} - className={ - element.backgroundColor === "transparent" - ? "pointer-events-none opacity-50" - : "" - } - /> - </div> + </SectionField> + </SectionFields> </SectionContent> </Section> ); |
