formatting
This commit is contained in:
parent
42f865aa50
commit
9fb8406cdb
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -635,4 +635,3 @@ function SortMenuItem({
|
|||
</DropdownMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -201,9 +201,9 @@ function PreviewCanvas({
|
|||
</div>
|
||||
</ContextMenuTrigger>
|
||||
<PreviewContextMenu
|
||||
onToggleFullscreen={onToggleFullscreen}
|
||||
containerRef={containerRef}
|
||||
/>
|
||||
onToggleFullscreen={onToggleFullscreen}
|
||||
containerRef={containerRef}
|
||||
/>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -42,10 +42,7 @@ export function PreviewInteractionOverlay({
|
|||
onCancel={cancelTextEdit}
|
||||
/>
|
||||
) : (
|
||||
<TransformHandles
|
||||
canvasRef={canvasRef}
|
||||
containerRef={containerRef}
|
||||
/>
|
||||
<TransformHandles canvasRef={canvasRef} containerRef={containerRef} />
|
||||
)}
|
||||
<SnapGuides
|
||||
lines={snapLines}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 })));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -9,5 +9,5 @@ interface EyeDropper {
|
|||
declare const EyeDropper:
|
||||
| {
|
||||
new (): EyeDropper;
|
||||
}
|
||||
}
|
||||
| undefined;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue