mirror of
https://github.com/jnsahaj/tweakcn.git
synced 2026-08-31 02:19:56 +08:00
open in v0
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
ResponsiveDialogTitle,
|
||||
ResponsiveDialogTrigger,
|
||||
} from "@/components/ui/revola";
|
||||
import { PostLoginActionType } from "@/hooks/use-post-login-action";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { usePathname, useSearchParams } from "next/navigation";
|
||||
@@ -20,6 +21,44 @@ interface AuthDialogProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
initialMode?: "signin" | "signup";
|
||||
trigger?: React.ReactNode; // Optional trigger element
|
||||
postLoginActionType?: PostLoginActionType | null;
|
||||
}
|
||||
|
||||
// Get contextual copy based on the post-login action
|
||||
function getContextualCopy(actionType?: PostLoginActionType | null) {
|
||||
switch (actionType) {
|
||||
case "SAVE_THEME":
|
||||
return {
|
||||
title: "Sign in to Save",
|
||||
description: "Sign in to save your theme and access it from anywhere",
|
||||
};
|
||||
case "SAVE_THEME_FOR_SHARE":
|
||||
return {
|
||||
title: "Sign in to Share",
|
||||
description: "Sign in to save and share your theme with others",
|
||||
};
|
||||
case "SAVE_THEME_FOR_V0":
|
||||
return {
|
||||
title: "Sign in to open in v0",
|
||||
description: "Sign in to save your theme and open it in v0",
|
||||
};
|
||||
case "AI_GENERATE_FROM_PAGE":
|
||||
case "AI_GENERATE_FROM_CHAT":
|
||||
case "AI_GENERATE_FROM_CHAT_SUGGESTION":
|
||||
case "AI_GENERATE_EDIT":
|
||||
case "AI_GENERATE_RETRY":
|
||||
return {
|
||||
title: "Sign in for AI",
|
||||
description: "Sign in to use AI-powered theme generation",
|
||||
};
|
||||
case "CHECKOUT":
|
||||
return {
|
||||
title: "Sign in to continue",
|
||||
description: "Sign in to complete your purchase",
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function AuthDialog({
|
||||
@@ -27,6 +66,7 @@ export function AuthDialog({
|
||||
onOpenChange,
|
||||
initialMode = "signin",
|
||||
trigger,
|
||||
postLoginActionType,
|
||||
}: AuthDialogProps) {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
@@ -34,6 +74,8 @@ export function AuthDialog({
|
||||
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
|
||||
const [isGithubLoading, setIsGithubLoading] = useState(false);
|
||||
|
||||
const contextualCopy = getContextualCopy(postLoginActionType);
|
||||
|
||||
const getCallbackUrl = () => {
|
||||
const baseUrl = pathname || "/editor/theme";
|
||||
const queryString = searchParams.toString();
|
||||
@@ -83,12 +125,13 @@ export function AuthDialog({
|
||||
<div className="space-y-4">
|
||||
<ResponsiveDialogHeader className="sm:pt-8">
|
||||
<ResponsiveDialogTitle className="text-center text-2xl font-bold">
|
||||
{isSignIn ? "Welcome back" : "Create account"}
|
||||
{contextualCopy?.title ?? (isSignIn ? "Welcome back" : "Create account")}
|
||||
</ResponsiveDialogTitle>
|
||||
<p className="text-muted-foreground text-center">
|
||||
{isSignIn
|
||||
? "Sign in to your account to continue"
|
||||
: "Sign up to get started with tweakcn"}
|
||||
{contextualCopy?.description ??
|
||||
(isSignIn
|
||||
? "Sign in to your account to continue"
|
||||
: "Sign up to get started with tweakcn")}
|
||||
</p>
|
||||
</ResponsiveDialogHeader>
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import { NextResponse } from "next/server";
|
||||
import { getTheme } from "@/actions/themes";
|
||||
import { generateThemeRegistryItemFromStyles } from "@/utils/registry/themes";
|
||||
import { registryItemSchema } from "shadcn/registry";
|
||||
import { getBuiltInThemeStyles } from "@/utils/theme-preset-helper";
|
||||
import { ThemeStyles } from "@/types/theme";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
@@ -10,8 +12,22 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
|
||||
const { id } = await params;
|
||||
|
||||
try {
|
||||
const theme = await getTheme(id);
|
||||
const generatedRegistryItem = generateThemeRegistryItemFromStyles(theme.name, theme.styles);
|
||||
// First, check if this is a built-in theme
|
||||
let themeName: string;
|
||||
let themeStyles: ThemeStyles;
|
||||
|
||||
const builtInTheme = getBuiltInThemeStyles(id.replace(/\.json$/, ""));
|
||||
if (builtInTheme) {
|
||||
themeName = builtInTheme.name;
|
||||
themeStyles = builtInTheme.styles;
|
||||
} else {
|
||||
// Fall back to database lookup for user-saved themes
|
||||
const theme = await getTheme(id);
|
||||
themeName = theme.name;
|
||||
themeStyles = theme.styles;
|
||||
}
|
||||
|
||||
const generatedRegistryItem = generateThemeRegistryItemFromStyles(themeName, themeStyles);
|
||||
|
||||
// Validate the generated registry item against the official shadcn registry item schema
|
||||
// https://ui.shadcn.com/docs/registry/registry-item-json
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getTheme } from "@/actions/themes";
|
||||
import { generateV0RegistryPayload } from "@/utils/registry/v0";
|
||||
import { getBuiltInThemeStyles } from "@/utils/theme-preset-helper";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
|
||||
try {
|
||||
// First, check if this is a built-in theme
|
||||
const builtInTheme = getBuiltInThemeStyles(id.replace(/\.json$/, ""));
|
||||
if (builtInTheme) {
|
||||
const payload = generateV0RegistryPayload(builtInTheme.name, builtInTheme.styles);
|
||||
return new NextResponse(JSON.stringify(payload), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Fall back to database lookup for user-saved themes
|
||||
const theme = await getTheme(id);
|
||||
const payload = generateV0RegistryPayload(theme.name, theme.styles);
|
||||
|
||||
return new NextResponse(JSON.stringify(payload), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error generating v0 registry payload:", error);
|
||||
|
||||
const isNotFound =
|
||||
error instanceof Error &&
|
||||
(error.name === "ThemeNotFoundError" || error.message.includes("not found"));
|
||||
|
||||
return new NextResponse(isNotFound ? "Theme not found" : "Failed to generate v0 payload", {
|
||||
status: isNotFound ? 404 : 500,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -32,5 +32,12 @@ export function AuthDialogWrapper() {
|
||||
}
|
||||
}, [session, isOpen, closeAuthDialog, postLoginAction, clearPostLoginAction, posthog]);
|
||||
|
||||
return <AuthDialog open={isOpen} onOpenChange={closeAuthDialog} initialMode={mode} />;
|
||||
return (
|
||||
<AuthDialog
|
||||
open={isOpen}
|
||||
onOpenChange={closeAuthDialog}
|
||||
initialMode={mode}
|
||||
postLoginActionType={postLoginAction?.type}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,17 +2,9 @@
|
||||
|
||||
import { ActionBarButtons } from "@/components/editor/action-bar/components/action-bar-buttons";
|
||||
import { HorizontalScrollArea } from "@/components/horizontal-scroll-area";
|
||||
import { DialogActionsProvider, useDialogActions } from "@/hooks/use-dialog-actions";
|
||||
import { useDialogActions } from "@/hooks/use-dialog-actions";
|
||||
|
||||
export function ActionBar() {
|
||||
return (
|
||||
<DialogActionsProvider>
|
||||
<ActionBarContent />
|
||||
</DialogActionsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function ActionBarContent() {
|
||||
const { isCreatingTheme, handleSaveClick, handleShareClick, setCssImportOpen, setCodePanelOpen } =
|
||||
useDialogActions();
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { DialogActionsProvider } from "@/hooks/use-dialog-actions";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { useEditorStore } from "@/store/editor-store";
|
||||
import { Theme, ThemeStyles } from "@/types/theme";
|
||||
import { Sliders } from "lucide-react";
|
||||
@@ -9,7 +11,6 @@ import React, { use, useEffect } from "react";
|
||||
import { ActionBar } from "./action-bar/action-bar";
|
||||
import ThemeControlPanel from "./theme-control-panel";
|
||||
import ThemePreviewPanel from "./theme-preview-panel";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
|
||||
interface EditorProps {
|
||||
themePromise: Promise<Theme | null>;
|
||||
@@ -64,20 +65,55 @@ const Editor: React.FC<EditorProps> = ({ themePromise }) => {
|
||||
// Mobile layout
|
||||
if (isMobile) {
|
||||
return (
|
||||
<DialogActionsProvider>
|
||||
<div className="relative isolate flex flex-1 overflow-hidden">
|
||||
<div className="size-full flex-1 overflow-hidden">
|
||||
<Tabs defaultValue="controls" className="h-full">
|
||||
<TabsList className="w-full rounded-none">
|
||||
<TabsTrigger value="controls" className="flex-1">
|
||||
<Sliders className="mr-2 h-4 w-4" />
|
||||
Controls
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="preview" className="flex-1">
|
||||
Preview
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="controls" className="mt-0 h-[calc(100%-2.5rem)]">
|
||||
<div className="flex h-full flex-col">
|
||||
<ThemeControlPanel
|
||||
styles={styles}
|
||||
onChange={handleStyleChange}
|
||||
currentMode={themeState.currentMode}
|
||||
themePromise={themePromise}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="preview" className="mt-0 h-[calc(100%-2.5rem)]">
|
||||
<div className="flex h-full flex-col">
|
||||
<ActionBar />
|
||||
<ThemePreviewPanel styles={styles} currentMode={themeState.currentMode} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</DialogActionsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop layout
|
||||
return (
|
||||
<DialogActionsProvider>
|
||||
<div className="relative isolate flex flex-1 overflow-hidden">
|
||||
<div className="size-full flex-1 overflow-hidden">
|
||||
<Tabs defaultValue="controls" className="h-full">
|
||||
<TabsList className="w-full rounded-none">
|
||||
<TabsTrigger value="controls" className="flex-1">
|
||||
<Sliders className="mr-2 h-4 w-4" />
|
||||
Controls
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="preview" className="flex-1">
|
||||
Preview
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="controls" className="mt-0 h-[calc(100%-2.5rem)]">
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="size-full">
|
||||
<ResizablePanelGroup direction="horizontal" className="isolate">
|
||||
<ResizablePanel
|
||||
defaultSize={30}
|
||||
minSize={20}
|
||||
maxSize={40}
|
||||
className="z-1 min-w-[max(20%,22rem)]"
|
||||
>
|
||||
<div className="relative isolate flex h-full flex-1 flex-col">
|
||||
<ThemeControlPanel
|
||||
styles={styles}
|
||||
onChange={handleStyleChange}
|
||||
@@ -85,51 +121,20 @@ const Editor: React.FC<EditorProps> = ({ themePromise }) => {
|
||||
themePromise={themePromise}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="preview" className="mt-0 h-[calc(100%-2.5rem)]">
|
||||
</ResizablePanel>
|
||||
<ResizableHandle />
|
||||
<ResizablePanel defaultSize={70}>
|
||||
<div className="flex h-full flex-col">
|
||||
<ActionBar />
|
||||
<ThemePreviewPanel styles={styles} currentMode={themeState.currentMode} />
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<ActionBar />
|
||||
<ThemePreviewPanel styles={styles} currentMode={themeState.currentMode} />
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Desktop layout
|
||||
return (
|
||||
<div className="relative isolate flex flex-1 overflow-hidden">
|
||||
<div className="size-full">
|
||||
<ResizablePanelGroup direction="horizontal" className="isolate">
|
||||
<ResizablePanel
|
||||
defaultSize={30}
|
||||
minSize={20}
|
||||
maxSize={40}
|
||||
className="z-1 min-w-[max(20%,22rem)]"
|
||||
>
|
||||
<div className="relative isolate flex h-full flex-1 flex-col">
|
||||
<ThemeControlPanel
|
||||
styles={styles}
|
||||
onChange={handleStyleChange}
|
||||
currentMode={themeState.currentMode}
|
||||
themePromise={themePromise}
|
||||
/>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle />
|
||||
<ResizablePanel defaultSize={70}>
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<ActionBar />
|
||||
<ThemePreviewPanel styles={styles} currentMode={themeState.currentMode} />
|
||||
</div>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</div>
|
||||
</div>
|
||||
</DialogActionsProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -13,18 +13,19 @@ import {
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||
import { Tabs, TabsContent, TabsList } from "@/components/ui/tabs";
|
||||
import { useDialogActions } from "@/hooks/use-dialog-actions";
|
||||
import { useFullscreen } from "@/hooks/use-fullscreen";
|
||||
import { useThemeInspector } from "@/hooks/use-theme-inspector";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ThemeEditorPreviewProps } from "@/types/theme";
|
||||
import { Inspect, Maximize, Minimize, MoreVertical } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useQueryState } from "nuqs";
|
||||
import { lazy } from "react";
|
||||
import InspectorOverlay from "./inspector-overlay";
|
||||
import ColorPreview from "./theme-preview/color-preview";
|
||||
import ExamplesPreviewContainer from "./theme-preview/examples-preview-container";
|
||||
import TabsTriggerPill from "./theme-preview/tabs-trigger-pill";
|
||||
import { useQueryState } from "nuqs";
|
||||
|
||||
const DemoCards = lazy(() => import("@/components/examples/cards"));
|
||||
const DemoMail = lazy(() => import("@/components/examples/mail"));
|
||||
@@ -33,11 +34,25 @@ const DemoPricing = lazy(() => import("@/components/examples/pricing/pricing"));
|
||||
const TypographyDemo = lazy(() => import("@/components/examples/typography/typography-demo"));
|
||||
const CustomDemo = lazy(() => import("@/components/examples/custom"));
|
||||
|
||||
const V0Logo = ({ className }: { className?: string }) => (
|
||||
<svg viewBox="0 0 40 20" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
|
||||
<path
|
||||
d="M23.3919 0H32.9188C36.7819 0 39.9136 3.13165 39.9136 6.99475V16.0805H36.0006V6.99475C36.0006 6.90167 35.9969 6.80925 35.9898 6.71766L26.4628 16.079C26.4949 16.08 26.5272 16.0805 26.5595 16.0805H36.0006V19.7762H26.5595C22.6964 19.7762 19.4788 16.6139 19.4788 12.7508V3.68923H23.3919V12.7508C23.3919 12.9253 23.4054 13.0977 23.4316 13.2668L33.1682 3.6995C33.0861 3.6927 33.003 3.68923 32.9188 3.68923H23.3919V0Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M13.7688 19.0956L0 3.68759H5.53933L13.6231 12.7337V3.68759H17.7535V17.5746C17.7535 19.6705 15.1654 20.6584 13.7688 19.0956Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const ThemePreviewPanel = ({ styles, currentMode }: ThemeEditorPreviewProps) => {
|
||||
const { isFullscreen, toggleFullscreen } = useFullscreen();
|
||||
const [activeTab, setActiveTab] = useQueryState("p", {
|
||||
defaultValue: "cards",
|
||||
});
|
||||
const { handleOpenInV0 } = useDialogActions();
|
||||
|
||||
const {
|
||||
rootRef,
|
||||
@@ -98,6 +113,13 @@ const ThemePreviewPanel = ({ styles, currentMode }: ThemeEditorPreviewProps) =>
|
||||
</TabsList>
|
||||
|
||||
<div className="flex items-center gap-0.5">
|
||||
<TooltipWrapper label="Open theme in v0" asChild>
|
||||
<Button variant="ghost" onClick={() => handleOpenInV0()} className="group px-2.5">
|
||||
<span className="flex items-center justify-center gap-1 transition-all group-hover:scale-110">
|
||||
Open in <V0Logo className="mb-0.5 !size-5" />
|
||||
</span>
|
||||
</Button>
|
||||
</TooltipWrapper>
|
||||
{isFullscreen && (
|
||||
<ThemeToggle
|
||||
variant="ghost"
|
||||
|
||||
@@ -14,6 +14,28 @@ import { parseCssInput } from "@/utils/parse-css-input";
|
||||
import { usePostHog } from "posthog-js/react";
|
||||
import { createContext, ReactNode, useContext, useState } from "react";
|
||||
|
||||
type PendingAction = "share" | "v0" | null;
|
||||
|
||||
// Get contextual copy for the save dialog based on the pending action
|
||||
function getSaveDialogCopy(pendingAction: PendingAction) {
|
||||
switch (pendingAction) {
|
||||
case "share":
|
||||
return {
|
||||
title: "Save to share",
|
||||
description: "Save your theme first to share it with others.",
|
||||
ctaLabel: "Save & Share",
|
||||
};
|
||||
case "v0":
|
||||
return {
|
||||
title: "Save to open in v0",
|
||||
description: "Save your theme first to open it in v0.",
|
||||
ctaLabel: "Save & Open in v0",
|
||||
};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
interface DialogActionsContextType {
|
||||
// Dialog states
|
||||
cssImportOpen: boolean;
|
||||
@@ -24,6 +46,7 @@ interface DialogActionsContextType {
|
||||
dialogKey: number;
|
||||
isCreatingTheme: boolean;
|
||||
isGeneratingTheme: boolean;
|
||||
pendingAction: PendingAction;
|
||||
|
||||
// Dialog actions
|
||||
setCssImportOpen: (open: boolean) => void;
|
||||
@@ -33,8 +56,9 @@ interface DialogActionsContextType {
|
||||
|
||||
// Handler functions
|
||||
handleCssImport: (css: string) => void;
|
||||
handleSaveClick: (options?: { shareAfterSave?: boolean }) => void;
|
||||
handleSaveClick: (options?: { shareAfterSave?: boolean; openInV0AfterSave?: boolean }) => void;
|
||||
handleShareClick: (id?: string) => Promise<void>;
|
||||
handleOpenInV0: (id?: string) => void;
|
||||
saveTheme: (themeName: string) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -42,7 +66,7 @@ function useDialogActionsStore(): DialogActionsContextType {
|
||||
const [cssImportOpen, setCssImportOpen] = useState(false);
|
||||
const [codePanelOpen, setCodePanelOpen] = useState(false);
|
||||
const [saveDialogOpen, setSaveDialogOpen] = useState(false);
|
||||
const [shareAfterSave, setShareAfterSave] = useState(false);
|
||||
const [pendingAction, setPendingAction] = useState<PendingAction>(null);
|
||||
const [shareDialogOpen, setShareDialogOpen] = useState(false);
|
||||
const [shareUrl, setShareUrl] = useState("");
|
||||
const [dialogKey, _setDialogKey] = useState(0);
|
||||
@@ -62,7 +86,12 @@ function useDialogActionsStore(): DialogActionsContextType {
|
||||
|
||||
usePostLoginAction("SAVE_THEME_FOR_SHARE", () => {
|
||||
setSaveDialogOpen(true);
|
||||
setShareAfterSave(true);
|
||||
setPendingAction("share");
|
||||
});
|
||||
|
||||
usePostLoginAction("SAVE_THEME_FOR_V0", () => {
|
||||
setSaveDialogOpen(true);
|
||||
setPendingAction("v0");
|
||||
});
|
||||
|
||||
const handleCssImport = (css: string) => {
|
||||
@@ -84,15 +113,21 @@ function useDialogActionsStore(): DialogActionsContextType {
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveClick = (options?: { shareAfterSave?: boolean }) => {
|
||||
const handleSaveClick = (options?: { shareAfterSave?: boolean; openInV0AfterSave?: boolean }) => {
|
||||
if (!session) {
|
||||
openAuthDialog("signin", options?.shareAfterSave ? "SAVE_THEME_FOR_SHARE" : "SAVE_THEME");
|
||||
let action: "SAVE_THEME" | "SAVE_THEME_FOR_SHARE" | "SAVE_THEME_FOR_V0" = "SAVE_THEME";
|
||||
if (options?.shareAfterSave) action = "SAVE_THEME_FOR_SHARE";
|
||||
if (options?.openInV0AfterSave) action = "SAVE_THEME_FOR_V0";
|
||||
openAuthDialog("signin", action);
|
||||
return;
|
||||
}
|
||||
|
||||
setSaveDialogOpen(true);
|
||||
if (options?.shareAfterSave) {
|
||||
setShareAfterSave(true);
|
||||
setPendingAction("share");
|
||||
}
|
||||
if (options?.openInV0AfterSave) {
|
||||
setPendingAction("v0");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -110,9 +145,12 @@ function useDialogActionsStore(): DialogActionsContextType {
|
||||
});
|
||||
if (!theme) return;
|
||||
applyThemePreset(theme?.id || themeState.preset || "default");
|
||||
if (shareAfterSave) {
|
||||
if (pendingAction === "share") {
|
||||
handleShareClick(theme?.id);
|
||||
setShareAfterSave(false);
|
||||
setPendingAction(null);
|
||||
} else if (pendingAction === "v0") {
|
||||
openInV0(theme?.id);
|
||||
setPendingAction(null);
|
||||
}
|
||||
setTimeout(() => {
|
||||
setSaveDialogOpen(false);
|
||||
@@ -153,6 +191,38 @@ function useDialogActionsStore(): DialogActionsContextType {
|
||||
setShareDialogOpen(true);
|
||||
};
|
||||
|
||||
// Internal helper to open v0 with a theme
|
||||
const openInV0 = (id?: string) => {
|
||||
const presetId = id ?? themeState.preset;
|
||||
if (!presetId) return;
|
||||
|
||||
const currentPreset = getPreset(presetId);
|
||||
const isSavedPreset = !!currentPreset && currentPreset.source === "SAVED";
|
||||
const themeName = currentPreset?.label || presetId;
|
||||
|
||||
posthog.capture("OPEN_IN_V0", {
|
||||
theme_id: presetId,
|
||||
theme_name: themeName,
|
||||
is_saved: isSavedPreset,
|
||||
});
|
||||
|
||||
const themeUrl = isSavedPreset
|
||||
? `https://tweakcn.com/r/v0/${presetId}`
|
||||
: `https://tweakcn.com/r/v0/${presetId}.json`;
|
||||
const title = `"${themeName}" from tweakcn`;
|
||||
const v0Url = `https://v0.dev/chat/api/open?url=${encodeURIComponent(themeUrl)}&title=${encodeURIComponent(title)}`;
|
||||
window.open(v0Url, "_blank", "noopener,noreferrer");
|
||||
};
|
||||
|
||||
const handleOpenInV0 = (id?: string) => {
|
||||
if (hasThemeChangedFromCheckpoint()) {
|
||||
handleSaveClick({ openInV0AfterSave: true });
|
||||
return;
|
||||
}
|
||||
|
||||
openInV0(id);
|
||||
};
|
||||
|
||||
const value = {
|
||||
// Dialog states
|
||||
cssImportOpen,
|
||||
@@ -163,6 +233,7 @@ function useDialogActionsStore(): DialogActionsContextType {
|
||||
dialogKey,
|
||||
isCreatingTheme: createThemeMutation.isPending,
|
||||
isGeneratingTheme,
|
||||
pendingAction,
|
||||
|
||||
// Dialog actions
|
||||
setCssImportOpen,
|
||||
@@ -174,6 +245,7 @@ function useDialogActionsStore(): DialogActionsContextType {
|
||||
handleCssImport,
|
||||
handleSaveClick,
|
||||
handleShareClick,
|
||||
handleOpenInV0,
|
||||
saveTheme,
|
||||
};
|
||||
|
||||
@@ -206,6 +278,7 @@ export function DialogActionsProvider({ children }: { children: ReactNode }) {
|
||||
onOpenChange={store.setSaveDialogOpen}
|
||||
onSave={store.saveTheme}
|
||||
isSaving={store.isCreatingTheme}
|
||||
{...getSaveDialogCopy(store.pendingAction)}
|
||||
/>
|
||||
<ShareDialog
|
||||
open={store.shareDialogOpen}
|
||||
|
||||
@@ -16,6 +16,7 @@ export type PostLoginActionType =
|
||||
| "AI_GENERATE_EDIT"
|
||||
| "AI_GENERATE_RETRY"
|
||||
| "SAVE_THEME_FOR_SHARE"
|
||||
| "SAVE_THEME_FOR_V0"
|
||||
| "CHECKOUT";
|
||||
|
||||
export interface PostLoginActionPayload<T = any> {
|
||||
|
||||
@@ -1,20 +1,62 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import { generateThemeRegistryFromPreset } from "@/utils/registry/themes";
|
||||
import { generateThemeRegistryItemFromStyles } from "@/utils/registry/themes";
|
||||
import { generateV0RegistryPayload } from "@/utils/registry/v0";
|
||||
import { defaultPresets } from "@/utils/theme-presets";
|
||||
import { defaultThemeState } from "@/config/theme";
|
||||
import { ThemeStyles } from "@/types/theme";
|
||||
|
||||
const THEMES_DIR = path.join(process.cwd(), "public", "r", "themes");
|
||||
const V0_DIR = path.join(process.cwd(), "public", "r", "v0");
|
||||
|
||||
// Ensure the themes directory exists
|
||||
if (!fs.existsSync(THEMES_DIR)) {
|
||||
fs.mkdirSync(THEMES_DIR, { recursive: true });
|
||||
// Ensure directories exist
|
||||
[THEMES_DIR, V0_DIR].forEach((dir) => {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
// Helper to get preset theme styles without going through the store
|
||||
function getPresetThemeStylesForScript(name: string): ThemeStyles {
|
||||
const defaultTheme = defaultThemeState.styles;
|
||||
if (name === "default") {
|
||||
return defaultTheme;
|
||||
}
|
||||
|
||||
const preset = defaultPresets[name];
|
||||
if (!preset) {
|
||||
return defaultTheme;
|
||||
}
|
||||
|
||||
return {
|
||||
light: {
|
||||
...defaultTheme.light,
|
||||
...(preset.styles.light || {}),
|
||||
},
|
||||
dark: {
|
||||
...defaultTheme.dark,
|
||||
...(preset.styles.light || {}),
|
||||
...(preset.styles.dark || {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Generate registry files for all presets
|
||||
Object.keys(defaultPresets).forEach((name) => {
|
||||
const registryItem = generateThemeRegistryFromPreset(name);
|
||||
const preset = defaultPresets[name];
|
||||
const themeStyles = getPresetThemeStylesForScript(name);
|
||||
const themeName = preset.label || name;
|
||||
|
||||
// Generate shadcn registry format
|
||||
const registryItem = generateThemeRegistryItemFromStyles(name, themeStyles);
|
||||
const filePath = path.join(THEMES_DIR, `${name}.json`);
|
||||
fs.writeFileSync(filePath, JSON.stringify(registryItem, null, 2));
|
||||
console.log(`Generated registry file for theme: ${name}`);
|
||||
|
||||
// Generate v0 format
|
||||
const v0Payload = generateV0RegistryPayload(themeName, themeStyles);
|
||||
const v0FilePath = path.join(V0_DIR, `${name}.json`);
|
||||
fs.writeFileSync(v0FilePath, JSON.stringify(v0Payload, null, 2));
|
||||
console.log(`Generated v0 file for theme: ${name}`);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
import { ThemeStyles, ThemeStyleProps } from "@/types/theme";
|
||||
import { colorFormatter } from "@/utils/color-converter";
|
||||
import { getShadowMap } from "@/utils/shadows";
|
||||
import { defaultLightThemeStyles, defaultDarkThemeStyles } from "@/config/theme";
|
||||
import { SYSTEM_FONTS } from "@/utils/fonts";
|
||||
|
||||
type FontConfig = {
|
||||
family: string;
|
||||
variable: string;
|
||||
variableName: string;
|
||||
};
|
||||
|
||||
function extractFontFamily(fontFamilyValue: string): string | null {
|
||||
if (!fontFamilyValue) return null;
|
||||
const firstFont = fontFamilyValue.split(",")[0].trim();
|
||||
const cleanFont = firstFont.replace(/['"]/g, "");
|
||||
if (SYSTEM_FONTS.includes(cleanFont.toLowerCase())) return null;
|
||||
return cleanFont;
|
||||
}
|
||||
|
||||
function toVariableName(fontFamily: string): string {
|
||||
// "Plus Jakarta Sans" -> "plusJakartaSans"
|
||||
const words = fontFamily.split(/\s+/);
|
||||
return words
|
||||
.map((word, i) => (i === 0 ? word.toLowerCase() : word.charAt(0).toUpperCase() + word.slice(1)))
|
||||
.join("");
|
||||
}
|
||||
|
||||
function toCssVariable(fontFamily: string): string {
|
||||
// "Plus Jakarta Sans" -> "--font-plus-jakarta-sans"
|
||||
return `--font-${fontFamily.toLowerCase().replace(/\s+/g, "-")}`;
|
||||
}
|
||||
|
||||
function extractGoogleFonts(themeStyles: ThemeStyles): FontConfig[] {
|
||||
const fonts: FontConfig[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
const fontKeys: (keyof ThemeStyleProps)[] = ["font-sans", "font-serif", "font-mono"];
|
||||
|
||||
for (const key of fontKeys) {
|
||||
const fontValue = themeStyles.light[key] || themeStyles.dark[key];
|
||||
if (!fontValue) continue;
|
||||
|
||||
const family = extractFontFamily(fontValue);
|
||||
if (!family || seen.has(family)) continue;
|
||||
|
||||
seen.add(family);
|
||||
fonts.push({
|
||||
family,
|
||||
variable: toCssVariable(family),
|
||||
variableName: toVariableName(family),
|
||||
});
|
||||
}
|
||||
|
||||
return fonts;
|
||||
}
|
||||
|
||||
function formatColor(color: string): string {
|
||||
return colorFormatter(color, "oklch");
|
||||
}
|
||||
|
||||
function generateColorVariables(styles: ThemeStyleProps): string {
|
||||
return ` --background: ${formatColor(styles.background)};
|
||||
--foreground: ${formatColor(styles.foreground)};
|
||||
--card: ${formatColor(styles.card)};
|
||||
--card-foreground: ${formatColor(styles["card-foreground"])};
|
||||
--popover: ${formatColor(styles.popover)};
|
||||
--popover-foreground: ${formatColor(styles["popover-foreground"])};
|
||||
--primary: ${formatColor(styles.primary)};
|
||||
--primary-foreground: ${formatColor(styles["primary-foreground"])};
|
||||
--secondary: ${formatColor(styles.secondary)};
|
||||
--secondary-foreground: ${formatColor(styles["secondary-foreground"])};
|
||||
--muted: ${formatColor(styles.muted)};
|
||||
--muted-foreground: ${formatColor(styles["muted-foreground"])};
|
||||
--accent: ${formatColor(styles.accent)};
|
||||
--accent-foreground: ${formatColor(styles["accent-foreground"])};
|
||||
--destructive: ${formatColor(styles.destructive)};
|
||||
--destructive-foreground: ${formatColor(styles["destructive-foreground"])};
|
||||
--border: ${formatColor(styles.border)};
|
||||
--input: ${formatColor(styles.input)};
|
||||
--ring: ${formatColor(styles.ring)};
|
||||
--chart-1: ${formatColor(styles["chart-1"])};
|
||||
--chart-2: ${formatColor(styles["chart-2"])};
|
||||
--chart-3: ${formatColor(styles["chart-3"])};
|
||||
--chart-4: ${formatColor(styles["chart-4"])};
|
||||
--chart-5: ${formatColor(styles["chart-5"])};
|
||||
--radius: ${styles.radius};
|
||||
--sidebar: ${formatColor(styles.sidebar)};
|
||||
--sidebar-foreground: ${formatColor(styles["sidebar-foreground"])};
|
||||
--sidebar-primary: ${formatColor(styles["sidebar-primary"])};
|
||||
--sidebar-primary-foreground: ${formatColor(styles["sidebar-primary-foreground"])};
|
||||
--sidebar-accent: ${formatColor(styles["sidebar-accent"])};
|
||||
--sidebar-accent-foreground: ${formatColor(styles["sidebar-accent-foreground"])};
|
||||
--sidebar-border: ${formatColor(styles["sidebar-border"])};
|
||||
--sidebar-ring: ${formatColor(styles["sidebar-ring"])};`;
|
||||
}
|
||||
|
||||
export function generateV0GlobalsCss(themeStyles: ThemeStyles): string {
|
||||
const light = { ...defaultLightThemeStyles, ...themeStyles.light };
|
||||
const dark = { ...defaultDarkThemeStyles, ...themeStyles.dark };
|
||||
|
||||
const lightShadows = getShadowMap({ styles: { light, dark }, currentMode: "light" });
|
||||
const darkShadows = getShadowMap({ styles: { light, dark }, currentMode: "dark" });
|
||||
|
||||
const lightVars = generateColorVariables(light);
|
||||
const darkVars = generateColorVariables(dark);
|
||||
|
||||
// Get font values with fallbacks
|
||||
// Transform "Roboto Mono, sans-serif" -> "Roboto Mono, Roboto Mono Fallback"
|
||||
const formatFontWithFallback = (fontValue: string): string => {
|
||||
const firstFont = fontValue.split(",")[0].trim().replace(/['"]/g, "");
|
||||
return `${firstFont}, ${firstFont} Fallback`;
|
||||
};
|
||||
|
||||
const fontSans = formatFontWithFallback(
|
||||
light["font-sans"] || defaultLightThemeStyles["font-sans"]
|
||||
);
|
||||
const fontMono = formatFontWithFallback(
|
||||
light["font-mono"] || defaultLightThemeStyles["font-mono"]
|
||||
);
|
||||
const fontSerif = formatFontWithFallback(
|
||||
light["font-serif"] || defaultLightThemeStyles["font-serif"]
|
||||
);
|
||||
|
||||
return `@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--font-sans: ${fontSans};
|
||||
--font-mono: ${fontMono};
|
||||
--font-serif: ${fontSerif};
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--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);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--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);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--radius-2xl: calc(var(--radius) + 8px);
|
||||
--radius-3xl: calc(var(--radius) + 12px);
|
||||
--radius-4xl: calc(var(--radius) + 16px);
|
||||
}
|
||||
|
||||
:root {
|
||||
${lightVars}
|
||||
--shadow-2xs: ${lightShadows["shadow-2xs"]};
|
||||
--shadow-xs: ${lightShadows["shadow-xs"]};
|
||||
--shadow-sm: ${lightShadows["shadow-sm"]};
|
||||
--shadow: ${lightShadows["shadow"]};
|
||||
--shadow-md: ${lightShadows["shadow-md"]};
|
||||
--shadow-lg: ${lightShadows["shadow-lg"]};
|
||||
--shadow-xl: ${lightShadows["shadow-xl"]};
|
||||
--shadow-2xl: ${lightShadows["shadow-2xl"]};
|
||||
}
|
||||
|
||||
.dark {
|
||||
${darkVars}
|
||||
--shadow-2xs: ${darkShadows["shadow-2xs"]};
|
||||
--shadow-xs: ${darkShadows["shadow-xs"]};
|
||||
--shadow-sm: ${darkShadows["shadow-sm"]};
|
||||
--shadow: ${darkShadows["shadow"]};
|
||||
--shadow-md: ${darkShadows["shadow-md"]};
|
||||
--shadow-lg: ${darkShadows["shadow-lg"]};
|
||||
--shadow-xl: ${darkShadows["shadow-xl"]};
|
||||
--shadow-2xl: ${darkShadows["shadow-2xl"]};
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}`;
|
||||
}
|
||||
|
||||
export function generateV0LayoutTsx(themeStyles: ThemeStyles): string {
|
||||
const fonts = extractGoogleFonts(themeStyles);
|
||||
const hasFonts = fonts.length > 0;
|
||||
|
||||
// Generate font imports
|
||||
const fontImports = hasFonts
|
||||
? `import { ${fonts.map((f) => f.family.replace(/\s+/g, "_")).join(", ")} } from "next/font/google";`
|
||||
: "";
|
||||
|
||||
// Generate font declarations
|
||||
const fontDeclarations = fonts
|
||||
.map(
|
||||
(f) => `const _${f.variableName} = ${f.family.replace(/\s+/g, "_")}({ subsets: ["latin"] });`
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
const htmlClassName = hasFonts ? `className="font-sans"` : "";
|
||||
|
||||
return `import type { Metadata } from "next";
|
||||
${fontImports}
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
${fontDeclarations}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" ${htmlClassName}>
|
||||
<body className="antialiased">
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}`;
|
||||
}
|
||||
|
||||
export function generateV0PageTsx(themeName: string): string {
|
||||
return `function ColorSwatch({ name, bgClass, label }: { name: string; bgClass: string; label: string }) {
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={\`w-12 h-12 rounded-md border shrink-0 \${bgClass}\`} />
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium truncate">{label}</p>
|
||||
<p className="text-xs text-muted-foreground font-mono">{name}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<main className="min-h-screen p-8 bg-background text-foreground">
|
||||
<div className="max-w-4xl mx-auto space-y-8">
|
||||
<div>
|
||||
<h1 className="text-4xl font-bold">${themeName}</h1>
|
||||
<p className="text-muted-foreground mt-2">A theme from tweakcn.</p>
|
||||
</div>
|
||||
|
||||
{/* Primary Colors */}
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-sm font-semibold text-muted-foreground">Primary Colors</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<ColorSwatch name="background" bgClass="bg-background" label="Background" />
|
||||
<ColorSwatch name="foreground" bgClass="bg-foreground" label="Foreground" />
|
||||
<ColorSwatch name="primary" bgClass="bg-primary" label="Primary" />
|
||||
<ColorSwatch name="primary-foreground" bgClass="bg-primary-foreground" label="Primary Foreground" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Secondary & Accent Colors */}
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-sm font-semibold text-muted-foreground">Secondary & Accent</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<ColorSwatch name="secondary" bgClass="bg-secondary" label="Secondary" />
|
||||
<ColorSwatch name="secondary-foreground" bgClass="bg-secondary-foreground" label="Secondary Foreground" />
|
||||
<ColorSwatch name="accent" bgClass="bg-accent" label="Accent" />
|
||||
<ColorSwatch name="accent-foreground" bgClass="bg-accent-foreground" label="Accent Foreground" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* UI Component Colors */}
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-sm font-semibold text-muted-foreground">UI Components</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<ColorSwatch name="card" bgClass="bg-card" label="Card" />
|
||||
<ColorSwatch name="card-foreground" bgClass="bg-card-foreground" label="Card Foreground" />
|
||||
<ColorSwatch name="popover" bgClass="bg-popover" label="Popover" />
|
||||
<ColorSwatch name="popover-foreground" bgClass="bg-popover-foreground" label="Popover Foreground" />
|
||||
<ColorSwatch name="muted" bgClass="bg-muted" label="Muted" />
|
||||
<ColorSwatch name="muted-foreground" bgClass="bg-muted-foreground" label="Muted Foreground" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Utility & Form Colors */}
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-sm font-semibold text-muted-foreground">Utility & Form</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<ColorSwatch name="border" bgClass="bg-border" label="Border" />
|
||||
<ColorSwatch name="input" bgClass="bg-input" label="Input" />
|
||||
<ColorSwatch name="ring" bgClass="bg-ring" label="Ring" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Status Colors */}
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-sm font-semibold text-muted-foreground">Status</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<ColorSwatch name="destructive" bgClass="bg-destructive" label="Destructive" />
|
||||
<ColorSwatch name="destructive-foreground" bgClass="bg-destructive-foreground" label="Destructive Foreground" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Chart Colors */}
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-sm font-semibold text-muted-foreground">Chart Colors</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||
<ColorSwatch name="chart-1" bgClass="bg-chart-1" label="Chart 1" />
|
||||
<ColorSwatch name="chart-2" bgClass="bg-chart-2" label="Chart 2" />
|
||||
<ColorSwatch name="chart-3" bgClass="bg-chart-3" label="Chart 3" />
|
||||
<ColorSwatch name="chart-4" bgClass="bg-chart-4" label="Chart 4" />
|
||||
<ColorSwatch name="chart-5" bgClass="bg-chart-5" label="Chart 5" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Sidebar Colors */}
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-sm font-semibold text-muted-foreground">Sidebar</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<ColorSwatch name="sidebar" bgClass="bg-sidebar" label="Sidebar" />
|
||||
<ColorSwatch name="sidebar-foreground" bgClass="bg-sidebar-foreground" label="Sidebar Foreground" />
|
||||
<ColorSwatch name="sidebar-primary" bgClass="bg-sidebar-primary" label="Sidebar Primary" />
|
||||
<ColorSwatch name="sidebar-primary-foreground" bgClass="bg-sidebar-primary-foreground" label="Sidebar Primary FG" />
|
||||
<ColorSwatch name="sidebar-accent" bgClass="bg-sidebar-accent" label="Sidebar Accent" />
|
||||
<ColorSwatch name="sidebar-accent-foreground" bgClass="bg-sidebar-accent-foreground" label="Sidebar Accent FG" />
|
||||
<ColorSwatch name="sidebar-border" bgClass="bg-sidebar-border" label="Sidebar Border" />
|
||||
<ColorSwatch name="sidebar-ring" bgClass="bg-sidebar-ring" label="Sidebar Ring" />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}`;
|
||||
}
|
||||
|
||||
export type V0RegistryFile = {
|
||||
path: string;
|
||||
content: string;
|
||||
type: "registry:file" | "registry:page";
|
||||
target: string;
|
||||
};
|
||||
|
||||
export type V0RegistryPayload = {
|
||||
name: string;
|
||||
files: V0RegistryFile[];
|
||||
type: "registry:item";
|
||||
};
|
||||
|
||||
export function generateV0RegistryPayload(
|
||||
themeName: string,
|
||||
themeStyles: ThemeStyles
|
||||
): V0RegistryPayload {
|
||||
return {
|
||||
name: themeName,
|
||||
type: "registry:item",
|
||||
files: [
|
||||
{
|
||||
path: "app/globals.css",
|
||||
content: generateV0GlobalsCss(themeStyles),
|
||||
type: "registry:file",
|
||||
target: "app/globals.css",
|
||||
},
|
||||
{
|
||||
path: "app/layout.tsx",
|
||||
content: generateV0LayoutTsx(themeStyles),
|
||||
type: "registry:page",
|
||||
target: "app/layout.tsx",
|
||||
},
|
||||
{
|
||||
path: "app/page.tsx",
|
||||
content: generateV0PageTsx(themeName),
|
||||
type: "registry:page",
|
||||
target: "app/page.tsx",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -1,28 +1,54 @@
|
||||
import { defaultThemeState } from "../config/theme";
|
||||
import { ThemeStyles } from "../types/theme";
|
||||
import { useThemePresetStore } from "../store/theme-preset-store";
|
||||
import { defaultPresets } from "./theme-presets";
|
||||
|
||||
/**
|
||||
* Get built-in theme styles by name (without using store).
|
||||
* Use this for server-side code where store access is not available.
|
||||
* Returns null if the preset doesn't exist.
|
||||
*/
|
||||
export function getBuiltInThemeStyles(name: string): { name: string; styles: ThemeStyles } | null {
|
||||
const preset = defaultPresets[name];
|
||||
if (!preset) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const styles = mergePresetWithDefaults(preset.styles);
|
||||
return {
|
||||
name: preset.label || name,
|
||||
styles,
|
||||
};
|
||||
}
|
||||
|
||||
function mergePresetWithDefaults(presetStyles: {
|
||||
light?: Partial<ThemeStyles["light"]>;
|
||||
dark?: Partial<ThemeStyles["dark"]>;
|
||||
}): ThemeStyles {
|
||||
const defaultTheme = defaultThemeState.styles;
|
||||
return {
|
||||
light: {
|
||||
...defaultTheme.light,
|
||||
...(presetStyles.light || {}),
|
||||
},
|
||||
dark: {
|
||||
...defaultTheme.dark,
|
||||
...(presetStyles.light || {}),
|
||||
...(presetStyles.dark || {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getPresetThemeStyles(name: string): ThemeStyles {
|
||||
const defaultTheme = defaultThemeState.styles;
|
||||
if (name === "default") {
|
||||
return defaultTheme;
|
||||
return defaultThemeState.styles;
|
||||
}
|
||||
|
||||
const store = useThemePresetStore.getState();
|
||||
const preset = store.getPreset(name);
|
||||
if (!preset) {
|
||||
return defaultTheme;
|
||||
return defaultThemeState.styles;
|
||||
}
|
||||
|
||||
return {
|
||||
light: {
|
||||
...defaultTheme.light,
|
||||
...(preset.styles.light || {}),
|
||||
},
|
||||
dark: {
|
||||
...defaultTheme.dark,
|
||||
...(preset.styles.light || {}),
|
||||
...(preset.styles.dark || {}),
|
||||
},
|
||||
};
|
||||
return mergePresetWithDefaults(preset.styles);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user