UNPKG

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
/** * 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}`; }); } }