Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 109 additions & 55 deletions components/editor/code-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,60 @@
import { useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import { Copy, Check, Heart } from "lucide-react";
import { ThemeEditorState } from "@/types/editor";
import { ScrollArea, ScrollBar } from "../ui/scroll-area";
// import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import {
Tabs,
TabsList,
TabsTrigger,
TabsContent,
TabsIndicator,
TabsList,
TabsTrigger,
} from "@/components/ui/base-ui-tabs";
import { Button } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
SelectItem,
} from "@/components/ui/select";
import { usePostHog } from "posthog-js/react";
import { Switch } from "@/components/ui/switch";
import { useDialogActions } from "@/hooks/use-dialog-actions";
import { useEditorStore } from "@/store/editor-store";
import { usePreferencesStore } from "@/store/preferences-store";
import { generateThemeCode, generateTailwindConfigCode } from "@/utils/theme-style-generator";
import { useThemePresetStore } from "@/store/theme-preset-store";
import { useDialogActions } from "@/hooks/use-dialog-actions";
import { ColorFormat } from "@/types";
import { ThemeEditorState } from "@/types/editor";
import {
generateTailwindConfigFileCode,
generateThemeCode,
GenerateVarsPreferences,
} from "@/utils/theme-style-generator";
import { Check, Copy, Heart, Settings } from "lucide-react";
import { usePostHog } from "posthog-js/react";
import { useEffect, useMemo, useState } from "react";

interface CodePanelProps {
themeEditorState: ThemeEditorState;
}

const EXPORT_CODE_TABS = {
CSS_CODE: "css-code",
TAILWIND_CONFIG_CODE: "tailwind-config-code",
};

