feat: major editor overhaul (assets, properties, timeline, fonts) (#709)

* feat: major editor overhaul (assets, properties, timeline, fonts)

Refactor editor core systems to standardize UI architecture and improve performance.

Assets & Properties:
- Replace monolithic property items with composable `Section` architecture.
- Add specialized sections for Transform, Blending, and Text.
- Implement `NumberField` with scrubbing and math evaluation.
- Add new ColorPicker with EyeDropper and multiple format support.
- Standardize asset panels using new `PanelView` layout.

Fonts & Stickers:
- Implement custom font atlas/sprite system for high-performance previews.
- Add virtualized FontPicker with search and favorites.
- Refactor stickers to use a provider-based architecture (icons, emoji, flags, shapes).
- Standardize sticker IDs to `provider:value` format.

Timeline & Interaction:
- Convert bookmarks to rich objects with notes, colors, and duration.
- Refactor drag-and-drop to use Command pattern (enabling proper undo/redo).
- Add Shift modifier to disable snapping during moves/resizes.
- Add new overlays for layout guides and text editing.

Renderer:
- Add support for multi-line text, custom line-height, and letter-spacing.
- Implement global composite operation (blend modes).
- Update sticker node to resolve dynamic provider IDs.

Infrastructure:
- Add storage migrations (v3->v6) for text weights, sticker IDs, and bookmarks.
- Update global styles and core UI components (Button, Input, Popover).

* add ts-nocheck directive to settings-legacy.tsx to suppress TypeScript errors

* fix: correct global composite operation assignment in TextNode to ensure proper blend mode handling

* deleted shadcn components with errors

* formatting

* fix linter issues

* migrate from next middleware to proxy

* add missing component back

* add breadcrumb back

* chore: add @radix-ui/react-primitive deps

* chore: more deps

* chore: add missing env vars to bun-ci

* next env
This commit is contained in:
Maze
2026-02-23 03:24:02 +01:00
committed by GitHub
parent fca99d6126
commit 93d1e3383c
215 changed files with 26980 additions and 8364 deletions
+280 -280
View File
@@ -1,280 +1,280 @@
import { webEnv } from "@opencut/env/web";
import { type NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { checkRateLimit } from "@/lib/rate-limit";
const searchParamsSchema = z.object({
q: z.string().max(500, "Query too long").optional(),
type: z.enum(["songs", "effects"]).optional(),
page: z.coerce.number().int().min(1).max(1000).default(1),
page_size: z.coerce.number().int().min(1).max(150).default(20),
sort: z
.enum(["downloads", "rating", "created", "score"])
.default("downloads"),
min_rating: z.coerce.number().min(0).max(5).default(3),
commercial_only: z.coerce.boolean().default(true),
});
const freesoundResultSchema = z.object({
id: z.number(),
name: z.string(),
description: z.string(),
url: z.string().url(),
previews: z
.object({
"preview-hq-mp3": z.string().url(),
"preview-lq-mp3": z.string().url(),
"preview-hq-ogg": z.string().url(),
"preview-lq-ogg": z.string().url(),
})
.optional(),
download: z.string().url().optional(),
duration: z.number(),
filesize: z.number(),
type: z.string(),
channels: z.number(),
bitrate: z.number(),
bitdepth: z.number(),
samplerate: z.number(),
username: z.string(),
tags: z.array(z.string()),
license: z.string(),
created: z.string(),
num_downloads: z.number().optional(),
avg_rating: z.number().optional(),
num_ratings: z.number().optional(),
});
const freesoundResponseSchema = z.object({
count: z.number(),
next: z.string().url().nullable(),
previous: z.string().url().nullable(),
results: z.array(freesoundResultSchema),
});
const transformedResultSchema = z.object({
id: z.number(),
name: z.string(),
description: z.string(),
url: z.string(),
previewUrl: z.string().optional(),
downloadUrl: z.string().optional(),
duration: z.number(),
filesize: z.number(),
type: z.string(),
channels: z.number(),
bitrate: z.number(),
bitdepth: z.number(),
samplerate: z.number(),
username: z.string(),
tags: z.array(z.string()),
license: z.string(),
created: z.string(),
downloads: z.number().optional(),
rating: z.number().optional(),
ratingCount: z.number().optional(),
});
const apiResponseSchema = z.object({
count: z.number(),
next: z.string().nullable(),
previous: z.string().nullable(),
results: z.array(transformedResultSchema),
query: z.string().optional(),
type: z.string(),
page: z.number(),
pageSize: z.number(),
sort: z.string(),
minRating: z.number().optional(),
});
function buildSortParameter({ query, sort }: { query?: string; sort: string }) {
if (!query) return `${sort}_desc`;
return sort === "score" ? "score" : `${sort}_desc`;
}
function applyEffectsFilters({
params,
min_rating,
commercial_only,
}: {
params: URLSearchParams;
min_rating: number;
commercial_only: boolean;
}) {
params.append("filter", "duration:[* TO 30.0]");
params.append("filter", `avg_rating:[${min_rating} TO *]`);
if (commercial_only) {
params.append(
"filter",
'license:("Attribution" OR "Creative Commons 0" OR "Attribution Noncommercial" OR "Attribution Commercial")',
);
}
params.append(
"filter",
"tag:sound-effect OR tag:sfx OR tag:foley OR tag:ambient OR tag:nature OR tag:mechanical OR tag:electronic OR tag:impact OR tag:whoosh OR tag:explosion",
);
}
function transformFreesoundResult(
result: z.infer<typeof freesoundResultSchema>,
) {
return {
id: result.id,
name: result.name,
description: result.description,
url: result.url,
previewUrl:
result.previews?.["preview-hq-mp3"] ||
result.previews?.["preview-lq-mp3"],
downloadUrl: result.download,
duration: result.duration,
filesize: result.filesize,
type: result.type,
channels: result.channels,
bitrate: result.bitrate,
bitdepth: result.bitdepth,
samplerate: result.samplerate,
username: result.username,
tags: result.tags,
license: result.license,
created: result.created,
downloads: result.num_downloads || 0,
rating: result.avg_rating || 0,
ratingCount: result.num_ratings || 0,
};
}
export async function GET(request: NextRequest) {
try {
const { limited } = await checkRateLimit({ request });
if (limited) {
return NextResponse.json({ error: "Too many requests" }, { status: 429 });
}
const { searchParams } = new URL(request.url);
const validationResult = searchParamsSchema.safeParse({
q: searchParams.get("q") || undefined,
type: searchParams.get("type") || undefined,
page: searchParams.get("page") || undefined,
page_size: searchParams.get("page_size") || undefined,
sort: searchParams.get("sort") || undefined,
min_rating: searchParams.get("min_rating") || undefined,
});
if (!validationResult.success) {
return NextResponse.json(
{
error: "Invalid parameters",
details: validationResult.error.flatten().fieldErrors,
},
{ status: 400 },
);
}
const {
q: query,
type,
page,
page_size: pageSize,
sort,
min_rating,
commercial_only,
} = validationResult.data;
if (type === "songs") {
return NextResponse.json(
{
error: "Songs are not available yet",
message:
"Song search functionality is coming soon. Try searching for sound effects instead.",
},
{ status: 501 },
);
}
const baseUrl = "https://freesound.org/apiv2/search/text/";
const sortParam = buildSortParameter({ query, sort });
const params = new URLSearchParams({
query: query || "",
token: webEnv.FREESOUND_API_KEY,
page: page.toString(),
page_size: pageSize.toString(),
sort: sortParam,
fields:
"id,name,description,url,previews,download,duration,filesize,type,channels,bitrate,bitdepth,samplerate,username,tags,license,created,num_downloads,avg_rating,num_ratings",
});
const isEffectsSearch = type === "effects" || !type;
if (isEffectsSearch) {
applyEffectsFilters({ params, min_rating, commercial_only });
}
const response = await fetch(`${baseUrl}?${params.toString()}`);
if (!response.ok) {
const errorText = await response.text();
console.error("Freesound API error:", response.status, errorText);
return NextResponse.json(
{ error: "Failed to search sounds" },
{ status: response.status },
);
}
const rawData = await response.json();
const freesoundValidation = freesoundResponseSchema.safeParse(rawData);
if (!freesoundValidation.success) {
console.error(
"Invalid Freesound API response:",
freesoundValidation.error,
);
return NextResponse.json(
{ error: "Invalid response from Freesound API" },
{ status: 502 },
);
}
const data = freesoundValidation.data;
const transformedResults = data.results.map(transformFreesoundResult);
const responseData = {
count: data.count,
next: data.next,
previous: data.previous,
results: transformedResults,
query: query || "",
type: type || "effects",
page,
pageSize,
sort,
minRating: min_rating,
};
const responseValidation = apiResponseSchema.safeParse(responseData);
if (!responseValidation.success) {
console.error(
"Invalid API response structure:",
responseValidation.error,
);
return NextResponse.json(
{ error: "Internal response formatting error" },
{ status: 500 },
);
}
return NextResponse.json(responseValidation.data);
} catch (error) {
console.error("Error searching sounds:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}
import { webEnv } from "@opencut/env/web";
import { type NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { checkRateLimit } from "@/lib/rate-limit";
const searchParamsSchema = z.object({
q: z.string().max(500, "Query too long").optional(),
type: z.enum(["songs", "effects"]).optional(),
page: z.coerce.number().int().min(1).max(1000).default(1),
page_size: z.coerce.number().int().min(1).max(150).default(20),
sort: z
.enum(["downloads", "rating", "created", "score"])
.default("downloads"),
min_rating: z.coerce.number().min(0).max(5).default(3),
commercial_only: z.coerce.boolean().default(true),
});
const freesoundResultSchema = z.object({
id: z.number(),
name: z.string(),
description: z.string(),
url: z.string().url(),
previews: z
.object({
"preview-hq-mp3": z.string().url(),
"preview-lq-mp3": z.string().url(),
"preview-hq-ogg": z.string().url(),
"preview-lq-ogg": z.string().url(),
})
.optional(),
download: z.string().url().optional(),
duration: z.number(),
filesize: z.number(),
type: z.string(),
channels: z.number(),
bitrate: z.number(),
bitdepth: z.number(),
samplerate: z.number(),
username: z.string(),
tags: z.array(z.string()),
license: z.string(),
created: z.string(),
num_downloads: z.number().optional(),
avg_rating: z.number().optional(),
num_ratings: z.number().optional(),
});
const freesoundResponseSchema = z.object({
count: z.number(),
next: z.string().url().nullable(),
previous: z.string().url().nullable(),
results: z.array(freesoundResultSchema),
});
const transformedResultSchema = z.object({
id: z.number(),
name: z.string(),
description: z.string(),
url: z.string(),
previewUrl: z.string().optional(),
downloadUrl: z.string().optional(),
duration: z.number(),
filesize: z.number(),
type: z.string(),
channels: z.number(),
bitrate: z.number(),
bitdepth: z.number(),
samplerate: z.number(),
username: z.string(),
tags: z.array(z.string()),
license: z.string(),
created: z.string(),
downloads: z.number().optional(),
rating: z.number().optional(),
ratingCount: z.number().optional(),
});
const apiResponseSchema = z.object({
count: z.number(),
next: z.string().nullable(),
previous: z.string().nullable(),
results: z.array(transformedResultSchema),
query: z.string().optional(),
type: z.string(),
page: z.number(),
pageSize: z.number(),
sort: z.string(),
minRating: z.number().optional(),
});
function buildSortParameter({ query, sort }: { query?: string; sort: string }) {
if (!query) return `${sort}_desc`;
return sort === "score" ? "score" : `${sort}_desc`;
}
function applyEffectsFilters({
params,
min_rating,
commercial_only,
}: {
params: URLSearchParams;
min_rating: number;
commercial_only: boolean;
}) {
params.append("filter", "duration:[* TO 30.0]");
params.append("filter", `avg_rating:[${min_rating} TO *]`);
if (commercial_only) {
params.append(
"filter",
'license:("Attribution" OR "Creative Commons 0" OR "Attribution Noncommercial" OR "Attribution Commercial")',
);
}
params.append(
"filter",
"tag:sound-effect OR tag:sfx OR tag:foley OR tag:ambient OR tag:nature OR tag:mechanical OR tag:electronic OR tag:impact OR tag:whoosh OR tag:explosion",
);
}
function transformFreesoundResult(
result: z.infer<typeof freesoundResultSchema>,
) {
return {
id: result.id,
name: result.name,
description: result.description,
url: result.url,
previewUrl:
result.previews?.["preview-hq-mp3"] ||
result.previews?.["preview-lq-mp3"],
downloadUrl: result.download,
duration: result.duration,
filesize: result.filesize,
type: result.type,
channels: result.channels,
bitrate: result.bitrate,
bitdepth: result.bitdepth,
samplerate: result.samplerate,
username: result.username,
tags: result.tags,
license: result.license,
created: result.created,
downloads: result.num_downloads || 0,
rating: result.avg_rating || 0,
ratingCount: result.num_ratings || 0,
};
}
export async function GET(request: NextRequest) {
try {
const { limited } = await checkRateLimit({ request });
if (limited) {
return NextResponse.json({ error: "Too many requests" }, { status: 429 });
}
const { searchParams } = new URL(request.url);
const validationResult = searchParamsSchema.safeParse({
q: searchParams.get("q") || undefined,
type: searchParams.get("type") || undefined,
page: searchParams.get("page") || undefined,
page_size: searchParams.get("page_size") || undefined,
sort: searchParams.get("sort") || undefined,
min_rating: searchParams.get("min_rating") || undefined,
});
if (!validationResult.success) {
return NextResponse.json(
{
error: "Invalid parameters",
details: validationResult.error.flatten().fieldErrors,
},
{ status: 400 },
);
}
const {
q: query,
type,
page,
page_size: pageSize,
sort,
min_rating,
commercial_only,
} = validationResult.data;
if (type === "songs") {
return NextResponse.json(
{
error: "Songs are not available yet",
message:
"Song search functionality is coming soon. Try searching for sound effects instead.",
},
{ status: 501 },
);
}
const baseUrl = "https://freesound.org/apiv2/search/text/";
const sortParam = buildSortParameter({ query, sort });
const params = new URLSearchParams({
query: query || "",
token: webEnv.FREESOUND_API_KEY,
page: page.toString(),
page_size: pageSize.toString(),
sort: sortParam,
fields:
"id,name,description,url,previews,download,duration,filesize,type,channels,bitrate,bitdepth,samplerate,username,tags,license,created,num_downloads,avg_rating,num_ratings",
});
const isEffectsSearch = type === "effects" || !type;
if (isEffectsSearch) {
applyEffectsFilters({ params, min_rating, commercial_only });
}
const response = await fetch(`${baseUrl}?${params.toString()}`);
if (!response.ok) {
const errorText = await response.text();
console.error("Freesound API error:", response.status, errorText);
return NextResponse.json(
{ error: "Failed to search sounds" },
{ status: response.status },
);
}
const rawData = await response.json();
const freesoundValidation = freesoundResponseSchema.safeParse(rawData);
if (!freesoundValidation.success) {
console.error(
"Invalid Freesound API response:",
freesoundValidation.error,
);
return NextResponse.json(
{ error: "Invalid response from Freesound API" },
{ status: 502 },
);
}
const data = freesoundValidation.data;
const transformedResults = data.results.map(transformFreesoundResult);
const responseData = {
count: data.count,
next: data.next,
previous: data.previous,
results: transformedResults,
query: query || "",
type: type || "effects",
page,
pageSize,
sort,
minRating: min_rating,
};
const responseValidation = apiResponseSchema.safeParse(responseData);
if (!responseValidation.success) {
console.error(
"Invalid API response structure:",
responseValidation.error,
);
return NextResponse.json(
{ error: "Internal response formatting error" },
{ status: 500 },
);
}
return NextResponse.json(responseValidation.data);
} catch (error) {
console.error("Error searching sounds:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}
+3 -1
View File
@@ -120,7 +120,9 @@ function PostMeta({ date, publishedAt }: { date: string; publishedAt: Date }) {
function PostTitle({ title }: { title: string }) {
return (
<h1 className="text-5xl font-bold tracking-tight md:text-4xl text-center">{title}</h1>
<h1 className="text-5xl font-bold tracking-tight md:text-4xl text-center">
{title}
</h1>
);
}
@@ -15,6 +15,7 @@ import { EditorProvider } from "@/components/providers/editor-provider";
import { Onboarding } from "@/components/editor/onboarding";
import { MigrationDialog } from "@/components/editor/dialogs/migration-dialog";
import { usePanelStore } from "@/stores/panel-store";
import { usePasteMedia } from "@/hooks/use-paste-media";
export default function Editor() {
const params = useParams();
@@ -35,6 +36,7 @@ export default function Editor() {
}
function EditorLayout() {
usePasteMedia();
const { panels, setPanel } = usePanelStore();
return (
+223 -224
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% 85.1%);
--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% 83.1%);
--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%, 7%);
--foreground: hsl(0 0% 87%);
--card: hsl(0, 0%, 7%);
--card-foreground: hsl(0 0% 87%);
--popover: hsl(0, 0%, 16%);
--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% 20%);
--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,166 +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-xl: 1.2rem;
--text-base: 0.92rem;
--text-base--line-height: calc(1.5 / 0.95);
--text-xs: 0.75rem;
--text-sm: 0.85rem;
--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;
}
}
-2
View File
@@ -245,7 +245,6 @@ function ProjectsToolbar({ projectIds }: { projectIds: string[] }) {
</Button>
</SortDropdown>
<Button
type="button"
variant="text"
className="text-muted-foreground"
onClick={() =>
@@ -768,7 +767,6 @@ function ProjectMenu({
<DropdownMenu open={isOpen} onOpenChange={onOpenChange}>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="background"
className={
isGrid