formatting

This commit is contained in:
Maze Winther 2026-02-22 17:36:02 +01:00
parent 42f865aa50
commit 9fb8406cdb
27 changed files with 468 additions and 407 deletions

View File

@ -8,115 +8,115 @@
@plugin "tailwindcss-animate";
:root {
--background: hsl(0, 0%, 100%);
--foreground: hsl(0 0% 11%);
--card: hsl(0, 0%, 100%);
--card-foreground: hsl(0 0% 11%);
--popover: hsl(0, 0%, 100%);
--popover-hover: hsl(0, 0%, 96%);
--popover-foreground: hsl(0 0% 2%);
--primary: #009dff;
--primary-foreground: hsl(0, 0%, 100%);
--secondary: hsl(204, 100%, 97%);
--secondary-border: hsl(204, 100%, 94%);
--secondary-foreground: hsl(200, 98%, 39%);
--muted: hsl(0 0% 85.1%);
--muted-foreground: hsl(0 0% 50%);
--accent: hsl(0, 0%, 96%);
--accent-foreground: hsl(0 0% 2%);
--destructive: hsl(0, 83%, 50%);
--destructive-foreground: hsl(0, 0%, 100%);
--constructive: hsl(141, 71%, 48%);
--constructive-foreground: hsl(0, 0%, 100%);
--border: hsl(0 0% 91%);
--input: hsl(0, 0%, 100%);
--ring: hsl(0, 0%, 55%);
--chart-1: hsl(220 70% 50%);
--chart-2: hsl(160 60% 45%);
--chart-3: hsl(30 80% 55%);
--chart-4: hsl(280 65% 60%);
--chart-5: hsl(340 75% 55%);
--sidebar-background: hsl(0 0% 96.1%);
--sidebar-foreground: hsl(0 0% 2%);
--sidebar-primary: hsl(0 0% 2%);
--sidebar-primary-foreground: hsl(0 0% 91%);
--sidebar-accent: hsl(0 0% 85.1%);
--sidebar-accent-foreground: hsl(0 0% 2%);
--sidebar-border: hsl(0 0% 85.1%);
--sidebar-ring: hsl(0 0% 16.9%);
--sidebar: hsl(0 0% 98%);
--background: hsl(0, 0%, 100%);
--foreground: hsl(0 0% 11%);
--card: hsl(0, 0%, 100%);
--card-foreground: hsl(0 0% 11%);
--popover: hsl(0, 0%, 100%);
--popover-hover: hsl(0, 0%, 96%);
--popover-foreground: hsl(0 0% 2%);
--primary: #009dff;
--primary-foreground: hsl(0, 0%, 100%);
--secondary: hsl(204, 100%, 97%);
--secondary-border: hsl(204, 100%, 94%);
--secondary-foreground: hsl(200, 98%, 39%);
--muted: hsl(0 0% 85.1%);
--muted-foreground: hsl(0 0% 50%);
--accent: hsl(0, 0%, 96%);
--accent-foreground: hsl(0 0% 2%);
--destructive: hsl(0, 83%, 50%);
--destructive-foreground: hsl(0, 0%, 100%);
--constructive: hsl(141, 71%, 48%);
--constructive-foreground: hsl(0, 0%, 100%);
--border: hsl(0 0% 91%);
--input: hsl(0, 0%, 100%);
--ring: hsl(0, 0%, 55%);
--chart-1: hsl(220 70% 50%);
--chart-2: hsl(160 60% 45%);
--chart-3: hsl(30 80% 55%);
--chart-4: hsl(280 65% 60%);
--chart-5: hsl(340 75% 55%);
--sidebar-background: hsl(0 0% 96.1%);
--sidebar-foreground: hsl(0 0% 2%);
--sidebar-primary: hsl(0 0% 2%);
--sidebar-primary-foreground: hsl(0 0% 91%);
--sidebar-accent: hsl(0 0% 85.1%);
--sidebar-accent-foreground: hsl(0 0% 2%);
--sidebar-border: hsl(0 0% 85.1%);
--sidebar-ring: hsl(0 0% 16.9%);
--sidebar: hsl(0 0% 98%);
}
.panel {
--background: hsl(216 13% 98%);
--foreground: hsl(0 0% 13%);
--card: hsl(0, 0%, 98%);
--card-foreground: hsl(0 0% 13%);
--primary-foreground: hsl(0, 0%, 98%);
--secondary: hsl(204, 100%, 95%);
--secondary-border: hsl(204, 100%, 92%);
--secondary-foreground: hsl(200, 98%, 37%);
--muted: hsl(0 0% 83.1%);
--muted-foreground: hsl(0 0% 48%);
--accent: hsl(0, 0%, 93%);
--accent-foreground: hsl(0 0% 5%);
--destructive: hsl(0, 83%, 50%);
--destructive-foreground: hsl(0, 0%, 98%);
--constructive: hsl(141, 71%, 48%);
--constructive-foreground: hsl(0, 0%, 98%);
--border: hsl(0 0% 89%);
--input: hsl(0 0% 93%);
--ring: hsl(0, 0%, 53%);
--background: hsl(216 13% 98%);
--foreground: hsl(0 0% 13%);
--card: hsl(0, 0%, 98%);
--card-foreground: hsl(0 0% 13%);
--primary-foreground: hsl(0, 0%, 98%);
--secondary: hsl(204, 100%, 95%);
--secondary-border: hsl(204, 100%, 92%);
--secondary-foreground: hsl(200, 98%, 37%);
--muted: hsl(0 0% 83.1%);
--muted-foreground: hsl(0 0% 48%);
--accent: hsl(0, 0%, 93%);
--accent-foreground: hsl(0 0% 5%);
--destructive: hsl(0, 83%, 50%);
--destructive-foreground: hsl(0, 0%, 98%);
--constructive: hsl(141, 71%, 48%);
--constructive-foreground: hsl(0, 0%, 98%);
--border: hsl(0 0% 89%);
--input: hsl(0 0% 93%);
--ring: hsl(0, 0%, 53%);
}
.dark {
--background: hsl(0, 0%, 5%);
--foreground: hsl(0 0% 87%);
--card: hsl(0, 0%, 5%);
--card-foreground: hsl(0 0% 87%);
--popover: hsl(0, 0%, 5%);
--popover-hover: hsl(0, 0%, 22%);
--popover-foreground: hsl(0 0% 95%);
--secondary: hsl(204, 100%, 12%);
--secondary-border: hsl(204, 100%, 15%);
--secondary-foreground: hsl(200, 98%, 61%);
--muted: hsl(0 0% 20%);
--accent: hsl(0, 0%, 14%);
--accent-foreground: hsl(0 0% 95%);
--constructive: hsl(141, 71%, 52%);
--border: hsl(0 0% 16%);
--input: hsl(0 0% 5%);
--ring: hsl(0, 0%, 50%);
--sidebar-background: hsl(0 0% 8%);
--sidebar-foreground: hsl(0 0% 95%);
--sidebar-primary: hsl(0 0% 95%);
--sidebar-primary-foreground: hsl(0 0% 15%);
--sidebar-accent: hsl(0 0% 20%);
--sidebar-accent-foreground: hsl(0 0% 95%);
--sidebar-border: hsl(0 0% 20%);
--sidebar-ring: hsl(0 0% 83.1%);
--sidebar: hsl(0 0% 6%);
--background: hsl(0, 0%, 5%);
--foreground: hsl(0 0% 87%);
--card: hsl(0, 0%, 5%);
--card-foreground: hsl(0 0% 87%);
--popover: hsl(0, 0%, 5%);
--popover-hover: hsl(0, 0%, 22%);
--popover-foreground: hsl(0 0% 95%);
--secondary: hsl(204, 100%, 12%);
--secondary-border: hsl(204, 100%, 15%);
--secondary-foreground: hsl(200, 98%, 61%);
--muted: hsl(0 0% 20%);
--accent: hsl(0, 0%, 14%);
--accent-foreground: hsl(0 0% 95%);
--constructive: hsl(141, 71%, 52%);
--border: hsl(0 0% 16%);
--input: hsl(0 0% 5%);
--ring: hsl(0, 0%, 50%);
--sidebar-background: hsl(0 0% 8%);
--sidebar-foreground: hsl(0 0% 95%);
--sidebar-primary: hsl(0 0% 95%);
--sidebar-primary-foreground: hsl(0 0% 15%);
--sidebar-accent: hsl(0 0% 20%);
--sidebar-accent-foreground: hsl(0 0% 95%);
--sidebar-border: hsl(0 0% 20%);
--sidebar-ring: hsl(0 0% 83.1%);
--sidebar: hsl(0 0% 6%);
}
.dark .panel {
--background: hsl(0 0% 10%);
--foreground: hsl(0 0% 85%);
--card: hsl(0, 0%, 10%);
--card-foreground: hsl(0 0% 85%);
--secondary: hsl(204, 100%, 12%);
--secondary-border: hsl(204, 100%, 17%);
--secondary-foreground: hsl(200, 98%, 63%);
--muted: hsl(0 0% 22%);
--accent: hsl(0, 0%, 15%);
--accent-foreground: hsl(0 0% 93%);
--constructive: hsl(141, 71%, 52%);
--border: hsl(0 0% 18%);
--input: hsl(0 0% 22%);
--ring: hsl(0, 0%, 52%);
--background: hsl(0 0% 10%);
--foreground: hsl(0 0% 85%);
--card: hsl(0, 0%, 10%);
--card-foreground: hsl(0 0% 85%);
--secondary: hsl(204, 100%, 12%);
--secondary-border: hsl(204, 100%, 17%);
--secondary-foreground: hsl(200, 98%, 63%);
--muted: hsl(0 0% 22%);
--accent: hsl(0, 0%, 15%);
--accent-foreground: hsl(0 0% 93%);
--constructive: hsl(141, 71%, 52%);
--border: hsl(0 0% 18%);
--input: hsl(0 0% 22%);
--ring: hsl(0, 0%, 52%);
}
@layer base {
/*
/*
The default border color has changed to `currentcolor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
@ -124,165 +124,165 @@
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
/* Other default base styles */
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
/* Prevent back/forward swipe */
overscroll-behavior-x: contain;
}
::selection {
@apply bg-primary/35 selection:text-primary-foreground;
}
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
/* Other default base styles */
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
/* Prevent back/forward swipe */
overscroll-behavior-x: contain;
}
::selection {
@apply bg-primary/35 selection:text-primary-foreground;
}
}
@theme inline {
/* Responsive breakpoints */
--breakpoint-xs: 30rem;
/* Responsive breakpoints */
--breakpoint-xs: 30rem;
/* Typography */
--font-sans: var(--font-inter), sans-serif;
/* Typography */
--font-sans: var(--font-inter), sans-serif;
/* Font sizes */
--text-xs: 0.72rem;
--text-sm: 0.79rem;
--text-base: 0.92rem;
--text-base--line-height: calc(1.5 / 0.95);
--text-xs--line-height: calc(1 / 0.8);
/* Font sizes */
--text-xs: 0.72rem;
--text-sm: 0.79rem;
--text-base: 0.92rem;
--text-base--line-height: calc(1.5 / 0.95);
--text-xs--line-height: calc(1 / 0.8);
/* Border radius */
--radius-lg: 0.82rem;
--radius-md: 0.65rem;
--radius-sm: 0.35rem;
/* Border radius */
--radius-lg: 0.82rem;
--radius-md: 0.65rem;
--radius-sm: 0.35rem;
/* Palette mapped to root design tokens */
--color-background: var(--background);
--color-foreground: var(--foreground);
/* Palette mapped to root design tokens */
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-hover: var(--popover-hover);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-popover-hover: var(--popover-hover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-border: var(--secondary-border);
--color-secondary-foreground: var(--secondary-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-border: var(--secondary-border);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-constructive: var(--constructive);
--color-constructive-foreground: var(--constructive-foreground);
--color-constructive: var(--constructive);
--color-constructive-foreground: var(--constructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
/* Chart colors */
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
/* Chart colors */
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
/* Sidebar */
--color-sidebar: var(--sidebar-background);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
/* Sidebar */
--color-sidebar: var(--sidebar-background);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
/* Animations */
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
/* Animations */
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
}
@utility scrollbar-hidden {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
@utility scrollbar-x-hidden {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar:horizontal {
display: none;
}
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar:horizontal {
display: none;
}
}
@utility scrollbar-y-hidden {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar:vertical {
display: none;
}
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar:vertical {
display: none;
}
}
@utility scrollbar-thin {
&::-webkit-scrollbar {
width: 6px;
height: 8px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
&::-webkit-scrollbar-thumb:hover {
background: var(--muted-foreground);
}
&::-webkit-scrollbar {
width: 6px;
height: 8px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 4px;
}
&::-webkit-scrollbar-thumb:hover {
background: var(--muted-foreground);
}
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@ -635,4 +635,3 @@ function SortMenuItem({
</DropdownMenuItem>
);
}

View File

@ -20,10 +20,7 @@ export function PreviewContextMenu({
return (
<ContextMenuContent className="w-56" container={containerRef.current}>
<ContextMenuItem
onClick={onToggleFullscreen}
inset
>
<ContextMenuItem onClick={onToggleFullscreen} inset>
Full screen
</ContextMenuItem>
<ContextMenuCheckboxItem

View File

@ -201,9 +201,9 @@ function PreviewCanvas({
</div>
</ContextMenuTrigger>
<PreviewContextMenu
onToggleFullscreen={onToggleFullscreen}
containerRef={containerRef}
/>
onToggleFullscreen={onToggleFullscreen}
containerRef={containerRef}
/>
</ContextMenu>
</div>
);

View File

@ -42,10 +42,7 @@ export function PreviewInteractionOverlay({
onCancel={cancelTextEdit}
/>
) : (
<TransformHandles
canvasRef={canvasRef}
containerRef={containerRef}
/>
<TransformHandles canvasRef={canvasRef} containerRef={containerRef} />
)}
<SnapGuides
lines={snapLines}

View File

@ -4,10 +4,7 @@ import { useTransformHandles } from "@/hooks/use-transform-handles";
import { useEditor } from "@/hooks/use-editor";
import { isVisualElement } from "@/lib/timeline/element-utils";
import { SnapGuides } from "./snap-guides";
import {
canvasToOverlay,
getDisplayScale,
} from "@/lib/preview/preview-coords";
import { canvasToOverlay, getDisplayScale } from "@/lib/preview/preview-coords";
import type { ElementBounds } from "@/lib/preview/element-bounds";
import { cn } from "@/utils/ui";
import { HugeiconsIcon } from "@hugeicons/react";
@ -19,7 +16,12 @@ const ROTATION_HANDLE_RADIUS = 10;
const CORNER_HIT_AREA_SIZE = 18;
type Corner = "top-left" | "top-right" | "bottom-left" | "bottom-right";
const CORNERS: Corner[] = ["top-left", "top-right", "bottom-left", "bottom-right"];
const CORNERS: Corner[] = [
"top-left",
"top-right",
"bottom-left",
"bottom-right",
];
function getCornerPosition({
bounds,
@ -45,11 +47,10 @@ function getCornerPosition({
};
}
function getRotationHandlePosition({
bounds,
}: {
bounds: ElementBounds;
}): { x: number; y: number } {
function getRotationHandlePosition({ bounds }: { bounds: ElementBounds }): {
x: number;
y: number;
} {
const angleRad = (bounds.rotation * Math.PI) / 180;
const cos = Math.cos(angleRad);
const sin = Math.sin(angleRad);
@ -107,8 +108,20 @@ export function TransformHandles({
const { canvasRect, containerRect } = overlayContext;
const displayScale = getDisplayScale({ canvasRect, canvasSize });
const toOverlay = ({ canvasX, canvasY }: { canvasX: number; canvasY: number }) =>
canvasToOverlay({ canvasX, canvasY, canvasRect, containerRect, canvasSize });
const toOverlay = ({
canvasX,
canvasY,
}: {
canvasX: number;
canvasY: number;
}) =>
canvasToOverlay({
canvasX,
canvasY,
canvasRect,
containerRect,
canvasSize,
});
const center = toOverlay({ canvasX: bounds.cx, canvasY: bounds.cy });
const outlineWidth = bounds.width * displayScale.x;
@ -235,7 +248,6 @@ function CornerHandle({
);
}
function RotationHandle({
screen,
onPointerDown,

View File

@ -395,22 +395,22 @@ function BookmarkPopoverContent({
/>
</div>
</div>
<Button
type="button"
variant="outline"
size="sm"
className="text-destructive hover:bg-destructive/10"
onClick={handleRemove}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
handleRemove();
}
}}
aria-label="delete bookmark"
>
<HugeiconsIcon icon={Delete02Icon} className="!size-3.5" />
Delete
</Button>
<Button
type="button"
variant="outline"
size="sm"
className="text-destructive hover:bg-destructive/10"
onClick={handleRemove}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
handleRemove();
}
}}
aria-label="delete bookmark"
>
<HugeiconsIcon icon={Delete02Icon} className="!size-3.5" />
Delete
</Button>
</>
);
}

View File

@ -1,7 +1,12 @@
import { forwardRef, useEffect, useRef, useState } from "react";
import { cn } from "@/utils/ui";
import { Input } from "./input";
import { Popover, PopoverClose, PopoverContent, PopoverTrigger } from "./popover";
import {
Popover,
PopoverClose,
PopoverContent,
PopoverTrigger,
} from "./popover";
import {
Select,
SelectContent,
@ -64,8 +69,8 @@ const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(
}
};
const hueDiff = Math.abs(h - internalHue);
const isSameHueWrapped = hueDiff < 1 || Math.abs(hueDiff - 360) < 1;
const displayHue = s === 0 || isSameHueWrapped ? internalHue : h;
const isSameHueWrapped = hueDiff < 1 || Math.abs(hueDiff - 360) < 1;
const displayHue = s === 0 || isSameHueWrapped ? internalHue : h;
useEffect(() => {
setInputValue(formatColorValue({ hex: value, format: colorFormat }));
@ -75,31 +80,37 @@ const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging) return;
if (isDragging === "saturation" && saturationRef.current) {
const rect = saturationRef.current.getBoundingClientRect();
const x = Math.max(
0,
Math.min(1, (e.clientX - rect.left) / rect.width),
);
const y = Math.max(
0,
Math.min(1, (e.clientY - rect.top) / rect.height),
);
const newHex = appendAlpha({ rgbHex: hsvToHex({ h: displayHue, s: x, v: 1 - y }), alpha });
latestDragColorRef.current = newHex;
onChange?.(newHex);
}
if (isDragging === "saturation" && saturationRef.current) {
const rect = saturationRef.current.getBoundingClientRect();
const x = Math.max(
0,
Math.min(1, (e.clientX - rect.left) / rect.width),
);
const y = Math.max(
0,
Math.min(1, (e.clientY - rect.top) / rect.height),
);
const newHex = appendAlpha({
rgbHex: hsvToHex({ h: displayHue, s: x, v: 1 - y }),
alpha,
});
latestDragColorRef.current = newHex;
onChange?.(newHex);
}
if (isDragging === "hue" && hueRef.current) {
const rect = hueRef.current.getBoundingClientRect();
const x = Math.max(
0,
Math.min(1, (e.clientX - rect.left) / rect.width),
);
const newH = x * 360;
setInternalHue(newH);
if (isDragging === "hue" && hueRef.current) {
const rect = hueRef.current.getBoundingClientRect();
const x = Math.max(
0,
Math.min(1, (e.clientX - rect.left) / rect.width),
);
const newH = x * 360;
setInternalHue(newH);
if (s > 0) {
const newHex = appendAlpha({ rgbHex: hsvToHex({ h: newH, s, v }), alpha });
const newHex = appendAlpha({
rgbHex: hsvToHex({ h: newH, s, v }),
alpha,
});
latestDragColorRef.current = newHex;
onChange?.(newHex);
}
@ -143,7 +154,10 @@ const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(
const rect = saturationElement.getBoundingClientRect();
const x = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
const y = Math.max(0, Math.min(1, (e.clientY - rect.top) / rect.height));
const newHex = appendAlpha({ rgbHex: hsvToHex({ h: displayHue, s: x, v: 1 - y }), alpha });
const newHex = appendAlpha({
rgbHex: hsvToHex({ h: displayHue, s: x, v: 1 - y }),
alpha,
});
latestDragColorRef.current = newHex;
onChange?.(newHex);
};
@ -158,7 +172,10 @@ const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(
const newH = x * 360;
setInternalHue(newH);
if (s > 0) {
const newHex = appendAlpha({ rgbHex: hsvToHex({ h: newH, s, v }), alpha });
const newHex = appendAlpha({
rgbHex: hsvToHex({ h: newH, s, v }),
alpha,
});
latestDragColorRef.current = newHex;
onChange?.(newHex);
}
@ -322,10 +339,10 @@ const ColorPicker = forwardRef<HTMLDivElement, ColorPickerProps>(
</Button>
)}
<PopoverClose asChild>
<Button variant="ghost" size="icon" type="button">
<HugeiconsIcon icon={Cancel01Icon} />
</Button>
</PopoverClose>
<Button variant="ghost" size="icon" type="button">
<HugeiconsIcon icon={Cancel01Icon} />
</Button>
</PopoverClose>
</div>
</header>
<div className="px-2 flex flex-col gap-3">

View File

@ -12,8 +12,7 @@ const inputVariants = cva(
{
variants: {
variant: {
default:
"selection:bg-primary selection:text-primary-foreground",
default: "selection:bg-primary selection:text-primary-foreground",
destructive:
"selection:bg-destructive selection:text-destructive-foreground focus-visible:border-destructive focus-visible:ring-destructive/10",
},

View File

@ -60,7 +60,7 @@ const TooltipContent = React.forwardRef<
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="absolute top-1/2 left-[-6px] -translate-y-1/2"
aria-hidden="true"
aria-hidden="true"
>
<path
d="M6 0L0 5L6 10V0Z"

View File

@ -9,7 +9,10 @@ import {
import { useEditor } from "@/hooks/use-editor";
import { useShiftKey } from "@/hooks/use-shift-key";
import { useElementSelection } from "@/hooks/timeline/element/use-element-selection";
import { DRAG_THRESHOLD_PX, TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
import {
DRAG_THRESHOLD_PX,
TIMELINE_CONSTANTS,
} from "@/constants/timeline-constants";
import { snapTimeToFrame } from "@/lib/time";
import { computeDropTarget } from "@/lib/timeline/drop-utils";
import { getMouseTimeFromClientX } from "@/lib/timeline/drag-utils";

View File

@ -98,8 +98,7 @@ export function useTimelineElementResize({
const deltaX = clientX - resizing.startX;
let deltaTime =
deltaX /
(TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel);
deltaX / (TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel);
let resizeSnapPoint: SnapPoint | null = null;
const projectFps = activeProject.settings.fps;

View File

@ -1,4 +1,10 @@
import { useState, useCallback, useEffect, useRef, type RefObject } from "react";
import {
useState,
useCallback,
useEffect,
useRef,
type RefObject,
} from "react";
import { useEditor } from "@/hooks/use-editor";
import { useShiftKey } from "@/hooks/use-shift-key";
import { DRAG_THRESHOLD_PX } from "@/constants/timeline-constants";
@ -177,10 +183,7 @@ export function useBookmarkDrag({
zoomLevel,
scrollLeft,
});
const clampedTime = Math.max(
0,
Math.min(mouseTime, duration),
);
const clampedTime = Math.max(0, Math.min(mouseTime, duration));
const frameSnappedTime = snapTimeToFrame({
time: clampedTime,
fps: activeProject.settings.fps,
@ -262,13 +265,7 @@ export function useBookmarkDrag({
}, [isPendingDrag, onSnapPointChange]);
const handleBookmarkMouseDown = useCallback(
({
event,
bookmark,
}: {
event: React.MouseEvent;
bookmark: Bookmark;
}) => {
({ event, bookmark }: { event: React.MouseEvent; bookmark: Bookmark }) => {
if (event.button !== 0) return;
event.preventDefault();

View File

@ -315,8 +315,7 @@ export function useTimelineDragDrop({
zoomLevel,
});
const trackType: TrackType =
asset.type === "audio" ? "audio" : "video";
const trackType: TrackType = asset.type === "audio" ? "audio" : "video";
const addMediaCmd = new AddMediaAssetCommand(projectId, asset);
const assetId = addMediaCmd.getAssetId();

View File

@ -65,52 +65,52 @@ export function useTimelinePlayhead({
Math.min(timelineContentWidth, relativeMouseX),
);
const rawTime = Math.max(
0,
Math.min(
const rawTime = Math.max(
0,
Math.min(
duration,
clampedMouseX / (TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel),
),
);
const framesPerSecond = activeProject.settings.fps;
const frameTime = getSnappedSeekTime({
rawTime,
duration,
clampedMouseX / (TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel),
),
);
fps: framesPerSecond,
});
const framesPerSecond = activeProject.settings.fps;
const frameTime = getSnappedSeekTime({
rawTime,
const bookmarks = editor.scenes.getActiveScene()?.bookmarks ?? [];
const bookmarkSnapPoints: SnapPoint[] = bookmarks.map((bookmark) => ({
time: bookmark.time,
type: "bookmark",
}));
const shouldSnapToBookmark =
!isShiftHeldRef.current && bookmarkSnapPoints.length > 0;
const snapResult = shouldSnapToBookmark
? snapToNearestPoint({
targetTime: frameTime,
snapPoints: bookmarkSnapPoints,
zoomLevel,
})
: null;
const time = snapResult?.snapPoint ? snapResult.snappedTime : frameTime;
setScrubTime(time);
seek({ time });
lastMouseXRef.current = event.clientX;
},
[
duration,
fps: framesPerSecond,
});
const bookmarks = editor.scenes.getActiveScene()?.bookmarks ?? [];
const bookmarkSnapPoints: SnapPoint[] = bookmarks.map((bookmark) => ({
time: bookmark.time,
type: "bookmark",
}));
const shouldSnapToBookmark =
!isShiftHeldRef.current && bookmarkSnapPoints.length > 0;
const snapResult = shouldSnapToBookmark
? snapToNearestPoint({
targetTime: frameTime,
snapPoints: bookmarkSnapPoints,
zoomLevel,
})
: null;
const time = snapResult?.snapPoint ? snapResult.snappedTime : frameTime;
setScrubTime(time);
seek({ time });
lastMouseXRef.current = event.clientX;
},
[
duration,
zoomLevel,
seek,
rulerRef,
activeProject.settings.fps,
isShiftHeldRef,
editor.scenes,
snapToNearestPoint,
],
zoomLevel,
seek,
rulerRef,
activeProject.settings.fps,
isShiftHeldRef,
editor.scenes,
snapToNearestPoint,
],
);
const handlePlayheadMouseDown = useCallback(

View File

@ -82,7 +82,8 @@ export function useTimelineSnapping({
for (const bookmark of bookmarks) {
if (
excludeBookmarkTime != null &&
Math.abs(bookmark.time - excludeBookmarkTime) < BOOKMARK_TIME_EPSILON
Math.abs(bookmark.time - excludeBookmarkTime) <
BOOKMARK_TIME_EPSILON
) {
continue;
}

View File

@ -91,7 +91,7 @@ export function usePasteMedia() {
console.error("Failed to paste media:", error);
toast.error("Failed to paste media");
}
}
};
window.addEventListener("paste", handlePaste);
return () => window.removeEventListener("paste", handlePaste);

View File

@ -65,7 +65,13 @@ function getCornerDistance({
bounds,
corner,
}: {
bounds: { cx: number; cy: number; width: number; height: number; rotation: number };
bounds: {
cx: number;
cy: number;
width: number;
height: number;
rotation: number;
};
corner: Corner;
}): number {
const halfW = bounds.width / 2;
@ -124,8 +130,7 @@ export function useTransformHandles({
: null;
const hasVisualSelection =
selectedWithBounds !== null &&
isVisualElement(selectedWithBounds.element);
selectedWithBounds !== null && isVisualElement(selectedWithBounds.element);
const handleCornerPointerDown = useCallback(
({ event, corner }: { event: React.PointerEvent; corner: Corner }) => {
@ -135,11 +140,11 @@ export function useTransformHandles({
const { bounds, trackId, elementId, element } = selectedWithBounds;
if (!isVisualElement(element)) return;
const initialDistance = getCornerDistance({ bounds, corner });
const baseWidth = bounds.width / element.transform.scale;
const baseHeight = bounds.height / element.transform.scale;
const initialDistance = getCornerDistance({ bounds, corner });
const baseWidth = bounds.width / element.transform.scale;
const baseHeight = bounds.height / element.transform.scale;
scaleStateRef.current = {
scaleStateRef.current = {
trackId,
elementId,
initialTransform: element.transform,
@ -197,7 +202,11 @@ export function useTransformHandles({
canvas: canvasRef.current,
});
if (scaleStateRef.current && activeHandle && activeHandle !== "rotation") {
if (
scaleStateRef.current &&
activeHandle &&
activeHandle !== "rotation"
) {
const {
trackId,
elementId,

View File

@ -2,10 +2,7 @@ import { Command } from "@/lib/commands/base-command";
import { EditorCore } from "@/core";
import type { Bookmark, TScene } from "@/types/timeline";
import { updateSceneInArray } from "@/lib/scenes";
import {
getFrameTime,
updateBookmarkInArray,
} from "@/lib/timeline/bookmarks";
import { getFrameTime, updateBookmarkInArray } from "@/lib/timeline/bookmarks";
export class UpdateBookmarkCommand extends Command {
private savedScenes: TScene[] | null = null;

View File

@ -86,7 +86,5 @@ export async function loadFonts({
families: string[];
}): Promise<void> {
const googleFonts = families.filter((family) => !SYSTEM_FONTS.has(family));
await Promise.all(
googleFonts.map((family) => loadFullFont({ family })),
);
await Promise.all(googleFonts.map((family) => loadFullFont({ family })));
}

View File

@ -32,7 +32,11 @@ function getVisualElementBounds({
canvasHeight: number;
sourceWidth: number;
sourceHeight: number;
transform: { scale: number; position: { x: number; y: number }; rotate: number };
transform: {
scale: number;
position: { x: number; y: number };
rotate: number;
};
}): ElementBounds {
const containScale = Math.min(
canvasWidth / sourceWidth,

View File

@ -282,7 +282,9 @@ export function snapRotation({
const nearestRotationSnap =
Math.round(proposedRotation / ROTATION_SNAP_STEP_DEGREES) *
ROTATION_SNAP_STEP_DEGREES;
const distanceToNearestSnap = Math.abs(proposedRotation - nearestRotationSnap);
const distanceToNearestSnap = Math.abs(
proposedRotation - nearestRotationSnap,
);
if (distanceToNearestSnap <= ROTATION_SNAP_THRESHOLD_DEGREES) {
return { snappedRotation: nearestRotationSnap, isSnapped: true };
}

View File

@ -141,6 +141,9 @@ export function getBookmarksActiveAtTime({
bookmark.duration != null && bookmark.duration > 0
? start + bookmark.duration
: start;
return time >= start - BOOKMARK_TIME_EPSILON && time <= end + BOOKMARK_TIME_EPSILON;
return (
time >= start - BOOKMARK_TIME_EPSILON &&
time <= end + BOOKMARK_TIME_EPSILON
);
});
}

View File

@ -5,20 +5,26 @@ import { v5Project } from "./fixtures";
describe("V5 to V6 Migration", () => {
test("converts number bookmarks to Bookmark objects", async () => {
const result = transformProjectV5ToV6({
project: v5Project as Parameters<typeof transformProjectV5ToV6>[0]["project"],
project: v5Project as Parameters<
typeof transformProjectV5ToV6
>[0]["project"],
});
expect(result.skipped).toBe(false);
expect(result.project.version).toBe(6);
const mainScene = (result.project.scenes as Array<{ bookmarks: unknown[] }>)[0];
const mainScene = (
result.project.scenes as Array<{ bookmarks: unknown[] }>
)[0];
expect(mainScene.bookmarks).toEqual([
{ time: 2.0 },
{ time: 5.5 },
{ time: 12.0 },
]);
const introScene = (result.project.scenes as Array<{ bookmarks: unknown[] }>)[1];
const introScene = (
result.project.scenes as Array<{ bookmarks: unknown[] }>
)[1];
expect(introScene.bookmarks).toEqual([]);
});
@ -29,7 +35,7 @@ describe("V5 to V6 Migration", () => {
version: 6,
scenes: [
{
...((v5Project as { scenes: unknown[] }).scenes[0]),
...(v5Project as { scenes: unknown[] }).scenes[0],
bookmarks: [{ time: 2 }, { time: 5 }],
},
],
@ -58,7 +64,8 @@ describe("V5 to V6 Migration", () => {
version: 5,
scenes: [
{
...(v5Project as { scenes: Array<Record<string, unknown>> }).scenes[0],
...(v5Project as { scenes: Array<Record<string, unknown>> })
.scenes[0],
bookmarks: [
{ time: 1, note: "Intro", color: "#ef4444" },
{ time: 5.5, duration: 2 },
@ -68,14 +75,15 @@ describe("V5 to V6 Migration", () => {
};
const result = transformProjectV5ToV6({
project:
projectWithRichBookmarks as Parameters<
typeof transformProjectV5ToV6
>[0]["project"],
project: projectWithRichBookmarks as Parameters<
typeof transformProjectV5ToV6
>[0]["project"],
});
expect(result.skipped).toBe(false);
const mainScene = (result.project.scenes as Array<{ bookmarks: unknown[] }>)[0];
const mainScene = (
result.project.scenes as Array<{ bookmarks: unknown[] }>
)[0];
expect(mainScene.bookmarks).toEqual([
{ time: 1, note: "Intro", color: "#ef4444" },
{ time: 5.5, duration: 2 },

View File

@ -22,7 +22,11 @@ function normalizeBookmarks({ raw }: { raw: unknown }): Bookmark[] {
.map((item): Bookmark | null => {
if (typeof item === "number") return { time: item };
const obj = item as Record<string, unknown>;
if (typeof obj !== "object" || obj === null || typeof obj.time !== "number") {
if (
typeof obj !== "object" ||
obj === null ||
typeof obj.time !== "number"
) {
return null;
}
return {

View File

@ -9,5 +9,5 @@ interface EyeDropper {
declare const EyeDropper:
| {
new (): EyeDropper;
}
}
| undefined;

View File

@ -1,10 +1,4 @@
import {
converter,
formatHex,
formatHex8,
parse,
type Rgb,
} from "culori";
import { converter, formatHex, formatHex8, parse, type Rgb } from "culori";
export type ColorFormat = "hex" | "rgb" | "hsl" | "hsv";
@ -22,7 +16,11 @@ export function hsvToHex({
h,
s,
v,
}: { h: number; s: number; v: number }): string {
}: {
h: number;
s: number;
v: number;
}): string {
const hex = formatHex({ mode: "hsv", h, s, v });
return hex.slice(1);
}
@ -44,7 +42,10 @@ export function parseHexAlpha({ hex }: { hex: string }): {
export function appendAlpha({
rgbHex,
alpha,
}: { rgbHex: string; alpha: number }): string {
}: {
rgbHex: string;
alpha: number;
}): string {
if (alpha >= 1) return rgbHex;
const hex8 = formatHex8({ mode: "rgb", r: 0, g: 0, b: 0, alpha });
const alphaHex = hex8.slice(7, 9);
@ -78,16 +79,16 @@ function colorToHexWithAlpha({ color }: { color: Rgb }): string {
export function extractColorFromText({
text,
}: { text: string }): string | null {
}: {
text: string;
}): string | null {
const cleaned = stripCssNoise({ text });
const color = toRgb(cleaned);
if (color) return colorToHexWithAlpha({ color });
// bare hex without # (culori needs the prefix)
const bareHexMatch = cleaned.match(
/^([0-9a-fA-F]{3,8})$/,
);
const bareHexMatch = cleaned.match(/^([0-9a-fA-F]{3,8})$/);
if (bareHexMatch) {
const withHash = toRgb(`#${bareHexMatch[1]}`);
if (withHash) return colorToHexWithAlpha({ color: withHash });
@ -147,19 +148,34 @@ export function parseColorInput({
case "rgb": {
const parts = input.split(",").map((part) => parseInt(part.trim(), 10));
if (parts.length < 3 || parts.some(Number.isNaN)) return null;
const color = { mode: "rgb" as const, r: parts[0] / 255, g: parts[1] / 255, b: parts[2] / 255 };
const color = {
mode: "rgb" as const,
r: parts[0] / 255,
g: parts[1] / 255,
b: parts[2] / 255,
};
return formatHex(color).slice(1);
}
case "hsl": {
const parts = input.split(",").map((part) => parseFloat(part.trim()));
if (parts.length < 3 || parts.some(Number.isNaN)) return null;
const color = { mode: "hsl" as const, h: parts[0], s: parts[1] / 100, l: parts[2] / 100 };
const color = {
mode: "hsl" as const,
h: parts[0],
s: parts[1] / 100,
l: parts[2] / 100,
};
return formatHex(color).slice(1);
}
case "hsv": {
const parts = input.split(",").map((part) => parseFloat(part.trim()));
if (parts.length < 3 || parts.some(Number.isNaN)) return null;
const color = { mode: "hsv" as const, h: parts[0], s: parts[1] / 100, v: parts[2] / 100 };
const color = {
mode: "hsv" as const,
h: parts[0],
s: parts[1] / 100,
v: parts[2] / 100,
};
return formatHex(color).slice(1);
}
}