Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
126 changes: 83 additions & 43 deletions components/editor/code-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
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 "../ui/tabs";
import { ColorFormat } from "../../types";
import { Select, SelectContent, SelectTrigger, SelectValue, SelectItem } from "../ui/select";
import { usePostHog } from "posthog-js/react";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { useDialogActions } from "@/hooks/use-dialog-actions";
import { useEditorStore } from "@/store/editor-store";
import { usePreferencesStore } from "@/store/preferences-store";
import { generateThemeCode } 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 { generateThemeCode } from "@/utils/theme-style-generator";
import { Check, Copy, Heart, Settings } from "lucide-react";
import { usePostHog } from "posthog-js/react";
import { useMemo, useState } from "react";

interface CodePanelProps {
themeEditorState: ThemeEditorState;
Expand All @@ -26,9 +34,11 @@ const CodePanel: React.FC<CodePanelProps> = ({ themeEditorState }) => {
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 @@ -37,7 +47,9 @@ const CodePanel: React.FC<CodePanelProps> = ({ themeEditorState }) => {
);
const getAvailableColorFormats = usePreferencesStore((state) => state.getAvailableColorFormats);

const code = generateThemeCode(themeEditorState, colorFormat, tailwindVersion);
const code = generateThemeCode(themeEditorState, colorFormat, tailwindVersion, {
includeFontVariables,
});

const getRegistryCommand = (preset: string) => {
const url = isSavedPreset
Expand Down Expand Up @@ -123,7 +135,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 @@ -158,36 +170,64 @@ 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");
}
}}
>
<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>
<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>
{getAvailableColorFormats().map((colorFormat) => (
<SelectItem key={colorFormat} value={colorFormat}>
{colorFormat}
</SelectItem>
))}
</SelectContent>
</Select>

<div className="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 h-8 w-fit gap-1 border-none outline-hidden focus:border-none focus:ring-transparent">
<SelectValue className="focus:ring-transparent" />
</SelectTrigger>
<SelectContent>
<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 h-8 w-fit gap-1 border-none outline-hidden focus:border-none focus:ring-transparent">
<SelectValue className="focus:ring-transparent" />
</SelectTrigger>
<SelectContent>
{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="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
defaultValue="index.css"
Expand All @@ -205,7 +245,7 @@ const CodePanel: React.FC<CodePanelProps> = ({ themeEditorState }) => {
variant="outline"
size="sm"
onClick={() => copyToClipboard(code)}
className="h-8"
className="h-8 max-md:w-8"
aria-label={copied ? "Copied to clipboard" : "Copy to clipboard"}
>
{copied ? (
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
124 changes: 76 additions & 48 deletions utils/theme-style-generator.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -68,29 +68,21 @@ const generateShadowVariables = (shadowMap: Record<string, string>): 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);`;
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 })
Expand All @@ -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);
Expand Down Expand Up @@ -151,32 +171,40 @@ 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 +
"}"
);
};

export const generateThemeCode = (
themeEditorState: ThemeEditorState,
colorFormat: ColorFormat = "hsl",
tailwindVersion: "3" | "4" = "3"
tailwindVersion: "3" | "4" = "3",
preferences: GenerateVarsPreferences = {}
): string => {
if (
!themeEditorState ||
Expand All @@ -189,10 +217,10 @@ export const generateThemeCode = (
const themeStyles = themeEditorState.styles as ThemeStyles;
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"
Expand Down