create-roadkit
Version:
Beautiful Next.js roadmap website generator with full-screen kanban boards, dark/light mode, and static export
471 lines (421 loc) • 13.6 kB
text/typescript
/**
* Theme system types and interfaces for roadkit
*
* This file defines the core types used throughout the theming system,
* ensuring type safety and consistency across all theme-related operations.
* Includes comprehensive Zod schemas for runtime validation.
*/
import { z } from 'zod';
/**
* Supported theme modes
* - light: Light mode theme
* - dark: Dark mode theme
*/
export type ThemeMode = 'light' | 'dark';
/**
* Available color schemes based on shadcn/ui themes
* These align with the official shadcn/ui color palettes
*/
export type ColorScheme =
| 'blue'
| 'green'
| 'orange'
| 'red'
| 'rose'
| 'stone'
| 'slate'
| 'gray'
| 'neutral'
| 'zinc'
| 'violet'
| 'yellow';
/**
* shadcn/ui styles
* - default: Original shadcn/ui style
* - new-york: New York variant with different aesthetics
*/
export type ShadcnStyle = 'default' | 'new-york';
/**
* CSS variable definition for dynamic theming
* Each variable includes the CSS property name and its HSL values
*/
export interface CSSVariable {
/** CSS custom property name (without --) */
name: string;
/** HSL color values as a string (e.g., "210 40% 10%") */
value: string;
/** Optional description of what this variable controls */
description?: string;
}
/**
* Color palette definition following shadcn/ui structure
* Contains all the semantic color tokens used in the design system
*/
export interface ColorPalette {
/** Background colors */
background: CSSVariable;
foreground: CSSVariable;
/** Card component colors */
card: CSSVariable;
'card-foreground': CSSVariable;
/** Popover component colors */
popover: CSSVariable;
'popover-foreground': CSSVariable;
/** Primary brand colors */
primary: CSSVariable;
'primary-foreground': CSSVariable;
/** Secondary colors */
secondary: CSSVariable;
'secondary-foreground': CSSVariable;
/** Muted colors for subtle elements */
muted: CSSVariable;
'muted-foreground': CSSVariable;
/** Accent colors for highlights */
accent: CSSVariable;
'accent-foreground': CSSVariable;
/** Destructive/error colors */
destructive: CSSVariable;
'destructive-foreground': CSSVariable;
/** Border colors */
border: CSSVariable;
input: CSSVariable;
ring: CSSVariable;
/** Chart colors for data visualization (shadcn/ui charts extension) */
'chart-1'?: CSSVariable;
'chart-2'?: CSSVariable;
'chart-3'?: CSSVariable;
'chart-4'?: CSSVariable;
'chart-5'?: CSSVariable;
}
/**
* Complete theme configuration for a specific mode (light/dark)
* Contains all the information needed to generate a theme
*/
export interface ThemeConfig {
/** Unique identifier for this theme configuration */
id: string;
/** Human-readable name */
name: string;
/** Theme mode (light/dark) */
mode: ThemeMode;
/** Color scheme identifier */
colorScheme: ColorScheme;
/** shadcn/ui style variant */
style: ShadcnStyle;
/** Complete color palette */
palette: ColorPalette;
/** Border radius values in CSS units */
borderRadius: {
sm: string;
md: string;
lg: string;
xl: string;
};
}
/**
* Complete theme definition supporting both light and dark modes
* This is the main interface for defining a complete theme
*/
export interface Theme {
/** Unique theme identifier */
id: string;
/** Display name for the theme */
name: string;
/** Primary color scheme */
colorScheme: ColorScheme;
/** shadcn/ui style variant */
style: ShadcnStyle;
/** Light mode configuration */
light: ThemeConfig;
/** Dark mode configuration */
dark: ThemeConfig;
/** Theme metadata */
meta: {
/** Theme description */
description: string;
/** Theme author/creator */
author: string;
/** Theme version */
version: string;
/** Whether theme meets WCAG accessibility standards */
accessible: boolean;
/** Tags for theme categorization */
tags: string[];
};
}
/**
* Options for generating theme files
*/
export interface ThemeGenerationOptions {
/** Target theme to generate */
theme: Theme;
/** Output directory for theme files */
outputDir: string;
/** Whether to include Tailwind configuration */
includeTailwindConfig: boolean;
/** Whether to include CSS variable files */
includeCSSVariables: boolean;
/** Whether to include component overrides */
includeComponentOverrides: boolean;
/** Custom prefix for CSS variables */
cssVariablePrefix?: string;
}
/**
* Theme validation result
*/
export interface ThemeValidationResult {
/** Whether the theme is valid */
valid: boolean;
/** Validation errors if any */
errors: string[];
/** Validation warnings */
warnings: string[];
/** Accessibility compliance check results */
accessibility: {
/** Whether theme meets WCAG AA standards */
wcagAA: boolean;
/** Whether theme meets WCAG AAA standards */
wcagAAA: boolean;
/** Specific contrast ratio issues */
contrastIssues: Array<{
/** Color pair that fails contrast requirements */
pair: [string, string];
/** Actual contrast ratio */
ratio: number;
/** Required minimum ratio */
required: number;
/** WCAG level (AA/AAA) */
level: 'AA' | 'AAA';
}>;
};
}
/**
* Theme registry for managing multiple themes
*/
export interface ThemeRegistry {
/** All registered themes */
themes: Map<string, Theme>;
/** Default theme ID */
defaultTheme: string;
/** Register a new theme */
register(theme: Theme): void;
/** Get theme by ID */
get(id: string): Theme | undefined;
/** Get all themes */
getAll(): Theme[];
/** Get themes by color scheme */
getByColorScheme(colorScheme: ColorScheme): Theme[];
/** Validate a theme */
validate(theme: Theme): ThemeValidationResult;
/** Export theme as CSS */
exportCSS(themeId: string, mode: ThemeMode): string;
/** Export theme as Tailwind config */
exportTailwindConfig(themeId: string): Record<string, any>;
}
/**
* Theme injection options for generated projects
*/
export interface ThemeInjectionOptions {
/** Target project directory */
projectDir: string;
/** Selected theme */
theme: Theme;
/** Whether to support theme switching */
enableThemeSwitching: boolean;
/** Whether to include dark mode support */
enableDarkMode: boolean;
/** Custom CSS file path relative to project root */
cssFilePath?: string;
/** Custom Tailwind config path relative to project root */
tailwindConfigPath?: string;
}
/**
* Zod Validation Schemas
*
* These schemas provide runtime validation for theme configurations,
* ensuring data integrity and catching configuration errors early.
*/
/**
* HSL color value schema
* Validates HSL color strings in various formats
*/
export const HSLColorSchema = z.string().regex(
/^(hsl\()?\s*\d{1,3}(deg)?\s*,?\s*\d{1,3}%\s*,?\s*\d{1,3}%\s*(,?\s*(0|1|0\.\d+))?\s*(\))?$|^\d{1,3}\s+\d{1,3}%\s+\d{1,3}%$/,
'Invalid HSL color format. Expected formats like "210 40% 50%" or "hsl(210, 40%, 50%)"'
);
/**
* CSS Variable schema
*/
export const CSSVariableSchema = z.object({
name: z.string().min(1, 'CSS variable name is required'),
value: HSLColorSchema,
description: z.string().optional(),
}).strict();
/**
* Color palette schema with all required semantic colors
*/
export const ColorPaletteSchema = z.object({
// Background colors
background: CSSVariableSchema,
foreground: CSSVariableSchema,
// Card component colors
card: CSSVariableSchema,
'card-foreground': CSSVariableSchema,
// Popover component colors
popover: CSSVariableSchema,
'popover-foreground': CSSVariableSchema,
// Primary brand colors
primary: CSSVariableSchema,
'primary-foreground': CSSVariableSchema,
// Secondary colors
secondary: CSSVariableSchema,
'secondary-foreground': CSSVariableSchema,
// Muted colors for subtle elements
muted: CSSVariableSchema,
'muted-foreground': CSSVariableSchema,
// Accent colors for highlights
accent: CSSVariableSchema,
'accent-foreground': CSSVariableSchema,
// Destructive/error colors
destructive: CSSVariableSchema,
'destructive-foreground': CSSVariableSchema,
// Border colors
border: CSSVariableSchema,
input: CSSVariableSchema,
ring: CSSVariableSchema,
// Chart colors for data visualization (optional)
'chart-1': CSSVariableSchema.optional(),
'chart-2': CSSVariableSchema.optional(),
'chart-3': CSSVariableSchema.optional(),
'chart-4': CSSVariableSchema.optional(),
'chart-5': CSSVariableSchema.optional(),
}).strict();
/**
* Border radius schema
*/
export const BorderRadiusSchema = z.object({
sm: z.string().regex(/^\d*\.?\d+(rem|px)$/, 'Invalid CSS length unit'),
md: z.string().regex(/^\d*\.?\d+(rem|px)$/, 'Invalid CSS length unit'),
lg: z.string().regex(/^\d*\.?\d+(rem|px)$/, 'Invalid CSS length unit'),
xl: z.string().regex(/^\d*\.?\d+(rem|px)$/, 'Invalid CSS length unit'),
}).strict();
/**
* Theme configuration schema
*/
export const ThemeConfigSchema = z.object({
id: z.string().min(1, 'Theme ID is required').regex(/^[a-z0-9-]+$/, 'Theme ID must contain only lowercase letters, numbers, and hyphens'),
name: z.string().min(1, 'Theme name is required'),
mode: z.enum(['light', 'dark']),
colorScheme: z.enum(['blue', 'green', 'orange', 'red', 'rose', 'stone', 'slate', 'gray', 'neutral', 'zinc', 'violet', 'yellow']),
style: z.enum(['default', 'new-york']),
palette: ColorPaletteSchema,
borderRadius: BorderRadiusSchema,
}).strict();
/**
* Complete theme schema
*/
export const ThemeSchema = z.object({
id: z.string().min(1, 'Theme ID is required').regex(/^[a-z0-9-]+$/, 'Theme ID must contain only lowercase letters, numbers, and hyphens'),
name: z.string().min(1, 'Theme name is required'),
colorScheme: z.enum(['blue', 'green', 'orange', 'red', 'rose', 'stone', 'slate', 'gray', 'neutral', 'zinc', 'violet', 'yellow']),
style: z.enum(['default', 'new-york']),
light: ThemeConfigSchema,
dark: ThemeConfigSchema,
meta: z.object({
description: z.string().min(1, 'Theme description is required'),
author: z.string().min(1, 'Theme author is required'),
version: z.string().regex(/^\d+\.\d+\.\d+$/, 'Version must follow semantic versioning (x.y.z)'),
accessible: z.boolean(),
tags: z.array(z.string()).min(1, 'At least one tag is required'),
}).strict(),
}).strict().refine((theme) => {
// Validate that light and dark themes have consistent IDs
return theme.light.id === `${theme.id}-light` && theme.dark.id === `${theme.id}-dark`;
}, {
message: 'Light and dark theme IDs must follow the pattern "{themeId}-light" and "{themeId}-dark"',
path: ['light', 'id']
}).refine((theme) => {
// Validate that both modes use the same color scheme
return theme.light.colorScheme === theme.colorScheme && theme.dark.colorScheme === theme.colorScheme;
}, {
message: 'Light and dark modes must use the same color scheme as the main theme',
path: ['colorScheme']
});
/**
* Theme generation options schema
*/
export const ThemeGenerationOptionsSchema = z.object({
theme: ThemeSchema,
outputDir: z.string().min(1, 'Output directory is required'),
includeTailwindConfig: z.boolean(),
includeCSSVariables: z.boolean(),
includeComponentOverrides: z.boolean(),
cssVariablePrefix: z.string().optional(),
}).strict();
/**
* Theme injection options schema
*/
export const ThemeInjectionOptionsSchema = z.object({
projectDir: z.string().min(1, 'Project directory is required'),
theme: ThemeSchema,
enableThemeSwitching: z.boolean(),
enableDarkMode: z.boolean(),
cssFilePath: z.string().optional(),
tailwindConfigPath: z.string().optional(),
}).strict();
/**
* Validation utility functions
*/
export class ThemeValidationUtils {
/**
* Validate a complete theme configuration
* @param theme - Theme to validate
* @returns Validation result with detailed error information
*/
static validateTheme(theme: unknown): { success: boolean; error?: z.ZodError; data?: Theme } {
const result = ThemeSchema.safeParse(theme);
if (result.success) {
return { success: true, data: result.data };
}
return { success: false, error: result.error };
}
/**
* Validate theme generation options
* @param options - Options to validate
* @returns Validation result
*/
static validateThemeGenerationOptions(
options: unknown
): { success: boolean; error?: z.ZodError; data?: ThemeGenerationOptions } {
const result = ThemeGenerationOptionsSchema.safeParse(options);
if (result.success) {
return { success: true, data: result.data };
}
return { success: false, error: result.error };
}
/**
* Validate theme injection options
* @param options - Options to validate
* @returns Validation result
*/
static validateThemeInjectionOptions(
options: unknown
): { success: boolean; error?: z.ZodError; data?: ThemeInjectionOptions } {
const result = ThemeInjectionOptionsSchema.safeParse(options);
if (result.success) {
return { success: true, data: result.data };
}
return { success: false, error: result.error };
}
/**
* Format validation errors into human-readable messages
* @param error - Zod validation error
* @returns Array of formatted error messages
*/
static formatValidationErrors(error: z.ZodError): string[] {
return error.errors.map(err => {
const path = err.path.length > 0 ? `${err.path.join('.')}: ` : '';
return `${path}${err.message}`;
});
}
}