@poupe/tailwindcss
Version:
TailwindCSS v4 plugin for Poupe UI framework with theme customization support
1 lines • 126 kB
Source Map (JSON)
{"version":3,"file":"tailwindcss.BlKYX9OC.mjs","sources":["../../src/theme/types.ts","../../src/theme/default-colors.ts","../../src/theme/shades.ts","../../src/theme/utils.ts","../../src/theme/options.ts","../../src/theme/shadows.ts","../../src/theme/variants.ts","../../src/theme/components.ts","../../src/theme/theme.ts","../../src/theme/config.ts","../../src/theme/match-utilities.ts","../../src/theme/plugin.ts"],"sourcesContent":["/* imports */\nimport {\n type ColorMap,\n type StandardDynamicSchemeKey,\n type StandardPaletteKey,\n} from '@poupe/theme-builder';\n\nimport {\n type KebabCase,\n} from '../utils/builder';\n\nimport {\n type Shades,\n} from './shades';\n\n/* re-exports */\nexport type { KebabCase } from 'type-fest';\n\nexport {\n type Color,\n type ColorMap,\n type StandardDynamicSchemeKey,\n type StandardPaletteKey,\n Hct,\n} from '@poupe/theme-builder';\n\nexport type Theme = {\n readonly options: ThemeOptions\n readonly paletteKeys: string[]\n readonly keys: string[]\n\n readonly dark: ColorMap<string> | undefined\n readonly light: ColorMap<string> | undefined\n readonly colors: Record<string, ThemeColorConfig>\n};\n\nexport type ThemeColorConfig = {\n value: string\n shades?: Record<number, string>\n};\n\n/**\n * Configuration options for defining a theme with customizable color schemes and styling.\n * @typeParam K - A generic type parameter representing additional color keys.\n **/\nexport type ThemeOptions<K extends string = string> = {\n /** Enable debug mode for theme generation. @defaultValue `false` */\n debug?: boolean\n\n /** Prefix for theme-related class names, defaults to 'md-'. @defaultValue `'md-'` */\n themePrefix: string\n\n /** Prefix for surface class names, defaults to 'surface-'.\n * Set to `false` to disable surface generation.\n * @defaultValue `'surface-'`\n * */\n surfacePrefix: string | false\n\n /** Prefix for shape class names, defaults to 'shape-'.\n * Set to `false` to disable shape generation.\n * @defaultValue `'shape-'`\n * */\n shapePrefix: string | false\n\n /** Flag to omit theme generation, @defaultValue `false` */\n omitTheme: boolean\n\n /** Flag to extend existing color palette, @defaultValue `false` */\n extendColors: boolean\n\n /** Dynamic color scheme type, @defaultValue `'content'` */\n scheme: StandardDynamicSchemeKey\n\n /** Theme contrast level, @defaultValue `0` */\n contrastLevel: number\n\n /** Suffix for dark theme variants, @defaultValue `''` */\n darkSuffix: string\n\n /** Suffix for light theme variants, @defaultValue `''` */\n lightSuffix: string\n\n /** Color shade values used in theme generation,\n * @defaultValue `[50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]`\n **/\n shades: Shades\n\n /** Disable print mode, @defaultValue `false` */\n disablePrintMode?: boolean\n\n /** Use CSS color-mix() for state colors instead of pre-calculated values\n * @defaultValue false\n */\n useColorMix?: boolean\n\n /** Color configuration for the theme. */\n colors: ThemeColors<K>\n};\n\n/**\n * Defines the color configuration for a theme, including primary and optional additional colors.\n * @typeParam K - A generic type parameter representing custom color keys.\n * @remarks\n * This type allows defining:\n * - A required primary color configuration\n * - Optional standard palette colors (excluding primary)\n * - Custom color keys transformed to kebab-case\n */\nexport type ThemeColors<K extends string> = {\n primary: string | ThemeColorOptions\n} & {\n [name in Exclude<StandardPaletteKey, 'primary'>]?: string | ThemeColorOptions\n} & {\n [name in KebabCase<K>]: boolean | string | ThemeColorOptions\n};\n\n/**\n * Defines the options for a color configuration within a theme.\n * @remarks\n * This type allows defining:\n * - The color value, either a string or an object with default and shade values.\n * - Whether the color should be harmonized.\n * - The shades associated with the color.\n */\nexport type ThemeColorOptions = {\n value?: string\n harmonized?: boolean\n shades?: Shades | true /** @defaultValue `[50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]` */\n};\n\nexport const defaultPrimaryColor = '#74bef5'; // blue from Tailwind CSS's logo\nexport const defaultThemePrefix = 'md-';\nexport const defaultThemeDarkSuffix = '';\nexport const defaultThemeLightSuffix = '';\nexport const defaultThemeContrastLevel = 0;\nexport const defaultThemeScheme: StandardDynamicSchemeKey = 'content';\nexport const defaultSurfacePrefix = 'surface-';\nexport const defaultShapePrefix = 'shape-';\n","import {\n type Color,\n defaultColors as cssNamedColors,\n} from '@poupe/theme-builder/core';\n\n/**\n * Modern Tailwind CSS color palette for web design.\n *\n * This collection provides the essential colors from Tailwind CSS v3+,\n * optimized for modern web interfaces and design systems.\n *\n * @remarks\n * - All values are in hexadecimal format (#rrggbb)\n * - Colors are organized by hue family for consistent theming\n * - Includes neutral grays (slate, gray, zinc, neutral, stone) for versatile UI elements\n * - Vibrant colors (red through rose) for accent and semantic use cases\n * - Takes precedence over CSS named colors via {@link withKnownColor}\n * - Falls back to complete CSS named color collection from `@poupe/theme-builder`\n *\n * @example\n * ```typescript\n * const primaryColor = defaultColors.blue; // '#3b82f6'\n * const neutralColor = defaultColors.slate; // '#64748b'\n * ```\n */\nexport const defaultColors = {\n // Neutral grays - versatile for backgrounds, text, and borders\n slate: '#64748b', // Cool gray with blue undertones\n gray: '#6b7280', // True neutral gray\n grey: '#6b7280', // British spelling alias for gray\n zinc: '#71717a', // Warm gray with slight brown undertones\n neutral: '#737373', // Pure neutral gray\n stone: '#78716c', // Warm gray with beige undertones\n\n // Vibrant color palette - semantic and accent colors\n red: '#ef4444', // Error states, warnings, destructive actions\n orange: '#f97316', // Warning states, energy, attention\n amber: '#f59e0b', // Caution, pending states\n yellow: '#eab308', // Highlights, notifications\n lime: '#84cc16', // Fresh, growth, eco-friendly\n green: '#22c55e', // Success states, confirmations\n emerald: '#10b981', // Prosperity, nature\n teal: '#14b8a6', // Balance, healing, professionalism\n cyan: '#06b6d4', // Information, technology\n sky: '#0ea5e9', // Open, freedom, clarity\n blue: '#3b82f6', // Primary actions, links, trust\n indigo: '#6366f1', // Deep focus, sophistication\n violet: '#8b5cf6', // Creativity, luxury\n purple: '#a855f7', // Innovation, mystery\n fuchsia: '#d946ef', // Bold, energetic, modern\n pink: '#ec4899', // Friendly, approachable, feminine\n rose: '#f43f5e', // Romance, warmth, passion\n\n // Essential monochrome - maximum contrast\n black: '#000', // Pure black for maximum contrast\n white: '#fff', // Pure white for maximum contrast\n};\n\n/**\n * Converts color names to their hexadecimal equivalents.\n *\n * Supports both Tailwind CSS colors and CSS named colors with precedence\n * given to Tailwind colors for modern web design consistency.\n *\n * @param c - The color value to process. Can be any valid Color type.\n * @returns The hexadecimal color value if the input is a known color name,\n * otherwise returns the original input unchanged.\n *\n * @example\n * ```typescript\n * withKnownColor('blue'); // '#3b82f6' (Tailwind blue)\n * withKnownColor('red'); // '#ef4444' (Tailwind red)\n * withKnownColor('crimson'); // '#dc143c' (CSS named color)\n * withKnownColor('#abc123'); // '#abc123' (unchanged)\n * withKnownColor('unknown'); // 'unknown' (unchanged)\n * ```\n *\n * @remarks\n * - Tailwind CSS colors take precedence over CSS named colors\n * - Case-insensitive matching is performed by converting input to lowercase\n * - Only processes strings that contain only letters (both uppercase and lowercase)\n * - Non-string inputs and invalid color names are returned unchanged\n * - Falls back to CSS named colors from `@poupe/theme-builder` for comprehensive coverage\n */\nexport function withKnownColor(c: string): string;\nexport function withKnownColor(c: number): number;\nexport function withKnownColor(c: Color): Color;\nexport function withKnownColor(c: Color): Color {\n if (typeof c !== 'string' || !reOnlyLetters.test(c)) {\n return c;\n }\n\n const name = c.toLowerCase();\n\n // Check Tailwind CSS colors first (takes precedence)\n if (name in defaultColors) {\n return defaultColors[name as keyof typeof defaultColors];\n }\n\n // Fall back to CSS named colors for comprehensive coverage\n if (name in cssNamedColors) {\n return cssNamedColors[name as keyof typeof cssNamedColors];\n }\n\n return c;\n};\n\n/**\n * Regular expression that matches strings containing only letters (uppercase and lowercase).\n * Used to identify potential CSS named color candidates.\n */\nconst reOnlyLetters = /^[a-zA-Z]+$/;\n","/**\n * This module provides utilities for generating, validating, and managing color shades\n * in a design system. It converts colors to different shade variants based on the HCT\n * (Hue, Chroma, Tone) color space, which provides perceptually consistent color gradations.\n *\n * Key features:\n * - Defines standard shade scales (50-950)\n * - Validates shade values for consistency\n * - Converts between color formats and shade values\n * - Supports custom shade definitions with flexible configuration\n * - Generates color palettes based on a single color reference\n *\n * @example\n * ```\n * // Generate standard shades from a color\n * const blueShades = makeHexShades('#0047AB');\n *\n * // Generate custom shades\n * const customShades = makeHexShades('#0047AB', [100, 300, 500, 700, 900]);\n * ```\n */\nimport {\n type Color,\n Hct,\n hct,\n hexFromHct,\n splitHct,\n} from '@poupe/theme-builder/core';\n\n/** Represents the possible types for color shades: an array of numbers or false */\nexport type Shades = number[] | false;\n\n/** Default color shades used when no custom shades are specified, representing a standard range of color intensity from lightest (50) to darkest (950) */\nexport const defaultShades = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];\n\n/**\n * Processes and validates color shades, with optional append mode and default handling.\n *\n * @param shades - An optional array of shade values, boolean, or undefined\n * @param defaults - Default shade values to use, defaults to `defaultShades`\n * @returns An object containing processed shades and a validation status\n *\n * - If `shades` is `false`, returns `false` shades\n * - If `shades` is `true` or `undefined`, returns default shades\n * - Supports negative values to append to default shades\n * - Validates each shade value\n * - Returns sorted unique shade values\n */\nexport function getShades(shades?: number[] | boolean | undefined, defaults: number[] | false = defaultShades): { shades: Shades; ok: boolean } {\n if (shades === false) {\n return { shades: false, ok: true };\n } else if (shades === true || shades === undefined) {\n return { shades: defaults, ok: true };\n } else if (!Array.isArray(shades)) {\n return { shades: false, ok: false };\n }\n\n let appendMode = false;\n const out = new Set<number>();\n\n for (let shade of shades) {\n if (shade < 0) {\n appendMode = true;\n shade = -shade;\n }\n\n if (!validShade(shade)) {\n return { shades: false, ok: false };\n }\n\n out.add(shade);\n }\n\n if (out.size === 0) {\n return { shades: false, ok: false };\n }\n\n if (appendMode && defaults) {\n for (const shade of defaults) {\n out.add(shade);\n }\n }\n\n return {\n shades: [...out].sort((a, b) => a - b),\n ok: true,\n };\n}\n\n/** @returns true if the given number is a valid shade value */\nexport function validShade(n: number): boolean {\n return n > 0 && n < 1000 && Math.round(n) == n;\n}\n\n/**\n * Generates a set of Hct color objects for each specified shade value.\n *\n * This function creates a palette of colors by varying the tone (lightness) while\n * preserving the hue and chroma of the input color. It maps each shade value to a\n * corresponding HCT color object.\n *\n * @param color - The base color to generate shades from\n * @param shades - An array of shade values (numbers between 1-999) or `false` to disable shade generation\n *\n * @returns A record mapping each shade value to its corresponding Hct color object,\n * or `undefined` if the provided shades are invalid or disabled\n *\n * @example\n * ```\n * // Generate standard shades from a color\n * const blueHct = hct('#0047AB');\n * const blueShades = makeShades(blueHct, [100, 300, 500, 700]);\n * // Result: { 100: Hct, 300: Hct, 500: Hct, 700: Hct }\n *\n * // Disable shades\n * const noShades = makeShades(blueHct, false);\n * // Result: undefined\n * ```\n */\nexport function makeShades(color: Color, shades: false): undefined;\nexport function makeShades<K extends number>(color: Color, shades: K[]): Record<K, Hct>;\nexport function makeShades<K extends number>(color: Color, shades: K[] | false): Record<K, Hct> | undefined;\nexport function makeShades<K extends number>(color: Color, shades: K[] | false): Record<K, Hct> | undefined {\n if (!shades) {\n return undefined;\n }\n\n const { h, c } = splitHct(hct(color));\n\n return Object.fromEntries(shades.map((shade) => {\n const tone = toTone(shade);\n\n return [shade, Hct.from(h, c, tone)];\n })) as Record<K, Hct>;\n}\n\nfunction toTone(shade: number): number {\n return Math.max(Math.min(1000 - shade, 1000), 0) / 10;\n}\n\n/**\n * Generates a set of hex color values for each specified shade.\n *\n * This is a convenience wrapper around `makeShades()` that converts the Hct color objects\n * to hexadecimal color strings for easier use in UI components and CSS.\n *\n * @param color - The base color to generate shades from\n * @param shades - An array of shade values, or `false` to disable shade generation.\n * Defaults to the standard shade scale (50-950)\n *\n * @returns A record mapping each shade value to its corresponding hex color string,\n * or `undefined` if the provided shades are invalid or disabled\n *\n * @example\n * ```\n * // Generate standard hex shades\n * const blueHexShades = makeHexShades('#0047AB');\n * // Result: { 50: '#E6F0FF', 100: '#CCE0FF', ... 950: '#00142E' }\n *\n * // Generate custom hex shades\n * const customShades = makeHexShades('#0047AB', [100, 500, 900]);\n * // Result: { 100: '#CCE0FF', 500: '#0047AB', 900: '#00183D' }\n *\n * // Disable shades\n * const noShades = makeHexShades('#0047AB', false);\n * // Result: undefined\n * ```\n */\nexport function makeHexShades(\n color: Color,\n shades: Shades = defaultShades,\n) {\n const validShades = makeShades<number>(color, shades);\n if (!validShades) {\n return undefined;\n }\n\n return Object.fromEntries(Object.entries(validShades).map(([shade, hct]) => {\n return [shade, hexFromHct(hct)];\n }));\n}\n","/* imports */\nimport {\n hexString,\n StandardPaletteKey,\n standardPaletteKeys,\n} from '@poupe/theme-builder';\n\nimport {\n withKnownColor,\n} from './default-colors';\n\nimport {\n getShades,\n} from './shades';\n\nimport {\n type Color,\n type ThemeColorOptions,\n} from './types';\n\n/* re-exports */\nexport {\n getShades,\n validShade,\n} from './shades';\n\nexport * from '../utils';\n\n/** @returns true if the given prefix is valid for theme CSS variables */\nexport function validThemePrefix(prefix: string): boolean {\n return !prefix || prefixRegex.test(prefix);\n}\n\nconst prefixRegex = /^([a-z][0-9a-z]*)(-[a-z][0-9a-z]*)*-?$/;\n\n/** @returns true if the given suffix is valid for theme CSS variables */\nexport function validThemeSuffix(suffix: string): boolean {\n return !suffix || suffixRegex.test(suffix);\n}\n\nconst suffixRegex = /^(-?[a-z][0-9a-z]*)(-[a-z][0-9a-z]*)*$/;\n\n/** @returns true if the name is a valid kebab-case color name */\nexport function validColorName(name: string): boolean {\n return colorNameRegex.test(name);\n}\n\nconst colorNameRegex = /^([a-z][0-9a-z]*)(-[a-z][0-9a-z]*)*$/;\n\n/**\n * Validates color options for a theme color.\n *\n * @param name - The name of the color\n * @param options - Theme color configuration options\n * @returns true if color options are valid, false otherwise\n */\nexport function validColorOptions(name: string, options: ThemeColorOptions): boolean {\n const { ok: shadeOK } = getShades(options.shades);\n if (!shadeOK) {\n return false;\n }\n\n const { ok: colorOK } = getColor(name, options.value);\n return colorOK;\n}\n\nconst isStandardKeyName = (name: string): boolean => standardPaletteKeys.includes(name as StandardPaletteKey);\n\nexport function getColor(name: string, value: Color | undefined): { ok: boolean; color?: string } {\n let v = value || (isStandardKeyName(name) ? undefined : name);\n if (v === undefined) {\n // standard keys can be undefined.\n // primary will take {@link defaultPrimaryColor},\n // and the other colors will take the generated value.\n return { ok: true };\n }\n\n // handle named colours early\n v = withKnownColor(v);\n\n try {\n return { ok: true, color: hexString(v) };\n } catch {\n return { ok: false };\n }\n}\n\nexport function defaultPrefix(name: string, prefix: string): string {\n return name === 'DEFAULT' ? prefix : `${prefix}-${name}`;\n}\n\nexport function debugLog(enabled: boolean = false, ...a: unknown[]) {\n if (enabled && typeof console !== 'undefined' && typeof console.log === 'function') {\n console.log(logPrefix, ...a);\n }\n}\n\nexport function warnLog(...a: unknown[]) {\n if (typeof console !== 'undefined' && typeof console.warn === 'function') {\n console.warn(logPrefix, ...a);\n }\n}\n\nconst logPrefix = '@poupe/tailwindcss';\n","import {\n type ThemeColorOptions,\n type ThemeColors,\n type ThemeOptions,\n defaultPrimaryColor,\n defaultShapePrefix,\n defaultSurfacePrefix,\n defaultThemeContrastLevel,\n defaultThemeDarkSuffix,\n defaultThemeLightSuffix,\n defaultThemePrefix,\n defaultThemeScheme,\n} from './types';\n\nimport {\n getColor,\n keys,\n toKebabCase,\n} from './utils';\n\nimport {\n type Shades,\n defaultShades,\n getShades,\n} from './shades';\n\nexport {\n type ThemeOptions,\n} from './types';\n\nexport function withDefaultThemeOptions<K extends string = string>(options: Partial<ThemeOptions<K>> = {}): ThemeOptions<K> {\n const { shades, ok } = getShades(options.shades, defaultShades);\n if (!ok) {\n throw new Error('Invalid shades configuration');\n }\n\n const colors = withDefaultThemeColorOptions(\n { ...(options.colors ?? { primary: {} }) } as ThemeColors<K>,\n shades,\n );\n\n return {\n debug: options.debug ?? false,\n themePrefix: options.themePrefix ?? defaultThemePrefix,\n surfacePrefix: options.surfacePrefix ?? defaultSurfacePrefix,\n shapePrefix: options.shapePrefix ?? defaultShapePrefix,\n omitTheme: options.omitTheme ?? false,\n extendColors: options.extendColors ?? false,\n useColorMix: options.useColorMix ?? false,\n\n darkSuffix: options.darkSuffix ?? defaultThemeDarkSuffix,\n lightSuffix: options.lightSuffix ?? defaultThemeLightSuffix,\n scheme: options.scheme ?? defaultThemeScheme,\n contrastLevel: options.contrastLevel ?? defaultThemeContrastLevel,\n\n shades,\n colors,\n };\n}\n\nexport function withDefaultThemeColorOptions<K extends string = string>(\n colors: ThemeColors<K>,\n defaultShades: Shades,\n): ThemeColors<K> {\n const out: Record<string, ThemeColorOptions> = {};\n\n for (const key of keys(colors)) {\n const name = toKebabCase(key);\n if (name in out) {\n throw new Error(`Duplicate normalized color name: ${key}/${name}`);\n }\n\n const color = flattenColorOptions(colors[key]);\n out[name] = cookColor(name, color, defaultShades);\n }\n\n return out as ThemeColors<K>;\n}\n\n/**\n * Processes and validates color configuration for a theme color.\n *\n * @param name - The name of the color (e.g., 'primary', 'secondary')\n * @param options - Color configuration options\n * @param defaultShades - Default shades to use if not specified\n * @returns Processed and validated theme color options\n * @throws an Error if color value or shades are invalid.\n */\nconst cookColor = (name: string, options: ThemeColorOptions | undefined, defaultShades: Shades): ThemeColorOptions => {\n // primary can't be harmonized, for all others it defaults to true.\n const { color: value, ok: colorOK } = getColor(name, options?.value);\n if (!colorOK) {\n throw new Error(`Invalid color value for ${name}: ${options?.value}`);\n }\n\n let shades: Shades = false;\n if (options?.shades !== false) {\n const { shades: colorShades, ok: shadesOK } = getShades(options?.shades, defaultShades);\n if (!shadesOK) {\n throw new Error(`Invalid shades for ${name}: ${options?.shades}`);\n }\n\n shades = colorShades;\n }\n\n return {\n value: value ?? (name === 'primary' ? defaultPrimaryColor : undefined),\n ...(name === 'primary' ? {} : { harmonized: options?.harmonized ?? true }),\n shades,\n };\n};\n\nexport function flattenColorOptions(color?: boolean | string | ThemeColorOptions): ThemeColorOptions | undefined {\n if (color === undefined) {\n return undefined;\n } else if (typeof color === 'boolean') {\n return { harmonized: color };\n } else if (typeof color === 'string') {\n return { value: color };\n } else {\n return color;\n }\n}\n","import {\n type CSSRules,\n renameRules,\n} from '@poupe/css';\n\nimport type { Theme } from './types';\nimport { defaultPrefix } from './utils';\n\n/**\n * Generates a set of shadow styles based on the provided theme configuration.\n *\n * @param theme - The theme configuration object containing theme prefix and options\n * @returns A tuple with two CSS rule objects: regular shadows and inset shadows\n * @remarks\n * This function creates the base shadow values that will be used for:\n * 1. Drop shadows (--drop-shadow-*): For use with the drop-shadow filter property\n * 2. Regular shadows (--shadow-*): For use with the box-shadow property\n * 3. Inset shadows (--inset-shadow-*): For inset box shadows\n *\n * Regular and drop shadows include z1 (lowest) through z5 (highest) elevation levels,\n * each with carefully calibrated opacity and offset values. Inset shadows provide a\n * pressed/inset effect with a single default style.\n *\n * All shadows use the `--{prefix}shadow-rgb` variable with appropriate opacity levels.\n * The RGB variable is automatically generated by `makeThemeBases()`:\n * - When dark/light themes exist: direct RGB values from Hct colors\n * - When dark/light themes don't exist: CSS Level 4 fallback syntax\n */\nexport const makeShadows = (theme: Theme, reset: boolean = true): [CSSRules, CSSRules] => {\n const { themePrefix } = theme.options;\n\n // Use RGB variable - automatically generated by makeThemeBases()\n const rgbValues = `var(--${themePrefix}shadow-rgb)`;\n\n // Helper function to create color with opacity using rgb()\n const color = (opacity: number) => `rgb(${rgbValues} / ${opacity.toFixed(2)})`;\n\n const c15 = color(0.15);\n const c17 = color(0.17);\n const c19 = color(0.19);\n const c20 = color(0.2);\n const c30 = color(0.3);\n const c37 = color(0.37);\n\n // Level 1: Subtle elevation - smallest footprint\n const z1 = `0 1px 4px 0 ${c37}`;\n\n // Level 2: Light elevation - default for most floating elements\n const z2 = `0 2px 2px 0 ${c20}, 0 6px 10px 0 ${c30}`;\n\n // Level 3: Medium elevation - for prominent UI elements\n const z3 = `0 11px 7px 0 ${c19}, 0 13px 25px 0 ${c30}`;\n\n // Level 4: High elevation - for important modal windows\n const z4 = `0 14px 12px 0 ${c17}, 0 20px 40px 0 ${c30}`;\n\n // Level 5: Maximum elevation - for critical or temporarily focused elements\n const z5 = `0 17px 17px 0 ${c15}, 0 27px 55px 0 ${c30}`;\n\n // Inset shadow - for pressed/active states\n const inset = `inset 0 2px 4px 0 ${c20}`;\n\n // No shadow - use transparent shadow to maintain composability\n const none = '0 0 0 0 transparent';\n\n // Define regular shadow variations\n const shadows: CSSRules = {\n ...(reset ? { '*': 'initial' } : {}),\n z1,\n z2,\n DEFAULT: z2, // Default shadow is equivalent to z2\n z3,\n z4,\n z5,\n none,\n };\n\n // Define inset shadow variations\n const insetShadows: CSSRules = {\n ...(reset ? { '*': 'initial' } : {}),\n DEFAULT: inset,\n none,\n };\n\n return [shadows, insetShadows];\n};\n\n/**\n * Generates prefixed CSS variables for all shadow types.\n *\n * @param theme - The theme configuration object\n * @returns An array of CSS rule objects with prefixed variables:\n * 1. Box shadow variables (--shadow-*)\n * 2. Drop shadow variables (--drop-shadow-*)\n * 3. Inset shadow variables (--inset-shadow-*)\n */\nexport const makeShadowRules = (theme: Theme, reset: boolean = true): CSSRules[] => {\n const [shadows, insetShadows] = makeShadows(theme, reset);\n\n return [\n renameRules(shadows, (s: string) => defaultPrefix(s, '--shadow')),\n renameRules(shadows, (s: string) => defaultPrefix(s, '--drop-shadow')),\n renameRules(insetShadows, (s: string) => defaultPrefix(s, '--inset-shadow')),\n ];\n};\n","import {\n processCSSSelectors,\n type CSSRuleObject,\n} from '@poupe/css';\n\nimport {\n type Config,\n} from '../utils/plugin';\n\nimport {\n type Theme,\n} from './types';\n\nexport type DarkModeStrategy = Exclude<Config['darkMode'], undefined>;\n\nexport function makeThemeVariants(\n theme: Readonly<Theme>,\n darkMode: DarkModeStrategy = 'class',\n): CSSRuleObject[] {\n const {\n disablePrintMode = false,\n } = theme.options;\n\n const dark = makeDark(darkMode);\n\n if (disablePrintMode) {\n return [\n {\n dark,\n },\n ];\n }\n\n const variants: CSSRuleObject[] = [\n {\n ...makePrintVariants(),\n dark: makeDarkNotPrint(dark),\n },\n ];\n\n return variants;\n}\n\nfunction makeDark(strategy: DarkModeStrategy = 'class'): CSSRuleObject {\n const darkSelector = getDarkMode(strategy).map(s => `&:where(${s})`);\n const out: CSSRuleObject = {};\n let p = out;\n\n for (const selector of darkSelector) {\n p[selector] = {};\n p = p[selector];\n }\n p['@slot'] = {};\n\n return out;\n}\n\nfunction makeDarkNotPrint(dark?: CSSRuleObject): CSSRuleObject {\n return {\n '@media not print': dark || makeDark(),\n };\n}\n\nfunction makePrintVariants(): CSSRuleObject {\n return {\n print: {\n '@media print': {\n '@slot': {},\n },\n },\n screen: {\n '@media screen': {\n '@slot': {},\n },\n },\n };\n}\n\n/**\n * Converts a Tailwind dark mode strategy into the corresponding CSS selector or media query string.\n * @param darkMode - The dark mode strategy to convert\n * @returns A string representation for use in CSS selectors or media queries\n */\nexport function getDarkMode(darkMode: DarkModeStrategy = 'class'): string[] {\n switch (darkMode) {\n case false:\n return [];\n case 'media':\n return ['@media (prefers-color-scheme: dark)'];\n case 'class':\n case 'selector':\n return [defaultDarkSelector];\n default: {\n if (Array.isArray(darkMode) && darkMode.length > 0) {\n const [mode, value] = darkMode;\n switch (mode) {\n case 'class':\n case 'selector':\n return processCSSSelectors(value) ?? [defaultDarkSelector];\n }\n\n // TODO: variant modes\n }\n\n throw new Error(`Invalid darkMode strategy: ${JSON.stringify(darkMode)}.`);\n }\n }\n}\n\nconst defaultDarkSelector = '.dark, .dark *';\n","import {\n stateLayerOpacities,\n} from '@poupe/theme-builder';\n\nimport {\n type Theme,\n defaultSurfacePrefix,\n defaultShapePrefix,\n} from './types';\n\nimport {\n type CSSRuleObject,\n debugLog,\n warnLog,\n} from './utils';\n\n/** Default opacity for scrim utilities when no modifier is provided */\nexport const DEFAULT_SCRIM_OPACITY = '32%';\n\nexport function makeThemeComponents(theme: Readonly<Theme>, tailwindPrefix: string = ''): Record<string, CSSRuleObject>[] {\n return [\n makeSurfaceComponents(theme, tailwindPrefix),\n makeInteractiveSurfaceComponents(theme, tailwindPrefix),\n makeZIndexComponents(theme),\n makeRippleComponents(theme),\n makeShapeComponents(theme),\n ];\n}\n\nexport function makeZIndexComponents(theme: Readonly<Theme>): Record<string, CSSRuleObject> {\n const { themePrefix } = theme.options;\n\n const out: Record<string, CSSRuleObject> = {\n ['scrim-*']: {\n // Dynamic scrim utility that accepts arbitrary z-index values with optional opacity modifier\n // The --value() pattern indicates this will be converted to matchUtilities\n // allowing usage like: scrim-[100], scrim-[1250]/50, scrim-[var(--custom-z)]/75\n '@apply fixed inset-0': {},\n 'z-index': '--value(integer, [integer])',\n 'background-color': `rgb(var(--${themePrefix}scrim-rgb) / var(--${themePrefix}scrim-opacity, ${DEFAULT_SCRIM_OPACITY}))`,\n [`--${themePrefix}scrim-opacity`]: '--modifier([percentage])',\n },\n };\n\n // semantic z-index scrim with opacity modifier support\n for (const name of ['base', 'content', 'drawer', 'modal', 'elevated', 'system']) {\n out[`scrim-${name}`] = {\n '@apply fixed inset-0': {},\n 'z-index': `var(--${themePrefix}z-scrim-${name})`,\n 'background-color': `rgb(var(--${themePrefix}scrim-rgb) / var(--${themePrefix}scrim-opacity, ${DEFAULT_SCRIM_OPACITY}))`,\n [`--${themePrefix}scrim-opacity`]: '--modifier([percentage])',\n };\n }\n\n // semantic z-index\n for (const name of ['navigation-persistent', 'navigation-floating', 'navigation-top', 'drawer', 'modal', 'snackbar', 'tooltip']) {\n out[`z-${name}`] = {\n 'z-index': `var(--${themePrefix}z-${name})`,\n };\n }\n\n return out;\n}\n\n/**\n * Configuration for special surface color pairings using patterns\n * `{}` will be replaced with the color name (primary, secondary, tertiary, etc.)\n * Each entry maps a background pattern to an array of text color patterns\n *\n * Pattern types determine which colors to expand:\n * - 'standard': primary, secondary, tertiary only (Material Design core colors)\n * - 'fixed': primary, secondary, tertiary only (fixed color variants)\n * - 'static': no pattern expansion (like inverse-surface)\n *\n * Note: Custom colors (blue, green, orange, etc.) get standard pairings\n * automatically through the first loop in findSurfacePairs()\n */\nconst SURFACE_PAIRING_PATTERNS: Record<string, { patterns: string[]; type: 'standard' | 'fixed' | 'static' }> = {\n // Dim variants (Material Design 2025) - use standard on-color\n '{}-dim': {\n patterns: ['on-{}'],\n type: 'standard',\n },\n\n // Fixed color pairings - {} replaced with primary/secondary/tertiary\n '{}-fixed': {\n patterns: ['on-{}-fixed', 'on-{}-fixed-variant'],\n type: 'fixed',\n },\n '{}-fixed-dim': {\n patterns: ['on-{}-fixed', 'on-{}-fixed-variant'],\n type: 'fixed',\n },\n\n // Inverse surface (no pattern needed)\n 'inverse-surface': {\n patterns: ['on-inverse-surface', 'inverse-primary'],\n type: 'static',\n },\n};\n\n/**\n * Represents a surface pair configuration\n */\ninterface SurfacePair {\n bgColor: string\n textColor: string\n surfaceName: string\n}\n\n/**\n * Generates the surface name for a color pair\n */\nfunction getSurfaceName(bgColor: string, textColor: string): string {\n // Special naming for inverse-surface + on-inverse-surface (standard inverse)\n if (bgColor === 'inverse-surface' && textColor === 'on-inverse-surface') {\n return 'inverse';\n }\n\n // Special naming for inverse-surface + inverse-primary\n if (bgColor === 'inverse-surface' && textColor === 'inverse-primary') {\n return 'inverse-primary';\n }\n\n // Special naming for fixed color variants\n if ((bgColor.endsWith('-fixed') || bgColor.endsWith('-fixed-dim')) && textColor.endsWith('-fixed-variant')) {\n // Extract the unique part from text color\n const prefix = textColor.replace(/-fixed-variant$/, '').replace(/^on-/, '');\n const bgSuffix = bgColor.endsWith('-dim') ? '-dim' : '';\n return `${prefix}-fixed${bgSuffix}-variant`;\n }\n\n // Default: just use the background color name\n return bgColor;\n}\n\n/**\n * Adds a surface pair to the map with the appropriate key\n */\nfunction addSurfacePair(\n pairs: Map<string, SurfacePair>,\n bgColor: string,\n textColor: string,\n): void {\n const surfaceName = getSurfaceName(bgColor, textColor);\n const key = textColor === `on-${bgColor}` ? bgColor : `${bgColor}:${textColor}`;\n pairs.set(key, {\n bgColor,\n textColor,\n surfaceName,\n });\n}\n\n/**\n * Finds standard color pairs (color + on-color, including containers)\n */\nfunction findStandardPairs(theme: Readonly<Theme>, pairs: Map<string, SurfacePair>): void {\n for (const name of theme.keys) {\n const onName = `on-${name}`;\n if (theme.colors[name] && theme.colors[onName]) {\n addSurfacePair(pairs, name, onName);\n }\n }\n}\n\n/**\n * Processes static surface patterns (non-expandable like inverse-surface)\n */\nfunction processStaticPattern(\n theme: Readonly<Theme>,\n pairs: Map<string, SurfacePair>,\n bgPattern: string,\n textPatterns: string[],\n): void {\n if (!theme.colors[bgPattern]) return;\n\n for (const textPattern of textPatterns) {\n if (theme.colors[textPattern]) {\n addSurfacePair(pairs, bgPattern, textPattern);\n }\n }\n}\n\n/**\n * Checks if a color key is a base color (not on-*, *-container, etc.)\n */\nfunction isBaseColor(colorKey: string): boolean {\n return !colorKey.includes('-') && !colorKey.startsWith('on-');\n}\n\n/**\n * Checks if a color should be processed for the given pattern type\n */\nfunction shouldProcessColor(colorKey: string, type: 'standard' | 'fixed'): boolean {\n if (type === 'fixed' || type === 'standard') {\n return ['primary', 'secondary', 'tertiary'].includes(colorKey);\n }\n return true;\n}\n\n/**\n * Expands pattern-based surface pairings\n */\nfunction expandPatternPairings(\n theme: Readonly<Theme>,\n pairs: Map<string, SurfacePair>,\n bgPattern: string,\n textPatterns: string[],\n type: 'standard' | 'fixed',\n): void {\n for (const colorKey of theme.keys) {\n // Skip if it's not a base color\n if (!isBaseColor(colorKey)) continue;\n\n const bg = bgPattern.replace('{}', colorKey);\n\n // Check if this specific pattern exists in the theme\n if (!theme.colors[bg]) continue;\n\n // Check if this color should be processed for this pattern type\n if (!shouldProcessColor(colorKey, type)) continue;\n\n // Try each text pattern\n for (const textPattern of textPatterns) {\n const text = textPattern.replace('{}', colorKey);\n if (theme.colors[text]) {\n addSurfacePair(pairs, bg, text);\n }\n }\n }\n}\n\n/**\n * Finds all valid surface color pairs in the theme\n */\nfunction findSurfacePairs(theme: Readonly<Theme>): Map<string, SurfacePair> {\n const pairs = new Map<string, SurfacePair>();\n\n // First, find all standard pairs (color + on-color)\n findStandardPairs(theme, pairs);\n\n // Then, expand pattern-based pairings\n for (const [bgPattern, config] of Object.entries(SURFACE_PAIRING_PATTERNS)) {\n const { patterns: textPatterns, type } = config;\n\n if (type === 'static') {\n processStaticPattern(theme, pairs, bgPattern, textPatterns);\n } else {\n expandPatternPairings(theme, pairs, bgPattern, textPatterns, type);\n }\n }\n\n return pairs;\n}\n\nexport function makeSurfaceComponents(theme: Readonly<Theme>, tailwindPrefix: string = ''): Record<string, CSSRuleObject> {\n const { surfacePrefix = defaultSurfacePrefix } = theme.options;\n if (!surfacePrefix) {\n return {};\n }\n\n const pairs = findSurfacePairs(theme);\n\n const surfaces: Record<string, CSSRuleObject> = {};\n\n const [bgPrefix, textPrefix] = ['bg-', 'text-'].map(prefix => `${tailwindPrefix}${prefix}`);\n\n for (const pair of pairs.values()) {\n const { bgColor, textColor, surfaceName } = pair;\n const { value } = assembleSurfaceComponent(bgColor, textColor, bgPrefix, textPrefix, surfacePrefix);\n\n // Use the preferred surface name from the pair\n const surfaceKey = makeSurfaceName(surfaceName, surfacePrefix);\n surfaces[surfaceKey] = value;\n }\n\n debugLog(theme.options.debug, 'surfaces', surfaces);\n return surfaces;\n}\n\n/**\n * Assembles a surface component by combining background and text color classes.\n *\n * @param colorName - The base color name for the background\n * @param onColorName - The color name for text on the surface\n * @param bgPrefix - Prefix for background color classes\n * @param textPrefix - Prefix for text color classes\n * @param surfacePrefix - Prefix for surface classes\n * @returns An object with a key for the surface class and its corresponding CSS rule object\n */\nexport function assembleSurfaceComponent(\n colorName: string,\n onColorName: string,\n bgPrefix: string,\n textPrefix: string,\n surfacePrefix: string,\n): { key: string; value: CSSRuleObject } {\n if (surfacePrefix === bgPrefix) {\n // extend bg- with text-on-\n return {\n key: `${bgPrefix}${colorName}`,\n value: {\n [`@apply ${textPrefix}${onColorName}`]: {},\n },\n };\n }\n // combine bg- and text-on-\n const surfaceName = makeSurfaceName(colorName, surfacePrefix);\n return {\n key: `${surfaceName}`,\n value: {\n [`@apply ${bgPrefix}${colorName} ${textPrefix}${onColorName}`]: {},\n },\n };\n}\n\n/**\n * Generates a surface name by prefixing a color name with a given prefix.\n *\n * @param colorName - The original color name to be transformed\n * @param prefix - The prefix to be added to the color name\n * @returns A surface name that avoids prefix duplication\n */\nexport function makeSurfaceName(colorName: string, prefix: string): string {\n // Special case for inverse in interactive context - maintain consistency\n if (colorName === 'inverse' && prefix === 'interactive-surface-') {\n return 'interactive-surface-inverse';\n }\n\n if (colorName.startsWith(prefix) || `${colorName}-` === prefix) {\n // prevent duplication\n return colorName;\n }\n\n // Use the component name generator logic to avoid duplicates\n if (prefix.endsWith('-')) {\n const prefixParts = prefix.slice(0, -1).split('-');\n const colorParts = colorName.split('-');\n\n // Find common parts to avoid duplication\n let commonParts = 0;\n for (let i = 0; i < Math.min(prefixParts.length, colorParts.length); i++) {\n if (prefixParts[prefixParts.length - 1 - i] === colorParts[i]) {\n commonParts++;\n } else {\n break;\n }\n }\n\n if (commonParts > 0) {\n // Remove common parts from the beginning of colorName\n const uniqueParts = colorParts.slice(commonParts).join('-');\n if (uniqueParts) {\n return `${prefix}${uniqueParts}`;\n }\n // If all parts are common, just use the prefix without dash\n return prefix.slice(0, -1);\n }\n }\n\n return `${prefix}${colorName}`;\n}\n\n/**\n * Checks if a color should have interactive states\n */\nfunction isInteractiveColor(theme: Readonly<Theme>, colorName: string): boolean {\n // Interactive colors that always have state variants in MD3\n const knownInteractiveColors = new Set([\n 'primary', 'secondary', 'tertiary', 'error',\n 'surface', 'surface-dim', 'surface-bright', 'surface-variant',\n 'surface-container', 'surface-container-lowest',\n 'surface-container-low', 'surface-container-high',\n 'surface-container-highest', 'inverse-surface',\n 'primary-container', 'secondary-container',\n 'tertiary-container', 'error-container',\n 'primary-fixed', 'secondary-fixed', 'tertiary-fixed',\n ]);\n\n // Include custom palette colors from theme\n for (const key of theme.paletteKeys) {\n knownInteractiveColors.add(key);\n knownInteractiveColors.add(`${key}-container`);\n knownInteractiveColors.add(`${key}-fixed`);\n }\n\n // Check if it's a known interactive color or has state variants defined\n return knownInteractiveColors.has(colorName)\n || theme.keys.includes(`${colorName}-hover`)\n || theme.keys.includes(`${colorName}-focus`)\n || theme.keys.includes(`${colorName}-pressed`)\n || theme.keys.includes(`${colorName}-disabled`);\n}\n\n/**\n * Generates interactive surface components that combine base surface colors\n * with their Material Design 3 state layers (hover, focus, pressed, disabled).\n */\nexport function makeInteractiveSurfaceComponents(\n theme: Readonly<Theme>,\n tailwindPrefix: string = '',\n): Record<string, CSSRuleObject> {\n const { themePrefix } = theme.options;\n\n const interactiveSurfaces: Record<string, CSSRuleObject> = {};\n\n // Find all surface pairs that have interactive states\n const pairs = findSurfacePairs(theme);\n const interactivePairs: SurfacePair[] = [];\n\n // Filter pairs to only include those with interactive states\n for (const pair of pairs.values()) {\n if (isInteractiveColor(theme, pair.bgColor)) {\n interactivePairs.push(pair);\n }\n }\n\n const [bgPrefix, textPrefix, borderPrefix] = ['bg-', 'text-', 'border-']\n .map(prefix => `${tailwindPrefix}${prefix}`);\n\n // Calculate disabled text opacity once\n const onDisabledOpacity = Math.round(stateLayerOpacities.onDisabled * 100);\n\n for (const pair of interactivePairs) {\n const { bgColor, textColor, surfaceName } = pair;\n\n // Generate the interactive surface name\n const interactiveSurfaceName = makeSurfaceName(surfaceName, 'interactive-surface-');\n\n // Build the interactive surface utility\n const stateClasses: string[] = [\n `${bgPrefix}${bgColor}`,\n `${textPrefix}${textColor}`,\n `${borderPrefix}${textColor}`,\n ];\n\n // Add hover state (only background changes)\n if (isInteractiveColor(theme, bgColor) || theme.keys.includes(`${bgColor}-hover`)) {\n stateClasses.push(`hover:${bgPrefix}${bgColor}-hover`);\n }\n\n // Add focus states (only background changes)\n if (isInteractiveColor(theme, bgColor) || theme.keys.includes(`${bgColor}-focus`)) {\n stateClasses.push(\n `focus:${bgPrefix}${bgColor}-focus`,\n `focus-visible:${bgPrefix}${bgColor}-focus`,\n `focus-within:${bgPrefix}${bgColor}-focus`,\n );\n }\n\n // Add pressed/active state (only background changes)\n if (isInteractiveColor(theme, bgColor) || theme.keys.includes(`${bgColor}-pressed`)) {\n stateClasses.push(`active:${bgPrefix}${bgColor}-pressed`);\n }\n\n // Add disabled state\n const hasDisabledBg = isInteractiveColor(theme, bgColor)\n || theme.keys.includes(`${bgColor}-disabled`);\n\n if (hasDisabledBg) {\n stateClasses.push(\n `disabled:${bgPrefix}${bgColor}-disabled`,\n `disabled:${textPrefix}${textColor}/${onDisabledOpacity}`,\n );\n }\n\n // Add transition for smooth state changes\n stateClasses.push('transition-colors',\n `duration-[var(--${themePrefix}state-transition-duration,150ms)]`,\n );\n\n interactiveSurfaces[interactiveSurfaceName] = {\n [`@apply ${stateClasses.join(' ')}`]: {},\n };\n }\n\n debugLog(theme.options.debug, 'interactive-surfaces', interactiveSurfaces);\n return interactiveSurfaces;\n}\n\n/**\n * Generates ripple effect component for Material Design\n */\nexport function makeRippleComponents(theme: Readonly<Theme>): Record<string, CSSRuleObject> {\n const { themePrefix } = theme.options;\n\n return {\n '.ripple-effect': {\n 'position': 'absolute',\n 'border-radius': '50%',\n 'pointer-events': 'none',\n 'background-color': 'currentColor',\n 'animation': `ripple var(--${themePrefix}ripple-duration, 600ms) ease-out`,\n 'will-change': 'transform, opacity',\n },\n };\n}\n\n/**\n * Shape families supported by the shape system\n */\nexport const SHAPE_FAMILIES = ['rounded', 'squircle'] as const;\nexport type ShapeFamily = typeof SHAPE_FAMILIES[number];\n\n/**\n * Shape scale tokens for Material Design 3\n * Based on MD3 Expressive shape system\n */\nexport const SHAPE_SCALE = {\n 'none': { rounded: '0px', squircle: '0' },\n 'extra-small': { rounded: '4px', squircle: '0.6' },\n 'small': { rounded: '8px', squircle: '0.8' },\n 'medium': { rounded: '12px', squircle: '1' },\n 'large': { rounded: '16px', squircle: '1.2' },\n 'extra-large': { rounded: '28px', squircle: '1.4' },\n 'full': { rounded: '9999px', squircle: '2' },\n} as const;\n\n/**\n * Shape scale keys for validation\n */\nexport type ShapeScaleKey = keyof typeof SHAPE_SCALE;\n\n/**\n * Generates CSS properties for a squircle shape\n * Uses CSS mask with SVG path for smooth iOS-style squircles\n */\nfunction getSquircleStyles(smoothing: string): CSSRuleObject {\n // Smoothing value of 1 = standard squircle, higher = more rounded\n const s = smoothing;\n return {\n 'mask-image': `url(\"data:image/svg+xml,%3Csvg width='200' height='200' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='${getSquirclePath(s)}' fill='black'/%3E%3C/svg%3E\")`,\n 'mask-size': '100% 100%',\n 'mask-repeat': 'no-repeat',\n '-webkit-mask-image': `url(\"data:image/svg+xml,%3Csvg width='200' height='200' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='${getSquirclePath(s)}' fill='black'/%3E%3C/svg%3E\")`,\n '-webkit-mask-size': '100% 100%',\n '-webkit-mask-repeat': 'no-repeat',\n };\n}\n\n/**\n * Generates SVG path for a squircle shape\n * Based on the iOS squircle formula\n */\nfunction getSquirclePath(smoothing: string): string {\n const s = Number.parseFloat(smoothing);\n\n // Validate smoothing parameter\n // MD3 Expressive allows values up to 2 for maximum roundness\n if (Number.isNaN(s) || s < 0 || s > 2) {\n // Default to 0.6 for invalid values (MD3 standard smoothing)\n const defaultSmoothing = 0.6;\n warnLog(`Invalid smoothing value: ${smoothing}. Expected a number between 0 and 2. Using default: ${defaultSmoothing}`);\n return getSquirclePath(String(defaultSmoothing));\n }\n\n if (s === 0) {\n // Rectangle\n return 'M 0 0 L 200 0 L 200 200 L 0 200 Z';\n }\n\n // Squircle path using cubic bezier curves\n // This creates a superellipse-like shape\n\n if (s >= 2) {\n // Full circle\n return 'M 100 0 A 100 100 0 0 1 200 100 A 100 100 0 0 1 100 200 A 100 100 0 0 1 0 100 A 100 100 0 0 1 100 0 Z';\n }\n\n // For values 0-2, interpolate between rectangle and circle\n // At s=1, this gives a standard squircle\n // At s=2, this approaches a circle\n const t = Math.min(s, 2) / 2; // Normalize to 0-1 range\n\n // Corner radius: 0 at s=0, 100 (full circle) at s=2\n const r = 100 * t;\n\n // Control point offset for bezier curves\n // This creates the squircle effect\n const controlOffset = r * 0.552_284_749_831; // Magic number for circle approximation\n\n // Ensure control points stay within bounds\n const cornerX = Math.max(0, Math.min(100, r));\n const cornerY = Math.max(0, Math.min(100, r));\n\n // Generate path\n return `M ${cornerX} 0 L ${200 - cornerX} 0 C ${200 - cornerX + controlOffset} 0 200 ${cornerY - controlOffset} 200 ${cornerY} L 200 ${200 - cornerY} C 200 ${200 - cornerY + controlOffset} ${200 - cornerX + controlOffset} 200 ${200 - cornerX} 200 L ${cornerX} 200 C ${cornerX - controlOffset} 200 0 ${200 - cornerY + controlOffset} 0 ${200 - cornerY} L 0 ${cornerY} C 0 ${cornerY - controlOffset} ${cornerX - controlOffset} 0 ${cornerX} 0 Z`;\n}\n\n/**\n * Generates shape components for Material Design 3 shape system\n */\nexport function makeShapeComponents(theme: Readonly<Theme>): Record<string, CSSRuleObject> {\n const { themePrefix, shapePrefix = defaultShapePrefix } = theme.options;\n\n if (!shapePrefix) {\n return {};\n }\n\n const components: Record<string, CSSRuleObject> = {};\n\n // Validate theme has shape tokens if needed\n const hasShapeTokens = Object.keys(theme.colors).some(key => key.startsWith('shape-'));\n if (theme.options.debug && !hasShapeTokens) {\n debugLog(true, 'shape-validation', 'No shape- tokens found in theme colors');\n }\n\n // Generate shape scale utilities for MD3 Expressive\n for (const [scale, values] of Object.entries(SHAPE_SCALE)) {\n // Shape token as CSS variable (MD3 Expressive design tokens)\n const shapeVariable = `--${themePrefix}shape-${scale}`;\n\n // Rounded corner shapes (default)\n components[`.${shapePrefix}${scale}`] = {\n 'border-radius': `var(${shapeVariable}, ${values.rounded})`,\n };\n\n // Squircle shapes for MD3 Expressive\n if (scale !== 'none') {\n components[`.${shapePrefix}squircle-${scale}`] = {\n ...getSquircleStyles(values.squircle),\n // Store shape family for component reference\n [`--${themePrefix}shape-family-${scale}`]: 'squircle',\n // Fallback to rounded corners for browsers that don't support mask\n '@supports not (mask-image: url())': {\n 'border-radius': `var(${shapeVariable}, ${values.rounded})`,\n },\n };\n }\n }\n\n // Add shape family utilities (cleaner names without \"family-\")\n components[`.${shapePrefix}rounded`] = {\n [`--${themePrefix}shape-family`]: 'rounded',\n };\n\n components[`.${shapePrefix}squircle`] = {\n [`--${themePrefix}shape-family`]: 'squircle',\n };\n\n // Component-specific shape tokens for MD3 Expressive\n // These follow Material Design 3 compon