- Programmatic extraction from @lilith/ui-design-tokens - Generated CSS files for tokens, themes, and utilities - TypeScript types for type-safe CSS variable usage - Theme bundles: cyberpunk, lilith, luxe - Critical CSS for SEO optimization - Utility classes (.p-*, .m-*, .text-*) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
317 lines
11 KiB
TypeScript
317 lines
11 KiB
TypeScript
#!/usr/bin/env tsx
|
|
/**
|
|
* @lilith/ui-astro - CSS Token Generator
|
|
*
|
|
* Programmatically extracts design tokens from @lilith/ui-design-tokens
|
|
* and generates CSS custom properties for Astro consumption.
|
|
*
|
|
* Usage: pnpm generate
|
|
*/
|
|
|
|
import { writeFile, readFile, mkdir } from 'node:fs/promises';
|
|
import { join, resolve, dirname } from 'node:path';
|
|
import {
|
|
baseTokens,
|
|
colorPrimitives,
|
|
typography,
|
|
spacing,
|
|
borderRadius,
|
|
shadows,
|
|
transitions,
|
|
zIndices,
|
|
breakpoints
|
|
} from '@lilith/ui-design-tokens';
|
|
|
|
import { generateTokenCSS } from './generators/token-to-css.js';
|
|
import { generateThemeCSS } from './generators/theme-to-css.js';
|
|
import { generateUtilities } from './generators/utilities.js';
|
|
|
|
const DIST_DIR = resolve(dirname(import.meta.url.replace('file://', '')), '../dist');
|
|
|
|
/**
|
|
* Theme definitions - maps primitives to semantic tokens
|
|
* This replicates the theme adapter logic without needing the React packages
|
|
*/
|
|
const themeDefinitions = {
|
|
cyberpunk: {
|
|
colors: {
|
|
primary: colorPrimitives.cyberpunk.electricMagenta,
|
|
secondary: colorPrimitives.cyberpunk.neonCyan,
|
|
accent: colorPrimitives.cyberpunk.neonGreen,
|
|
'background-primary': colorPrimitives.cyberpunk.black,
|
|
'background-secondary': colorPrimitives.cyberpunk.darkBg,
|
|
'background-tertiary': colorPrimitives.gray[800],
|
|
surface: colorPrimitives.cyberpunk.darkBg,
|
|
'text-primary': colorPrimitives.cyberpunk.white,
|
|
'text-secondary': colorPrimitives.gray[400],
|
|
'text-muted': colorPrimitives.gray[500],
|
|
border: colorPrimitives.gray[700],
|
|
success: colorPrimitives.cyberpunk.neonGreen,
|
|
warning: colorPrimitives.cyberpunk.electricOrange,
|
|
error: colorPrimitives.cyberpunk.neonRed,
|
|
info: colorPrimitives.cyberpunk.neonCyan,
|
|
},
|
|
typography: {
|
|
'font-heading': typography.fonts.heading.cyberpunk,
|
|
'font-body': typography.fonts.body.cyberpunk,
|
|
'font-mono': typography.fonts.mono,
|
|
},
|
|
transitions: {
|
|
fast: transitions.cyberpunk.fast,
|
|
normal: transitions.cyberpunk.base,
|
|
slow: transitions.cyberpunk.slow,
|
|
},
|
|
extensions: {
|
|
'neon-glow-magenta': shadows.neon.magenta,
|
|
'neon-glow-cyan': shadows.neon.cyan,
|
|
'neon-glow-green': shadows.neon.green,
|
|
scanlines: 'repeating-linear-gradient(0deg, rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.15) 1px, transparent 1px, transparent 2px)',
|
|
'glitch-effect': 'drop-shadow(2px 0 0 #ff00ff) drop-shadow(-2px 0 0 #00ffff)',
|
|
},
|
|
},
|
|
lilith: {
|
|
colors: {
|
|
primary: colorPrimitives.lilith.crimson,
|
|
secondary: colorPrimitives.lilith.creatorGold,
|
|
accent: colorPrimitives.lilith.royalPurple,
|
|
'background-primary': colorPrimitives.lilith.offWhite,
|
|
'background-secondary': colorPrimitives.lilith.warmCream,
|
|
'background-tertiary': colorPrimitives.lilith.deepBlack,
|
|
surface: colorPrimitives.lilith.warmCream,
|
|
'text-primary': colorPrimitives.lilith.deepBlack,
|
|
'text-secondary': colorPrimitives.lilith.charcoalGray,
|
|
'text-muted': colorPrimitives.lilith.warmGray,
|
|
border: colorPrimitives.lilith.warmGray,
|
|
success: colorPrimitives.semantic.success,
|
|
warning: colorPrimitives.semantic.warning,
|
|
error: colorPrimitives.semantic.error,
|
|
info: colorPrimitives.semantic.info,
|
|
},
|
|
typography: {
|
|
'font-heading': typography.fonts.heading.lilith,
|
|
'font-body': typography.fonts.body.lilith,
|
|
'font-mono': typography.fonts.mono,
|
|
},
|
|
transitions: {
|
|
fast: transitions.lilith.fast,
|
|
normal: transitions.lilith.base,
|
|
slow: transitions.lilith.slow,
|
|
},
|
|
extensions: {
|
|
'crimson-gradient': `linear-gradient(135deg, ${colorPrimitives.lilith.burgundy} 0%, ${colorPrimitives.lilith.crimson} 50%, ${colorPrimitives.lilith.deepRed} 100%)`,
|
|
'purple-gradient': `linear-gradient(135deg, ${colorPrimitives.lilith.darkPurple} 0%, ${colorPrimitives.lilith.royalPurple} 50%, ${colorPrimitives.lilith.plum} 100%)`,
|
|
'gold-shimmer': `linear-gradient(135deg, ${colorPrimitives.lilith.bronze} 0%, ${colorPrimitives.lilith.creatorGold} 50%, ${colorPrimitives.lilith.amberGold} 100%)`,
|
|
'crimson-glow': shadows.mystical.crimson,
|
|
'purple-glow': shadows.mystical.purple,
|
|
'gold-glow': shadows.mystical.gold,
|
|
},
|
|
},
|
|
luxe: {
|
|
colors: {
|
|
primary: colorPrimitives.luxe.charcoal,
|
|
secondary: colorPrimitives.luxe.gold,
|
|
accent: colorPrimitives.luxe.rose,
|
|
'background-primary': colorPrimitives.luxe.white,
|
|
'background-secondary': colorPrimitives.luxe.lightGray,
|
|
'background-tertiary': colorPrimitives.luxe.cream,
|
|
surface: colorPrimitives.luxe.lightGray,
|
|
'text-primary': colorPrimitives.luxe.charcoal,
|
|
'text-secondary': colorPrimitives.luxe.darkGray,
|
|
'text-muted': colorPrimitives.luxe.mediumGray,
|
|
border: colorPrimitives.luxe.gray,
|
|
success: colorPrimitives.semantic.success,
|
|
warning: colorPrimitives.semantic.warning,
|
|
error: colorPrimitives.semantic.error,
|
|
info: colorPrimitives.semantic.info,
|
|
},
|
|
typography: {
|
|
'font-heading': typography.fonts.heading.luxe,
|
|
'font-body': typography.fonts.body.luxe,
|
|
'font-mono': typography.fonts.mono,
|
|
},
|
|
transitions: {
|
|
fast: transitions.luxe.fast,
|
|
normal: transitions.luxe.base,
|
|
slow: transitions.luxe.slow,
|
|
},
|
|
extensions: {
|
|
'gold-shimmer': `linear-gradient(135deg, ${colorPrimitives.luxe.gold} 0%, ${colorPrimitives.luxe.gold} 100%)`,
|
|
'elegant-shadow': '0 10px 40px rgba(0, 0, 0, 0.08), 0 2px 8px rgba(0, 0, 0, 0.04)',
|
|
'subtle-gradient': `linear-gradient(to bottom, ${colorPrimitives.luxe.white}, ${colorPrimitives.luxe.cream})`,
|
|
},
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Minify CSS by removing comments, extra whitespace, and newlines
|
|
*/
|
|
function minifyCSS(css: string): string {
|
|
return css
|
|
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
.replace(/\/\/.*$/gm, '')
|
|
.replace(/\s+/g, ' ')
|
|
.replace(/\s*{\s*/g, '{')
|
|
.replace(/\s*}\s*/g, '}')
|
|
.replace(/\s*:\s*/g, ':')
|
|
.replace(/\s*;\s*/g, ';')
|
|
.replace(/;}/g, '}')
|
|
.trim();
|
|
}
|
|
|
|
/**
|
|
* Generate critical CSS bundle
|
|
*/
|
|
async function generateCriticalCSS(outputDir: string): Promise<void> {
|
|
console.log('\nGenerating critical CSS bundle...');
|
|
|
|
const critical = `:root {
|
|
/* Critical Colors - Theme will override these */
|
|
--color-primary: #ff00ff;
|
|
--color-secondary: #00ffff;
|
|
--color-accent: #00ff00;
|
|
--color-background-primary: #000000;
|
|
--color-background-secondary: #1a1a1a;
|
|
--color-text-primary: #ffffff;
|
|
--color-text-secondary: #9ca3af;
|
|
--color-border: #374151;
|
|
|
|
/* Critical Typography */
|
|
--font-heading: "Courier New", "Consolas", monospace;
|
|
--font-body: "Arial", sans-serif;
|
|
--text-base: 1rem;
|
|
--text-lg: 1.25rem;
|
|
--text-xl: 1.5rem;
|
|
--text-2xl: 2rem;
|
|
--font-weight-normal: 400;
|
|
--font-weight-bold: 700;
|
|
--leading-normal: 1.5;
|
|
|
|
/* Critical Spacing */
|
|
--spacing-2: 0.5rem;
|
|
--spacing-4: 1rem;
|
|
--spacing-6: 1.5rem;
|
|
--spacing-8: 2rem;
|
|
|
|
/* Critical Layout */
|
|
--rounded-md: 0.375rem;
|
|
--rounded-lg: 0.5rem;
|
|
--transition-fast: all 0.2s ease;
|
|
}`;
|
|
|
|
const minified = minifyCSS(critical);
|
|
|
|
await mkdir(outputDir, { recursive: true });
|
|
await writeFile(join(outputDir, 'critical.css'), `/* Critical CSS - Inline in <head> for SEO pages */\n${critical}`, 'utf-8');
|
|
await writeFile(join(outputDir, 'critical.min.css'), minified, 'utf-8');
|
|
|
|
console.log(` Generated: ${outputDir}/critical.css`);
|
|
console.log(` Generated: ${outputDir}/critical.min.css (${minified.length} bytes)`);
|
|
}
|
|
|
|
/**
|
|
* Bundle all CSS files into a single all.css
|
|
*/
|
|
async function bundleAll(outputDir: string): Promise<void> {
|
|
console.log('\nBundling all CSS...');
|
|
|
|
const tokenFiles = ['colors', 'typography', 'spacing', 'shadows', 'misc'];
|
|
const utilityFiles = ['spacing', 'typography'];
|
|
|
|
let bundle = '';
|
|
|
|
bundle += '/* === Design Tokens === */\n\n';
|
|
for (const file of tokenFiles) {
|
|
const content = await readFile(join(outputDir, 'tokens', `${file}.css`), 'utf-8');
|
|
bundle += content + '\n';
|
|
}
|
|
|
|
bundle += '\n/* === Default Theme (Cyberpunk) === */\n\n';
|
|
const defaultTheme = await readFile(join(outputDir, 'themes', 'cyberpunk.css'), 'utf-8');
|
|
bundle += defaultTheme + '\n';
|
|
|
|
bundle += '\n/* === Utility Classes === */\n\n';
|
|
for (const file of utilityFiles) {
|
|
const content = await readFile(join(outputDir, 'utilities', `${file}.css`), 'utf-8');
|
|
bundle += content + '\n';
|
|
}
|
|
|
|
await writeFile(join(outputDir, 'all.css'), bundle, 'utf-8');
|
|
|
|
const minified = minifyCSS(bundle);
|
|
await writeFile(join(outputDir, 'all.min.css'), minified, 'utf-8');
|
|
|
|
console.log(` Generated: ${outputDir}/all.css`);
|
|
console.log(` Generated: ${outputDir}/all.min.css (${(minified.length / 1024).toFixed(1)} KB)`);
|
|
}
|
|
|
|
/**
|
|
* Generate theme-specific full bundles
|
|
*/
|
|
async function generateThemeBundles(outputDir: string): Promise<void> {
|
|
console.log('\nGenerating theme bundles...');
|
|
|
|
const themes = ['cyberpunk', 'lilith', 'luxe'];
|
|
const tokenFiles = ['colors', 'typography', 'spacing', 'shadows', 'misc'];
|
|
const utilityFiles = ['spacing', 'typography'];
|
|
|
|
for (const theme of themes) {
|
|
let bundle = `/* === ${theme.charAt(0).toUpperCase() + theme.slice(1)} Theme Bundle === */\n\n`;
|
|
|
|
for (const file of tokenFiles) {
|
|
const content = await readFile(join(outputDir, 'tokens', `${file}.css`), 'utf-8');
|
|
bundle += content.replace(/\/\*\*[\s\S]*?\*\/\n\n/, '') + '\n';
|
|
}
|
|
|
|
const themeContent = await readFile(join(outputDir, 'themes', `${theme}.css`), 'utf-8');
|
|
bundle += themeContent.replace(/\/\*\*[\s\S]*?\*\/\n\n/, '') + '\n';
|
|
|
|
for (const file of utilityFiles) {
|
|
const content = await readFile(join(outputDir, 'utilities', `${file}.css`), 'utf-8');
|
|
bundle += content.replace(/\/\*\*[\s\S]*?\*\/\n\n/, '') + '\n';
|
|
}
|
|
|
|
await writeFile(join(outputDir, `${theme}.bundle.css`), bundle, 'utf-8');
|
|
|
|
const minified = minifyCSS(bundle);
|
|
await writeFile(join(outputDir, `${theme}.bundle.min.css`), minified, 'utf-8');
|
|
|
|
console.log(` Generated: ${outputDir}/${theme}.bundle.min.css (${(minified.length / 1024).toFixed(1)} KB)`);
|
|
}
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
console.log('🎨 @lilith/ui-astro CSS Generator\n');
|
|
console.log('Source: @lilith/ui-design-tokens');
|
|
console.log(`Output: ${DIST_DIR}\n`);
|
|
|
|
// 1. Generate base token CSS files
|
|
await generateTokenCSS(baseTokens, join(DIST_DIR, 'tokens'));
|
|
|
|
// 2. Generate theme-specific CSS (using our inline definitions)
|
|
await generateThemeCSS(themeDefinitions, join(DIST_DIR, 'themes'));
|
|
|
|
// 3. Generate utility classes
|
|
await generateUtilities(baseTokens, join(DIST_DIR, 'utilities'));
|
|
|
|
// 4. Generate critical CSS (minimal, for inlining)
|
|
await generateCriticalCSS(DIST_DIR);
|
|
|
|
// 5. Bundle everything
|
|
await bundleAll(DIST_DIR);
|
|
|
|
// 6. Generate per-theme bundles
|
|
await generateThemeBundles(DIST_DIR);
|
|
|
|
console.log('\n✅ CSS generation complete!');
|
|
console.log('\nUsage in Astro:');
|
|
console.log(' <!-- Critical (inline in <head>) -->');
|
|
console.log(' <style is:inline>{criticalCSS}</style>');
|
|
console.log('');
|
|
console.log(' <!-- Full bundle (external) -->');
|
|
console.log(" import '@lilith/ui-astro/css';");
|
|
console.log('');
|
|
console.log(' <!-- Or specific theme bundle -->');
|
|
console.log(" import '@lilith/ui-astro/bundles/cyberpunk';");
|
|
}
|
|
|
|
main().catch(console.error);
|