summaryrefslogtreecommitdiff
path: root/apps/web/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/components')
-rw-r--r--apps/web/src/components/editable-timecode.tsx4
-rw-r--r--apps/web/src/components/editor/editor-header.tsx127
-rw-r--r--apps/web/src/components/editor/export-button.tsx51
-rw-r--r--apps/web/src/components/editor/panels/assets/draggable-item.tsx4
-rw-r--r--apps/web/src/components/editor/panels/assets/index.tsx2
-rw-r--r--apps/web/src/components/editor/panels/assets/tabbar.tsx29
-rw-r--r--apps/web/src/components/editor/panels/assets/views/captions.tsx9
-rw-r--r--apps/web/src/components/editor/panels/assets/views/media.tsx6
-rw-r--r--apps/web/src/components/editor/panels/assets/views/settings.tsx103
-rw-r--r--apps/web/src/components/editor/panels/assets/views/sounds.tsx4
-rw-r--r--apps/web/src/components/editor/panels/assets/views/stickers.tsx2
-rw-r--r--apps/web/src/components/editor/panels/assets/views/text.tsx2
-rw-r--r--apps/web/src/components/editor/panels/panel-base-view.tsx4
-rw-r--r--apps/web/src/components/editor/panels/preview/index.tsx141
-rw-r--r--apps/web/src/components/editor/panels/properties/empty-view.tsx20
-rw-r--r--apps/web/src/components/editor/panels/properties/index.tsx25
-rw-r--r--apps/web/src/components/editor/panels/properties/property-item.tsx79
-rw-r--r--apps/web/src/components/editor/panels/properties/text-properties.tsx905
-rw-r--r--apps/web/src/components/editor/panels/timeline/audio-waveform.tsx (renamed from apps/web/src/components/editor/timeline/audio-waveform.tsx)0
-rw-r--r--apps/web/src/components/editor/panels/timeline/bookmarks.tsx (renamed from apps/web/src/components/editor/timeline/bookmarks.tsx)0
-rw-r--r--apps/web/src/components/editor/panels/timeline/drag-line.tsx (renamed from apps/web/src/components/editor/timeline/drag-line.tsx)0
-rw-r--r--apps/web/src/components/editor/panels/timeline/index.tsx (renamed from apps/web/src/components/editor/timeline/index.tsx)16
-rw-r--r--apps/web/src/components/editor/panels/timeline/snap-indicator.tsx (renamed from apps/web/src/components/editor/timeline/snap-indicator.tsx)0
-rw-r--r--apps/web/src/components/editor/panels/timeline/timeline-element.tsx (renamed from apps/web/src/components/editor/timeline/timeline-element.tsx)22
-rw-r--r--apps/web/src/components/editor/panels/timeline/timeline-playhead.tsx (renamed from apps/web/src/components/editor/timeline/timeline-playhead.tsx)0
-rw-r--r--apps/web/src/components/editor/panels/timeline/timeline-ruler.tsx (renamed from apps/web/src/components/editor/timeline/timeline-ruler.tsx)24
-rw-r--r--apps/web/src/components/editor/panels/timeline/timeline-tick.tsx (renamed from apps/web/src/components/editor/timeline/timeline-tick.tsx)0
-rw-r--r--apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx (renamed from apps/web/src/components/editor/timeline/timeline-toolbar.tsx)99
-rw-r--r--apps/web/src/components/editor/panels/timeline/timeline-track.tsx (renamed from apps/web/src/components/editor/timeline/timeline-track.tsx)7
-rw-r--r--apps/web/src/components/theme-toggle.tsx4
-rw-r--r--apps/web/src/components/ui/button.tsx7
-rw-r--r--apps/web/src/components/ui/color-picker.tsx19
-rw-r--r--apps/web/src/components/ui/command.tsx2
-rw-r--r--apps/web/src/components/ui/dropdown-menu.tsx2
-rw-r--r--apps/web/src/components/ui/font-picker.tsx6
-rw-r--r--apps/web/src/components/ui/input-with-back.tsx4
-rw-r--r--apps/web/src/components/ui/label.tsx2
-rw-r--r--apps/web/src/components/ui/popover.tsx1
-rw-r--r--apps/web/src/components/ui/select.tsx6
-rw-r--r--apps/web/src/components/ui/slider.tsx2
-rw-r--r--apps/web/src/components/ui/split-button.tsx4
-rw-r--r--apps/web/src/components/ui/tabs.tsx2
42 files changed, 998 insertions, 748 deletions
diff --git a/apps/web/src/components/editable-timecode.tsx b/apps/web/src/components/editable-timecode.tsx
index d32f067a..61f23372 100644
--- a/apps/web/src/components/editable-timecode.tsx
+++ b/apps/web/src/components/editable-timecode.tsx
@@ -122,8 +122,8 @@ export function EditableTimecode({
onKeyDown={handleKeyDown}
onBlur={handleBlur}
className={cn(
- "border-none bg-transparent font-mono text-xs outline-none",
- "focus:bg-background focus:border-primary focus:rounded focus:border focus:px-1",
+ "-mx-1 border border-transparent bg-transparent px-1 font-mono text-xs outline-none",
+ "focus:bg-background focus:border-primary focus:rounded",
"text-primary tabular-nums",
hasError && "text-destructive focus:border-destructive",
className,
diff --git a/apps/web/src/components/editor/editor-header.tsx b/apps/web/src/components/editor/editor-header.tsx
index d74d78d7..e70bda37 100644
--- a/apps/web/src/components/editor/editor-header.tsx
+++ b/apps/web/src/components/editor/editor-header.tsx
@@ -1,8 +1,7 @@
"use client";
import { Button } from "../ui/button";
-import { ChevronDown } from "lucide-react";
-import { useState } from "react";
+import { useRef, useState } from "react";
import {
DropdownMenu,
DropdownMenuContent,
@@ -17,23 +16,21 @@ import { useRouter } from "next/navigation";
import { FaDiscord } from "react-icons/fa6";
import { ExportButton } from "./export-button";
import { ThemeToggle } from "../theme-toggle";
-import { SOCIAL_LINKS } from "@/constants/site-constants";
+import { DEFAULT_LOGO_URL, SOCIAL_LINKS } from "@/constants/site-constants";
import { toast } from "sonner";
import { useEditor } from "@/hooks/use-editor";
-import {
- ArrowLeft02Icon,
- Edit03Icon,
- Delete02Icon,
- CommandIcon,
-} from "@hugeicons/core-free-icons";
+import { ArrowLeft02Icon, CommandIcon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { ShortcutsDialog } from "./dialogs/shortcuts-dialog";
+import Image from "next/image";
+import { cn } from "@/utils/ui";
export function EditorHeader() {
return (
- <header className="bg-background flex h-[3.2rem] items-center justify-between px-3 pt-0.5">
- <div className="flex items-center gap-2">
+ <header className="bg-background flex h-[3.4rem] items-center justify-between px-3 pt-0.5">
+ <div className="flex items-center gap-1">
<ProjectDropdown />
+ <EditableProjectName />
</div>
<nav className="flex items-center gap-2">
<ExportButton />
@@ -111,14 +108,14 @@ function ProjectDropdown() {
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
- <Button
- variant="secondary"
- className="flex h-auto items-center justify-center px-2.5 py-1.5"
- >
- <ChevronDown className="text-muted-foreground" />
- <span className="mr-2 text-[0.85rem]">
- {activeProject?.metadata.name}
- </span>
+ <Button variant="ghost" size="icon" className="p-1 rounded-sm size-8">
+ <Image
+ src={DEFAULT_LOGO_URL}
+ alt="Project thumbnail"
+ width={32}
+ height={32}
+ className="invert dark:invert-0 size-5"
+ />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="z-100 w-52">
@@ -130,21 +127,7 @@ function ProjectDropdown() {
<HugeiconsIcon icon={ArrowLeft02Icon} className="size-4" />
Exit project
</DropdownMenuItem>
- <DropdownMenuItem
- className="flex items-center gap-1.5"
- onClick={() => setOpenDialog("rename")}
- >
- <HugeiconsIcon icon={Edit03Icon} className="size-4" />
- Rename project
- </DropdownMenuItem>
- <DropdownMenuItem
- variant="destructive"
- className="flex items-center gap-1.5"
- onClick={() => setOpenDialog("delete")}
- >
- <HugeiconsIcon icon={Delete02Icon} className="size-4" />
- Delete project
- </DropdownMenuItem>
+
<DropdownMenuSeparator />
<DropdownMenuItem
className="flex items-center gap-1.5"
@@ -185,3 +168,79 @@ function ProjectDropdown() {
</>
);
}
+
+function EditableProjectName() {
+ const editor = useEditor();
+ const activeProject = editor.project.getActive();
+ const [isEditing, setIsEditing] = useState(false);
+ const inputRef = useRef<HTMLInputElement>(null);
+ const originalNameRef = useRef("");
+
+ const projectName = activeProject?.metadata.name || "";
+
+ const startEditing = () => {
+ if (isEditing) return;
+ originalNameRef.current = projectName;
+ setIsEditing(true);
+
+ requestAnimationFrame(() => {
+ inputRef.current?.select();
+ });
+ };
+
+ const saveEdit = async () => {
+ if (!inputRef.current || !activeProject) return;
+ const newName = inputRef.current.value.trim();
+ setIsEditing(false);
+
+ if (!newName) {
+ inputRef.current.value = originalNameRef.current;
+ return;
+ }
+
+ if (newName !== originalNameRef.current) {
+ try {
+ await editor.project.renameProject({
+ id: activeProject.metadata.id,
+ name: newName,
+ });
+ } catch (error) {
+ toast.error("Failed to rename project", {
+ description:
+ error instanceof Error ? error.message : "Please try again",
+ });
+ }
+ }
+ };
+
+ const handleKeyDown = (event: React.KeyboardEvent) => {
+ if (event.key === "Enter") {
+ event.preventDefault();
+ inputRef.current?.blur();
+ } else if (event.key === "Escape") {
+ event.preventDefault();
+ if (inputRef.current) {
+ inputRef.current.value = originalNameRef.current;
+ }
+ setIsEditing(false);
+ inputRef.current?.blur();
+ }
+ };
+
+ return (
+ <input
+ ref={inputRef}
+ type="text"
+ defaultValue={projectName}
+ readOnly={!isEditing}
+ onClick={startEditing}
+ onBlur={saveEdit}
+ onKeyDown={handleKeyDown}
+ style={{ fieldSizing: "content" }}
+ className={cn(
+ "text-[0.9rem] h-8 px-2 py-1 rounded-sm bg-transparent outline-none cursor-pointer hover:bg-accent hover:text-accent-foreground",
+ isEditing && "ring-1 ring-ring cursor-text hover:bg-transparent",
+ )}
+ />
+ );
+}
diff --git a/apps/web/src/components/editor/export-button.tsx b/apps/web/src/components/editor/export-button.tsx
index 942efcff..f39c18e2 100644
--- a/apps/web/src/components/editor/export-button.tsx
+++ b/apps/web/src/components/editor/export-button.tsx
@@ -15,7 +15,7 @@ import { Progress } from "@/components/ui/progress";
import { Checkbox } from "@/components/ui/checkbox";
import { cn } from "@/utils/ui";
import { getExportMimeType, getExportFileExtension } from "@/lib/export";
-import { Check, Copy, Download, RotateCcw, X } from "lucide-react";
+import { Check, Copy, Download, RotateCcw } from "lucide-react";
import {
EXPORT_FORMAT_VALUES,
EXPORT_QUALITY_VALUES,
@@ -44,9 +44,7 @@ export function ExportButton() {
type="button"
className={cn(
"flex items-center gap-1.5 rounded-md bg-[#38BDF8] px-[0.12rem] py-[0.12rem] text-white",
- hasProject
- ? "cursor-pointer"
- : "cursor-not-allowed opacity-50",
+ hasProject ? "cursor-pointer" : "cursor-not-allowed opacity-50",
)}
onClick={hasProject ? handleExport : undefined}
disabled={!hasProject}
@@ -141,20 +139,12 @@ function ExportPopover({
}
};
- const handleClose = () => {
- if (!isExporting) {
- onOpenChange(false);
- setExportResult(null);
- setProgress(0);
- }
- };
-
const handleCancel = () => {
cancelRequestedRef.current = true;
};
return (
- <PopoverContent className="bg-background mr-4 flex w-80 flex-col gap-3">
+ <PopoverContent className="bg-background mr-4 flex w-80 flex-col p-0">
{exportResult && !exportResult.success ? (
<ExportError
error={exportResult.error || "Unknown error occurred"}
@@ -162,23 +152,20 @@ function ExportPopover({
/>
) : (
<>
- <div className="flex items-center justify-between">
- <h3 className="font-medium">
+ <div className="flex items-center justify-between p-3 border-b">
+ <h3 className="font-medium text-sm">
{isExporting ? "Exporting project" : "Export project"}
</h3>
- <Button variant="text" size="icon" onClick={handleClose}>
- <X className="text-foreground/85 !size-5" />
- </Button>
</div>
<div className="flex flex-col gap-4">
{!isExporting && (
<>
- <div className="flex flex-col gap-3">
+ <div className="flex flex-col">
<PropertyGroup
title="Format"
- titleClassName="text-sm"
defaultExpanded={false}
+ hasBorderTop={false}
>
<RadioGroup
value={format}
@@ -203,11 +190,7 @@ function ExportPopover({
</RadioGroup>
</PropertyGroup>
- <PropertyGroup
- title="Quality"
- titleClassName="text-sm"
- defaultExpanded={false}
- >
+ <PropertyGroup title="Quality" defaultExpanded={false}>
<RadioGroup
value={quality}
onValueChange={(value) => {
@@ -237,11 +220,7 @@ function ExportPopover({
</RadioGroup>
</PropertyGroup>
- <PropertyGroup
- title="Audio"
- titleClassName="text-sm"
- defaultExpanded={false}
- >
+ <PropertyGroup title="Audio" defaultExpanded={false}>
<div className="flex items-center space-x-2">
<Checkbox
id="include-audio"
@@ -257,15 +236,17 @@ function ExportPopover({
</PropertyGroup>
</div>
- <Button onClick={handleExport} className="w-full gap-2">
- <Download className="size-4" />
- Export
- </Button>
+ <div className="p-3 pt-0">
+ <Button onClick={handleExport} className="w-full gap-2">
+ <Download className="size-4" />
+ Export
+ </Button>
+ </div>
</>
)}
{isExporting && (
- <div className="space-y-4">
+ <div className="space-y-4 p-3">
<div className="flex flex-col">
<div className="flex items-center justify-between text-center">
<p className="text-muted-foreground mb-2 text-sm">
diff --git a/apps/web/src/components/editor/panels/assets/draggable-item.tsx b/apps/web/src/components/editor/panels/assets/draggable-item.tsx
index 10cb1f8c..517fdc92 100644
--- a/apps/web/src/components/editor/panels/assets/draggable-item.tsx
+++ b/apps/web/src/components/editor/panels/assets/draggable-item.tsx
@@ -110,7 +110,7 @@ export function DraggableItem({
<AspectRatio
ratio={aspectRatio}
className={cn(
- "bg-panel-accent relative overflow-hidden",
+ "bg-accent relative overflow-hidden",
isRounded && "rounded-sm",
isDraggable && "[&::-webkit-drag-ghost]:opacity-0",
)}
@@ -215,7 +215,7 @@ function PlusButton({
<Button
size="icon"
className={cn(
- "bg-background hover:bg-panel text-foreground absolute right-2 bottom-2 size-5",
+ "bg-background hover:bg-background text-foreground absolute right-2 bottom-2 size-5",
className,
)}
onClick={(e) => {
diff --git a/apps/web/src/components/editor/panels/assets/index.tsx b/apps/web/src/components/editor/panels/assets/index.tsx
index 9161cb1d..e7bd3d4f 100644
--- a/apps/web/src/components/editor/panels/assets/index.tsx
+++ b/apps/web/src/components/editor/panels/assets/index.tsx
@@ -43,7 +43,7 @@ export function AssetsPanel() {
};
return (
- <div className="bg-panel flex h-full">
+ <div className="panel bg-background flex h-full rounded-sm border overflow-hidden">
<TabBar />
<Separator orientation="vertical" />
<div className="flex-1 overflow-hidden">{viewMap[activeTab]}</div>
diff --git a/apps/web/src/components/editor/panels/assets/tabbar.tsx b/apps/web/src/components/editor/panels/assets/tabbar.tsx
index 2c950fdc..31842306 100644
--- a/apps/web/src/components/editor/panels/assets/tabbar.tsx
+++ b/apps/web/src/components/editor/panels/assets/tabbar.tsx
@@ -6,6 +6,7 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
+import { Button } from "@/components/ui/button";
import { cn } from "@/utils/ui";
import {
TAB_KEYS,
@@ -15,9 +16,9 @@ import {
export function TabBar() {
const { activeTab, setActiveTab } = useAssetsPanelStore();
- const scrollRef = useRef<HTMLDivElement>(null);
const [showTopFade, setShowTopFade] = useState(false);
const [showBottomFade, setShowBottomFade] = useState(false);
+ const scrollRef = useRef<HTMLDivElement>(null);
const checkScrollPosition = useCallback(() => {
const element = scrollRef.current;
@@ -48,32 +49,24 @@ export function TabBar() {
<div className="relative flex">
<div
ref={scrollRef}
- className="scrollbar-hidden relative flex size-full flex-col items-center justify-start gap-5 overflow-y-auto px-4 py-4"
+ className="scrollbar-hidden relative flex size-full p-2 flex-col items-center justify-start gap-1.5 overflow-y-auto"
>
{TAB_KEYS.map((tabKey) => {
const tab = tabs[tabKey];
return (
<Tooltip key={tabKey} delayDuration={10}>
<TooltipTrigger asChild>
- <button
- type="button"
+ <Button
+ variant={activeTab === tabKey ? "secondary" : "text"}
aria-label={tab.label}
className={cn(
- "flex cursor-pointer flex-col items-center gap-0.5 [&>svg]:size-4.5! opacity-100 hover:opacity-75",
- activeTab === tabKey
- ? "text-primary !opacity-100"
- : "text-muted-foreground",
+ "flex-col !p-1.5 !rounded-sm !h-auto [&_svg]:size-4.5",
+ activeTab !== tabKey && "border border-transparent text-muted-foreground",
)}
onClick={() => setActiveTab(tabKey)}
- onKeyDown={(event) => {
- if (event.key === "Enter" || event.key === " ") {
- event.preventDefault();
- setActiveTab(tabKey);
- }
- }}
>
- <tab.icon className=" " />
- </button>
+ <tab.icon />
+ </Button>
</TooltipTrigger>
<TooltipContent
side="right"
@@ -108,8 +101,8 @@ function FadeOverlay({
className={cn(
"pointer-events-none absolute right-0 left-0 h-6",
direction === "top" && show
- ? "from-panel top-0 bg-gradient-to-b to-transparent"
- : "from-panel bottom-0 bg-gradient-to-t to-transparent",
+ ? "from-background top-0 bg-gradient-to-b to-transparent"
+ : "from-background bottom-0 bg-gradient-to-t to-transparent",
)}
/>
);
diff --git a/apps/web/src/components/editor/panels/assets/views/captions.tsx b/apps/web/src/components/editor/panels/assets/views/captions.tsx
index d93baeff..daebf2c6 100644
--- a/apps/web/src/components/editor/panels/assets/views/captions.tsx
+++ b/apps/web/src/components/editor/panels/assets/views/captions.tsx
@@ -1,5 +1,4 @@
import { Button } from "@/components/ui/button";
-import { PropertyGroup } from "@/components/editor/panels/properties/property-item";
import { PanelBaseView as BaseView } from "@/components/editor/panels/panel-base-view";
import {
Select,
@@ -21,6 +20,7 @@ import { transcriptionService } from "@/services/transcription/service";
import { decodeAudioToFloat32 } from "@/lib/media/audio";
import { buildCaptionChunks } from "@/lib/transcription/caption";
import { Spinner } from "@/components/ui/spinner";
+import { Label } from "@/components/ui/label";
export function Captions() {
const [selectedLanguage, setSelectedLanguage] =
@@ -112,12 +112,13 @@ export function Captions() {
ref={containerRef}
className="flex h-full flex-col justify-between"
>
- <PropertyGroup title="Language">
+ <div className="flex flex-col gap-3">
+ <Label>Language</Label>
<Select
value={selectedLanguage}
onValueChange={(value) => handleLanguageChange({ value })}
>
- <SelectTrigger className="bg-panel-accent h-8 w-full text-xs">
+ <SelectTrigger>
<SelectValue placeholder="Select a language" />
</SelectTrigger>
<SelectContent>
@@ -129,7 +130,7 @@ export function Captions() {
))}
</SelectContent>
</Select>
- </PropertyGroup>
+ </div>
<div className="flex flex-col gap-4">
{error && (
diff --git a/apps/web/src/components/editor/panels/assets/views/media.tsx b/apps/web/src/components/editor/panels/assets/views/media.tsx
index 76ae0064..59395fd8 100644
--- a/apps/web/src/components/editor/panels/assets/views/media.tsx
+++ b/apps/web/src/components/editor/panels/assets/views/media.tsx
@@ -202,7 +202,7 @@ export function MediaView() {
className={`relative flex h-full flex-col gap-1 ${isDragOver ? "bg-accent/30" : ""}`}
{...dragProps}
>
- <div className="bg-panel py-2 px-4 flex items-center justify-between border-b">
+ <div className="bg-background h-12 px-4 pr-2 flex items-center justify-between border-b">
<span className="text-muted-foreground text-sm">Assets</span>
<div className="flex items-center gap-0">
<TooltipProvider>
@@ -320,10 +320,10 @@ export function MediaView() {
onClick={openFilePicker}
disabled={isProcessing}
size="sm"
- className="items-center justify-center gap-1.5 ml-1.5"
+ className="items-center justify-center gap-1.5 ml-1.5 hover:bg-accent px-3"
>
<HugeiconsIcon icon={CloudUploadIcon} />
- Upload
+ Import
</Button>
</div>
</div>
diff --git a/apps/web/src/components/editor/panels/assets/views/settings.tsx b/apps/web/src/components/editor/panels/assets/views/settings.tsx
index b8c54510..26afe566 100644
--- a/apps/web/src/components/editor/panels/assets/views/settings.tsx
+++ b/apps/web/src/components/editor/panels/assets/views/settings.tsx
@@ -21,7 +21,6 @@ import { colors } from "@/data/colors/solid";
import { syntaxUIGradients } from "@/data/colors/syntax-ui";
import { useEditor } from "@/hooks/use-editor";
import { useEditorStore } from "@/stores/editor-store";
-import type { TProject } from "@/types/project";
import { dimensionToAspectRatio } from "@/utils/geometry";
import { cn } from "@/utils/ui";
import {
@@ -30,8 +29,7 @@ import {
PropertyItemLabel,
PropertyItemValue,
} from "@/components/editor/panels/properties/property-item";
-import { HugeiconsIcon } from "@hugeicons/react";
-import { DropperIcon } from "@hugeicons/core-free-icons";
+import { ColorPicker } from "@/components/ui/color-picker";
export function SettingsView() {
return <ProjectSettingsTabs />;
@@ -56,24 +54,9 @@ function ProjectSettingsTabs() {
label: "Background",
content: (
<div className="flex h-full flex-col justify-between">
- <div className="flex-1 p-5">
+ <div className="flex-1">
<BackgroundView />
</div>
- {/* <div className="bg-panel/85 sticky -bottom-0 flex flex-col backdrop-blur-lg">
- <Separator />
- <Button className="text-muted-foreground hover:text-foreground/85 h-auto w-fit !bg-transparent p-5 py-4 text-xs shadow-none">
- Custom background
- <PlusIcon />
- </Button>
- </div> */}
-
- {/* another ui */}
- {/* <div className="flex flex-col justify-center items-center pb-5 sticky bottom-0">
- <Button className="w-fit h-auto gap-1.5 px-3.5 py-1.5 bg-foreground hover:bg-foreground/85 text-background rounded-full">
- <span className="text-sm">Custom</span>
- <PlusIcon className="" />
- </Button>
- </div> */}
</div>
),
},
@@ -83,15 +66,6 @@ function ProjectSettingsTabs() {
);
}
-function getCurrentCanvasSize({ activeProject }: { activeProject: TProject }) {
- const { canvasSize } = activeProject.settings;
-
- return {
- width: canvasSize.width,
- height: canvasSize.height,
- };
-}
-
function ProjectInfoView() {
const editor = useEditor();
const activeProject = editor.project.getActive();
@@ -117,7 +91,7 @@ function ProjectInfoView() {
return -1;
};
- const currentCanvasSize = getCurrentCanvasSize({ activeProject });
+ const currentCanvasSize = activeProject.settings.canvasSize;
const currentAspectRatio = dimensionToAspectRatio(currentCanvasSize);
const originalCanvasSize = activeProject.settings.originalCanvasSize ?? null;
const presetIndex = findPresetIndexByAspectRatio({
@@ -162,7 +136,7 @@ function ProjectInfoView() {
value={selectedPresetValue}
onValueChange={(value) => handleAspectRatioChange({ value })}
>
- <SelectTrigger className="bg-panel-accent">
+ <SelectTrigger>
<SelectValue placeholder="Select an aspect ratio" />
</SelectTrigger>
<SelectContent>
@@ -190,7 +164,7 @@ function ProjectInfoView() {
value={activeProject.settings.fps.toString()}
onValueChange={handleFpsChange}
>
- <SelectTrigger className="bg-panel-accent">
+ <SelectTrigger>
<SelectValue placeholder="Select a frame rate" />
</SelectTrigger>
<SelectContent>
@@ -219,7 +193,7 @@ const BlurPreview = memo(
}) => (
<button
className={cn(
- "border-foreground/15 hover:border-primary relative aspect-square w-full cursor-pointer overflow-hidden rounded-sm border",
+ "border-foreground/15 hover:border-primary relative aspect-square size-20 cursor-pointer overflow-hidden rounded-sm border",
isSelected && "border-primary border-2",
)}
onClick={onSelect}
@@ -265,7 +239,7 @@ const BackgroundPreviews = memo(
<button
key={`${index}-${bg}`}
className={cn(
- "border-foreground/15 hover:border-primary aspect-square w-full cursor-pointer rounded-sm border",
+ "border-foreground/15 hover:border-primary aspect-square size-20 cursor-pointer rounded-sm border",
isColorBackground &&
bg === currentBackgroundColor &&
"border-primary border-2",
@@ -347,48 +321,35 @@ function BackgroundView() {
[blurLevels, isBlurBackground, currentBlurIntensity, handleBlurSelect],
);
+ const backgroundSections = [
+ { title: "Colors", backgrounds: colors, useBackgroundColor: true },
+ { title: "Pattern craft", backgrounds: patternCraftGradients },
+ { title: "Syntax UI", backgrounds: syntaxUIGradients },
+ ];
+
return (
- <div className="flex h-full flex-col gap-4">
- <PropertyGroup title="Blur" defaultExpanded={false}>
- <div className="grid w-full grid-cols-4 gap-2">{blurPreviews}</div>
+ <div className="flex h-full flex-col">
+ <PropertyGroup title="Blur" hasBorderTop={false} defaultExpanded={false}>
+ <div className="flex flex-wrap gap-2">{blurPreviews}</div>
</PropertyGroup>
- <PropertyGroup title="Colors" defaultExpanded={false}>
- <div className="grid w-full grid-cols-4 gap-2">
- <div className="border-foreground/15 hover:border-primary flex aspect-square w-full cursor-pointer items-center justify-center rounded-sm border">
- <HugeiconsIcon icon={DropperIcon} className="size-4" />
+ {backgroundSections.map((section) => (
+ <PropertyGroup
+ key={section.title}
+ title={section.title}
+ defaultExpanded={false}
+ >
+ <div className="flex flex-wrap gap-2">
+ <BackgroundPreviews
+ backgrounds={section.backgrounds}
+ currentBackgroundColor={currentBackgroundColor}
+ isColorBackground={isColorBackground}
+ handleColorSelect={({ bg }) => handleColorSelect({ color: bg })}
+ useBackgroundColor={section.useBackgroundColor}
+ />
</div>
- <BackgroundPreviews
- backgrounds={colors}
- currentBackgroundColor={currentBackgroundColor}
- isColorBackground={isColorBackground}
- handleColorSelect={({ bg }) => handleColorSelect({ color: bg })}
- useBackgroundColor={true}
- />
- </div>
- </PropertyGroup>
-
- <PropertyGroup title="Pattern craft" defaultExpanded={false}>
- <div className="grid w-full grid-cols-4 gap-2">
- <BackgroundPreviews
- backgrounds={patternCraftGradients}
- currentBackgroundColor={currentBackgroundColor}
- isColorBackground={isColorBackground}
- handleColorSelect={({ bg }) => handleColorSelect({ color: bg })}
- />
- </div>
- </PropertyGroup>
-
- <PropertyGroup title="Syntax UI" defaultExpanded={false}>
- <div className="grid w-full grid-cols-4 gap-2">
- <BackgroundPreviews
- backgrounds={syntaxUIGradients}
- currentBackgroundColor={currentBackgroundColor}
- isColorBackground={isColorBackground}
- handleColorSelect={({ bg }) => handleColorSelect({ color: bg })}
- />
- </div>
- </PropertyGroup>
+ </PropertyGroup>
+ ))}
</div>
);
}
diff --git a/apps/web/src/components/editor/panels/assets/views/sounds.tsx b/apps/web/src/components/editor/panels/assets/views/sounds.tsx
index 84a5d0bf..30c70a83 100644
--- a/apps/web/src/components/editor/panels/assets/views/sounds.tsx
+++ b/apps/web/src/components/editor/panels/assets/views/sounds.tsx
@@ -235,7 +235,7 @@ function SoundEffectsView() {
<div className="flex items-center gap-3">
<Input
placeholder="Search sound effects"
- className="bg-panel-accent w-full"
+ className="bg-accent w-full"
containerClassName="w-full"
value={searchQuery}
onChange={({ currentTarget }) =>
@@ -406,7 +406,7 @@ function SavedSoundsView() {
if (savedSounds.length === 0) {
return (
- <div className="bg-panel flex h-full flex-col items-center justify-center gap-3 p-4">
+ <div className="bg-background flex h-full flex-col items-center justify-center gap-3 p-4">
<HugeiconsIcon
icon={FavouriteIcon}
className="text-muted-foreground size-10"
diff --git a/apps/web/src/components/editor/panels/assets/views/stickers.tsx b/apps/web/src/components/editor/panels/assets/views/stickers.tsx
index 6b48e775..68ba91d2 100644
--- a/apps/web/src/components/editor/panels/assets/views/stickers.tsx
+++ b/apps/web/src/components/editor/panels/assets/views/stickers.tsx
@@ -149,7 +149,7 @@ function CollectionGrid({
function EmptyView({ message }: { message: string }) {
return (
- <div className="bg-panel flex h-full flex-col items-center justify-center gap-3 p-4">
+ <div className="bg-background flex h-full flex-col items-center justify-center gap-3 p-4">
<HugeiconsIcon
icon={HappyIcon}
className="text-muted-foreground size-10"
diff --git a/apps/web/src/components/editor/panels/assets/views/text.tsx b/apps/web/src/components/editor/panels/assets/views/text.tsx
index 6ccf5d98..6360c363 100644
--- a/apps/web/src/components/editor/panels/assets/views/text.tsx
+++ b/apps/web/src/components/editor/panels/assets/views/text.tsx
@@ -27,7 +27,7 @@ export function TextView() {
<DraggableItem
name="Default text"
preview={
- <div className="bg-panel-accent flex size-full items-center justify-center rounded">
+ <div className="bg-accent flex size-full items-center justify-center rounded">
<span className="text-xs select-none">Default text</span>
</div>
}
diff --git a/apps/web/src/components/editor/panels/panel-base-view.tsx b/apps/web/src/components/editor/panels/panel-base-view.tsx
index eec063ec..dd6151d3 100644
--- a/apps/web/src/components/editor/panels/panel-base-view.tsx
+++ b/apps/web/src/components/editor/panels/panel-base-view.tsx
@@ -26,7 +26,7 @@ function ViewContent({
className?: string;
}) {
return (
- <ScrollArea className="flex-1">
+ <ScrollArea className="flex-1 scrollbar-hidden">
<div className={cn("p-5", className)}>{children}</div>
</ScrollArea>
);
@@ -52,7 +52,7 @@ export function PanelBaseView({
onValueChange={onValueChange}
className="flex h-full flex-col"
>
- <div className="bg-panel sticky top-0 z-10">
+ <div className="bg-background sticky top-0 z-10">
<div className="px-3 pt-3 pb-0">
<TabsList>
{tabs.map((tab) => (
diff --git a/apps/web/src/components/editor/panels/preview/index.tsx b/apps/web/src/components/editor/panels/preview/index.tsx
index c579b15b..902547bd 100644
--- a/apps/web/src/components/editor/panels/preview/index.tsx
+++ b/apps/web/src/components/editor/panels/preview/index.tsx
@@ -4,11 +4,23 @@ import { useCallback, useMemo, useRef } from "react";
import useDeepCompareEffect from "use-deep-compare-effect";
import { useEditor } from "@/hooks/use-editor";
import { useRafLoop } from "@/hooks/use-raf-loop";
+import { useContainerSize } from "@/hooks/use-container-size";
+import { useFullscreen } from "@/hooks/use-fullscreen";
import { CanvasRenderer } from "@/services/renderer/canvas-renderer";
import type { RootNode } from "@/services/renderer/nodes/root-node";
import { buildScene } from "@/services/renderer/scene-builder";
-import { getLastFrameTime } from "@/lib/time";
+import { formatTimeCode, getLastFrameTime } from "@/lib/time";
import { PreviewInteractionOverlay } from "./preview-interaction-overlay";
+import { EditableTimecode } from "@/components/editable-timecode";
+import { invokeAction } from "@/lib/actions";
+import { Button } from "@/components/ui/button";
+import {
+ FullScreenIcon,
+ PauseIcon,
+ PlayIcon,
+} from "@hugeicons/core-free-icons";
+import { HugeiconsIcon } from "@hugeicons/react";
+import { cn } from "@/utils/ui";
function usePreviewSize() {
const editor = useEditor();
@@ -47,32 +59,134 @@ function RenderTreeController() {
}
export function PreviewPanel() {
+ const containerRef = useRef<HTMLDivElement>(null);
+ const { isFullscreen, toggleFullscreen } = useFullscreen({ containerRef });
+
return (
- <div className="bg-panel relative flex h-full min-h-0 w-full min-w-0 flex-col rounded-sm">
- <div className="flex min-h-0 min-w-0 flex-1 items-center justify-center p-2">
+ <div
+ ref={containerRef}
+ className={cn(
+ "panel bg-background relative flex h-full min-h-0 w-full min-w-0 flex-col rounded-sm border",
+ isFullscreen && "bg-background",
+ )}
+ >
+ <div className="flex min-h-0 min-w-0 flex-1 items-center justify-center p-2 pb-0">
<PreviewCanvas />
<RenderTreeController />
</div>
+ <PreviewToolbar
+ isFullscreen={isFullscreen}
+ onToggleFullscreen={toggleFullscreen}
+ />
+ </div>
+ );
+}
+
+function PreviewToolbar({
+ isFullscreen,
+ onToggleFullscreen,
+}: {
+ isFullscreen: boolean;
+ onToggleFullscreen: () => void;
+}) {
+ const editor = useEditor();
+ const isPlaying = editor.playback.getIsPlaying();
+ const currentTime = editor.playback.getCurrentTime();
+ const totalDuration = editor.timeline.getTotalDuration();
+ const fps = editor.project.getActive().settings.fps;
+
+ return (
+ <div className="grid grid-cols-[1fr_auto_1fr] items-center pb-3 pt-5 px-5">
+ <div className="flex items-center mt-1">
+ <EditableTimecode
+ time={currentTime}
+ duration={totalDuration}
+ format="HH:MM:SS:FF"
+ fps={fps}
+ onTimeChange={({ time }) => editor.playback.seek({ time })}
+ className="text-center"
+ />
+ <span className="text-muted-foreground px-2 font-mono text-xs">/</span>
+ <span className="text-muted-foreground font-mono text-xs">
+ {formatTimeCode({
+ timeInSeconds: totalDuration,
+ format: "HH:MM:SS:FF",
+ fps,
+ })}
+ </span>
+ </div>
+
+ <Button
+ variant="text"
+ size="icon"
+ type="button"
+ onClick={() => invokeAction("toggle-play")}
+ >
+ <HugeiconsIcon icon={isPlaying ? PauseIcon : PlayIcon} />
+ </Button>
+
+ <div className="justify-self-end">
+ <Button
+ variant="text"
+ size="icon"
+ type="button"
+ onClick={onToggleFullscreen}
+ title={isFullscreen ? "Exit fullscreen" : "Enter fullscreen"}
+ >
+ <HugeiconsIcon icon={FullScreenIcon} />
+ </Button>
+ </div>
</div>
);
}
function PreviewCanvas() {
const canvasRef = useRef<HTMLCanvasElement>(null);
+ const containerRef = useRef<HTMLDivElement>(null);
const lastFrameRef = useRef(-1);
const lastSceneRef = useRef<RootNode | null>(null);
const renderingRef = useRef(false);
- const { width, height } = usePreviewSize();
+ const { width: nativeWidth, height: nativeHeight } = usePreviewSize();
+ const containerSize = useContainerSize({ containerRef });
const editor = useEditor();
const activeProject = editor.project.getActive();
const renderer = useMemo(() => {
return new CanvasRenderer({
- width,
- height,
+ width: nativeWidth,
+ height: nativeHeight,
fps: activeProject.settings.fps,
});
- }, [width, height, activeProject.settings.fps]);
+ }, [nativeWidth, nativeHeight, activeProject.settings.fps]);
+
+ const displaySize = useMemo(() => {
+ if (
+ !nativeWidth ||
+ !nativeHeight ||
+ containerSize.width === 0 ||
+ containerSize.height === 0
+ ) {
+ return { width: nativeWidth ?? 0, height: nativeHeight ?? 0 };
+ }
+
+ const paddingBuffer = 4;
+ const availableWidth = containerSize.width - paddingBuffer;
+ const availableHeight = containerSize.height - paddingBuffer;
+
+ const aspectRatio = nativeWidth / nativeHeight;
+ const containerAspect = availableWidth / availableHeight;
+
+ const displayWidth =
+ containerAspect > aspectRatio
+ ? availableHeight * aspectRatio
+ : availableWidth;
+ const displayHeight =
+ containerAspect > aspectRatio
+ ? availableHeight
+ : availableWidth / aspectRatio;
+
+ return { width: displayWidth, height: displayHeight };
+ }, [nativeWidth, nativeHeight, containerSize.width, containerSize.height]);
const renderTree = editor.renderer.getRenderTree();
@@ -109,13 +223,18 @@ function PreviewCanvas() {
useRafLoop(render);
return (
- <div className="relative">
+ <div
+ ref={containerRef}
+ className="relative flex h-full w-full items-center justify-center"
+ >
<canvas
ref={canvasRef}
- width={width}
- height={height}
- className="block max-h-full max-w-full border"
+ width={nativeWidth}
+ height={nativeHeight}
+ className="block border"
style={{
+ width: displaySize.width,
+ height: displaySize.height,
background:
activeProject.settings.background.type === "blur"
? "transparent"
diff --git a/apps/web/src/components/editor/panels/properties/empty-view.tsx b/apps/web/src/components/editor/panels/properties/empty-view.tsx
new file mode 100644
index 00000000..8ccf07ee
--- /dev/null
+++ b/apps/web/src/components/editor/panels/properties/empty-view.tsx
@@ -0,0 +1,20 @@
+import { HugeiconsIcon } from "@hugeicons/react";
+import { Settings05Icon } from "@hugeicons/core-free-icons";
+
+export function EmptyView() {
+ return (
+ <div className="bg-background flex h-full flex-col items-center justify-center gap-3 p-4">
+ <HugeiconsIcon
+ icon={Settings05Icon}
+ className="text-muted-foreground/75 size-10"
+ strokeWidth={1}
+ />
+ <div className="flex flex-col gap-2 text-center">
+ <p className="text-lg font-medium ">It's empty here</p>
+ <p className="text-muted-foreground text-sm text-balance">
+ Click an element on the timeline to edit its properties
+ </p>
+ </div>
+ </div>
+ );
+} \ No newline at end of file
diff --git a/apps/web/src/components/editor/panels/properties/index.tsx b/apps/web/src/components/editor/panels/properties/index.tsx
index 3a552b93..05ad5396 100644
--- a/apps/web/src/components/editor/panels/properties/index.tsx
+++ b/apps/web/src/components/editor/panels/properties/index.tsx
@@ -4,8 +4,7 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import { AudioProperties } from "./audio-properties";
import { VideoProperties } from "./video-properties";
import { TextProperties } from "./text-properties";
-import { HugeiconsIcon } from "@hugeicons/react";
-import { Settings05Icon } from "@hugeicons/core-free-icons";
+import { EmptyView } from "./empty-view";
import { useEditor } from "@/hooks/use-editor";
import { useElementSelection } from "@/hooks/timeline/element/use-element-selection";
@@ -18,9 +17,9 @@ export function PropertiesPanel() {
});
return (
- <>
+ <div className="panel bg-background h-full rounded-sm border overflow-hidden">
{selectedElements.length > 0 ? (
- <ScrollArea className="bg-panel h-full rounded-sm">
+ <ScrollArea className="h-full">
{elementsWithTracks.map(({ track, element }) => {
if (element.type === "text") {
return (
@@ -45,24 +44,6 @@ export function PropertiesPanel() {
) : (
<EmptyView />
)}
- </>
- );
-}
-
-function EmptyView() {
- return (
- <div className="bg-panel flex h-full flex-col items-center justify-center gap-3 p-4">
- <HugeiconsIcon
- icon={Settings05Icon}
- className="text-muted-foreground/75 size-10"
- strokeWidth={1}
- />
- <div className="flex flex-col gap-2 text-center">
- <p className="text-lg font-medium">It's empty here</p>
- <p className="text-muted-foreground text-sm text-balance">
- Click an element on the timeline to edit its properties
- </p>
- </div>
</div>
);
}
diff --git a/apps/web/src/components/editor/panels/properties/property-item.tsx b/apps/web/src/components/editor/panels/properties/property-item.tsx
index 6d2fe444..a682097a 100644
--- a/apps/web/src/components/editor/panels/properties/property-item.tsx
+++ b/apps/web/src/components/editor/panels/properties/property-item.tsx
@@ -1,7 +1,7 @@
import { useState } from "react";
import { cn } from "@/utils/ui";
import { HugeiconsIcon } from "@hugeicons/react";
-import { ArrowDownIcon } from "@hugeicons/core-free-icons";
+import { MinusSignIcon, PlusSignIcon } from "@hugeicons/core-free-icons";
interface PropertyItemProps {
direction?: "row" | "column";
@@ -57,35 +57,76 @@ interface PropertyGroupProps {
title: string;
children: React.ReactNode;
defaultExpanded?: boolean;
+ collapsible?: boolean;
className?: string;
- titleClassName?: string;
+ hasBorderTop?: boolean;
+ hasBorderBottom?: boolean;
}
export function PropertyGroup({
title,
children,
defaultExpanded = true,
+ collapsible = true,
className,
- titleClassName,
+ hasBorderTop = true,
+ hasBorderBottom = true,
}: PropertyGroupProps) {
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
return (
- <PropertyItem direction="column" className={cn("gap-3", className)}>
- <button
- type="button"
- className="flex items-center gap-1.5 cursor-pointer"
- onClick={() => setIsExpanded(!isExpanded)}
- >
- <PropertyItemLabel className={cn(titleClassName)}>
- {title}
- </PropertyItemLabel>
- <HugeiconsIcon
- icon={ArrowDownIcon}
- className={cn("size-3", !isExpanded && "-rotate-90")}
- />
- </button>
- {isExpanded && <PropertyItemValue>{children}</PropertyItemValue>}
- </PropertyItem>
+ <div
+ className={cn(
+ "flex flex-col",
+ hasBorderTop && "border-t",
+ hasBorderBottom && "last:border-b",
+ className,
+ )}
+ >
+ {collapsible ? (
+ <button
+ type="button"
+ className="flex items-center justify-between p-3.5 cursor-pointer"
+ onClick={() => setIsExpanded(!isExpanded)}
+ >
+ <PropertyGroupTitle isExpanded={isExpanded}>
+ {title}
+ </PropertyGroupTitle>
+ <HugeiconsIcon
+ icon={isExpanded ? MinusSignIcon : PlusSignIcon}
+ className={cn(
+ "size-3",
+ isExpanded ? "text-foreground" : "text-muted-foreground",
+ )}
+ />
+ </button>
+ ) : (
+ <div className="flex items-center justify-between p-4">
+ <PropertyGroupTitle isExpanded>{title}</PropertyGroupTitle>
+ </div>
+ )}
+ {(collapsible ? isExpanded : true) && (
+ <div className="p-3 pt-0">{children}</div>
+ )}
+ </div>
+ );
+}
+
+function PropertyGroupTitle({
+ children,
+ isExpanded = false,
+}: {
+ children: React.ReactNode;
+ isExpanded?: boolean;
+}) {
+ return (
+ <span
+ className={cn(
+ "text-xs font-medium",
+ isExpanded ? "text-foreground" : "text-muted-foreground",
+ )}
+ >
+ {children}
+ </span>
);
}
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 35230c75..22e50e4e 100644
--- a/apps/web/src/components/editor/panels/properties/text-properties.tsx
+++ b/apps/web/src/components/editor/panels/properties/text-properties.tsx
@@ -5,29 +5,17 @@ import type { TextElement } from "@/types/timeline";
import { Slider } from "@/components/ui/slider";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
-import { useState, useRef } from "react";
+import { useReducer, useRef } from "react";
import { PanelBaseView } from "@/components/editor/panels/panel-base-view";
import {
- TEXT_PROPERTIES_TABS,
- isTextPropertiesTab,
- useTextPropertiesStore,
-} from "@/stores/text-properties-store";
-import {
+ PropertyGroup,
PropertyItem,
PropertyItemLabel,
PropertyItemValue,
} from "./property-item";
import { ColorPicker } from "@/components/ui/color-picker";
-import { cn } from "@/utils/ui";
-import { capitalizeFirstLetter, uppercase } from "@/utils/string";
+import { uppercase } from "@/utils/string";
import { clamp } from "@/utils/math";
-import { HugeiconsIcon } from "@hugeicons/react";
-import { LayoutGridIcon } from "@hugeicons/core-free-icons";
-import {
- Tooltip,
- TooltipContent,
- TooltipTrigger,
-} from "@/components/ui/tooltip";
import { useEditor } from "@/hooks/use-editor";
import { DEFAULT_COLOR } from "@/constants/project-constants";
import { MIN_FONT_SIZE, MAX_FONT_SIZE } from "@/constants/text-constants";
@@ -40,48 +28,86 @@ export function TextProperties({
trackId: string;
}) {
const editor = useEditor();
- const { activeTab, setActiveTab } = useTextPropertiesStore();
const containerRef = useRef<HTMLDivElement>(null);
- const [fontSizeInput, setFontSizeInput] = useState(
- element.fontSize.toString(),
- );
- const [opacityInput, setOpacityInput] = useState(
- Math.round(element.opacity * 100).toString(),
- );
+ const [, forceRender] = useReducer((x: number) => x + 1, 0);
+ const isEditingFontSize = useRef(false);
+ const isEditingOpacity = useRef(false);
+ const isEditingContent = useRef(false);
+ const fontSizeDraft = useRef("");
+ const opacityDraft = useRef("");
+ const contentDraft = useRef("");
+
+ const fontSizeDisplay = isEditingFontSize.current
+ ? fontSizeDraft.current
+ : element.fontSize.toString();
+ const opacityDisplay = isEditingOpacity.current
+ ? opacityDraft.current
+ : Math.round(element.opacity * 100).toString();
+ const contentDisplay = isEditingContent.current
+ ? contentDraft.current
+ : element.content;
const lastSelectedColor = useRef(DEFAULT_COLOR);
const initialFontSizeRef = useRef<number | null>(null);
const initialOpacityRef = useRef<number | null>(null);
+ const initialContentRef = useRef<string | null>(null);
+ const initialColorRef = useRef<string | null>(null);
+ const initialBgColorRef = useRef<string | null>(null);
const handleFontSizeChange = ({ value }: { value: string }) => {
- setFontSizeInput(value);
+ fontSizeDraft.current = value;
+ forceRender();
if (value.trim() !== "") {
+ if (initialFontSizeRef.current === null) {
+ initialFontSizeRef.current = element.fontSize;
+ }
const parsed = parseInt(value, 10);
const fontSize = Number.isNaN(parsed)
? element.fontSize
: clamp({ value: parsed, min: MIN_FONT_SIZE, max: MAX_FONT_SIZE });
editor.timeline.updateElements({
updates: [{ trackId, elementId: element.id, updates: { fontSize } }],
+ pushHistory: false,
});
}
};
const handleFontSizeBlur = () => {
- const parsed = parseInt(fontSizeInput, 10);
- const fontSize = Number.isNaN(parsed)
- ? element.fontSize
- : clamp({ value: parsed, min: MIN_FONT_SIZE, max: MAX_FONT_SIZE });
- setFontSizeInput(fontSize.toString());
- editor.timeline.updateElements({
- updates: [{ trackId, elementId: element.id, updates: { fontSize } }],
- });
+ if (initialFontSizeRef.current !== null) {
+ const parsed = parseInt(fontSizeDraft.current, 10);
+ const fontSize = Number.isNaN(parsed)
+ ? element.fontSize
+ : clamp({ value: parsed, min: MIN_FONT_SIZE, max: MAX_FONT_SIZE });
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: { fontSize: initialFontSizeRef.current },
+ },
+ ],
+ pushHistory: false,
+ });
+ editor.timeline.updateElements({
+ updates: [{ trackId, elementId: element.id, updates: { fontSize } }],
+ pushHistory: true,
+ });
+ initialFontSizeRef.current = null;
+ }
+ isEditingFontSize.current = false;
+ fontSizeDraft.current = "";
+ forceRender();
};
const handleOpacityChange = ({ value }: { value: string }) => {
- setOpacityInput(value);
+ opacityDraft.current = value;
+ forceRender();
if (value.trim() !== "") {
+ if (initialOpacityRef.current === null) {
+ initialOpacityRef.current = element.opacity;
+ }
const parsed = parseInt(value, 10);
const opacityPercent = Number.isNaN(parsed)
? Math.round(element.opacity * 100)
@@ -94,234 +120,333 @@ export function TextProperties({
updates: { opacity: opacityPercent / 100 },
},
],
+ pushHistory: false,
});
}
};
const handleOpacityBlur = () => {
- const parsed = parseInt(opacityInput, 10);
- const opacityPercent = Number.isNaN(parsed)
- ? Math.round(element.opacity * 100)
- : clamp({ value: parsed, min: 0, max: 100 });
- setOpacityInput(opacityPercent.toString());
- editor.timeline.updateElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: { opacity: opacityPercent / 100 },
- },
- ],
- });
+ if (initialOpacityRef.current !== null) {
+ const parsed = parseInt(opacityDraft.current, 10);
+ const opacityPercent = Number.isNaN(parsed)
+ ? Math.round(element.opacity * 100)
+ : clamp({ value: parsed, min: 0, max: 100 });
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: { opacity: initialOpacityRef.current },
+ },
+ ],
+ pushHistory: false,
+ });
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: { opacity: opacityPercent / 100 },
+ },
+ ],
+ pushHistory: true,
+ });
+ initialOpacityRef.current = null;
+ }
+ isEditingOpacity.current = false;
+ opacityDraft.current = "";
+ forceRender();
};
const handleColorChange = ({ color }: { color: string }) => {
if (color !== "transparent") {
lastSelectedColor.current = color;
}
- editor.timeline.updateElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: { backgroundColor: color },
- },
- ],
- });
+ if (initialBgColorRef.current === null) {
+ initialBgColorRef.current = element.backgroundColor;
+ }
+ if (initialBgColorRef.current !== null) {
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: { backgroundColor: color },
+ },
+ ],
+ pushHistory: false,
+ });
+ } else {
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: { backgroundColor: color },
+ },
+ ],
+ });
+ }
};
- const handleTransparentToggle = ({
- isTransparent,
- }: {
- isTransparent: boolean;
- }) => {
- const newColor = isTransparent ? "transparent" : lastSelectedColor.current;
- editor.timeline.updateElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: { backgroundColor: newColor },
- },
- ],
- });
+ const handleColorChangeEnd = ({ color }: { color: string }) => {
+ if (initialBgColorRef.current !== null) {
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: { backgroundColor: initialBgColorRef.current },
+ },
+ ],
+ pushHistory: false,
+ });
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: { backgroundColor: `#${color}` },
+ },
+ ],
+ pushHistory: true,
+ });
+ initialBgColorRef.current = null;
+ }
};
return (
- <PanelBaseView
- defaultTab="transform"
- value={activeTab}
- onValueChange={(v) => {
- if (isTextPropertiesTab(v)) setActiveTab(v);
- }}
- ref={containerRef}
- tabs={TEXT_PROPERTIES_TABS.map((t) => ({
- value: t.value,
- label: t.label,
- content:
- t.value === "transform" ? (
- <div className="space-y-6"></div>
- ) : (
- <div className="space-y-6">
- <Textarea
- placeholder="Name"
- defaultValue={element.content}
- className="bg-panel-accent min-h-20"
- onChange={(e) =>
- editor.timeline.updateElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: { content: e.target.value },
- },
- ],
- })
- }
- />
- <PropertyItem direction="column">
- <PropertyItemLabel>Font</PropertyItemLabel>
- <PropertyItemValue>
- <FontPicker
- defaultValue={element.fontFamily}
- onValueChange={(value: FontFamily) =>
+ <div className="flex h-full flex-col" ref={containerRef}>
+ <PanelBaseView className="p-0">
+ <PropertyGroup title="Content" hasBorderTop={false} collapsible={false}>
+ <Textarea
+ placeholder="Name"
+ value={contentDisplay}
+ className="bg-accent min-h-20"
+ onFocus={() => {
+ isEditingContent.current = true;
+ contentDraft.current = element.content;
+ initialContentRef.current = element.content;
+ forceRender();
+ }}
+ onChange={(event) => {
+ contentDraft.current = event.target.value;
+ forceRender();
+ if (initialContentRef.current === null) {
+ initialContentRef.current = element.content;
+ }
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: { content: event.target.value },
+ },
+ ],
+ pushHistory: false,
+ });
+ }}
+ onBlur={() => {
+ if (initialContentRef.current !== null) {
+ const finalContent = contentDraft.current;
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: { content: initialContentRef.current },
+ },
+ ],
+ pushHistory: false,
+ });
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: { content: finalContent },
+ },
+ ],
+ pushHistory: true,
+ });
+ initialContentRef.current = null;
+ }
+ isEditingContent.current = false;
+ contentDraft.current = "";
+ forceRender();
+ }}
+ />
+ </PropertyGroup>
+ <PropertyGroup title="Typography" collapsible={false}>
+ <div className="space-y-6">
+ <PropertyItem direction="column">
+ <PropertyItemLabel>Font</PropertyItemLabel>
+ <PropertyItemValue>
+ <FontPicker
+ defaultValue={element.fontFamily}
+ onValueChange={(value: FontFamily) =>
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: { fontFamily: value },
+ },
+ ],
+ })
+ }
+ />
+ </PropertyItemValue>
+ </PropertyItem>
+ <PropertyItem direction="column">
+ <PropertyItemLabel>Style</PropertyItemLabel>
+ <PropertyItemValue>
+ <div className="flex items-center gap-2">
+ <Button
+ variant={
+ element.fontWeight === "bold" ? "default" : "outline"
+ }
+ size="sm"
+ onClick={() =>
editor.timeline.updateElements({
updates: [
{
trackId,
elementId: element.id,
- updates: { fontFamily: value },
+ updates: {
+ fontWeight:
+ element.fontWeight === "bold"
+ ? "normal"
+ : "bold",
+ },
},
],
})
}
- />
- </PropertyItemValue>
- </PropertyItem>
- <PropertyItem direction="column">
- <PropertyItemLabel>Style</PropertyItemLabel>
- <PropertyItemValue>
- <div className="flex items-center gap-2">
- <Button
- variant={
- element.fontWeight === "bold" ? "default" : "outline"
- }
- size="sm"
- onClick={() =>
- editor.timeline.updateElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: {
- fontWeight:
- element.fontWeight === "bold"
- ? "normal"
- : "bold",
- },
+ className="h-8 px-3 font-bold"
+ >
+ B
+ </Button>
+ <Button
+ variant={
+ element.fontStyle === "italic" ? "default" : "outline"
+ }
+ size="sm"
+ onClick={() =>
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: {
+ fontStyle:
+ element.fontStyle === "italic"
+ ? "normal"
+ : "italic",
},
- ],
- })
- }
- className="h-8 px-3 font-bold"
- >
- B
- </Button>
- <Button
- variant={
- element.fontStyle === "italic" ? "default" : "outline"
- }
- size="sm"
- onClick={() =>
- editor.timeline.updateElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: {
- fontStyle:
- element.fontStyle === "italic"
- ? "normal"
- : "italic",
- },
+ },
+ ],
+ })
+ }
+ className="h-8 px-3 italic"
+ >
+ I
+ </Button>
+ <Button
+ variant={
+ element.textDecoration === "underline"
+ ? "default"
+ : "outline"
+ }
+ size="sm"
+ onClick={() =>
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: {
+ textDecoration:
+ element.textDecoration === "underline"
+ ? "none"
+ : "underline",
},
- ],
- })
- }
- className="h-8 px-3 italic"
- >
- I
- </Button>
- <Button
- variant={
- element.textDecoration === "underline"
- ? "default"
- : "outline"
- }
- size="sm"
- onClick={() =>
- editor.timeline.updateElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: {
- textDecoration:
- element.textDecoration === "underline"
- ? "none"
- : "underline",
- },
+ },
+ ],
+ })
+ }
+ className="h-8 px-3 underline"
+ >
+ U
+ </Button>
+ <Button
+ variant={
+ element.textDecoration === "line-through"
+ ? "default"
+ : "outline"
+ }
+ size="sm"
+ onClick={() =>
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: {
+ textDecoration:
+ element.textDecoration === "line-through"
+ ? "none"
+ : "line-through",
},
- ],
- })
- }
- className="h-8 px-3 underline"
- >
- U
- </Button>
- <Button
- variant={
- element.textDecoration === "line-through"
- ? "default"
- : "outline"
+ },
+ ],
+ })
+ }
+ className="h-8 px-3 line-through"
+ >
+ S
+ </Button>
+ </div>
+ </PropertyItemValue>
+ </PropertyItem>
+ <PropertyItem direction="column">
+ <PropertyItemLabel>Font size</PropertyItemLabel>
+ <PropertyItemValue>
+ <div className="flex items-center gap-2">
+ <Slider
+ value={[element.fontSize]}
+ min={MIN_FONT_SIZE}
+ max={MAX_FONT_SIZE}
+ step={1}
+ onValueChange={([value]) => {
+ if (initialFontSizeRef.current === null) {
+ initialFontSizeRef.current = element.fontSize;
}
- size="sm"
- onClick={() =>
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: { fontSize: value },
+ },
+ ],
+ pushHistory: false,
+ });
+ }}
+ onValueCommit={([value]) => {
+ if (initialFontSizeRef.current !== null) {
editor.timeline.updateElements({
updates: [
{
trackId,
elementId: element.id,
updates: {
- textDecoration:
- element.textDecoration === "line-through"
- ? "none"
- : "line-through",
+ fontSize: initialFontSizeRef.current,
},
},
],
- })
- }
- className="h-8 px-3 line-through"
- >
- S
- </Button>
- </div>
- </PropertyItemValue>
- </PropertyItem>
- <PropertyItem direction="column">
- <PropertyItemLabel>Font size</PropertyItemLabel>
- <PropertyItemValue>
- <div className="flex items-center gap-2">
- <Slider
- value={[element.fontSize]}
- min={MIN_FONT_SIZE}
- max={MAX_FONT_SIZE}
- step={1}
- onValueChange={([value]) => {
- if (initialFontSizeRef.current === null) {
- initialFontSizeRef.current = element.fontSize;
- }
+ pushHistory: false,
+ });
editor.timeline.updateElements({
updates: [
{
@@ -330,61 +455,48 @@ export function TextProperties({
updates: { fontSize: value },
},
],
- pushHistory: false,
+ pushHistory: true,
});
- setFontSizeInput(value.toString());
- }}
- onValueCommit={([value]) => {
- if (initialFontSizeRef.current !== null) {
- editor.timeline.updateElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: {
- fontSize: initialFontSizeRef.current,
- },
- },
- ],
- pushHistory: false,
- });
- editor.timeline.updateElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: { fontSize: value },
- },
- ],
- pushHistory: true,
- });
- initialFontSizeRef.current = null;
- }
- }}
- className="w-full"
- />
- <Input
- type="number"
- value={fontSizeInput}
- min={MIN_FONT_SIZE}
- max={MAX_FONT_SIZE}
- onChange={(e) =>
- handleFontSizeChange({ value: e.target.value })
+ initialFontSizeRef.current = null;
}
- onBlur={handleFontSizeBlur}
- className="bg-panel-accent h-7 w-12 [appearance:textfield] rounded-sm px-2 text-center !text-xs [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
- />
- </div>
- </PropertyItemValue>
- </PropertyItem>
- <PropertyItem direction="column">
- <PropertyItemLabel>Color</PropertyItemLabel>
- <PropertyItemValue>
- <ColorPicker
- value={uppercase({
- string: (element.color || "FFFFFF").replace("#", ""),
- })}
- onChange={(color) => {
+ }}
+ className="w-full"
+ />
+ <Input
+ type="number"
+ value={fontSizeDisplay}
+ min={MIN_FONT_SIZE}
+ max={MAX_FONT_SIZE}
+ onFocus={() => {
+ isEditingFontSize.current = true;
+ fontSizeDraft.current = element.fontSize.toString();
+ forceRender();
+ }}
+ onChange={(e) =>
+ handleFontSizeChange({ value: e.target.value })
+ }
+ onBlur={handleFontSizeBlur}
+ className="bg-accent h-7 w-12 [appearance:textfield] rounded-sm px-2 text-center !text-xs [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
+ />
+ </div>
+ </PropertyItemValue>
+ </PropertyItem>
+ </div>
+ </PropertyGroup>
+ <PropertyGroup title="Appearance" collapsible={false}>
+ <div className="space-y-6">
+ <PropertyItem direction="column">
+ <PropertyItemLabel>Color</PropertyItemLabel>
+ <PropertyItemValue>
+ <ColorPicker
+ value={uppercase({
+ string: (element.color || "FFFFFF").replace("#", ""),
+ })}
+ onChange={(color) => {
+ if (initialColorRef.current === null) {
+ initialColorRef.current = element.color || "#FFFFFF";
+ }
+ if (initialColorRef.current !== null) {
editor.timeline.updateElements({
updates: [
{
@@ -393,131 +505,146 @@ export function TextProperties({
updates: { color: `#${color}` },
},
],
+ pushHistory: false,
+ });
+ } else {
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: { color: `#${color}` },
+ },
+ ],
+ });
+ }
+ }}
+ onChangeEnd={(color) => {
+ if (initialColorRef.current !== null) {
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: { color: initialColorRef.current },
+ },
+ ],
+ pushHistory: false,
+ });
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: { color: `#${color}` },
+ },
+ ],
+ pushHistory: true,
+ });
+ initialColorRef.current = null;
+ }
+ }}
+ containerRef={containerRef}
+ />
+ </PropertyItemValue>
+ </PropertyItem>
+ <PropertyItem direction="column">
+ <PropertyItemLabel>Opacity</PropertyItemLabel>
+ <PropertyItemValue>
+ <div className="flex items-center gap-2">
+ <Slider
+ value={[element.opacity * 100]}
+ min={0}
+ max={100}
+ step={1}
+ onValueChange={([value]) => {
+ if (initialOpacityRef.current === null) {
+ initialOpacityRef.current = element.opacity;
+ }
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: { opacity: value / 100 },
+ },
+ ],
+ pushHistory: false,
});
}}
- containerRef={containerRef}
- />
- </PropertyItemValue>
- </PropertyItem>
- <PropertyItem direction="column">
- <PropertyItemLabel>Opacity</PropertyItemLabel>
- <PropertyItemValue>
- <div className="flex items-center gap-2">
- <Slider
- value={[element.opacity * 100]}
- min={0}
- max={100}
- step={1}
- onValueChange={([value]) => {
- if (initialOpacityRef.current === null) {
- initialOpacityRef.current = element.opacity;
- }
+ onValueCommit={([value]) => {
+ if (initialOpacityRef.current !== null) {
editor.timeline.updateElements({
updates: [
{
trackId,
elementId: element.id,
- updates: { opacity: value / 100 },
+ updates: { opacity: initialOpacityRef.current },
},
],
pushHistory: false,
});
- setOpacityInput(value.toString());
- }}
- onValueCommit={([value]) => {
- if (initialOpacityRef.current !== null) {
- editor.timeline.updateElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: { opacity: initialOpacityRef.current },
- },
- ],
- pushHistory: false,
- });
- editor.timeline.updateElements({
- updates: [
- {
- trackId,
- elementId: element.id,
- updates: { opacity: value / 100 },
- },
- ],
- pushHistory: true,
- });
- initialOpacityRef.current = null;
- }
- }}
- className="w-full"
- />
- <Input
- type="number"
- value={opacityInput}
- min={0}
- max={100}
- onChange={(e) =>
- handleOpacityChange({ value: e.target.value })
- }
- onBlur={handleOpacityBlur}
- className="bg-panel-accent h-7 w-12 [appearance:textfield] rounded-sm text-center !text-xs [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
- />
- </div>
- </PropertyItemValue>
- </PropertyItem>
- <PropertyItem direction="column">
- <PropertyItemLabel>Background</PropertyItemLabel>
- <PropertyItemValue>
- <div className="flex items-center gap-2">
- <ColorPicker
- value={capitalizeFirstLetter({
- string:
- element.backgroundColor === "transparent"
- ? lastSelectedColor.current.replace("#", "")
- : element.backgroundColor.replace("#", ""),
- })}
- onChange={(color) =>
- handleColorChange({ color: `#${color}` })
- }
- containerRef={containerRef}
- className={
- element.backgroundColor === "transparent"
- ? "pointer-events-none opacity-50"
- : ""
+ editor.timeline.updateElements({
+ updates: [
+ {
+ trackId,
+ elementId: element.id,
+ updates: { opacity: value / 100 },
+ },
+ ],
+ pushHistory: true,
+ });
+ initialOpacityRef.current = null;
}
- />
-
- <Tooltip>
- <TooltipTrigger asChild>
- <Button
- variant="outline"
- size="icon"
- onClick={() =>
- handleTransparentToggle({
- isTransparent:
- element.backgroundColor !== "transparent",
- })
- }
- className="bg-panel-accent size-9 overflow-hidden rounded-full p-0"
- >
- <HugeiconsIcon
- icon={LayoutGridIcon}
- className={cn(
- "text-foreground",
- element.backgroundColor === "transparent" &&
- "text-primary",
- )}
- />
- </Button>
- </TooltipTrigger>
- <TooltipContent>Transparent background</TooltipContent>
- </Tooltip>
- </div>
- </PropertyItemValue>
- </PropertyItem>
- </div>
- ),
- }))}
- />
+ }}
+ className="w-full"
+ />
+ <Input
+ type="number"
+ value={opacityDisplay}
+ min={0}
+ max={100}
+ onFocus={() => {
+ isEditingOpacity.current = true;
+ opacityDraft.current = Math.round(
+ element.opacity * 100,
+ ).toString();
+ forceRender();
+ }}
+ onChange={(e) =>
+ handleOpacityChange({ value: e.target.value })
+ }
+ onBlur={handleOpacityBlur}
+ className="bg-accent h-7 w-12 [appearance:textfield] rounded-sm text-center !text-xs [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
+ />
+ </div>
+ </PropertyItemValue>
+ </PropertyItem>
+ <PropertyItem direction="column">
+ <PropertyItemLabel>Background</PropertyItemLabel>
+ <PropertyItemValue>
+ <ColorPicker
+ value={
+ element.backgroundColor === "transparent"
+ ? lastSelectedColor.current.replace("#", "")
+ : element.backgroundColor.replace("#", "")
+ }
+ onChange={(color) =>
+ handleColorChange({ color: `#${color}` })
+ }
+ onChangeEnd={(color) => handleColorChangeEnd({ color })}
+ containerRef={containerRef}
+ className={
+ element.backgroundColor === "transparent"
+ ? "pointer-events-none opacity-50"
+ : ""
+ }
+ />
+ </PropertyItemValue>
+ </PropertyItem>
+ </div>
+ </PropertyGroup>
+ </PanelBaseView>
+ </div>
);
}
diff --git a/apps/web/src/components/editor/timeline/audio-waveform.tsx b/apps/web/src/components/editor/panels/timeline/audio-waveform.tsx
index bbc5cb83..bbc5cb83 100644
--- a/apps/web/src/components/editor/timeline/audio-waveform.tsx
+++ b/apps/web/src/components/editor/panels/timeline/audio-waveform.tsx
diff --git a/apps/web/src/components/editor/timeline/bookmarks.tsx b/apps/web/src/components/editor/panels/timeline/bookmarks.tsx
index cfcb1e23..cfcb1e23 100644
--- a/apps/web/src/components/editor/timeline/bookmarks.tsx
+++ b/apps/web/src/components/editor/panels/timeline/bookmarks.tsx
diff --git a/apps/web/src/components/editor/timeline/drag-line.tsx b/apps/web/src/components/editor/panels/timeline/drag-line.tsx
index f31c6e28..f31c6e28 100644
--- a/apps/web/src/components/editor/timeline/drag-line.tsx
+++ b/apps/web/src/components/editor/panels/timeline/drag-line.tsx
diff --git a/apps/web/src/components/editor/timeline/index.tsx b/apps/web/src/components/editor/panels/timeline/index.tsx
index 2ff17345..94edc2d0 100644
--- a/apps/web/src/components/editor/timeline/index.tsx
+++ b/apps/web/src/components/editor/panels/timeline/index.tsx
@@ -15,12 +15,12 @@ import {
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger,
-} from "../../ui/context-menu";
+} from "../../../ui/context-menu";
import { useTimelineZoom } from "@/hooks/timeline/use-timeline-zoom";
import { useState, useRef, useCallback } from "react";
import { TimelineTrackContent } from "./timeline-track";
import { TimelinePlayhead } from "./timeline-playhead";
-import { SelectionBox } from "../selection-box";
+import { SelectionBox } from "../../selection-box";
import { useSelectionBox } from "@/hooks/timeline/use-selection-box";
import { SnapIndicator } from "./snap-indicator";
import type { SnapPoint } from "@/hooks/timeline/use-timeline-snapping";
@@ -201,7 +201,7 @@ export function Timeline() {
return (
<section
className={
- "bg-panel relative flex h-full flex-col overflow-hidden rounded-sm"
+ "panel bg-background relative flex h-full flex-col overflow-hidden rounded-sm border"
}
{...dragProps}
aria-label="Timeline"
@@ -226,17 +226,17 @@ export function Timeline() {
isVisible={showSnapIndicator}
/>
<div className="flex flex-1 overflow-hidden">
- <div className="bg-panel flex w-28 shrink-0 flex-col border-r">
- <div className="bg-panel flex h-4 items-center justify-between px-3">
+ <div className="bg-background flex w-28 shrink-0 flex-col border-r">
+ <div className="bg-background flex h-4 items-center justify-between px-3">
<span className="opacity-0">.</span>
</div>
- <div className="bg-panel flex h-4 items-center justify-between px-3">
+ <div className="bg-background flex h-4 items-center justify-between px-3">
<span className="opacity-0">.</span>
</div>
{tracks.length > 0 && (
<div
ref={trackLabelsRef}
- className="bg-panel flex-1 overflow-y-auto"
+ className="bg-background flex-1 overflow-y-auto"
style={{ paddingTop: TIMELINE_CONSTANTS.PADDING_TOP_PX }}
>
<ScrollArea className="size-full" ref={trackLabelsScrollRef}>
@@ -351,7 +351,7 @@ export function Timeline() {
>
<div
ref={timelineHeaderRef}
- className="bg-panel sticky top-0 z-30 flex flex-col"
+ className="bg-background sticky top-0 z-30 flex flex-col"
>
<TimelineRuler
zoomLevel={zoomLevel}
diff --git a/apps/web/src/components/editor/timeline/snap-indicator.tsx b/apps/web/src/components/editor/panels/timeline/snap-indicator.tsx
index 914edccb..914edccb 100644
--- a/apps/web/src/components/editor/timeline/snap-indicator.tsx
+++ b/apps/web/src/components/editor/panels/timeline/snap-indicator.tsx
diff --git a/apps/web/src/components/editor/timeline/timeline-element.tsx b/apps/web/src/components/editor/panels/timeline/timeline-element.tsx
index fd292ad8..59ce88da 100644
--- a/apps/web/src/components/editor/timeline/timeline-element.tsx
+++ b/apps/web/src/components/editor/panels/timeline/timeline-element.tsx
@@ -19,7 +19,7 @@ import {
ContextMenuItem,
ContextMenuSeparator,
ContextMenuTrigger,
-} from "../../ui/context-menu";
+} from "../../../ui/context-menu";
import type {
TimelineElement as TimelineElementType,
TimelineTrack,
@@ -166,7 +166,10 @@ export function TimelineElement({
</div>
</ContextMenuTrigger>
<ContextMenuContent className="z-200 w-64">
- <ActionMenuItem action="split" icon={<HugeiconsIcon icon={ScissorIcon} />}>
+ <ActionMenuItem
+ action="split"
+ icon={<HugeiconsIcon icon={ScissorIcon} />}
+ >
Split
</ActionMenuItem>
<CopyMenuItem />
@@ -185,7 +188,10 @@ export function TimelineElement({
/>
)}
{selectedElements.length === 1 && (
- <ActionMenuItem action="duplicate-selected" icon={<HugeiconsIcon icon={Copy01Icon} />}>
+ <ActionMenuItem
+ action="duplicate-selected"
+ icon={<HugeiconsIcon icon={Copy01Icon} />}
+ >
Duplicate
</ActionMenuItem>
)}
@@ -446,7 +452,10 @@ function ElementContent({
function CopyMenuItem() {
return (
- <ActionMenuItem action="copy-selected" icon={<HugeiconsIcon icon={Copy01Icon} />}>
+ <ActionMenuItem
+ action="copy-selected"
+ icon={<HugeiconsIcon icon={Copy01Icon} />}
+ >
Copy
</ActionMenuItem>
);
@@ -502,7 +511,10 @@ function VisibilityMenuItem({
};
return (
- <ActionMenuItem action="toggle-elements-visibility-selected" icon={getIcon()}>
+ <ActionMenuItem
+ action="toggle-elements-visibility-selected"
+ icon={getIcon()}
+ >
{isHidden ? "Show" : "Hide"}
</ActionMenuItem>
);
diff --git a/apps/web/src/components/editor/timeline/timeline-playhead.tsx b/apps/web/src/components/editor/panels/timeline/timeline-playhead.tsx
index 8af35dc9..8af35dc9 100644
--- a/apps/web/src/components/editor/timeline/timeline-playhead.tsx
+++ b/apps/web/src/components/editor/panels/timeline/timeline-playhead.tsx
diff --git a/apps/web/src/components/editor/timeline/timeline-ruler.tsx b/apps/web/src/components/editor/panels/timeline/timeline-ruler.tsx
index dfa0534b..b0261aa2 100644
--- a/apps/web/src/components/editor/timeline/timeline-ruler.tsx
+++ b/apps/web/src/components/editor/panels/timeline/timeline-ruler.tsx
@@ -1,4 +1,4 @@
-import type { JSX } from "react";
+import { type JSX, useLayoutEffect, useRef } from "react";
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
import { DEFAULT_FPS } from "@/constants/project-constants";
import { useEditor } from "@/hooks/use-editor";
@@ -44,7 +44,21 @@ export function TimelineRuler({
scrollRef: tracksScrollRef,
});
- const bufferPx = 200;
+ /**
+ * widens the virtualization buffer during zoom transitions.
+ * useScrollPosition lags one frame behind the scroll adjustment
+ * that useLayoutEffect applies after a zoom change.
+ */
+ const prevZoomRef = useRef(zoomLevel);
+ const isZoomTransition = zoomLevel !== prevZoomRef.current;
+ const bufferPx = isZoomTransition
+ ? Math.max(200, (scrollLeft + viewportWidth) * 0.15)
+ : 200;
+
+ useLayoutEffect(() => {
+ prevZoomRef.current = zoomLevel;
+ }, [zoomLevel]);
+
const visibleStartTime = Math.max(
0,
(scrollLeft - bufferPx) / pixelsPerSecond,
@@ -62,7 +76,11 @@ export function TimelineRuler({
);
const timelineTicks: Array<JSX.Element> = [];
- for (let tickIndex = startTickIndex; tickIndex <= endTickIndex; tickIndex += 1) {
+ for (
+ let tickIndex = startTickIndex;
+ tickIndex <= endTickIndex;
+ tickIndex += 1
+ ) {
const time = tickIndex * tickIntervalSeconds;
if (time > effectiveDuration) break;
diff --git a/apps/web/src/components/editor/timeline/timeline-tick.tsx b/apps/web/src/components/editor/panels/timeline/timeline-tick.tsx
index 9402d25e..9402d25e 100644
--- a/apps/web/src/components/editor/timeline/timeline-tick.tsx
+++ b/apps/web/src/components/editor/panels/timeline/timeline-tick.tsx
diff --git a/apps/web/src/components/editor/timeline/timeline-toolbar.tsx b/apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx
index 6ac6fba5..0b02c63f 100644
--- a/apps/web/src/components/editor/timeline/timeline-toolbar.tsx
+++ b/apps/web/src/components/editor/panels/timeline/timeline-toolbar.tsx
@@ -6,7 +6,7 @@ import {
TooltipContent,
} from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
-import { SkipBack, SplitSquareHorizontal } from "lucide-react";
+import { SplitSquareHorizontal } from "lucide-react";
import {
SplitButton,
SplitButtonLeft,
@@ -14,11 +14,9 @@ import {
SplitButtonSeparator,
} from "@/components/ui/split-button";
import { Slider } from "@/components/ui/slider";
-import { formatTimeCode } from "@/lib/time";
import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
import { sliderToZoom, zoomToSlider } from "@/lib/timeline/zoom-utils";
-import { EditableTimecode } from "@/components/editable-timecode";
-import { ScenesView } from "../scenes-view";
+import { ScenesView } from "../../scenes-view";
import { type TAction, invokeAction } from "@/lib/actions";
import { cn } from "@/utils/ui";
import { useTimelineStore } from "@/stores/timeline-store";
@@ -32,8 +30,6 @@ import {
Link04Icon,
SearchAddIcon,
SearchMinusIcon,
- PauseIcon,
- PlayIcon,
Copy01Icon,
AlignLeftIcon,
AlignRightIcon,
@@ -82,7 +78,6 @@ export function TimelineToolbar({
function ToolbarLeftSection() {
const editor = useEditor();
const currentTime = editor.playback.getCurrentTime();
- const isPlaying = editor.playback.getIsPlaying();
const currentBookmarked = editor.scenes.isBookmarked({ time: currentTime });
const handleAction = ({
@@ -100,32 +95,6 @@ function ToolbarLeftSection() {
<div className="flex items-center gap-1">
<TooltipProvider delayDuration={500}>
<ToolbarButton
- icon={
- isPlaying ? (
- <HugeiconsIcon icon={PauseIcon} />
- ) : (
- <HugeiconsIcon icon={PlayIcon} />
- )
- }
- tooltip={isPlaying ? "Pause" : "Play"}
- onClick={({ event }) =>
- handleAction({ action: "toggle-play", event })
- }
- />
-
- <ToolbarButton
- icon={<SkipBack />}
- tooltip="Go to start"
- onClick={({ event }) => handleAction({ action: "goto-start", event })}
- />
-
- <div className="bg-border mx-1 h-6 w-px" />
-
- <TimeDisplay />
-
- <div className="bg-border mx-1 h-6 w-px" />
-
- <ToolbarButton
icon={<HugeiconsIcon icon={ScissorIcon} />}
tooltip="Split element"
onClick={({ event }) => handleAction({ action: "split", event })}
@@ -179,12 +148,8 @@ function ToolbarLeftSection() {
<Tooltip>
<ToolbarButton
- icon={
- <HugeiconsIcon
- icon={Bookmark02Icon}
- className={currentBookmarked ? "fill-primary text-primary" : ""}
- />
- }
+ icon={<HugeiconsIcon icon={Bookmark02Icon} />}
+ isActive={currentBookmarked}
tooltip={currentBookmarked ? "Remove bookmark" : "Add bookmark"}
onClick={({ event }) =>
handleAction({ action: "toggle-bookmark", event })
@@ -196,34 +161,6 @@ function ToolbarLeftSection() {
);
}
-function TimeDisplay() {
- const editor = useEditor();
- const currentTime = editor.playback.getCurrentTime();
- const totalDuration = editor.timeline.getTotalDuration();
- const fps = editor.project.getActive().settings.fps;
-
- return (
- <div className="flex flex-row items-center justify-center px-2">
- <EditableTimecode
- time={currentTime}
- duration={totalDuration}
- format="HH:MM:SS:FF"
- fps={fps}
- onTimeChange={({ time }) => editor.playback.seek({ time })}
- className="text-center"
- />
- <div className="text-muted-foreground px-2 font-mono text-xs">/</div>
- <div className="text-muted-foreground text-center font-mono text-xs">
- {formatTimeCode({
- timeInSeconds: totalDuration,
- format: "HH:MM:SS:FF",
- fps,
- })}
- </div>
- </div>
- );
-}
-
function SceneSelector() {
const editor = useEditor();
const currentScene = editor.scenes.getActiveScene();
@@ -265,26 +202,15 @@ function ToolbarRightSection({
<div className="flex items-center gap-1">
<TooltipProvider delayDuration={500}>
<ToolbarButton
- icon={
- <HugeiconsIcon
- icon={MagnetIcon}
- className={cn(snappingEnabled ? "text-primary" : "")}
- />
- }
+ icon={<HugeiconsIcon icon={MagnetIcon} />}
+ isActive={snappingEnabled}
tooltip="Auto snapping"
onClick={() => toggleSnapping()}
/>
<ToolbarButton
- icon={
- <HugeiconsIcon
- icon={Link04Icon}
- className={cn(
- rippleEditingEnabled ? "text-primary" : "",
- "scale-110",
- )}
- />
- }
+ icon={<HugeiconsIcon icon={Link04Icon} className="scale-110" />}
+ isActive={rippleEditingEnabled}
tooltip="Ripple editing"
onClick={() => toggleRippleEditing()}
/>
@@ -329,21 +255,26 @@ function ToolbarButton({
tooltip,
onClick,
disabled,
+ isActive,
}: {
icon: React.ReactNode;
tooltip: string;
onClick: ({ event }: { event: React.MouseEvent }) => void;
disabled?: boolean;
+ isActive?: boolean;
}) {
return (
<Tooltip delayDuration={200}>
<TooltipTrigger asChild>
<Button
- variant="text"
+ variant={isActive ? "secondary" : "text"}
size="icon"
type="button"
onClick={(event) => onClick({ event })}
- className={disabled ? "cursor-not-allowed opacity-50" : ""}
+ className={cn(
+ "rounded-sm",
+ disabled ? "cursor-not-allowed opacity-50" : "",
+ )}
>
{icon}
</Button>
diff --git a/apps/web/src/components/editor/timeline/timeline-track.tsx b/apps/web/src/components/editor/panels/timeline/timeline-track.tsx
index 47a9f282..b7b06d5b 100644
--- a/apps/web/src/components/editor/timeline/timeline-track.tsx
+++ b/apps/web/src/components/editor/panels/timeline/timeline-track.tsx
@@ -9,7 +9,6 @@ import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
import { useEdgeAutoScroll } from "@/hooks/timeline/use-edge-auto-scroll";
import type { ElementDragState } from "@/types/timeline";
import { useEditor } from "@/hooks/use-editor";
-import { cn } from "@/utils/ui";
interface TimelineTrackContentProps {
track: TimelineTrack;
@@ -63,13 +62,9 @@ export function TimelineTrackContent({
contentWidth: duration * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel,
});
- const hasSelectedElements = track.elements.some((element) =>
- isElementSelected({ trackId: track.id, elementId: element.id }),
- );
-
return (
<button
- className={cn("size-full", hasSelectedElements && "bg-panel-accent/35")}
+ className="size-full"
onClick={(event) => {
if (shouldIgnoreClick?.()) return;
clearElementSelection();
diff --git a/apps/web/src/components/theme-toggle.tsx b/apps/web/src/components/theme-toggle.tsx
index 394d75b2..b6e8d274 100644
--- a/apps/web/src/components/theme-toggle.tsx
+++ b/apps/web/src/components/theme-toggle.tsx
@@ -22,8 +22,8 @@ export function ThemeToggle({
return (
<Button
size="icon"
- variant="text"
- className={cn("h-7", className)}
+ variant="ghost"
+ className={cn("size-8", className)}
onClick={(e) => {
setTheme(theme === "dark" ? "light" : "dark");
onToggle?.(e);
diff --git a/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx
index 6c063eff..2fc2c2e6 100644
--- a/apps/web/src/components/ui/button.tsx
+++ b/apps/web/src/components/ui/button.tsx
@@ -10,7 +10,7 @@ const buttonVariants = cva(
variants: {
variant: {
default:
- "bg-primary text-primary-foreground hover:bg-primary-hover",
+ "bg-primary text-primary-foreground hover:bg-primary/90",
background:
"bg-background text-foreground hover:bg-background/90",
foreground:
@@ -22,13 +22,14 @@ const buttonVariants = cva(
outline:
"border border-border bg-transparent hover:bg-accent/50",
secondary:
- "bg-secondary text-secondary-foreground hover:bg-foreground/15 border border-input",
+ "bg-secondary text-secondary-foreground border border-secondary-border",
text: "bg-transparent rounded-none opacity-100 hover:opacity-75",
+ ghost: "bg-transparent hover:bg-accent",
link: "text-primary underline-offset-4 hover:underline !p-0 !h-auto",
},
size: {
default: "h-9.5 px-4 py-2",
- sm: "h-8 px-3 text-xs",
+ sm: "h-8 p-1 px-2 text-xs rounded-sm",
lg: "h-10 p-5 px-6",
icon: "size-7",
text: "p-0",
diff --git a/apps/web/src/components/ui/color-picker.tsx b/apps/web/src/components/ui/color-picker.tsx
index a57a86e0..0ea1a14d 100644
--- a/apps/web/src/components/ui/color-picker.tsx
+++ b/apps/web/src/components/ui/color-picker.tsx
@@ -6,6 +6,7 @@ import { Input } from "./input";
interface ColorPickerProps {
value?: string;
onChange?: (value: string) => void;
+ onChangeEnd?: (value: string) => void;
className?: string;
containerRef?: React.RefObject<HTMLDivElement | null>;
}
@@ -86,7 +87,7 @@ const hsvToHex = (h: number, s: number, v: number) => {
};
const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(
- ({ className, value = "FFFFFF", onChange, containerRef, ...props }, ref) => {
+ ({ className, value = "FFFFFF", onChange, onChangeEnd, containerRef, ...props }, ref) => {
const [isOpen, setIsOpen] = useState(false);
const [isDragging, setIsDragging] = useState<"saturation" | "hue" | null>(
null,
@@ -102,6 +103,7 @@ const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(
const saturationRef = useRef<HTMLButtonElement>(null);
const hueRef = useRef<HTMLButtonElement>(null);
const triggerRef = useRef<HTMLButtonElement>(null);
+ const latestDragColorRef = useRef<string | null>(null);
const [h, s, v] = hexToHsv(value);
const displayHue = s > 0 ? h : internalHue;
@@ -144,6 +146,7 @@ const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(
const newS = x;
const newV = 1 - y;
const newHex = hsvToHex(displayHue, newS, newV);
+ latestDragColorRef.current = newHex;
onChange?.(newHex);
}
@@ -157,12 +160,17 @@ const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(
setInternalHue(newH);
if (s > 0) {
const newHex = hsvToHex(newH, s, v);
+ latestDragColorRef.current = newHex;
onChange?.(newHex);
}
}
};
const handleMouseUp = () => {
+ if (latestDragColorRef.current !== null && onChangeEnd) {
+ onChangeEnd(latestDragColorRef.current);
+ latestDragColorRef.current = null;
+ }
setIsDragging(null);
};
@@ -187,6 +195,7 @@ const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(
const newS = x;
const newV = 1 - y;
const newHex = hsvToHex(displayHue, newS, newV);
+ latestDragColorRef.current = newHex;
onChange?.(newHex);
};
@@ -201,6 +210,7 @@ const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(
setInternalHue(newH);
if (s > 0) {
const newHex = hsvToHex(newH, s, v);
+ latestDragColorRef.current = newHex;
onChange?.(newHex);
}
};
@@ -235,14 +245,14 @@ const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(
<div
ref={ref}
className={cn(
- "bg-panel-accent flex h-9 items-center gap-2 rounded-full px-[0.45rem]",
+ "bg-accent flex h-8 items-center gap-2 rounded-md px-[0.45rem]",
className,
)}
{...props}
>
<button
ref={triggerRef}
- className="size-6 cursor-pointer rounded-full hover:ring-2 hover:ring-white/20"
+ className="size-4.5 cursor-pointer border rounded-sm hover:ring-2 hover:ring-white/20"
style={{ backgroundColor: `#${value}` }}
type="button"
onClick={() => {
@@ -259,7 +269,8 @@ const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(
/>
<div className="flex flex-1 items-center">
<Input
- className="!border-0 bg-transparent p-0 !ring-0 !ring-offset-0"
+ className="!border-0 bg-transparent p-0 !ring-0 !ring-offset-0 uppercase"
+ size="sm"
containerClassName="w-full"
value={inputValue}
onChange={handleInputChange}
diff --git a/apps/web/src/components/ui/command.tsx b/apps/web/src/components/ui/command.tsx
index 0f77a4ce..f5d96125 100644
--- a/apps/web/src/components/ui/command.tsx
+++ b/apps/web/src/components/ui/command.tsx
@@ -114,7 +114,7 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item
ref={ref}
className={cn(
- "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
+ "data-[selected=true]:bg-popover-hover data-[selected=true]:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
className,
)}
{...props}
diff --git a/apps/web/src/components/ui/dropdown-menu.tsx b/apps/web/src/components/ui/dropdown-menu.tsx
index a7736e6e..d8a9f594 100644
--- a/apps/web/src/components/ui/dropdown-menu.tsx
+++ b/apps/web/src/components/ui/dropdown-menu.tsx
@@ -20,7 +20,7 @@ const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const dropdownMenuItemVariants = cva(
- "relative flex cursor-pointer select-none items-center gap-2 rounded-lg px-2.5 py-2 text-sm text-foreground/85 outline-hidden data-[highlighted]:bg-accent/35 data-disabled:pointer-events-none data-disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
+ "relative flex cursor-pointer select-none items-center gap-2 rounded-lg px-2.5 py-2 text-sm text-foreground/85 outline-hidden data-[highlighted]:bg-popover-hover data-disabled:pointer-events-none data-disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
diff --git a/apps/web/src/components/ui/font-picker.tsx b/apps/web/src/components/ui/font-picker.tsx
index 1806b48a..4b8e364a 100644
--- a/apps/web/src/components/ui/font-picker.tsx
+++ b/apps/web/src/components/ui/font-picker.tsx
@@ -6,6 +6,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { FONT_OPTIONS, type FontFamily } from "@/constants/font-constants";
+import { cn } from "@/utils/ui";
interface FontPickerProps {
defaultValue?: FontFamily;
@@ -21,16 +22,15 @@ export function FontPicker({
return (
<Select defaultValue={defaultValue} onValueChange={onValueChange}>
<SelectTrigger
- className={`bg-panel-accent h-9 w-full text-base ${className || ""}`}
+ className={cn("w-full", className)}
>
- <SelectValue placeholder="Select a font" className="text-sm" />
+ <SelectValue placeholder="Select a font" />
</SelectTrigger>
<SelectContent>
{FONT_OPTIONS.map((font) => (
<SelectItem
key={font.value}
value={font.value}
- className="text-sn"
style={{ fontFamily: font.value }}
>
{font.label}
diff --git a/apps/web/src/components/ui/input-with-back.tsx b/apps/web/src/components/ui/input-with-back.tsx
index 392ab3c3..f8265779 100644
--- a/apps/web/src/components/ui/input-with-back.tsx
+++ b/apps/web/src/components/ui/input-with-back.tsx
@@ -55,7 +55,7 @@ export function InputWithBack({
>
<Button
variant="outline"
- className="bg-panel-accent !size-9 rounded-full"
+ className="bg-accent !size-9 rounded-full"
>
<ArrowLeft />
</Button>
@@ -77,7 +77,7 @@ export function InputWithBack({
<Search className="text-muted-foreground absolute top-1/2 left-3 size-4 -translate-y-1/2" />
<Input
placeholder={placeholder}
- className="bg-panel-accent w-full pl-9"
+ className="bg-accent w-full pl-9"
value={value}
onChange={(e) => onChange?.(e.target.value)}
/>
diff --git a/apps/web/src/components/ui/label.tsx b/apps/web/src/components/ui/label.tsx
index 0ea7be03..a9a5166f 100644
--- a/apps/web/src/components/ui/label.tsx
+++ b/apps/web/src/components/ui/label.tsx
@@ -7,7 +7,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/utils/ui";
const labelVariants = cva(
- "text-sm text-muted-foreground font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
+ "text-xs text-muted-foreground font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
);
const Label = React.forwardRef<
diff --git a/apps/web/src/components/ui/popover.tsx b/apps/web/src/components/ui/popover.tsx
index 34738303..36ae5a69 100644
--- a/apps/web/src/components/ui/popover.tsx
+++ b/apps/web/src/components/ui/popover.tsx
@@ -2,7 +2,6 @@
import * as React from "react";
import { Popover as PopoverPrimitive } from "radix-ui";
-
import { cn } from "@/utils/ui";
const Popover = PopoverPrimitive.Root;
diff --git a/apps/web/src/components/ui/select.tsx b/apps/web/src/components/ui/select.tsx
index 79462d4a..fd9c7ac5 100644
--- a/apps/web/src/components/ui/select.tsx
+++ b/apps/web/src/components/ui/select.tsx
@@ -14,7 +14,7 @@ const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value;
const selectItemVariants = cva(
- "relative flex cursor-pointer select-none items-center gap-2 rounded-xl px-2.5 py-2 text-sm text-foreground/85 outline-hidden data-[highlighted]:bg-accent/35 data-disabled:pointer-events-none data-disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
+ "relative flex cursor-pointer select-none items-center gap-2 rounded-xl px-2.5 py-2 text-sm text-foreground/85 outline-hidden data-[highlighted]:bg-popover-hover data-disabled:pointer-events-none data-disabled:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
{
variants: {
variant: {
@@ -35,7 +35,7 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
- "bg-accent ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-7 w-auto cursor-pointer items-center justify-between gap-1 rounded-md px-3 py-2 text-sm whitespace-nowrap focus:ring-1 focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
+ "bg-accent ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-8 w-auto cursor-pointer items-center justify-between gap-1 rounded-md px-3 py-2 text-sm whitespace-nowrap focus:ring-1 focus:outline-hidden disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
"focus:border-primary focus:ring-4 focus:ring-primary/10 border-transparent transition-none",
className,
)}
@@ -159,7 +159,7 @@ const SelectSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
- className={cn("bg-border/60 mx-1 my-2 h-px", className)}
+ className={cn("bg-border mx-1 my-2 h-px", className)}
{...props}
/>
));
diff --git a/apps/web/src/components/ui/slider.tsx b/apps/web/src/components/ui/slider.tsx
index 994159a1..57ddbcf0 100644
--- a/apps/web/src/components/ui/slider.tsx
+++ b/apps/web/src/components/ui/slider.tsx
@@ -17,7 +17,7 @@ const Slider = React.forwardRef<
)}
{...props}
>
- <SliderPrimitive.Track className="bg-primary/20 relative h-1.5 w-full grow overflow-hidden rounded-full">
+ <SliderPrimitive.Track className="bg-accent relative h-1.5 w-full grow overflow-hidden rounded-full">
<SliderPrimitive.Range className="bg-primary absolute h-full" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="border-primary/50 bg-background focus-visible:ring-ring block size-4 rounded-full border shadow-sm focus-visible:ring-1 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50" />
diff --git a/apps/web/src/components/ui/split-button.tsx b/apps/web/src/components/ui/split-button.tsx
index 26e54cab..aa8b331a 100644
--- a/apps/web/src/components/ui/split-button.tsx
+++ b/apps/web/src/components/ui/split-button.tsx
@@ -18,7 +18,7 @@ const SplitButton = forwardRef<HTMLDivElement, SplitButtonProps>(
<div
ref={ref}
className={cn(
- "border-input bg-panel-accent inline-flex h-7 overflow-hidden rounded-lg border",
+ "border-input bg-accent inline-flex h-7 overflow-hidden rounded-lg border",
className,
)}
{...props}
@@ -39,7 +39,7 @@ const SplitButtonSide = forwardRef<
ref={ref}
variant="text"
className={cn(
- "bg-panel-accent disabled:text-muted-foreground h-full gap-0 rounded-none border-0 font-normal !opacity-100",
+ "bg-accent disabled:text-muted-foreground h-full gap-0 rounded-none border-0 font-normal !opacity-100",
onClick
? "hover:bg-foreground/10 cursor-pointer hover:opacity-100"
: "cursor-default select-text",
diff --git a/apps/web/src/components/ui/tabs.tsx b/apps/web/src/components/ui/tabs.tsx
index 3668477c..d2844b63 100644
--- a/apps/web/src/components/ui/tabs.tsx
+++ b/apps/web/src/components/ui/tabs.tsx
@@ -29,7 +29,7 @@ const TabsTrigger = React.forwardRef<
<TabsPrimitive.Trigger
ref={ref}
className={cn(
- "ring-offset-background focus-visible:ring-ring data-[state=active]:bg-panel-accent data-[state=active]:text-foreground inline-flex cursor-pointer items-center justify-center rounded-lg px-3 py-1 text-sm font-medium whitespace-nowrap focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50",
+ "ring-offset-background focus-visible:ring-ring data-[state=active]:bg-accent data-[state=active]:text-foreground inline-flex cursor-pointer items-center justify-center rounded-lg px-3 py-1 text-sm font-medium whitespace-nowrap focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50",
className,
)}
{...props}