Skip to main content
Glama
Jpisnice
by Jpisnice
theme-style-generator.ts16.9 kB
// ============================================================================ // STANDALONE THEME STYLE GENERATOR - No External Dependencies // ============================================================================ // Types type ThemeMode = "light" | "dark"; type ColorFormat = "hsl" | "rgb" | "oklch" | "hex"; interface ThemeStyleProps { background: string; foreground: string; card: string; "card-foreground": string; popover: string; "popover-foreground": string; primary: string; "primary-foreground": string; secondary: string; "secondary-foreground": string; muted: string; "muted-foreground": string; accent: string; "accent-foreground": string; destructive: string; "destructive-foreground": string; border: string; input: string; ring: string; "chart-1": string; "chart-2": string; "chart-3": string; "chart-4": string; "chart-5": string; sidebar: string; "sidebar-foreground": string; "sidebar-primary": string; "sidebar-primary-foreground": string; "sidebar-accent": string; "sidebar-accent-foreground": string; "sidebar-border": string; "sidebar-ring": string; "font-sans": string; "font-serif": string; "font-mono": string; radius: string; "shadow-color": string; "shadow-opacity": string; "shadow-blur": string; "shadow-spread": string; "shadow-offset-x": string; "shadow-offset-y": string; "letter-spacing": string; spacing?: string; } interface ThemeStyles { light: ThemeStyleProps; dark: ThemeStyleProps; } interface ThemeEditorState { preset?: string; styles: ThemeStyles; currentMode: "light" | "dark"; hslAdjustments?: { hueShift: number; saturationScale: number; lightnessScale: number; }; } // Default Light Theme Styles const defaultLightThemeStyles: ThemeStyleProps = { background: "oklch(1 0 0)", foreground: "oklch(0.145 0 0)", card: "oklch(1 0 0)", "card-foreground": "oklch(0.145 0 0)", popover: "oklch(1 0 0)", "popover-foreground": "oklch(0.145 0 0)", primary: "oklch(0.205 0 0)", "primary-foreground": "oklch(0.985 0 0)", secondary: "oklch(0.97 0 0)", "secondary-foreground": "oklch(0.205 0 0)", muted: "oklch(0.97 0 0)", "muted-foreground": "oklch(0.556 0 0)", accent: "oklch(0.97 0 0)", "accent-foreground": "oklch(0.205 0 0)", destructive: "oklch(0.577 0.245 27.325)", "destructive-foreground": "oklch(1 0 0)", border: "oklch(0.922 0 0)", input: "oklch(0.922 0 0)", ring: "oklch(0.708 0 0)", "chart-1": "oklch(0.81 0.10 252)", "chart-2": "oklch(0.62 0.19 260)", "chart-3": "oklch(0.55 0.22 263)", "chart-4": "oklch(0.49 0.22 264)", "chart-5": "oklch(0.42 0.18 266)", radius: "0.625rem", sidebar: "oklch(0.985 0 0)", "sidebar-foreground": "oklch(0.145 0 0)", "sidebar-primary": "oklch(0.205 0 0)", "sidebar-primary-foreground": "oklch(0.985 0 0)", "sidebar-accent": "oklch(0.97 0 0)", "sidebar-accent-foreground": "oklch(0.205 0 0)", "sidebar-border": "oklch(0.922 0 0)", "sidebar-ring": "oklch(0.708 0 0)", "font-sans": "ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'", "font-serif": 'ui-serif, Georgia, Cambria, "Times New Roman", Times, serif', "font-mono": 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', "shadow-color": "oklch(0 0 0)", "shadow-opacity": "0.1", "shadow-blur": "3px", "shadow-spread": "0px", "shadow-offset-x": "0", "shadow-offset-y": "1px", "letter-spacing": "0em", spacing: "0.25rem", }; // Color Converter (inlined) const formatNumber = (num?: number) => { if (!num) return "0"; return num % 1 === 0 ? num : num.toFixed(4); }; const colorFormatter = ( colorValue: string, format: ColorFormat = "hsl", tailwindVersion: "3" | "4" = "3" ): string => { // For standalone version, return the color as-is if it's already in a valid format // This avoids the culori dependency if (format === "hsl" && colorValue.includes("oklch")) { // Simple oklch to hsl placeholder - returns the value as-is return colorValue; } return colorValue; }; // Shadow Map Generator (inlined & simplified) const getShadowMap = (themeStyles: ThemeStyles, currentMode: ThemeMode) => { const styles = themeStyles[currentMode]; const shadowColor = styles["shadow-color"]; const offsetX = styles["shadow-offset-x"]; const offsetY = styles["shadow-offset-y"]; const blur = styles["shadow-blur"]; const spread = styles["shadow-spread"]; const opacity = parseFloat(styles["shadow-opacity"]); const color = (opacityMultiplier: number) => `${shadowColor} / ${(opacity * opacityMultiplier).toFixed(2)}`; const secondLayer = (fixedOffsetY: string, fixedBlur: string): string => { const offsetX2 = offsetX; const offsetY2 = fixedOffsetY; const blur2 = fixedBlur; const spread2 = (parseFloat(spread?.replace("px", "") ?? "0") - 1).toString() + "px"; const color2 = color(1.0); return `${offsetX2} ${offsetY2} ${blur2} ${spread2} ${color2}`; }; const shadowMap: Record<string, string> = { "shadow-2xs": `${offsetX} ${offsetY} ${blur} ${spread} ${color(0.5)}`, "shadow-xs": `${offsetX} ${offsetY} ${blur} ${spread} ${color(0.5)}`, "shadow-2xl": `${offsetX} ${offsetY} ${blur} ${spread} ${color(2.5)}`, "shadow-sm": `${offsetX} ${offsetY} ${blur} ${spread} ${color( 1.0 )}, ${secondLayer("1px", "2px")}`, shadow: `${offsetX} ${offsetY} ${blur} ${spread} ${color(1.0)}, ${secondLayer("1px", "2px")}`, "shadow-md": `${offsetX} ${offsetY} ${blur} ${spread} ${color( 1.0 )}, ${secondLayer("2px", "4px")}`, "shadow-lg": `${offsetX} ${offsetY} ${blur} ${spread} ${color( 1.0 )}, ${secondLayer("4px", "6px")}`, "shadow-xl": `${offsetX} ${offsetY} ${blur} ${spread} ${color( 1.0 )}, ${secondLayer("8px", "10px")}`, }; return shadowMap; }; const generateColorVariables = ( themeStyles: ThemeStyles, mode: ThemeMode, formatColor: (color: string) => string ): string => { const styles = themeStyles[mode]; 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"])}; --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"])};`; }; const generateFontVariables = (themeStyles: ThemeStyles, mode: ThemeMode): string => { const styles = themeStyles[mode]; return ` --font-sans: ${styles["font-sans"]}; --font-serif: ${styles["font-serif"]}; --font-mono: ${styles["font-mono"]};`; }; const generateShadowVariables = (shadowMap: Record<string, string>): string => { return ` --shadow-2xs: ${shadowMap["shadow-2xs"]}; --shadow-xs: ${shadowMap["shadow-xs"]}; --shadow-sm: ${shadowMap["shadow-sm"]}; --shadow: ${shadowMap["shadow"]}; --shadow-md: ${shadowMap["shadow-md"]}; --shadow-lg: ${shadowMap["shadow-lg"]}; --shadow-xl: ${shadowMap["shadow-xl"]}; --shadow-2xl: ${shadowMap["shadow-2xl"]};`; }; const generateRawShadowVariables = (themeStyles: ThemeStyles, mode: ThemeMode): string => { const styles = themeStyles[mode]; return ` --shadow-x: ${styles["shadow-offset-x"]}; --shadow-y: ${styles["shadow-offset-y"]}; --shadow-blur: ${styles["shadow-blur"]}; --shadow-spread: ${styles["shadow-spread"]}; --shadow-opacity: ${styles["shadow-opacity"]}; --shadow-color: ${styles["shadow-color"]};`; }; 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);`; }; const generateThemeVariables = ( themeStyles: ThemeStyles, mode: ThemeMode, formatColor: (color: string) => string ): string => { const selector = mode === "dark" ? ".dark" : ":root"; const colorVars = generateColorVariables(themeStyles, mode, formatColor); const fontVars = generateFontVariables(themeStyles, mode); const radiusVar = `\n --radius: ${themeStyles[mode].radius};`; const shadowVars = generateShadowVariables(getShadowMap(themeStyles, mode)); const rawShadowVars = generateRawShadowVariables(themeStyles, mode); const spacingVar = mode === "light" ? `\n --spacing: ${themeStyles["light"].spacing ?? defaultLightThemeStyles.spacing};` : ""; const trackingVars = mode === "light" ? `\n --tracking-normal: ${themeStyles["light"]["letter-spacing"] ?? defaultLightThemeStyles["letter-spacing"]};` : ""; return ( selector + " {" + colorVars + fontVars + radiusVar + rawShadowVars + shadowVars + trackingVars + spacingVar + "\n}" ); }; const generateTailwindV4ThemeInline = (themeStyles: ThemeStyles): string => { return `@theme inline { --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); --font-sans: var(--font-sans); --font-mono: var(--font-mono); --font-serif: var(--font-serif); --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); --radius-xl: calc(var(--radius) + 4px); --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)} }`; }; const generateTailwindV3Config = ( _themeStyles: ThemeStyles, colorFormat: ColorFormat = "hsl" ): string => { const colorToken = (key: string) => { return colorFormat === "hsl" ? `"hsl(var(--${key}))"` : `"var(--${key})"`; }; return `/** @type {import('tailwindcss').Config} */ module.exports = { darkMode: ["class"], theme: { extend: { colors: { border: ${colorToken("border")}, input: ${colorToken("input")}, ring: ${colorToken("ring")}, background: ${colorToken("background")}, foreground: ${colorToken("foreground")}, primary: { DEFAULT: ${colorToken("primary")}, foreground: ${colorToken("primary-foreground")}, }, secondary: { DEFAULT: ${colorToken("secondary")}, foreground: ${colorToken("secondary-foreground")}, }, destructive: { DEFAULT: ${colorToken("destructive")}, foreground: ${colorToken("destructive-foreground")}, }, muted: { DEFAULT: ${colorToken("muted")}, foreground: ${colorToken("muted-foreground")}, }, accent: { DEFAULT: ${colorToken("accent")}, foreground: ${colorToken("accent-foreground")}, }, popover: { DEFAULT: ${colorToken("popover")}, foreground: ${colorToken("popover-foreground")}, }, card: { DEFAULT: ${colorToken("card")}, foreground: ${colorToken("card-foreground")}, }, sidebar: { DEFAULT: ${colorToken("sidebar")}, foreground: ${colorToken("sidebar-foreground")}, primary: ${colorToken("sidebar-primary")}, "primary-foreground": ${colorToken("sidebar-primary-foreground")}, accent: ${colorToken("sidebar-accent")}, "accent-foreground": ${colorToken("sidebar-accent-foreground")}, border: ${colorToken("sidebar-border")}, ring: ${colorToken("sidebar-ring")}, }, chart: { 1: ${colorToken("chart-1")}, 2: ${colorToken("chart-2")}, 3: ${colorToken("chart-3")}, 4: ${colorToken("chart-4")}, 5: ${colorToken("chart-5")}, }, }, borderRadius: { xl: "calc(var(--radius) + 4px)", lg: "var(--radius)", md: "calc(var(--radius) - 2px)", sm: "calc(var(--radius) - 4px)", }, fontFamily: { sans: ["var(--font-sans)"], serif: ["var(--font-serif)"], mono: ["var(--font-mono)"], }, }, }, }`; }; export const generateThemeCode = ( themeEditorState: ThemeEditorState, colorFormat: ColorFormat = "hsl", tailwindVersion: "3" | "4" = "3" ): string => { if ( !themeEditorState || !("light" in themeEditorState.styles) || !("dark" in themeEditorState.styles) ) { throw new Error("Invalid theme styles: missing light or dark mode"); } 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 tailwindV4Theme = tailwindVersion === "4" ? `\n\n${generateTailwindV4ThemeInline(themeStyles)}` : ""; const bodyLetterSpacing = themeStyles["light"]["letter-spacing"] !== "0em" ? "\n\nbody {\n letter-spacing: var(--tracking-normal);\n}" : ""; return `${lightTheme}\n\n${darkTheme}${tailwindV4Theme}${bodyLetterSpacing}`; }; export const generateTailwindConfigCode = ( themeEditorState: ThemeEditorState, colorFormat: ColorFormat = "hsl", _tailwindVersion: "3" | "4" = "3" ): string => { if ( !themeEditorState || !("light" in themeEditorState.styles) || !("dark" in themeEditorState.styles) ) { throw new Error("Invalid theme styles: missing light or dark mode"); } const themeStyles = themeEditorState.styles as ThemeStyles; return generateTailwindV3Config(themeStyles, colorFormat); };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Jpisnice/shadcn-ui-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server