From c46d28891fee62ca2d3132da876af6411a41d9da Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Wed, 15 Apr 2026 15:45:50 +0200 Subject: feat: text / custom masks, fix preview pan, refactor selection --- .../editor/panels/properties/tabs/masks-tab.tsx | 148 +++++++++++++++++++-- 1 file changed, 135 insertions(+), 13 deletions(-) (limited to 'apps/web/src/components/editor/panels/properties/tabs') 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 cfc69720..090fce64 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 @@ -1,8 +1,11 @@ "use client"; import type { MaskableElement } from "@/lib/timeline"; -import type { Mask, MaskType } from "@/lib/masks/types"; -import type { NumberParamDefinition, SelectParamDefinition } from "@/lib/params"; +import type { Mask, MaskType, TextMask } from "@/lib/masks/types"; +import type { + NumberParamDefinition, + SelectParamDefinition, +} from "@/lib/params"; import { masksRegistry, buildDefaultMaskInstance } from "@/lib/masks"; import { useEditor } from "@/hooks/use-editor"; import { useElementPreview } from "@/hooks/use-element-preview"; @@ -10,14 +13,17 @@ import { useMenuPreview } from "@/hooks/use-menu-preview"; import { getVisibleElementsWithBounds } from "@/lib/preview/element-bounds"; import { HugeiconsIcon } from "@hugeicons/react"; import { + ArrowExpandIcon, Delete02Icon, FeatherIcon, PlusSignIcon, RotateClockwiseIcon, + TextFontIcon, } from "@hugeicons/core-free-icons"; import { useMemo, useState } from "react"; import { Button } from "@/components/ui/button"; import { ColorPicker } from "@/components/ui/color-picker"; +import { FontPicker } from "@/components/ui/font-picker"; import { DropdownMenu, DropdownMenuContent, @@ -32,6 +38,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { Textarea } from "@/components/ui/textarea"; import { Tooltip, TooltipContent, @@ -52,7 +59,12 @@ import { SectionTitle, } from "@/components/section"; import { usePropertyDraft } from "../hooks/use-property-draft"; -import { OcMirrorIcon, OcShapesIcon } from "@/components/icons"; +import { + OcMirrorIcon, + OcShapesIcon, + OcTextHeightIcon, + OcTextWidthIcon, +} from "@/components/icons"; import { cn } from "@/utils/ui"; type MasksTabProps = { @@ -78,6 +90,10 @@ type PreviewParamHandler = ( type RegisteredMaskDefinition = ReturnType<(typeof masksRegistry)["get"]>; +function isTextMask(mask: Mask): mask is TextMask { + return mask.type === "text"; +} + export function MasksTab({ element, trackId }: MasksTabProps) { const editor = useEditor(); const { renderElement, previewUpdates, commit } = @@ -370,12 +386,23 @@ function MaskParamsFields({ previewParam(key)(value); const previewStrokeColor = previewParam("strokeColor"); const strokeAlignParam = definition.params.find( - (param): param is SelectParamDefinition => + (param): param is SelectParamDefinition => param.key === "strokeAlign" && param.type === "select", ); return ( + {isTextMask(mask) ? ( + + ) : null} {definition.features.hasPosition && "centerX" in mask.params && "centerY" in mask.params && ( @@ -491,7 +518,9 @@ function MaskParamsFields({ {definition.features.sizeMode === "uniform" && "scale" in mask.params && ( : "S" + } param={getNumberParamDefinition({ definition, key: "scale", @@ -587,6 +616,100 @@ function MaskParamsFields({ ); } +const LETTER_SPACING_PARAM: NumberParamDefinition = { + key: "letterSpacing", + label: "Letter spacing", + type: "number", + default: 0, + min: -100, + max: 500, + step: 1, +}; + +const LINE_HEIGHT_PARAM: NumberParamDefinition = { + key: "lineHeight", + label: "Line height", + type: "number", + default: 1.2, + min: 0.1, + max: 10, + step: 0.1, +}; + +function TextMaskFields({ + mask, + previewParam, + onCommit, + fontSizeParam, +}: { + mask: TextMask; + previewParam: PreviewParamHandler; + onCommit: () => void; + fontSizeParam: NumberParamDefinition; +}) { + const content = usePropertyDraft({ + displayValue: mask.params.content, + parse: (input) => input, + onPreview: (value) => previewParam("content")(value), + onCommit, + }); + + const previewNumberParam = (key: string) => (value: number) => + previewParam(key)(value); + + return ( + <> + +