ui-astro/scripts/generate.ts
Lilith 7e99959ffa Initial release: Astro-compatible CSS tokens from cyberpunk-ui
- 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>
2026-01-01 22:12:27 -08:00

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);