diff --git a/components/editor/code-panel.tsx b/components/editor/code-panel.tsx index f37419b3..f1a36492 100644 --- a/components/editor/code-panel.tsx +++ b/components/editor/code-panel.tsx @@ -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 = ({ themeEditorState }) => { const [registryCopied, setRegistryCopied] = useState(false); + const [activeTab, setActiveTab] = useState(EXPORT_CODE_TABS.CSS_CODE); const [copied, setCopied] = useState(false); - const [activeTab, setActiveTab] = useState("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); @@ -51,8 +63,12 @@ const CodePanel: React.FC = ({ 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 @@ -105,6 +121,13 @@ const CodePanel: React.FC = ({ 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 }) => (
{(["pnpm", "npm", "yarn", "bun"] as const).map((pm) => ( @@ -125,7 +148,7 @@ const CodePanel: React.FC = ({ themeEditorState }) => { ); return ( -
+

Theme Code

@@ -138,7 +161,7 @@ const CodePanel: React.FC = ({ 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 ? : } @@ -173,51 +196,80 @@ const CodePanel: React.FC = ({ themeEditorState }) => {
-
- - +
+
+ + +
+ + + + + + + +
+
+ Include font variables + + If you handle fonts separately, turn this OFF. + +
+ setIncludeFontVariables(checked)} + /> +
+
+
- + index.css {tailwindVersion === "3" && ( - + tailwind.config.ts )} @@ -228,8 +280,10 @@ const CodePanel: React.FC = ({ themeEditorState }) => {
- +
               {code}
@@ -258,7 +312,7 @@ const CodePanel: React.FC = ({ themeEditorState }) => {
         
 
         {tailwindVersion === "3" && (
-          
+          
             
               
                 {configCode}
diff --git a/store/preferences-store.ts b/store/preferences-store.ts
index 3a2ef45b..e8445f95 100644
--- a/store/preferences-store.ts
+++ b/store/preferences-store.ts
@@ -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";
@@ -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;
@@ -29,6 +31,7 @@ export const usePreferencesStore = create()(
     (set, get) => ({
       tailwindVersion: "4",
       colorFormat: "oklch",
+      includeFontVariables: true,
       packageManager: "pnpm",
       colorSelectorTab: "list",
       chatSuggestionsOpen: true,
@@ -46,6 +49,9 @@ export const usePreferencesStore = create()(
           set({ colorFormat: format });
         }
       },
+      setIncludeFontVariables: (includeFontVars: boolean) => {
+        set({ includeFontVariables: includeFontVars });
+      },
       setPackageManager: (pm: PackageManager) => {
         set({ packageManager: pm });
       },
diff --git a/utils/theme-style-generator.ts b/utils/theme-style-generator.ts
index bb856ebe..e0047d61 100644
--- a/utils/theme-style-generator.ts
+++ b/utils/theme-style-generator.ts
@@ -1,9 +1,9 @@
-import { ThemeEditorState } from "@/types/editor";
-import { colorFormatter } from "./color-converter";
-import { ColorFormat } from "../types";
-import { getShadowMap } from "./shadows";
 import { defaultLightThemeStyles } from "@/config/theme";
+import { ColorFormat } from "@/types";
+import { ThemeEditorState } from "@/types/editor";
 import { ThemeStyles } from "@/types/theme";
+import { colorFormatter } from "@/utils/color-converter";
+import { getShadowMap } from "@/utils/shadows";
 
 type ThemeMode = "light" | "dark";
 
@@ -68,29 +68,21 @@ const generateShadowVariables = (shadowMap: Record): string => {
   --shadow-2xl: ${shadowMap["shadow-2xl"]};`;
 };
 
-const generateTrackingVariables = (themeStyles: ThemeStyles): string => {
-  const styles = themeStyles["light"];
-  if (styles["letter-spacing"] === "0em") {
-    return "";
-  }
-  return `
-
-  --tracking-tighter: calc(var(--tracking-normal) - 0.05em);
-  --tracking-tight: calc(var(--tracking-normal) - 0.025em);
-  --tracking-normal: var(--tracking-normal);
-  --tracking-wide: calc(var(--tracking-normal) + 0.025em);
-  --tracking-wider: calc(var(--tracking-normal) + 0.05em);
-  --tracking-widest: calc(var(--tracking-normal) + 0.1em);`;
+export type GenerateVarsPreferences = {
+  includeFontVariables?: boolean;
 };
 
 const generateThemeVariables = (
   themeStyles: ThemeStyles,
   mode: ThemeMode,
-  formatColor: (color: string) => string
-): string => {
+  formatColor: (color: string) => string,
+  preferences: GenerateVarsPreferences
+) => {
+  const { includeFontVariables = true } = preferences;
+
   const selector = mode === "dark" ? ".dark" : ":root";
   const colorVars = generateColorVariables(themeStyles, mode, formatColor);
-  const fontVars = generateFontVariables(themeStyles, mode);
+  const fontVars = includeFontVariables ? generateFontVariables(themeStyles, mode) : "";
   const radiusVar = `\n  --radius: ${themeStyles[mode].radius};`;
   const shadowVars = generateShadowVariables(
     getShadowMap({ styles: themeStyles, currentMode: mode })
@@ -105,22 +97,50 @@ const generateThemeVariables = (
       ? `\n  --tracking-normal: ${themeStyles["light"]["letter-spacing"] ?? defaultLightThemeStyles["letter-spacing"]};`
       : "";
 
-  return (
-    selector +
-    " {" +
-    colorVars +
-    fontVars +
-    radiusVar +
-    shadowVars +
-    trackingVars +
-    spacingVar +
-    "\n}"
-  );
+  if (mode === "light") {
+    return (
+      selector +
+      " {" +
+      fontVars +
+      radiusVar +
+      colorVars +
+      shadowVars +
+      trackingVars +
+      spacingVar +
+      "\n}"
+    );
+  }
+
+  if (mode === "dark") {
+    return selector + " {" + colorVars + shadowVars + "\n}";
+  }
 };
 
-const generateTailwindV4ThemeInline = (themeStyles: ThemeStyles): string => {
-  return `@theme inline {
-  --color-background: var(--background);
+const generateInlineTrackingVariables = (themeStyles: ThemeStyles): string => {
+  const styles = themeStyles["light"];
+  if (styles["letter-spacing"] === "0em") return "";
+
+  return `\n  --tracking-tighter: calc(var(--tracking-normal) - 0.05em);
+  --tracking-tight: calc(var(--tracking-normal) - 0.025em);
+  --tracking-normal: var(--tracking-normal);
+  --tracking-wide: calc(var(--tracking-normal) + 0.025em);
+  --tracking-wider: calc(var(--tracking-normal) + 0.05em);
+  --tracking-widest: calc(var(--tracking-normal) + 0.1em);\n`;
+};
+
+const generateTailwindV4ThemeInline = (
+  themeStyles: ThemeStyles,
+  preferences: GenerateVarsPreferences
+): string => {
+  const { includeFontVariables = true } = preferences;
+
+  const fontVarsInline = includeFontVariables
+    ? `\n  --font-sans: var(--font-sans);
+  --font-mono: var(--font-mono);
+  --font-serif: var(--font-serif);\n`
+    : "";
+
+  const colorVarsInline = `\n  --color-background: var(--background);
   --color-foreground: var(--foreground);
   --color-card: var(--card);
   --color-card-foreground: var(--card-foreground);
@@ -151,34 +171,56 @@ const generateTailwindV4ThemeInline = (themeStyles: ThemeStyles): string => {
   --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);
-
-  --font-sans: var(--font-sans);
-  --font-mono: var(--font-mono);
-  --font-serif: var(--font-serif);
+  --color-sidebar-ring: var(--sidebar-ring);\n`;
 
-  --radius-sm: calc(var(--radius) - 4px);
+  const radiusVarsInline = `\n  --radius-sm: calc(var(--radius) - 4px);
   --radius-md: calc(var(--radius) - 2px);
   --radius-lg: var(--radius);
-  --radius-xl: calc(var(--radius) + 4px);
+  --radius-xl: calc(var(--radius) + 4px);\n`;
 
-  --shadow-2xs: var(--shadow-2xs);
+  const shadowVarsInline = `\n  --shadow-2xs: var(--shadow-2xs);
   --shadow-xs: var(--shadow-xs);
   --shadow-sm: var(--shadow-sm);
   --shadow: var(--shadow);
   --shadow-md: var(--shadow-md);
   --shadow-lg: var(--shadow-lg);
   --shadow-xl: var(--shadow-xl);
-  --shadow-2xl: var(--shadow-2xl);${generateTrackingVariables(themeStyles)}
-}`;
+  --shadow-2xl: var(--shadow-2xl);\n`;
+
+  const trackingVarsInline = generateInlineTrackingVariables(themeStyles);
+
+  return (
+    "@theme inline {" +
+    fontVarsInline +
+    colorVarsInline +
+    radiusVarsInline +
+    shadowVarsInline +
+    trackingVarsInline +
+    "}"
+  );
 };
 
-const generateTailwindV3Config = (_themeStyles: ThemeStyles): string => {
-  return `/** @type {import('tailwindcss').Config} */
+const generateTailwindV3ConfigFile = (
+  _themeStyles: ThemeStyles,
+  preferences: GenerateVarsPreferences
+): string => {
+  const { includeFontVariables = true } = preferences;
+
+  const fontFamilyBlock = includeFontVariables
+    ? `fontFamily: {
+        sans: "var(--font-sans)",
+        serif: "var(--font-serif)",
+        mono: "var(--font-mono)",
+      },`
+    : "";
+
+  const code = `
+/** @type {import('tailwindcss').Config} */
 module.exports = {
   darkMode: ["class"],
   theme: {
     extend: {
+      ${fontFamilyBlock}
       colors: {
         border: "var(--border)",
         input: "var(--input)",
@@ -236,20 +278,22 @@ module.exports = {
         md: "calc(var(--radius) - 2px)",
         sm: "calc(var(--radius) - 4px)",
       },
-      fontFamily: {
-        sans: ["var(--font-sans)"],
-        serif: ["var(--font-serif)"],
-        mono: ["var(--font-mono)"],
-      },
     },
   },
 }`;
+
+  return code
+    .trim()
+    .split("\n")
+    .filter((line) => line.trim() !== "")
+    .join("\n");
 };
 
 export const generateThemeCode = (
   themeEditorState: ThemeEditorState,
   colorFormat: ColorFormat = "hsl",
-  tailwindVersion: "3" | "4" = "3"
+  tailwindVersion: "3" | "4" = "3",
+  preferences: GenerateVarsPreferences = {}
 ): string => {
   if (
     !themeEditorState ||
@@ -259,13 +303,13 @@ export const generateThemeCode = (
     throw new Error("Invalid theme styles: missing light or dark mode");
   }
 
-  const themeStyles = themeEditorState.styles as ThemeStyles;
+  const themeStyles = themeEditorState.styles;
   const formatColor = (color: string) => colorFormatter(color, colorFormat, tailwindVersion);
 
-  const lightTheme = generateThemeVariables(themeStyles, "light", formatColor);
-  const darkTheme = generateThemeVariables(themeStyles, "dark", formatColor);
+  const lightTheme = generateThemeVariables(themeStyles, "light", formatColor, preferences);
+  const darkTheme = generateThemeVariables(themeStyles, "dark", formatColor, preferences);
   const tailwindV4Theme =
-    tailwindVersion === "4" ? `\n\n${generateTailwindV4ThemeInline(themeStyles)}` : "";
+    tailwindVersion === "4" ? `\n\n${generateTailwindV4ThemeInline(themeStyles, preferences)}` : "";
 
   const bodyLetterSpacing =
     themeStyles["light"]["letter-spacing"] !== "0em"
@@ -275,9 +319,9 @@ export const generateThemeCode = (
   return `${lightTheme}\n\n${darkTheme}${tailwindV4Theme}${bodyLetterSpacing}`;
 };
 
-export const generateTailwindConfigCode = (
+export const generateTailwindConfigFileCode = (
   themeEditorState: ThemeEditorState,
-  _tailwindVersion: "3" | "4" = "3"
+  preferences: GenerateVarsPreferences = {}
 ): string => {
   if (
     !themeEditorState ||
@@ -287,6 +331,6 @@ export const generateTailwindConfigCode = (
     throw new Error("Invalid theme styles: missing light or dark mode");
   }
 
-  const themeStyles = themeEditorState.styles as ThemeStyles;
-  return generateTailwindV3Config(themeStyles);
+  const themeStyles = themeEditorState.styles;
+  return generateTailwindV3ConfigFile(themeStyles, preferences);
 };