From ed02ba872c02eafed45f406c68d389d0a1112a4e Mon Sep 17 00:00:00 2001 From: cashmere Date: Thu, 30 Jul 2026 18:17:45 +0200 Subject: feat: Electron desktop app, native ffmpeg export, fully static web - apps/desktop: Electron shell serving the static web build over a privileged opencut:// protocol; exports stream frames over IPC to a bundled ffmpeg (libx264/libvpx-vp9, BT.709 tagged) with backpressure. electron-builder packaging: AppImage, deb, rpm. - apps/web: dead backend deleted (better-auth, drizzle/pg, upstash, botid, freesound proxy, blog, rss, env validation); next build emits a zero-env static export; /editor/[project_id] -> /editor?project=. - Native exporter mirrors SceneExporter; activates when the preload bridge is present; web keeps the mediabunny path. - Docker self-hosting now builds and serves the static export via nginx. - Release workflow: v* tags publish static web tarball + Linux packages. - Changelog 0.4.0; README/AGENTS/CONTRIBUTING/roadmap updated. - GPUI/Rust rewrite parked on branch rust-rewrite. --- apps/web/src/app/api/auth/[...all]/route.ts | 4 - apps/web/src/app/api/health/route.ts | 3 - apps/web/src/app/api/sounds/search/route.ts | 280 --------------------- apps/web/src/app/blog/[slug]/page.tsx | 135 ---------- apps/web/src/app/blog/page.tsx | 52 ---- apps/web/src/app/changelog/[version]/page.tsx | 2 + apps/web/src/app/editor/[project_id]/page.tsx | 209 --------------- apps/web/src/app/editor/page.tsx | 217 ++++++++++++++++ apps/web/src/app/layout.tsx | 12 +- apps/web/src/app/projects/page.tsx | 10 +- apps/web/src/app/robots.ts | 2 + apps/web/src/app/rss.xml/route.ts | 46 ---- apps/web/src/app/sitemap.ts | 100 +++----- apps/web/src/auth/client.ts | 6 - apps/web/src/auth/rate-limit.ts | 21 -- apps/web/src/auth/server.ts | 43 ---- apps/web/src/blog/query.ts | 133 ---------- apps/web/src/blog/types.ts | 92 ------- apps/web/src/changelog/entries/0.4.0.md | 50 ++-- apps/web/src/components/editor/export-button.tsx | 18 ++ .../src/components/providers/editor-provider.tsx | 2 +- apps/web/src/core/managers/renderer-manager.ts | 42 +++- apps/web/src/db/index.ts | 19 -- apps/web/src/db/schema.ts | 70 ------ apps/web/src/desktop/bridge.ts | 35 +++ apps/web/src/env/web.ts | 30 --- apps/web/src/export/index.ts | 1 + apps/web/src/services/renderer/native-exporter.ts | 159 ++++++++++++ 28 files changed, 550 insertions(+), 1243 deletions(-) delete mode 100644 apps/web/src/app/api/auth/[...all]/route.ts delete mode 100644 apps/web/src/app/api/health/route.ts delete mode 100644 apps/web/src/app/api/sounds/search/route.ts delete mode 100644 apps/web/src/app/blog/[slug]/page.tsx delete mode 100644 apps/web/src/app/blog/page.tsx delete mode 100644 apps/web/src/app/editor/[project_id]/page.tsx create mode 100644 apps/web/src/app/editor/page.tsx delete mode 100644 apps/web/src/app/rss.xml/route.ts delete mode 100644 apps/web/src/auth/client.ts delete mode 100644 apps/web/src/auth/rate-limit.ts delete mode 100644 apps/web/src/auth/server.ts delete mode 100644 apps/web/src/blog/query.ts delete mode 100644 apps/web/src/blog/types.ts delete mode 100644 apps/web/src/db/index.ts delete mode 100644 apps/web/src/db/schema.ts create mode 100644 apps/web/src/desktop/bridge.ts delete mode 100644 apps/web/src/env/web.ts create mode 100644 apps/web/src/services/renderer/native-exporter.ts (limited to 'apps/web/src') diff --git a/apps/web/src/app/api/auth/[...all]/route.ts b/apps/web/src/app/api/auth/[...all]/route.ts deleted file mode 100644 index edf0d0c2..00000000 --- a/apps/web/src/app/api/auth/[...all]/route.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { auth } from "@/auth/server"; -import { toNextJsHandler } from "better-auth/next-js"; - -export const { POST, GET } = toNextJsHandler(auth); diff --git a/apps/web/src/app/api/health/route.ts b/apps/web/src/app/api/health/route.ts deleted file mode 100644 index dbe6fdb1..00000000 --- a/apps/web/src/app/api/health/route.ts +++ /dev/null @@ -1,3 +0,0 @@ -export async function GET() { - return new Response("OK", { status: 200 }); -} diff --git a/apps/web/src/app/api/sounds/search/route.ts b/apps/web/src/app/api/sounds/search/route.ts deleted file mode 100644 index aa7a1b60..00000000 --- a/apps/web/src/app/api/sounds/search/route.ts +++ /dev/null @@ -1,280 +0,0 @@ -import { webEnv } from "@/env/web"; -import { type NextRequest, NextResponse } from "next/server"; -import { z } from "zod"; -import { checkRateLimit } from "@/auth/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, -) { - 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 }, - ); - } -} diff --git a/apps/web/src/app/blog/[slug]/page.tsx b/apps/web/src/app/blog/[slug]/page.tsx deleted file mode 100644 index dba9b504..00000000 --- a/apps/web/src/app/blog/[slug]/page.tsx +++ /dev/null @@ -1,135 +0,0 @@ -import type { Metadata } from "next"; -import Image from "next/image"; -import { notFound } from "next/navigation"; -import { BasePage } from "@/app/base-page"; -import Prose from "@/components/ui/prose"; -import { Separator } from "@/components/ui/separator"; -import { getPosts, getSinglePost, processHtmlContent } from "@/blog/query"; -import type { Author, Post } from "@/blog/types"; - -type PageProps = { - params: Promise<{ slug: string }>; - searchParams: Promise<{ [key: string]: string | string[] | undefined }>; -}; - -export async function generateMetadata({ - params, -}: PageProps): Promise { - const slug = (await params).slug; - - const data = await getSinglePost({ slug }); - - if (!data || !data.post) return {}; - - return { - title: data.post.title, - description: data.post.description, - twitter: { - title: `${data.post.title}`, - description: `${data.post.description}`, - card: "summary_large_image", - images: [ - { - url: data.post.coverImage, - width: "1200", - height: "630", - alt: data.post.title, - }, - ], - }, - openGraph: { - type: "article", - images: [ - { - url: data.post.coverImage, - width: "1200", - height: "630", - alt: data.post.title, - }, - ], - title: data.post.title, - description: data.post.description, - publishedTime: new Date(data.post.publishedAt).toISOString(), - authors: data.post.authors.map((author: Author) => author.name), - }, - }; -} - -export async function generateStaticParams() { - const data = await getPosts(); - if (!data || !data.posts.length) return []; - - return data.posts.map((post) => ({ - slug: post.slug, - })); -} - -export default async function BlogPostPage({ params }: PageProps) { - const slug = (await params).slug; - const data = await getSinglePost({ slug }); - if (!data || !data.post) return notFound(); - - const html = await processHtmlContent({ html: data.post.content }); - - return ( - - - - - - ); -} - -function PostHeader({ post }: { post: Post }) { - const formattedDate = new Date(post.publishedAt).toLocaleDateString("en-US", { - day: "numeric", - month: "long", - year: "numeric", - }); - - return ( -
- - - {post.coverImage && } -
- ); -} - -function PostCoverImage({ post }: { post: Post }) { - return ( -
- {post.title} -
- ); -} - -function PostMeta({ date, publishedAt }: { date: string; publishedAt: Date }) { - return ( -
- -
- ); -} - -function PostTitle({ title }: { title: string }) { - return ( -

- {title} -

- ); -} - -function PostContent({ html }: { html: string }) { - return ( -
- -
- ); -} diff --git a/apps/web/src/app/blog/page.tsx b/apps/web/src/app/blog/page.tsx deleted file mode 100644 index b40bc2fc..00000000 --- a/apps/web/src/app/blog/page.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import type { Metadata } from "next"; -import Link from "next/link"; -import { BasePage } from "@/app/base-page"; -import { Separator } from "@/components/ui/separator"; -import { getPosts } from "@/blog/query"; -import type { Post } from "@/blog/types"; - -export const metadata: Metadata = { - title: "Blog - OpenCut", - description: - "Read the latest news and updates about OpenCut, the free and open-source video editor.", - openGraph: { - title: "Blog - OpenCut", - description: - "Read the latest news and updates about OpenCut, the free and open-source video editor.", - type: "website", - }, -}; - -export default async function BlogPage() { - const data = await getPosts().catch(() => null); - if (!data || !data.posts) return
No posts yet
; - - return ( - -
- {data.posts.map((post) => ( -
- - -
- ))} -
-
- ); -} - -function BlogPostItem({ post }: { post: Post }) { - return ( - -
-
-

{post.title}

-

{post.description}

-
-
- - ); -} diff --git a/apps/web/src/app/changelog/[version]/page.tsx b/apps/web/src/app/changelog/[version]/page.tsx index 6c529ce1..b6623cf3 100644 --- a/apps/web/src/app/changelog/[version]/page.tsx +++ b/apps/web/src/app/changelog/[version]/page.tsx @@ -15,6 +15,8 @@ import { CopyMarkdownButton } from "@/changelog/components/copy-markdown-button" type Props = { params: Promise<{ version: string }> }; +export const dynamicParams = false; + export async function generateStaticParams() { return getSortedReleases().map((release) => ({ version: release.version })); } diff --git a/apps/web/src/app/editor/[project_id]/page.tsx b/apps/web/src/app/editor/[project_id]/page.tsx deleted file mode 100644 index b759b9c2..00000000 --- a/apps/web/src/app/editor/[project_id]/page.tsx +++ /dev/null @@ -1,209 +0,0 @@ -"use client"; - -import { useParams } from "next/navigation"; -import { - ResizablePanelGroup, - ResizablePanel, - ResizableHandle, -} from "@/components/ui/resizable"; -import { AssetsPanel } from "@/components/editor/panels/assets"; -import { PropertiesPanel } from "@/components/editor/panels/properties"; -import { Timeline } from "@/timeline/components"; -import { PreviewPanel } from "@/preview/components"; -import { EditorHeader } from "@/components/editor/editor-header"; -import { EditorProvider } from "@/components/providers/editor-provider"; -import { Onboarding } from "@/components/editor/onboarding"; -import { MigrationDialog } from "@/project/components/migration-dialog"; -import { usePanelStore } from "@/editor/panel-store"; -import { usePasteMedia } from "@/media/use-paste-media"; -import { MobileGate } from "@/components/editor/mobile-gate"; -import { useMemo, useState } from "react"; -import { useEditor } from "@/editor/use-editor"; -import { Cancel01Icon } from "@hugeicons/core-free-icons"; -import { HugeiconsIcon } from "@hugeicons/react"; -import { Button } from "@/components/ui/button"; -import { ChangelogNotification } from "@/changelog/components/changelog-notification"; -import { - createPreviewOverlayControl, - isPreviewOverlayVisible, - mergePreviewOverlaySources, -} from "@/preview/overlays"; -import { usePreviewStore } from "@/preview/preview-store"; -import { getGuidePreviewOverlaySource } from "@/guides"; -import { - bookmarkNotesPreviewOverlay, - getBookmarkPreviewOverlaySource, -} from "@/timeline/bookmarks/index"; - -export default function Editor() { - const params = useParams<{ project_id: string }>(); - const projectId = params.project_id; - - return ( - - -
- - -
- -
- - - -
-
-
- ); -} - -function DegradedRendererBanner() { - const isDegraded = useEditor((e) => e.renderer.isDegraded); - const [dismissed, setDismissed] = useState(false); - if (!isDegraded || dismissed) return null; - - return ( -
- For the best experience, open OpenCut in Chrome. - -
- ); -} - -function EditorLayout() { - usePasteMedia(); - const { panels, setPanel } = usePanelStore(); - const activeScene = useEditor((editor) => - editor.scenes.getActiveSceneOrNull(), - ); - const currentTime = useEditor((editor) => editor.playback.getCurrentTime()); - const activeGuide = usePreviewStore((state) => state.activeGuide); - const overlays = usePreviewStore((state) => state.overlays); - const setOverlayVisibility = usePreviewStore( - (state) => state.setOverlayVisibility, - ); - const showBookmarkNotes = isPreviewOverlayVisible({ - overlay: bookmarkNotesPreviewOverlay, - overlays, - }); - - const overlaySource = useMemo( - () => - mergePreviewOverlaySources({ - sources: [ - getGuidePreviewOverlaySource({ - guideId: activeGuide, - }), - activeScene - ? getBookmarkPreviewOverlaySource({ - bookmarks: activeScene.bookmarks, - time: currentTime, - isVisible: showBookmarkNotes, - }) - : { - definitions: [bookmarkNotesPreviewOverlay], - instances: [], - }, - ], - }), - [activeGuide, activeScene, currentTime, showBookmarkNotes], - ); - - const overlayControls = useMemo( - () => - overlaySource.definitions.map((overlay) => - createPreviewOverlayControl({ overlay, overlays }), - ), - [overlaySource.definitions, overlays], - ); - - return ( - { - setPanel({ - panel: "mainContent", - size: sizes[0] ?? panels.mainContent, - }); - setPanel({ - panel: "timeline", - size: sizes[1] ?? panels.timeline, - }); - }} - > - - { - setPanel({ panel: "tools", size: sizes[0] ?? panels.tools }); - setPanel({ panel: "preview", size: sizes[1] ?? panels.preview }); - setPanel({ - panel: "properties", - size: sizes[2] ?? panels.properties, - }); - }} - > - - - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/apps/web/src/app/editor/page.tsx b/apps/web/src/app/editor/page.tsx new file mode 100644 index 00000000..8229774a --- /dev/null +++ b/apps/web/src/app/editor/page.tsx @@ -0,0 +1,217 @@ +"use client"; + +import { useSearchParams } from "next/navigation"; +import { + ResizablePanelGroup, + ResizablePanel, + ResizableHandle, +} from "@/components/ui/resizable"; +import { AssetsPanel } from "@/components/editor/panels/assets"; +import { PropertiesPanel } from "@/components/editor/panels/properties"; +import { Timeline } from "@/timeline/components"; +import { PreviewPanel } from "@/preview/components"; +import { EditorHeader } from "@/components/editor/editor-header"; +import { EditorProvider } from "@/components/providers/editor-provider"; +import { Onboarding } from "@/components/editor/onboarding"; +import { MigrationDialog } from "@/project/components/migration-dialog"; +import { usePanelStore } from "@/editor/panel-store"; +import { usePasteMedia } from "@/media/use-paste-media"; +import { MobileGate } from "@/components/editor/mobile-gate"; +import { Suspense, useMemo, useState } from "react"; +import { useEditor } from "@/editor/use-editor"; +import { Cancel01Icon } from "@hugeicons/core-free-icons"; +import { HugeiconsIcon } from "@hugeicons/react"; +import { Button } from "@/components/ui/button"; +import { ChangelogNotification } from "@/changelog/components/changelog-notification"; +import { + createPreviewOverlayControl, + isPreviewOverlayVisible, + mergePreviewOverlaySources, +} from "@/preview/overlays"; +import { usePreviewStore } from "@/preview/preview-store"; +import { getGuidePreviewOverlaySource } from "@/guides"; +import { + bookmarkNotesPreviewOverlay, + getBookmarkPreviewOverlaySource, +} from "@/timeline/bookmarks/index"; + +export default function Editor() { + return ( + + + + ); +} + +function EditorView() { + const searchParams = useSearchParams(); + const projectId = searchParams.get("project") ?? ""; + + return ( + + +
+ + +
+ +
+ + + +
+
+
+ ); +} + +function DegradedRendererBanner() { + const isDegraded = useEditor((e) => e.renderer.isDegraded); + const [dismissed, setDismissed] = useState(false); + if (!isDegraded || dismissed) return null; + + return ( +
+ For the best experience, open OpenCut in Chrome. + +
+ ); +} + +function EditorLayout() { + usePasteMedia(); + const { panels, setPanel } = usePanelStore(); + const activeScene = useEditor((editor) => + editor.scenes.getActiveSceneOrNull(), + ); + const currentTime = useEditor((editor) => editor.playback.getCurrentTime()); + const activeGuide = usePreviewStore((state) => state.activeGuide); + const overlays = usePreviewStore((state) => state.overlays); + const setOverlayVisibility = usePreviewStore( + (state) => state.setOverlayVisibility, + ); + const showBookmarkNotes = isPreviewOverlayVisible({ + overlay: bookmarkNotesPreviewOverlay, + overlays, + }); + + const overlaySource = useMemo( + () => + mergePreviewOverlaySources({ + sources: [ + getGuidePreviewOverlaySource({ + guideId: activeGuide, + }), + activeScene + ? getBookmarkPreviewOverlaySource({ + bookmarks: activeScene.bookmarks, + time: currentTime, + isVisible: showBookmarkNotes, + }) + : { + definitions: [bookmarkNotesPreviewOverlay], + instances: [], + }, + ], + }), + [activeGuide, activeScene, currentTime, showBookmarkNotes], + ); + + const overlayControls = useMemo( + () => + overlaySource.definitions.map((overlay) => + createPreviewOverlayControl({ overlay, overlays }), + ), + [overlaySource.definitions, overlays], + ); + + return ( + { + setPanel({ + panel: "mainContent", + size: sizes[0] ?? panels.mainContent, + }); + setPanel({ + panel: "timeline", + size: sizes[1] ?? panels.timeline, + }); + }} + > + + { + setPanel({ panel: "tools", size: sizes[0] ?? panels.tools }); + setPanel({ panel: "preview", size: sizes[1] ?? panels.preview }); + setPanel({ + panel: "properties", + size: sizes[2] ?? panels.properties, + }); + }} + > + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 558ae538..9713d9d5 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -5,21 +5,12 @@ import { Toaster } from "../components/ui/sonner"; import { ChangelogNotification } from "@/changelog/components/changelog-notification"; import { TooltipProvider } from "../components/ui/tooltip"; import { baseMetaData } from "./metadata"; -import { BotIdClient } from "botid/client"; -import { webEnv } from "@/env/web"; import { Inter } from "next/font/google"; const siteFont = Inter({ subsets: ["latin"] }); export const metadata = baseMetaData; -const protectedRoutes = [ - { - path: "/none", - method: "GET", - }, -]; - export default function RootLayout({ children, }: Readonly<{ @@ -28,7 +19,6 @@ export default function RootLayout({ return ( - {process.env.NODE_ENV === "development" && ( <>