replace contents with tweakcn-next

This commit is contained in:
Sahaj Jain
2025-04-17 14:18:28 +05:30
parent 8521b6a356
commit b3f5de8a3d
340 changed files with 33366 additions and 3383 deletions
+10
View File
@@ -0,0 +1,10 @@
export function applyStyleToElement(
element: HTMLElement,
key: string,
value: string
) {
element.setAttribute(
`style`,
`${element.getAttribute("style") || ""}--${key}: ${value};`
);
}
+44
View File
@@ -0,0 +1,44 @@
import * as culori from "culori";
import { ColorFormat } from "../types";
const formatNumber = (num?: number) => {
if (!num) return "0";
return num % 1 === 0 ? num : num.toFixed(2);
};
export const colorFormatter = (
colorValue: string,
format: ColorFormat = "hsl",
tailwindVersion: "3" | "4" = "3"
): string => {
try {
const color = culori.parse(colorValue);
if (!color) throw new Error("Invalid color input");
switch (format) {
case "hsl": {
const hsl = culori.converter("hsl")(color);
if (tailwindVersion === "4") {
return `hsl(${formatNumber(hsl.h)} ${formatNumber(hsl.s * 100)}% ${formatNumber(hsl.l * 100)}%)`;
}
return `${formatNumber(hsl.h)} ${formatNumber(hsl.s * 100)}% ${formatNumber(hsl.l * 100)}%`;
}
case "rgb":
return culori.formatRgb(color); // e.g., "rgb(64, 128, 192)"
case "oklch": {
const oklch = culori.converter("oklch")(color);
return `oklch(${formatNumber(oklch.l)} ${formatNumber(oklch.c)} ${formatNumber(oklch.h)})`;
}
case "hex":
return culori.formatHex(color); // e.g., "#4080c0"
default:
return colorValue;
}
} catch (error) {
console.error(`Failed to convert color: ${colorValue}`, error);
return colorValue;
}
};
export const convertToHSL = (colorValue: string): string =>
colorFormatter(colorValue, "hsl");
+9
View File
@@ -0,0 +1,9 @@
export function debounce(fn: (...args: any[]) => void, delay: number) {
let timeoutId;
return function (...args: any[]) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
+110
View File
@@ -0,0 +1,110 @@
import { ThemeStyleProps } from "@/types/theme";
import { colorFormatter } from "./color-converter";
export const variableNames = [
"background",
"foreground",
"card",
"card-foreground",
"popover",
"popover-foreground",
"primary",
"primary-foreground",
"secondary",
"secondary-foreground",
"muted",
"muted-foreground",
"accent",
"accent-foreground",
"destructive",
"destructive-foreground",
"border",
"input",
"ring",
"chart-1",
"chart-2",
"chart-3",
"chart-4",
"chart-5",
"radius",
"font-sans",
"font-serif",
"font-mono",
"shadow-color",
"shadow-opacity",
"shadow-blur",
"shadow-spread",
"shadow-offset-x",
"shadow-offset-y",
"shadow",
"shadow-2xs",
"shadow-xs",
"shadow-sm",
"shadow-md",
"shadow-lg",
"shadow-xl",
"shadow-2xl",
];
const nonColorVariables = ["font-sans", "font-serif", "font-mono", "radius"];
const VARIABLE_PREFIX = "--";
export const parseCssInput = (input: string) => {
const lightColors: ThemeStyleProps = {} as ThemeStyleProps;
const darkColors: ThemeStyleProps = {} as ThemeStyleProps;
try {
const rootContent = extractCssBlockContent(input, ":root");
const darkContent = extractCssBlockContent(input, ".dark");
if (rootContent) {
parseColorVariables(rootContent, lightColors, variableNames);
}
if (darkContent) {
parseColorVariables(darkContent, darkColors, variableNames);
}
} catch (error) {
console.error("Error parsing CSS input:", error);
}
return { lightColors, darkColors };
};
const extractCssBlockContent = (input: string, selector: string): string | null => {
const regex = new RegExp(`${escapeRegExp(selector)}\\s*{([^}]+)}`);
return input.match(regex)?.[1]?.trim() || null;
};
const parseColorVariables = (
cssContent: string,
target: ThemeStyleProps,
validNames: string[]
) => {
const variableDeclarations = cssContent.match(/--[^:]+:\s*[^;]+/g) || [];
variableDeclarations.forEach((declaration) => {
const [name, value] = declaration.split(":").map((part) => part.trim());
const cleanName = name.replace(VARIABLE_PREFIX, "");
if (validNames.includes(cleanName)) {
if (nonColorVariables.includes(cleanName)) {
target[cleanName] = value;
return;
}
const colorValue = processColorValue(value);
const formattedValue = colorFormatter(colorValue, "hex");
target[cleanName] = formattedValue;
}
});
};
const processColorValue = (value: string): string => {
return /^\d/.test(value) ? `hsl(${value})` : value;
};
// Helper function to escape regex special characters
const escapeRegExp = (string: string): string => {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
};
+80
View File
@@ -0,0 +1,80 @@
import { colorFormatter } from "./color-converter";
import { applyStyleToElement } from "./apply-style-to-element";
import { ThemeEditorState } from "../types/theme";
import { defaultThemeState } from "../config/theme";
export const getShadowMap = (themeEditorState: ThemeEditorState) => {
const mode = themeEditorState.currentMode;
const styles = {
...defaultThemeState.styles.light,
...defaultThemeState.styles[mode],
...themeEditorState.styles[mode],
};
const shadowColor = styles["shadow-color"];
const hsl = colorFormatter(shadowColor, "hsl", "3");
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) =>
`hsl(${hsl} / ${(opacity * opacityMultiplier).toFixed(2)})`;
const secondLayer = (fixedOffsetY: string, fixedBlur: string): string => {
// Use the same offsetX as the first layer
const offsetX2 = offsetX;
// Use the fixed offsetY specific to the shadow size
const offsetY2 = fixedOffsetY;
// Use the fixed blur specific to the shadow size
const blur2 = fixedBlur;
// Calculate spread relative to the first layer's spread variable
const spread2 =
(parseFloat(spread?.replace("px", "") ?? "0") - 1).toString() + "px";
// Use the same color function (opacity can still be overridden by --shadow-opacity)
const color2 = color(1.0); // Default opacity for second layer is 0.1 in examples
return `${offsetX2} ${offsetY2} ${blur2} ${spread2} ${color2}`;
};
// Map shadow names to their CSS variable string structures
const shadowMap: { [key: string]: string } = {
// Single layer shadows - use base variables directly
"shadow-2xs": `${offsetX} ${offsetY} ${blur} ${spread} ${color(0.5)}`, // Assumes vars set appropriately (e.g., y=1, blur=0, spread=0)
"shadow-xs": `${offsetX} ${offsetY} ${blur} ${spread} ${color(0.5)}`, // Assumes vars set appropriately (e.g., y=1, blur=2, spread=0)
"shadow-2xl": `${offsetX} ${offsetY} ${blur} ${spread} ${color(2.5)}`, // Assumes vars set appropriately (e.g., y=25, blur=50, spread=-12)
// Two layer shadows - use base vars for layer 1, mix fixed/calculated for layer 2
"shadow-sm": `${offsetX} ${offsetY} ${blur} ${spread} ${color(
1.0
)}, ${secondLayer("1px", "2px")}`,
shadow: `${offsetX} ${offsetY} ${blur} ${spread} ${color(1.0)}, ${secondLayer(
"1px",
"2px"
)}`, // Alias for the 'shadow:' example line
"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;
};
// Function to set shadow CSS variables
export function setShadowVariables(themeEditorState: ThemeEditorState) {
const root = document.documentElement;
const shadows = getShadowMap(themeEditorState);
Object.entries(shadows).forEach(([name, value]) => {
applyStyleToElement(root, name, value);
});
}
+85
View File
@@ -0,0 +1,85 @@
import { ThemeEditorState, ThemeStyleProps } from "../types/theme";
const sansSerifFontNames = [
"Inter",
"Roboto",
"Open Sans",
"Poppins",
"Montserrat",
"Outfit",
"Plus Jakarta Sans",
"DM Sans",
"Geist",
"Oxanium",
];
const serifFontNames = [
"Merriweather",
"Playfair Display",
"Lora",
"Source Serif Pro",
"Libre Baskerville",
"Space Grotesk",
];
const monoFontNames = [
"JetBrains Mono",
"Fira Code",
"Source Code Pro",
"IBM Plex Mono",
"Roboto Mono",
"Space Mono",
"Geist Mono",
];
export const fonts = {
// Sans-serif fonts
Inter: "Inter, sans-serif",
Roboto: "Roboto, sans-serif",
"Open Sans": "Open Sans, sans-serif",
Poppins: "Poppins, sans-serif",
Montserrat: "Montserrat, sans-serif",
Outfit: "Outfit, sans-serif",
"Plus Jakarta Sans": "Plus Jakarta Sans, sans-serif",
"DM Sans": "DM Sans, sans-serif",
"IBM Plex Sans": "IBM Plex Sans, sans-serif",
Geist: "Geist, sans-serif",
Oxanium: "Oxanium, sans-serif",
// Serif fonts
Merriweather: "Merriweather, serif",
"Playfair Display": "Playfair Display, serif",
Lora: "Lora, serif",
"Source Serif Pro": "Source Serif Pro, serif",
"Libre Baskerville": "Libre Baskerville, serif",
"Space Grotesk": "Space Grotesk, serif",
// Monospace fonts
"JetBrains Mono": "JetBrains Mono, monospace",
"Fira Code": "Fira Code, monospace",
"Source Code Pro": "Source Code Pro, monospace",
"IBM Plex Mono": "IBM Plex Mono, monospace",
"Roboto Mono": "Roboto Mono, monospace",
"Space Mono": "Space Mono, monospace",
"Geist Mono": "Geist Mono, monospace",
};
export const sansSerifFonts = Object.fromEntries(
Object.entries(fonts).filter(([key]) => sansSerifFontNames.includes(key))
);
export const serifFonts = Object.fromEntries(
Object.entries(fonts).filter(([key]) => serifFontNames.includes(key))
);
export const monoFonts = Object.fromEntries(
Object.entries(fonts).filter(([key]) => monoFontNames.includes(key))
);
export const getAppliedThemeFont = (
state: ThemeEditorState,
fontKey: keyof ThemeStyleProps
): string | null => {
const fontSans = state.styles.light[fontKey];
// find key of font in fonts object based on value
const key = Object.keys(fonts).find((key) => fonts[key].includes(fontSans));
return key ? key : null;
};
File diff suppressed because it is too large Load Diff
+211
View File
@@ -0,0 +1,211 @@
import { ThemeEditorState, ThemeStyles } from "@/types/theme";
import { colorFormatter } from "./color-converter";
import { ColorFormat } from "../types";
import { getShadowMap } from "./shadows";
import { defaultLightThemeStyles } from "@/config/theme";
type ThemeMode = "light" | "dark";
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 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({ styles: themeStyles, currentMode: mode })
);
const spacingVar =
mode === "light" &&
themeStyles["light"].spacing !== defaultLightThemeStyles.spacing
? `\n --spacing: ${themeStyles["light"].spacing};`
: "";
const trackingVars =
mode === "light" &&
themeStyles["light"]["letter-spacing"] !==
defaultLightThemeStyles["letter-spacing"]
? `\n --tracking-normal: ${themeStyles["light"]["letter-spacing"]};`
: "";
return (
selector +
" {" +
colorVars +
fontVars +
radiusVar +
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)}
}`;
};
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}`;
};