const CodePanel: React.FC<CodePanelProps> = ({ themeEditorState }) => {
const [registryCopied, setRegistryCopied] = useState(false);
const [activeTab, setActiveTab] = useState(EXPORT_CODE_TABS.CSS_CODE);
const [copied, setCopied] = useState(false);
const [activeTab, setActiveTab] = useState<string>("index.css");
const posthog = usePostHog();
const { handleSaveClick } = useDialogActions();

const preset = useEditorStore((state) => state.themeState.preset);
const colorFormat = usePreferencesStore((state) => state.colorFormat);
const tailwindVersion = usePreferencesStore((state) => state.tailwindVersion);
const includeFontVariables = usePreferencesStore((state) => state.includeFontVariables);
const packageManager = usePreferencesStore((state) => state.packageManager);
const setColorFormat = usePreferencesStore((state) => state.setColorFormat);
const setTailwindVersion = usePreferencesStore((state) => state.setTailwindVersion);
const setIncludeFontVariables = usePreferencesStore((state) => state.setIncludeFontVariables);
const setPackageManager = usePreferencesStore((state) => state.setPackageManager);
const hasUnsavedChanges = useEditorStore((state) => state.hasUnsavedChanges);

Expand All @@ -51,8 +63,12 @@ const CodePanel: React.FC<CodePanelProps> = ({ themeEditorState }) => {
);
const getAvailableColorFormats = usePreferencesStore((state) => state.getAvailableColorFormats);

const code = generateThemeCode(themeEditorState, colorFormat, tailwindVersion);
const configCode = generateTailwindConfigCode(themeEditorState, tailwindVersion);
const preferences: GenerateVarsPreferences = {
includeFontVariables,
};

const code = generateThemeCode(themeEditorState, colorFormat, tailwindVersion, preferences);
const configCode = generateTailwindConfigFileCode(themeEditorState, preferences);

const getRegistryCommand = (preset: string) => {
const url = isSavedPreset
Expand Down Expand Up @@ -105,6 +121,13 @@ const CodePanel: React.FC<CodePanelProps> = ({ themeEditorState }) => {
return preset && preset !== "default" && !hasUnsavedChanges();
}, [preset, hasUnsavedChanges]);

// Auto-switch to CSS file when switching from v3 to v4
useEffect(() => {
if (tailwindVersion === "4" && activeTab === EXPORT_CODE_TABS.TAILWIND_CONFIG_CODE) {
setActiveTab(EXPORT_CODE_TABS.CSS_CODE);
}
}, [tailwindVersion, activeTab]);

const PackageManagerHeader = ({ actionButton }: { actionButton: React.ReactNode }) => (
<div className="flex border-b">
{(["pnpm", "npm", "yarn", "bun"] as const).map((pm) => (
Expand All @@ -125,7 +148,7 @@ const CodePanel: React.FC<CodePanelProps> = ({ themeEditorState }) => {
);

return (
<div className="flex h-full flex-col">
<div className="isolate flex h-full flex-col">
<div className="mb-4 flex-none">
<div className="flex items-center justify-between gap-2">
<h2 className="text-lg font-semibold">Theme Code</h2>
Expand All @@ -138,7 +161,7 @@ const CodePanel: React.FC<CodePanelProps> = ({ themeEditorState }) => {
variant="ghost"
size="sm"
onClick={copyRegistryCommand}
className="ml-auto h-8"
className="ml-auto size-8"
aria-label={registryCopied ? "Copied to clipboard" : "Copy to clipboard"}
>
{registryCopied ? <Check className="size-4" /> : <Copy className="size-4" />}
Expand Down Expand Up @@ -173,51 +196,80 @@ const CodePanel: React.FC<CodePanelProps> = ({ themeEditorState }) => {
</div>
</div>
</div>
<div className="mb-4 flex items-center gap-2">
<Select
value={tailwindVersion}
onValueChange={(value: "3" | "4") => {
setTailwindVersion(value);
if (value === "4" && colorFormat === "hsl") {
setColorFormat("oklch");
setActiveTab("index.css");
}
}}
>
<SelectTrigger className="bg-muted/50 w-fit gap-1 border-none outline-hidden focus:border-none focus:ring-transparent">
<SelectValue className="focus:ring-transparent" />
</SelectTrigger>
<SelectContent className="z-99999">
<SelectItem value="3">Tailwind v3</SelectItem>
<SelectItem value="4">Tailwind v4</SelectItem>
</SelectContent>
</Select>
<Select value={colorFormat} onValueChange={(value: ColorFormat) => setColorFormat(value)}>
<SelectTrigger className="bg-muted/50 w-fit gap-1 border-none outline-hidden focus:border-none focus:ring-transparent">
<SelectValue className="focus:ring-transparent" />
</SelectTrigger>
<SelectContent className="z-99999">
{getAvailableColorFormats().map((colorFormat) => (
<SelectItem key={colorFormat} value={colorFormat}>
{colorFormat}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="relative isolate mb-4 flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<Select
value={tailwindVersion}
onValueChange={(value: "3" | "4") => {
setTailwindVersion(value);
if (value === "4" && colorFormat === "hsl") {
setColorFormat("oklch");
}
}}
>
<SelectTrigger className="bg-muted/50 w-fit gap-1 border-none outline-hidden focus:border-none focus:ring-transparent">
<SelectValue className="focus:ring-transparent" />
</SelectTrigger>
<SelectContent className="z-99999">
<SelectItem value="3">Tailwind v3</SelectItem>
<SelectItem value="4">Tailwind v4</SelectItem>
</SelectContent>
</Select>
<Select value={colorFormat} onValueChange={(value: ColorFormat) => setColorFormat(value)}>
<SelectTrigger className="bg-muted/50 w-fit gap-1 border-none outline-hidden focus:border-none focus:ring-transparent">
<SelectValue className="focus:ring-transparent" />
</SelectTrigger>
<SelectContent className="z-99999">
{getAvailableColorFormats().map((colorFormat) => (
<SelectItem key={colorFormat} value={colorFormat}>
{colorFormat}
</SelectItem>
))}
</SelectContent>
</Select>
</div>

<Popover>
<PopoverTrigger asChild>
<Button variant="outline" size="sm" className="gap-1.5 shadow-sm max-md:w-8">
<Settings />
<span className="sr-only md:not-sr-only">Preferences</span>
</Button>
</PopoverTrigger>

<PopoverContent align="end" className="z-99999 w-[300px] space-y-2">
<div className="flex justify-between gap-4 rounded-lg">
<div className="flex flex-col gap-1">
<span className="text-sm font-medium">Include font variables</span>
<span className="text-muted-foreground text-xs text-pretty">
If you handle fonts separately, turn this OFF.
</span>
</div>
<Switch
className="ml-auto shrink-0"
checked={includeFontVariables}
onCheckedChange={(checked) => setIncludeFontVariables(checked)}
/>
</div>
</PopoverContent>
</Popover>
</div>
<Tabs
value={activeTab}
onValueChange={setActiveTab}
defaultValue="index.css"
defaultValue={EXPORT_CODE_TABS.CSS_CODE}
className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-lg border"
>
<div className="bg-muted/50 flex flex-none items-center justify-between border-b px-4 py-2">
<TabsList className="h-8 bg-transparent p-0">
<TabsTrigger value="index.css" className="h-7 px-3 text-sm font-medium">
<TabsTrigger value={EXPORT_CODE_TABS.CSS_CODE} className="h-8 px-2 text-sm font-medium">
index.css
</TabsTrigger>
{tailwindVersion === "3" && (
<TabsTrigger value="tailwind.config.ts" className="h-7 px-3 text-sm font-medium">
<TabsTrigger
value={EXPORT_CODE_TABS.TAILWIND_CONFIG_CODE}
className="h-8 px-2 text-sm font-medium"
>
tailwind.config.ts
</TabsTrigger>
)}
Expand All @@ -228,8 +280,10 @@ const CodePanel: React.FC<CodePanelProps> = ({ themeEditorState }) => {
<Button
variant="outline"
size="sm"
onClick={() => copyToClipboard(activeTab === "index.css" ? code : configCode)}
className="h-8"
onClick={() =>
copyToClipboard(activeTab === EXPORT_CODE_TABS.CSS_CODE ? code : configCode)
}
className="h-8 max-md:w-8"
aria-label={copied ? "Copied to clipboard" : "Copy to clipboard"}
>
{copied ? (
Expand All @@ -247,7 +301,7 @@ const CodePanel: React.FC<CodePanelProps> = ({ themeEditorState }) => {
</div>
</div>

<TabsContent value="index.css" className="overflow-hidden">
<TabsContent value={EXPORT_CODE_TABS.CSS_CODE} className="overflow-hidden">
<ScrollArea className="relative h-full">
<pre className="h-full p-4 text-sm">
<code>{code}</code>
Expand All @@ -258,7 +312,7 @@ const CodePanel: React.FC<CodePanelProps> = ({ themeEditorState }) => {
</TabsContent>

{tailwindVersion === "3" && (
<TabsContent value="tailwind.config.ts" className="overflow-hidden">
<TabsContent value={EXPORT_CODE_TABS.TAILWIND_CONFIG_CODE} className="overflow-hidden">
<ScrollArea className="relative h-full">
<pre className="h-full p-4 text-sm">
<code>{configCode}</code>
Expand Down
8 changes: 7 additions & 1 deletion store/preferences-store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ColorFormat } from "@/types";
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { ColorFormat } from "@/types";

type PackageManager = "pnpm" | "npm" | "yarn" | "bun";
export type ColorSelectorTab = "list" | "palette";
Expand All @@ -13,11 +13,13 @@ const colorFormatsByVersion = {
interface PreferencesStore {
tailwindVersion: "3" | "4";
colorFormat: ColorFormat;
includeFontVariables: boolean;
packageManager: PackageManager;
colorSelectorTab: ColorSelectorTab;
chatSuggestionsOpen: boolean;
setTailwindVersion: (version: "3" | "4") => void;
setColorFormat: (format: ColorFormat) => void;
setIncludeFontVariables: (includeFontVars: boolean) => void;
setPackageManager: (pm: PackageManager) => void;
setColorSelectorTab: (tab: ColorSelectorTab) => void;
setChatSuggestionsOpen: (open: boolean) => void;
Expand All @@ -29,6 +31,7 @@ export const usePreferencesStore = create<PreferencesStore>()(
(set, get) => ({
tailwindVersion: "4",
colorFormat: "oklch",
includeFontVariables: true,
packageManager: "pnpm",
colorSelectorTab: "list",
chatSuggestionsOpen: true,
Expand All @@ -46,6 +49,9 @@ export const usePreferencesStore = create<PreferencesStore>()(
set({ colorFormat: format });
}
},
setIncludeFontVariables: (includeFontVars: boolean) => {
set({ includeFontVariables: includeFontVars });
},
setPackageManager: (pm: PackageManager) => {
set({ packageManager: pm });
},
Expand Down
Loading