From 8ccc741880888e91b489e6b0d231e6ea15570a7e Mon Sep 17 00:00:00 2001 From: Dallas Peters Date: Thu, 5 Feb 2026 21:58:56 +0000 Subject: [PATCH 1/6] Add sync-optics-data script Extracts tokens and components from Optics source repo. Generates optics-data.ts with 29 real components. Co-Authored-By: Claude Opus 4.5 --- scripts/sync-optics-data.ts | 872 ++++++++++++++++++++++++++++++++++++ scripts/tsconfig.json | 13 + 2 files changed, 885 insertions(+) create mode 100644 scripts/sync-optics-data.ts create mode 100644 scripts/tsconfig.json diff --git a/scripts/sync-optics-data.ts b/scripts/sync-optics-data.ts new file mode 100644 index 0000000..3bd75f1 --- /dev/null +++ b/scripts/sync-optics-data.ts @@ -0,0 +1,872 @@ +#!/usr/bin/env npx ts-node + +/** + * Sync Optics Data Script + * Run: npm run sync-data + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +// ESM compatibility +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Configuration +const OPTICS_PACKAGE = '@rolemodel/optics'; +const OPTICS_SOURCE_REPO = path.join(__dirname, '../../optics'); // Assumes optics repo is sibling +const OUTPUT_FILE = path.join(__dirname, '../src/optics-data.ts'); + +// Token categories matching Optics Storybook +type TokenCategory = + | 'animation' + | 'border' + | 'breakpoint' + | 'color' + | 'encoded-image' + | 'input' + | 'opacity' + | 'shadow' + | 'sizing' + | 'spacing' + | 'typography' + | 'z-index'; + +interface DesignToken { + name: string; + cssVar: string; + value: string; + category: TokenCategory; + description: string; +} + +interface CSSPattern { + name: string; + description: string; + className: string; + type: 'component' | 'layout' | 'utility'; + modifiers: string[]; + elements: string[]; + exampleHtml: string; + docsUrl: string; +} + +interface Documentation { + section: string; + title: string; + content: string; + tokens: string[]; +} + +// ============================================================================ +// Find Optics paths +// ============================================================================ + +function findOpticsPackage(): string | null { + const possiblePaths = [ + path.join(__dirname, '../node_modules', OPTICS_PACKAGE), + path.join(__dirname, '../../node_modules', OPTICS_PACKAGE), + ]; + + for (const p of possiblePaths) { + if (fs.existsSync(p)) { + return p; + } + } + return null; +} + +function findOpticsSourceRepo(): string | null { + if (fs.existsSync(OPTICS_SOURCE_REPO) && fs.existsSync(path.join(OPTICS_SOURCE_REPO, 'src/stories'))) { + return OPTICS_SOURCE_REPO; + } + return null; +} + +// ============================================================================ +// Token Extraction from tokens.json +// ============================================================================ + +function flattenTokens(obj: any, prefix: string = ''): DesignToken[] { + const tokens: DesignToken[] = []; + + for (const [key, value] of Object.entries(obj)) { + const name = prefix ? `${prefix}-${key}` : key; + + if (typeof value === 'string') { + const category = categorizeToken(name); + tokens.push({ + name, + cssVar: `--op-${name}`, + value, + category, + description: generateTokenDescription(name, category), + }); + } else if (typeof value === 'object' && value !== null) { + tokens.push(...flattenTokens(value, name)); + } + } + + return tokens; +} + +function categorizeToken(name: string): TokenCategory { + if (name.includes('z-index') || name.startsWith('z-')) return 'z-index'; + if (name.startsWith('animation-') || name.startsWith('transition-') || name.startsWith('duration-') || name.startsWith('easing-')) return 'animation'; + if (name.startsWith('radius-') || name.startsWith('border-')) return 'border'; + if (name.startsWith('breakpoint-') || name.startsWith('screen-')) return 'breakpoint'; + if (name.startsWith('input-')) return 'input'; + if (name.startsWith('opacity-') || name.includes('-opacity')) return 'opacity'; + if (name.startsWith('shadow-') || name.includes('-shadow')) return 'shadow'; + if (name.startsWith('size-') || name.includes('-width') || name.includes('-height')) return 'sizing'; + if (name.startsWith('space-') || name.startsWith('gap-')) return 'spacing'; + if (name.startsWith('font-') || name.startsWith('line-height') || name.startsWith('letter-') || name.startsWith('text-')) return 'typography'; + if (name.startsWith('encoded-') || name.startsWith('image-')) return 'encoded-image'; + if (name.startsWith('color-') || name.includes('-color-') || name.includes('-on-') || name.includes('-base') || name.includes('-plus-') || name.includes('-minus-')) return 'color'; + return 'sizing'; +} + +function generateTokenDescription(name: string, category: TokenCategory): string { + if (category === 'color') { + if (name.includes('-on-')) { + const parts = name.split('-on-'); + return `Text color for use ON ${parts[1]} background. MUST be paired with matching background color.`; + } + if (name.endsWith('-h')) return `Hue component (HSL) for ${name.replace('-h', '')}`; + if (name.endsWith('-s')) return `Saturation component (HSL) for ${name.replace('-s', '')}`; + if (name.endsWith('-l')) return `Lightness component (HSL) for ${name.replace('-l', '')}`; + } + return `${category} token: ${name}`; +} + +// ============================================================================ +// CSS Token Extraction from dist CSS (fallback) +// ============================================================================ + +function extractTokensFromCSS(cssContent: string): DesignToken[] { + const tokens: DesignToken[] = []; + const customPropRegex = /--op-([a-z0-9-]+):\s*([^;]+);/g; + let match; + + while ((match = customPropRegex.exec(cssContent)) !== null) { + const name = match[1]; + const value = match[2].trim(); + const category = categorizeToken(name); + + tokens.push({ + name, + cssVar: `--op-${name}`, + value, + category, + description: generateTokenDescription(name, category), + }); + } + + return tokens; +} + +// ============================================================================ +// CSS Class Extraction for validation +// ============================================================================ + +function extractCSSClasses(cssContent: string): Set { + const classes = new Set(); + const classRegex = /\.([a-z][a-z0-9_-]*)/g; + let match; + + while ((match = classRegex.exec(cssContent)) !== null) { + classes.add(match[1]); + } + + return classes; +} + +// ============================================================================ +// Storybook MDX Parsing +// ============================================================================ + +interface ClassDoc { + className: string; + description: string; + isModifier: boolean; + baseClass?: string; +} + +function parseComponentMdx(mdxPath: string, componentName: string): { + componentDescription: string; + classes: ClassDoc[]; +} { + const content = fs.readFileSync(mdxPath, 'utf-8'); + + // Extract description (first paragraph after # ComponentName) + const descMatch = content.match(new RegExp(`# ${componentName}[\\s\\S]*?\\n\\n([^#\\n<][^\\n]+)`)); + const componentDescription = descMatch ? descMatch[1].trim() : ''; + + // Extract ALL class documentation: `.class-name` Description text + const classes: ClassDoc[] = []; + const classDocRegex = /`\.([a-z][a-z0-9_-]*(?:--[a-z0-9-]+)?)`\s+([^`\n]+)/g; + let classMatch; + + while ((classMatch = classDocRegex.exec(content)) !== null) { + const className = classMatch[1]; + const description = classMatch[2].trim(); + const isModifier = className.includes('--'); + const baseClass = isModifier ? className.split('--')[0] : undefined; + + classes.push({ className, description, isModifier, baseClass }); + } + + return { componentDescription, classes }; +} + +function getBaseClasses(classes: ClassDoc[]): string[] { + // Get unique base classes (non-modifiers) + const baseClasses = classes + .filter(c => !c.isModifier) + .map(c => c.className); + return [...new Set(baseClasses)]; +} + +// ============================================================================ +// Storybook Stories Parsing +// ============================================================================ + +function parseStoriesFile(storiesPath: string): { variants: string[]; sizes: string[] } { + const content = fs.readFileSync(storiesPath, 'utf-8'); + + // Extract variant options + const variantMatch = content.match(/variant:\s*\{[^}]*options:\s*\[([^\]]+)\]/); + const variants = variantMatch + ? variantMatch[1].split(',').map(v => v.trim().replace(/['"]/g, '')) + : []; + + // Extract size options + const sizeMatch = content.match(/size:\s*\{[^}]*options:\s*\[([^\]]+)\]/); + const sizes = sizeMatch + ? sizeMatch[1].split(',').map(s => s.trim().replace(/['"]/g, '')) + : []; + + return { variants, sizes }; +} + +// ============================================================================ +// Scrape Components from Storybook Source +// NOTE: Requires the Optics git repo to be cloned at ../../optics +// Storybook files are NOT included in the npm package +// ============================================================================ + +function scrapeComponentsFromStorybook(sourceRepo: string, opticsPackagePath: string, validClasses: Set): CSSPattern[] { + const patterns: CSSPattern[] = []; + const componentsDir = path.join(sourceRepo, 'src/stories/Components'); + const cssComponentsDir = path.join(opticsPackagePath, 'dist/css/components'); + + const processDirectory = (dir: string, type: 'component' | 'layout' | 'utility') => { + if (!fs.existsSync(dir)) return; + + const items = fs.readdirSync(dir); + + for (const item of items) { + const itemPath = path.join(dir, item); + if (!fs.statSync(itemPath).isDirectory()) continue; + + const mdxPath = path.join(itemPath, `${item}.mdx`); + const storiesPath = path.join(itemPath, `${item}.stories.js`); + + if (!fs.existsSync(mdxPath)) continue; + + console.log(` Parsing ${item}...`); + + // Parse MDX for ALL class documentation + const mdxData = parseComponentMdx(mdxPath, item); + const baseClasses = getBaseClasses(mdxData.classes); + + // Parse stories for variants + const storiesData = fs.existsSync(storiesPath) ? parseStoriesFile(storiesPath) : { variants: [], sizes: [] }; + + // For multi-class components (like Form), use primary class, store all classes + if (baseClasses.length > 1) { + // Use first class as primary, collect all modifiers/elements across all base classes + const primaryClass = baseClasses[0]; + const allModifiers: string[] = []; + const allElements: string[] = []; + + for (const baseClass of baseClasses) { + if (!validClasses.has(baseClass)) continue; + + // Collect modifiers + const mods = Array.from(validClasses).filter(c => c.startsWith(`${baseClass}--`)); + allModifiers.push(...mods); + + // Collect elements + const elems = Array.from(validClasses).filter(c => c.startsWith(`${baseClass}__`)); + allElements.push(...elems); + } + + patterns.push({ + name: item, + description: mdxData.componentDescription || `${item} component`, + className: primaryClass, + type, + modifiers: [...new Set(allModifiers)].sort(), + elements: [...new Set([...baseClasses.slice(1), ...allElements])].sort(), // Include other base classes as "elements" + exampleHtml: generateExampleHtml(primaryClass, [], allElements), + docsUrl: `https://docs.optics.rolemodel.design/?path=/docs/components-${item.toLowerCase()}--docs`, + }); + } else { + // Single base class component - use first base class or derive from component name + const className = baseClasses[0] || deriveClassName(item, validClasses); + if (!className || !validClasses.has(className)) { + console.log(` ⚠️ No valid base class found for ${item}`); + continue; + } + + // Build modifiers list from MDX and validate against actual CSS + let modifiers = mdxData.classes + .filter(c => c.isModifier) + .map(c => c.className) + .filter(m => validClasses.has(m)); + + // Add variant-based modifiers from stories + for (const variant of storiesData.variants) { + if (variant !== 'default') { + const modClass = `${className}--${variant}`; + if (validClasses.has(modClass) && !modifiers.includes(modClass)) { + modifiers.push(modClass); + } + } + } + + // Add size-based modifiers from stories + for (const size of storiesData.sizes) { + const sizeClass = `${className}--${size}`; + if (validClasses.has(sizeClass) && !modifiers.includes(sizeClass)) { + modifiers.push(sizeClass); + } + } + + // Also find modifiers from CSS + const cssModifiers = Array.from(validClasses) + .filter(c => c.startsWith(`${className}--`)); + + modifiers = [...new Set([...modifiers, ...cssModifiers])].sort(); + + // Extract elements (BEM __element classes) + const elements = Array.from(validClasses) + .filter(c => c.startsWith(`${className}__`)) + .sort(); + + patterns.push({ + name: item, + description: mdxData.componentDescription || `${item} component`, + className, + type, + modifiers, + elements, + exampleHtml: generateExampleHtml(className, modifiers, elements), + docsUrl: `https://docs.optics.rolemodel.design/?path=/docs/${type === 'component' ? 'components' : type === 'layout' ? 'layout' : 'utilities'}-${item.toLowerCase()}--docs`, + }); + } + } + }; + + // Helper to derive class name from component name + function deriveClassName(componentName: string, validClasses: Set): string | null { + // Try common patterns + const patterns = [ + componentName.toLowerCase(), + componentName.toLowerCase().replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, ''), + componentName.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, ''), + ]; + + for (const pattern of patterns) { + if (validClasses.has(pattern)) return pattern; + } + + // Try finding a class that starts with component name + const prefix = componentName.toLowerCase(); + for (const cls of validClasses) { + if (cls.startsWith(prefix) && !cls.includes('--') && !cls.includes('__')) { + return cls; + } + } + + return null; + } + + processDirectory(componentsDir, 'component'); + // Skip utilities - they're CSS helpers, not components + + // Add layout utilities manually (op-stack, op-cluster, op-split) + const layoutPatterns = ['op-stack', 'op-cluster', 'op-split']; + for (const className of layoutPatterns) { + if (validClasses.has(className)) { + patterns.push({ + name: className.replace('op-', '').charAt(0).toUpperCase() + className.replace('op-', '').slice(1), + description: `Layout utility: ${className}`, + className, + type: 'layout', + modifiers: [], + elements: [], + exampleHtml: `
...
`, + docsUrl: `https://docs.optics.rolemodel.design/?path=/docs/layout-${className.replace('op-', '')}--docs`, + }); + } + } + + // Add data-attribute patterns (Tooltip, etc.) + patterns.push(...extractDataAttributePatterns(cssComponentsDir)); + + return patterns; +} + +// Extract patterns that use data attributes instead of classes (e.g., Tooltip) +function extractDataAttributePatterns(componentsDir: string): CSSPattern[] { + const patterns: CSSPattern[] = []; + + if (!fs.existsSync(componentsDir)) return patterns; + + const dataAttrComponents: Record = { + 'tooltip.css': { + attr: 'data-tooltip-text', + description: 'CSS-only tooltip using data attributes', + positions: ['top', 'bottom', 'left', 'right'] + } + }; + + for (const [cssFile, config] of Object.entries(dataAttrComponents)) { + const cssPath = path.join(componentsDir, cssFile); + if (!fs.existsSync(cssPath)) continue; + + const name = cssFile.replace('.css', '').split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(''); + + patterns.push({ + name, + description: config.description, + className: `[${config.attr}]`, + type: 'component', + modifiers: config.positions?.map(p => `[data-tooltip-position="${p}"]`) || [], + elements: [], + exampleHtml: ``, + docsUrl: `https://docs.optics.rolemodel.design/?path=/docs/components-${cssFile.replace('.css', '')}--docs`, + }); + } + + return patterns; +} + +function generateExampleHtml(className: string, modifiers: string[], elements: string[]): string { + if (elements.length > 0) { + const elementsHtml = elements + .slice(0, 3) + .map(el => `
...
`) + .join('\n'); + return `
\n${elementsHtml}\n
`; + } + + if (className === 'btn') { + return ``; + } + + return `
...
`; +} + +// ============================================================================ +// Derive Components from CSS Files (when Storybook not available) +// Uses the component CSS files in dist/css/components/ from npm package +// ============================================================================ + +function deriveComponentsFromCSS(opticsPath: string, validClasses: Set): CSSPattern[] { + const patterns: CSSPattern[] = []; + const componentsDir = path.join(opticsPath, 'dist/css/components'); + + if (!fs.existsSync(componentsDir)) { + console.log(' ⚠️ Components CSS directory not found'); + return patterns; + } + + const cssFiles = fs.readdirSync(componentsDir).filter(f => f.endsWith('.css') && f !== 'index.css'); + + for (const cssFile of cssFiles) { + const cssPath = path.join(componentsDir, cssFile); + const cssContent = fs.readFileSync(cssPath, 'utf-8'); + + // Extract all classes from this CSS file + const fileClasses = new Set(); + const classRegex = /\.([a-z][a-z0-9_-]*)/g; + let match; + while ((match = classRegex.exec(cssContent)) !== null) { + fileClasses.add(match[1]); + } + + // Find base classes (no -- or __) + const baseClasses = Array.from(fileClasses).filter(c => !c.includes('--') && !c.includes('__')); + + // Group classes by their base + for (const baseClass of baseClasses) { + if (!validClasses.has(baseClass)) continue; + + // Skip if we already have this base class + if (patterns.some(p => p.className === baseClass)) continue; + + // Find modifiers and elements for this base class + const modifiers = Array.from(fileClasses) + .filter(c => c.startsWith(`${baseClass}--`)) + .sort(); + + const elements = Array.from(fileClasses) + .filter(c => c.startsWith(`${baseClass}__`)) + .sort(); + + // Generate readable name from class + const name = baseClass + .split('-') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(''); + + // Generate description from CSS file header comment if available + const headerMatch = cssContent.match(/\/\*\*?\s*\n?\s*\*?\s*([^*\n]+)/); + const description = headerMatch ? headerMatch[1].trim() : `${name} component`; + + patterns.push({ + name, + description, + className: baseClass, + type: 'component', + modifiers, + elements, + exampleHtml: generateExampleHtml(baseClass, modifiers, elements), + docsUrl: `https://docs.optics.rolemodel.design/?path=/docs/components-${cssFile.replace('.css', '')}--docs`, + }); + } + } + + // Add layout utilities (these are in core/utilities, not components) + const layoutPatterns = ['op-stack', 'op-cluster', 'op-split']; + for (const className of layoutPatterns) { + if (validClasses.has(className)) { + const name = className.replace('op-', '').charAt(0).toUpperCase() + className.replace('op-', '').slice(1); + patterns.push({ + name, + description: `Layout utility: ${className}`, + className, + type: 'layout', + modifiers: Array.from(validClasses).filter(c => c.startsWith(`${className}--`)), + elements: [], + exampleHtml: `
...
`, + docsUrl: `https://docs.optics.rolemodel.design/?path=/docs/layout-${className.replace('op-', '')}--docs`, + }); + } + } + + return patterns; +} + +// ============================================================================ +// Documentation - Preserve existing documentation from optics-data.ts +// ============================================================================ + +interface ExistingDoc { + section: string; + title: string; + content: string; + tokens?: string[]; +} + +function readExistingDocumentation(): ExistingDoc[] { + // Read existing optics-data.ts to preserve manually written documentation + if (!fs.existsSync(OUTPUT_FILE)) { + return getDefaultDocumentation(); + } + + const content = fs.readFileSync(OUTPUT_FILE, 'utf-8'); + + // Extract documentation array from file + const docMatch = content.match(/export const documentation: Documentation\[\] = \[([\s\S]*?)\];(?=\s*$|\s*\/\/|\s*export)/); + if (!docMatch) { + return getDefaultDocumentation(); + } + + try { + // Parse the documentation array (it's JSON-like) + const docArrayStr = '[' + docMatch[1] + ']'; + // Remove trailing commas before parsing + const cleanedStr = docArrayStr.replace(/,(\s*[\]}])/g, '$1'); + return JSON.parse(cleanedStr); + } catch { + console.log(' ⚠️ Could not parse existing documentation, using defaults'); + return getDefaultDocumentation(); + } +} + +function getDefaultDocumentation(): ExistingDoc[] { + return [ + { + section: 'overview', + title: 'Optics Overview', + content: 'Optics is a CSS-only design system. It provides CSS custom properties (tokens) and utility classes - NOT JavaScript components. Use the provided CSS classes and tokens; do not write custom CSS for patterns that already exist.' + }, + { + section: 'color-pairing', + title: 'Color Pairing Rule', + content: 'CRITICAL: Background and text colors must ALWAYS be paired. Never use --op-color-{family}-{scale} without also setting color to --op-color-{family}-on-{scale}. The "on" tokens are calculated for proper contrast against their matching background.' + }, + { + section: 'color-system', + title: 'HSL Color System', + content: 'Optics uses HSL-based colors defined by -h (hue), -s (saturation), -l (lightness) tokens. A full scale is generated from plus-max (lightest) to minus-max (darkest). Each scale step has a matching "on-" token for text.' + }, + { + section: 'use-existing', + title: 'Use Existing Classes', + content: 'Don\'t write custom CSS for components that already exist. Use .btn for buttons, .card for cards, .op-stack/.op-cluster/.op-split for layouts. Only write custom CSS when truly extending the system.' + } + ]; +} + +function getDocumentation(tokens: DesignToken[]): Documentation[] { + // Preserve existing manually-written documentation + const existing = readExistingDocumentation(); + + // Add tokens field based on section content + return existing.map((doc: ExistingDoc): Documentation => { + let sectionTokens: string[] = []; + + // Auto-populate tokens based on section type + if (doc.section === 'color-system' || doc.section === 'color-pairing') { + sectionTokens = tokens.filter((t: DesignToken) => t.category === 'color').map((t: DesignToken) => t.name); + } else if (doc.section === 'spacing') { + sectionTokens = tokens.filter((t: DesignToken) => t.category === 'spacing').map((t: DesignToken) => t.name); + } else if (doc.section === 'typography') { + sectionTokens = tokens.filter((t: DesignToken) => t.category === 'typography').map((t: DesignToken) => t.name); + } + + return { + section: doc.section, + title: doc.title, + content: doc.content, + tokens: doc.tokens || sectionTokens + }; + }); +} + +// ============================================================================ +// Output Generation +// ============================================================================ + +function generateOutputFile( + tokens: DesignToken[], + patterns: CSSPattern[], + docs: Documentation[], + version: string +): string { + const tokenCategories = Array.from(new Set(tokens.map(t => t.category))).sort(); + + return `/** + * Optics Design System Data + * AUTO-GENERATED - Run: npm run sync-data + * Version: ${version} | Generated: ${new Date().toISOString()} + */ + +export type TokenCategory = ${tokenCategories.map(c => `'${c}'`).join(' | ')}; + +export interface DesignToken { + name: string; + cssVar: string; + value: string; + category: TokenCategory; + description: string; +} + +export interface CSSPattern { + name: string; + description: string; + className: string; + type: 'component' | 'layout' | 'utility'; + modifiers: string[]; + elements: string[]; + exampleHtml: string; + docsUrl: string; +} + +export interface Component extends CSSPattern { + tokens: string[]; + usage: string; + examples: string[]; +} + +export interface Documentation { + section: string; + title: string; + content: string; + tokens: string[]; +} + +export const designTokens: DesignToken[] = ${JSON.stringify(tokens, null, 2)}; + +export const cssPatterns: CSSPattern[] = ${JSON.stringify(patterns, null, 2)}; + +// Backwards compatibility: components alias with extended interface +export const components: Component[] = cssPatterns.map(p => ({ + ...p, + tokens: p.modifiers, + usage: p.description, + examples: p.exampleHtml ? [p.exampleHtml] : [], +})); + +export const documentation: Documentation[] = ${JSON.stringify(docs, null, 2)}; +`; +} + +// ============================================================================ +// Main +// ============================================================================ + +async function main() { + console.log('🔄 Syncing Optics data...\n'); + + // Find Optics package + const opticsPath = findOpticsPackage(); + if (!opticsPath) { + console.error('❌ Could not find @rolemodel/optics package'); + console.error(' Run: npm install @rolemodel/optics'); + process.exit(1); + } + console.log(`📦 Found Optics package at: ${opticsPath}`); + + // Find Optics source repo + const sourceRepo = findOpticsSourceRepo(); + if (sourceRepo) { + console.log(`📂 Found Optics source repo at: ${sourceRepo}`); + } else { + console.log(`⚠️ Optics source repo not found at ${OPTICS_SOURCE_REPO}`); + console.log(' Component data will use fallback patterns'); + } + + // Read package version + const packageJson = JSON.parse(fs.readFileSync(path.join(opticsPath, 'package.json'), 'utf-8')); + const version = packageJson.version; + console.log(`📌 Version: ${version}\n`); + + // 1. Extract tokens from tokens.json if available + let allTokens: DesignToken[] = []; + const tokensJsonPath = path.join(opticsPath, 'dist/tokens/tokens.json'); + + if (fs.existsSync(tokensJsonPath)) { + console.log('📊 Parsing tokens.json...'); + const tokensJson = JSON.parse(fs.readFileSync(tokensJsonPath, 'utf-8')); + allTokens = flattenTokens(tokensJson.op || tokensJson); + console.log(` ✓ Found ${allTokens.length} tokens from tokens.json`); + } else { + // Fallback: parse CSS files + console.log('📄 Parsing CSS files for tokens...'); + const cssDistPath = path.join(opticsPath, 'dist/css'); + + const findCSSFiles = (dir: string): string[] => { + const files: string[] = []; + if (!fs.existsSync(dir)) return files; + + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...findCSSFiles(fullPath)); + } else if (entry.name.endsWith('.css')) { + files.push(fullPath); + } + } + return files; + }; + + const cssFiles = findCSSFiles(cssDistPath); + for (const cssFile of cssFiles) { + const content = fs.readFileSync(cssFile, 'utf-8'); + allTokens.push(...extractTokensFromCSS(content)); + } + + // Dedupe + const seenTokens = new Set(); + allTokens = allTokens.filter(t => { + if (seenTokens.has(t.name)) return false; + seenTokens.add(t.name); + return true; + }); + console.log(` ✓ Found ${allTokens.length} tokens from CSS`); + } + + // 2. Extract valid CSS classes for validation + console.log('\n🎨 Extracting CSS classes for validation...'); + let validClasses = new Set(); + const cssDistPath = path.join(opticsPath, 'dist/css'); + + const findCSSFiles = (dir: string): string[] => { + const files: string[] = []; + if (!fs.existsSync(dir)) return files; + + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + files.push(...findCSSFiles(fullPath)); + } else if (entry.name.endsWith('.css')) { + files.push(fullPath); + } + } + return files; + }; + + const cssFiles = findCSSFiles(cssDistPath); + for (const cssFile of cssFiles) { + const content = fs.readFileSync(cssFile, 'utf-8'); + const classes = extractCSSClasses(content); + classes.forEach(c => validClasses.add(c)); + } + console.log(` ✓ Found ${validClasses.size} valid CSS classes`); + + // 3. Scrape components from Storybook source or derive from CSS + let patterns: CSSPattern[] = []; + + if (sourceRepo) { + console.log('\n📖 Scraping components from Storybook source...'); + patterns = scrapeComponentsFromStorybook(sourceRepo, opticsPath, validClasses); + console.log(` ✓ Found ${patterns.length} components`); + } else { + console.log('\n📄 Deriving components from CSS files in npm package...'); + console.log(' (For richer documentation, clone https://github.com/RoleModel/optics to ../../optics)'); + patterns = deriveComponentsFromCSS(opticsPath, validClasses); + console.log(` ✓ Derived ${patterns.length} components from CSS`); + } + + // 4. Get documentation + console.log('\n📚 Loading documentation...'); + const docs = getDocumentation(allTokens); + console.log(` ✓ ${docs.length} documentation sections`); + + // 5. Generate output + console.log('\n✍️ Generating optics-data.ts...'); + const output = generateOutputFile(allTokens, patterns, docs, version); + fs.writeFileSync(OUTPUT_FILE, output); + console.log(` ✓ Written to ${OUTPUT_FILE}`); + + // Summary + const byCategory: Record = {}; + allTokens.forEach(t => { + byCategory[t.category] = (byCategory[t.category] || 0) + 1; + }); + + console.log('\n✅ Sync complete!'); + console.log(` - Optics v${version}`); + console.log(` - ${allTokens.length} tokens across ${Object.keys(byCategory).length} categories`); + console.log(` - ${patterns.length} CSS patterns`); + console.log(` - ${docs.length} documentation sections`); + + console.log('\n Token categories:'); + Object.entries(byCategory).sort().forEach(([cat, count]) => { + console.log(` - ${cat}: ${count}`); + }); +} + +main().catch(err => { + console.error('Error:', err); + process.exit(1); +}); diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json new file mode 100644 index 0000000..885e347 --- /dev/null +++ b/scripts/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022"], + "moduleResolution": "node", + "esModuleInterop": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true + }, + "include": ["*.ts"] +} From c5b03dcb621694cc660dc3c1aae3df4560eefd1a Mon Sep 17 00:00:00 2001 From: Dallas Peters Date: Thu, 5 Feb 2026 21:59:02 +0000 Subject: [PATCH 2/6] Add component-tokens utility Helper to get token dependencies for components. Co-Authored-By: Claude Opus 4.5 --- src/utils/component-tokens.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/utils/component-tokens.ts diff --git a/src/utils/component-tokens.ts b/src/utils/component-tokens.ts new file mode 100644 index 0000000..d1d7029 --- /dev/null +++ b/src/utils/component-tokens.ts @@ -0,0 +1,29 @@ +/** + * Component token dependency utilities + */ + +import { components, designTokens, type DesignToken } from '../optics-data.js'; + +/** + * Get component token dependencies + */ +export function getComponentTokenDependencies(componentName: string) { + const component = components.find(c => + c.name.toLowerCase() === componentName.toLowerCase() + ); + + if (!component) { + return null; + } + + const tokenDetails = component.tokens.map((tokenName: string) => + designTokens.find(t => t.name === tokenName) + ).filter((token): token is DesignToken => token !== undefined); + + return { + component: component.name, + description: component.description, + tokenCount: component.tokens.length, + tokens: tokenDetails + }; +} From ae03fe0f01e51b32da0cc92ae7f825c64662da3a Mon Sep 17 00:00:00 2001 From: Dallas Peters Date: Thu, 5 Feb 2026 21:59:08 +0000 Subject: [PATCH 3/6] Regenerate optics-data with 29 components Replaces 152 bloated entries with actual components: - 26 from Storybook (Accordion through Tooltip) - 3 layout utilities (Stack, Cluster, Split) Co-Authored-By: Claude Opus 4.5 --- src/optics-data.ts | 2537 ++++++++++++++++++++++---------------------- 1 file changed, 1278 insertions(+), 1259 deletions(-) diff --git a/src/optics-data.ts b/src/optics-data.ts index 95ff948..7673be6 100644 --- a/src/optics-data.ts +++ b/src/optics-data.ts @@ -1,1576 +1,1595 @@ /** * Optics Design System Data - * This file contains the core design tokens, components, and documentation - * structure for the Optics design system. + * AUTO-GENERATED - Run: npm run sync-data + * Version: 2.3.0 | Generated: 2026-02-05T21:51:53.056Z */ +export type TokenCategory = 'animation' | 'border' | 'breakpoint' | 'color' | 'encoded-image' | 'input' | 'opacity' | 'shadow' | 'sizing' | 'spacing' | 'typography' | 'z-index'; + export interface DesignToken { name: string; + cssVar: string; value: string; - category: string; - description?: string; + category: TokenCategory; + description: string; } -export interface Component { +export interface CSSPattern { name: string; description: string; + className: string; + type: 'component' | 'layout' | 'utility'; + modifiers: string[]; + elements: string[]; + exampleHtml: string; + docsUrl: string; +} + +export interface Component extends CSSPattern { tokens: string[]; usage: string; - examples?: string[]; + examples: string[]; } export interface Documentation { section: string; title: string; content: string; - tokens?: string[]; + tokens: string[]; } -/** - * Design Tokens - Core visual design elements from Optics Design System - * Source: https://docs.optics.rolemodel.design - */ export const designTokens: DesignToken[] = [ - // Base Color HSL Values (These are the foundation - all other colors are derived from these) { - name: 'op-color-primary-h', - value: '216', - category: 'color', - description: 'Primary color hue (HSL)' + "name": "color-white", + "cssVar": "--op-color-white", + "value": "hsl(0deg 100% 100%)", + "category": "color", + "description": "color token: color-white" }, { - name: 'op-color-primary-s', - value: '58%', - category: 'color', - description: 'Primary color saturation (HSL)' + "name": "color-black", + "cssVar": "--op-color-black", + "value": "hsl(0deg 0% 0%)", + "category": "color", + "description": "color token: color-black" }, { - name: 'op-color-primary-l', - value: '48%', - category: 'color', - description: 'Primary color lightness (HSL)' + "name": "color-primary-h", + "cssVar": "--op-color-primary-h", + "value": "216", + "category": "color", + "description": "Hue component (HSL) for color-primary" }, { - name: 'op-color-neutral-h', - value: '216', - category: 'color', - description: 'Neutral color hue (HSL, inherits from primary)' + "name": "color-primary-s", + "cssVar": "--op-color-primary-s", + "value": "58%", + "category": "color", + "description": "Saturation component (HSL) for color-primary" }, { - name: 'op-color-neutral-s', - value: '4%', - category: 'color', - description: 'Neutral color saturation (HSL)' + "name": "color-primary-l", + "cssVar": "--op-color-primary-l", + "value": "48%", + "category": "color", + "description": "Lightness component (HSL) for color-primary" }, { - name: 'op-color-neutral-l', - value: '48%', - category: 'color', - description: 'Neutral color lightness (HSL)' + "name": "color-primary-original", + "cssVar": "--op-color-primary-original", + "value": "hsl(var(--op-color-primary-h) var(--op-color-primary-s) var(--op-color-primary-l))", + "category": "color", + "description": "color token: color-primary-original" }, - // Alert Colors HSL { - name: 'op-color-alerts-warning-h', - value: '47', - category: 'color', - description: 'Warning alert hue (HSL)' + "name": "color-neutral-h", + "cssVar": "--op-color-neutral-h", + "value": "var(--op-color-primary-h)", + "category": "color", + "description": "Hue component (HSL) for color-neutral" }, { - name: 'op-color-alerts-warning-s', - value: '100%', - category: 'color', - description: 'Warning alert saturation (HSL)' + "name": "color-neutral-s", + "cssVar": "--op-color-neutral-s", + "value": "4%", + "category": "color", + "description": "Saturation component (HSL) for color-neutral" }, { - name: 'op-color-alerts-warning-l', - value: '61%', - category: 'color', - description: 'Warning alert lightness (HSL)' + "name": "color-neutral-l", + "cssVar": "--op-color-neutral-l", + "value": "var(--op-color-primary-l)", + "category": "color", + "description": "Lightness component (HSL) for color-neutral" }, { - name: 'op-color-alerts-danger-h', - value: '0', - category: 'color', - description: 'Danger alert hue (HSL)' + "name": "color-neutral-original", + "cssVar": "--op-color-neutral-original", + "value": "hsl(var(--op-color-neutral-h) var(--op-color-neutral-s) var(--op-color-neutral-l))", + "category": "color", + "description": "color token: color-neutral-original" }, { - name: 'op-color-alerts-danger-s', - value: '99%', - category: 'color', - description: 'Danger alert saturation (HSL)' + "name": "color-alerts-warning-h", + "cssVar": "--op-color-alerts-warning-h", + "value": "47", + "category": "color", + "description": "Hue component (HSL) for color-alerts-warning" }, { - name: 'op-color-alerts-danger-l', - value: '76%', - category: 'color', - description: 'Danger alert lightness (HSL)' + "name": "color-alerts-warning-s", + "cssVar": "--op-color-alerts-warning-s", + "value": "100%", + "category": "color", + "description": "Saturation component (HSL) for color-alerts-warning" }, { - name: 'op-color-alerts-info-h', - value: '216', - category: 'color', - description: 'Info alert hue (HSL)' + "name": "color-alerts-warning-l", + "cssVar": "--op-color-alerts-warning-l", + "value": "61%", + "category": "color", + "description": "Lightness component (HSL) for color-alerts-warning" }, { - name: 'op-color-alerts-info-s', - value: '58%', - category: 'color', - description: 'Info alert saturation (HSL)' + "name": "color-alerts-warning-original", + "cssVar": "--op-color-alerts-warning-original", + "value": "hsl(var(--op-color-alerts-warning-h) var(--op-color-alerts-warning-s) var(--op-color-alerts-warning-l))", + "category": "color", + "description": "color token: color-alerts-warning-original" }, { - name: 'op-color-alerts-info-l', - value: '48%', - category: 'color', - description: 'Info alert lightness (HSL)' + "name": "color-alerts-danger-h", + "cssVar": "--op-color-alerts-danger-h", + "value": "0", + "category": "color", + "description": "Hue component (HSL) for color-alerts-danger" }, { - name: 'op-color-alerts-notice-h', - value: '130', - category: 'color', - description: 'Notice alert hue (HSL)' + "name": "color-alerts-danger-s", + "cssVar": "--op-color-alerts-danger-s", + "value": "99%", + "category": "color", + "description": "Saturation component (HSL) for color-alerts-danger" }, { - name: 'op-color-alerts-notice-s', - value: '61%', - category: 'color', - description: 'Notice alert saturation (HSL)' + "name": "color-alerts-danger-l", + "cssVar": "--op-color-alerts-danger-l", + "value": "76%", + "category": "color", + "description": "Lightness component (HSL) for color-alerts-danger" }, { - name: 'op-color-alerts-notice-l', - value: '64%', - category: 'color', - description: 'Notice alert lightness (HSL)' + "name": "color-alerts-danger-original", + "cssVar": "--op-color-alerts-danger-original", + "value": "hsl(var(--op-color-alerts-danger-h) var(--op-color-alerts-danger-s) var(--op-color-alerts-danger-l))", + "category": "color", + "description": "color token: color-alerts-danger-original" }, - // Primary Color Scale - Main Scale - // Note: Same pattern applies to neutral, alerts-warning, alerts-danger, alerts-info, alerts-notice { - name: 'op-color-primary-plus-max', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 100%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 12%))', - category: 'color', - description: 'Primary color lightest - light mode: 100%, dark mode: 12%' + "name": "color-alerts-info-h", + "cssVar": "--op-color-alerts-info-h", + "value": "216", + "category": "color", + "description": "Hue component (HSL) for color-alerts-info" }, { - name: 'op-color-primary-plus-eight', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 98%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 14%))', - category: 'color', - description: 'Primary color scale +8' + "name": "color-alerts-info-s", + "cssVar": "--op-color-alerts-info-s", + "value": "58%", + "category": "color", + "description": "Saturation component (HSL) for color-alerts-info" }, { - name: 'op-color-primary-plus-seven', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 96%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 16%))', - category: 'color', - description: 'Primary color scale +7' + "name": "color-alerts-info-l", + "cssVar": "--op-color-alerts-info-l", + "value": "48%", + "category": "color", + "description": "Lightness component (HSL) for color-alerts-info" }, { - name: 'op-color-primary-plus-six', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 94%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 20%))', - category: 'color', - description: 'Primary color scale +6' + "name": "color-alerts-info-original", + "cssVar": "--op-color-alerts-info-original", + "value": "hsl(var(--op-color-alerts-info-h) var(--op-color-alerts-info-s) var(--op-color-alerts-info-l))", + "category": "color", + "description": "color token: color-alerts-info-original" }, { - name: 'op-color-primary-plus-five', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 90%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 24%))', - category: 'color', - description: 'Primary color scale +5' + "name": "color-alerts-notice-h", + "cssVar": "--op-color-alerts-notice-h", + "value": "130", + "category": "color", + "description": "Hue component (HSL) for color-alerts-notice" }, { - name: 'op-color-primary-plus-four', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 84%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 26%))', - category: 'color', - description: 'Primary color scale +4' + "name": "color-alerts-notice-s", + "cssVar": "--op-color-alerts-notice-s", + "value": "61%", + "category": "color", + "description": "Saturation component (HSL) for color-alerts-notice" }, { - name: 'op-color-primary-plus-three', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 70%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 29%))', - category: 'color', - description: 'Primary color scale +3' + "name": "color-alerts-notice-l", + "cssVar": "--op-color-alerts-notice-l", + "value": "64%", + "category": "color", + "description": "Lightness component (HSL) for color-alerts-notice" }, { - name: 'op-color-primary-plus-two', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 64%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 32%))', - category: 'color', - description: 'Primary color scale +2' + "name": "color-alerts-notice-original", + "cssVar": "--op-color-alerts-notice-original", + "value": "hsl(var(--op-color-alerts-notice-h) var(--op-color-alerts-notice-s) var(--op-color-alerts-notice-l))", + "category": "color", + "description": "color token: color-alerts-notice-original" }, { - name: 'op-color-primary-plus-one', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 45%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 35%))', - category: 'color', - description: 'Primary color scale +1' + "name": "color-border", + "cssVar": "--op-color-border", + "value": "var(--op-color-neutral-plus-five)", + "category": "color", + "description": "color token: color-border" }, { - name: 'op-color-primary-base', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 40%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 38%))', - category: 'color', - description: 'Primary color base' + "name": "color-background", + "cssVar": "--op-color-background", + "value": "var(--op-color-neutral-plus-eight)", + "category": "color", + "description": "color token: color-background" }, { - name: 'op-color-primary-minus-one', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 36%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 40%))', - category: 'color', - description: 'Primary color scale -1' + "name": "color-on-background", + "cssVar": "--op-color-on-background", + "value": "var(--op-color-neutral-on-plus-eight)", + "category": "color", + "description": "Text color for use ON background background. MUST be paired with matching background color." }, { - name: 'op-color-primary-minus-two', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 32%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 45%))', - category: 'color', - description: 'Primary color scale -2' + "name": "opacity-none", + "cssVar": "--op-opacity-none", + "value": "0", + "category": "opacity", + "description": "opacity token: opacity-none" }, { - name: 'op-color-primary-minus-three', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 28%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 48%))', - category: 'color', - description: 'Primary color scale -3' + "name": "opacity-overlay", + "cssVar": "--op-opacity-overlay", + "value": "0.2", + "category": "opacity", + "description": "opacity token: opacity-overlay" }, { - name: 'op-color-primary-minus-four', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 24%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 52%))', - category: 'color', - description: 'Primary color scale -4' + "name": "opacity-disabled", + "cssVar": "--op-opacity-disabled", + "value": "0.4", + "category": "opacity", + "description": "opacity token: opacity-disabled" }, { - name: 'op-color-primary-minus-five', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 20%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 64%))', - category: 'color', - description: 'Primary color scale -5' + "name": "opacity-half", + "cssVar": "--op-opacity-half", + "value": "0.5", + "category": "opacity", + "description": "opacity token: opacity-half" }, { - name: 'op-color-primary-minus-six', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 16%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 72%))', - category: 'color', - description: 'Primary color scale -6' + "name": "opacity-full", + "cssVar": "--op-opacity-full", + "value": "1", + "category": "opacity", + "description": "opacity token: opacity-full" }, { - name: 'op-color-primary-minus-seven', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 8%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 80%))', - category: 'color', - description: 'Primary color scale -7' + "name": "breakpoint-x-small", + "cssVar": "--op-breakpoint-x-small", + "value": "512px", + "category": "breakpoint", + "description": "breakpoint token: breakpoint-x-small" }, { - name: 'op-color-primary-minus-eight', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 4%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 88%))', - category: 'color', - description: 'Primary color scale -8' + "name": "breakpoint-small", + "cssVar": "--op-breakpoint-small", + "value": "768px", + "category": "breakpoint", + "description": "breakpoint token: breakpoint-small" }, { - name: 'op-color-primary-minus-max', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 0%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 100%))', - category: 'color', - description: 'Primary color darkest - light mode: 0%, dark mode: 100%' + "name": "breakpoint-medium", + "cssVar": "--op-breakpoint-medium", + "value": "1024px", + "category": "breakpoint", + "description": "breakpoint token: breakpoint-medium" }, - - // Primary Color Scale - "On" Scale (for text/content colors that appear on the main scale colors) - // Note: Each has a base and "-alt" variant. Same pattern applies to neutral, alerts-warning, alerts-danger, alerts-info, alerts-notice { - name: 'op-color-primary-on-plus-max', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 0%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 100%))', - category: 'color', - description: 'Text color for primary-plus-max backgrounds' + "name": "breakpoint-large", + "cssVar": "--op-breakpoint-large", + "value": "1280px", + "category": "breakpoint", + "description": "breakpoint token: breakpoint-large" }, { - name: 'op-color-primary-on-plus-max-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 20%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 78%))', - category: 'color', - description: 'Alt text color for primary-plus-max backgrounds' + "name": "breakpoint-x-large", + "cssVar": "--op-breakpoint-x-large", + "value": "1440px", + "category": "breakpoint", + "description": "breakpoint token: breakpoint-x-large" }, { - name: 'op-color-primary-on-plus-eight', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 4%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 88%))', - category: 'color', - description: 'Text color for primary-plus-eight backgrounds' + "name": "radius-small", + "cssVar": "--op-radius-small", + "value": "2px", + "category": "border", + "description": "border token: radius-small" }, { - name: 'op-color-primary-on-plus-eight-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 24%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 70%))', - category: 'color', - description: 'Alt text color for primary-plus-eight backgrounds' + "name": "radius-medium", + "cssVar": "--op-radius-medium", + "value": "4px", + "category": "border", + "description": "border token: radius-medium" }, { - name: 'op-color-primary-on-plus-seven', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 8%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 80%))', - category: 'color', - description: 'Text color for primary-plus-seven backgrounds' + "name": "radius-large", + "cssVar": "--op-radius-large", + "value": "8px", + "category": "border", + "description": "border token: radius-large" }, { - name: 'op-color-primary-on-plus-seven-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 28%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 64%))', - category: 'color', - description: 'Alt text color for primary-plus-seven backgrounds' + "name": "radius-x-large", + "cssVar": "--op-radius-x-large", + "value": "12px", + "category": "border", + "description": "border token: radius-x-large" }, { - name: 'op-color-primary-on-plus-six', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 16%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 72%))', - category: 'color', - description: 'Text color for primary-plus-six backgrounds' + "name": "radius-2x-large", + "cssVar": "--op-radius-2x-large", + "value": "16px", + "category": "border", + "description": "border token: radius-2x-large" }, { - name: 'op-color-primary-on-plus-six-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 26%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 96%))', - category: 'color', - description: 'Alt text color for primary-plus-six backgrounds' + "name": "radius-circle", + "cssVar": "--op-radius-circle", + "value": "50%", + "category": "border", + "description": "border token: radius-circle" }, { - name: 'op-color-primary-on-plus-five', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 20%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 72%))', - category: 'color', - description: 'Text color for primary-plus-five backgrounds' + "name": "radius-pill", + "cssVar": "--op-radius-pill", + "value": "9999px", + "category": "border", + "description": "border token: radius-pill" }, { - name: 'op-color-primary-on-plus-five-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 40%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 86%))', - category: 'color', - description: 'Alt text color for primary-plus-five backgrounds' + "name": "border-width", + "cssVar": "--op-border-width", + "value": "1px", + "category": "border", + "description": "border token: border-width" }, { - name: 'op-color-primary-on-plus-four', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 24%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 80%))', - category: 'color', - description: 'Text color for primary-plus-four backgrounds' + "name": "border-width-large", + "cssVar": "--op-border-width-large", + "value": "2px", + "category": "border", + "description": "border token: border-width-large" }, { - name: 'op-color-primary-on-plus-four-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 4%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 92%))', - category: 'color', - description: 'Alt text color for primary-plus-four backgrounds' + "name": "border-width-x-large", + "cssVar": "--op-border-width-x-large", + "value": "4px", + "category": "border", + "description": "border token: border-width-x-large" }, { - name: 'op-color-primary-on-plus-three', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 20%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 78%))', - category: 'color', - description: 'Text color for primary-plus-three backgrounds' + "name": "border-none", + "cssVar": "--op-border-none", + "value": "0 0 0 0", + "category": "border", + "description": "border token: border-none" }, { - name: 'op-color-primary-on-plus-three-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 10%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 98%))', - category: 'color', - description: 'Alt text color for primary-plus-three backgrounds' + "name": "border-all", + "cssVar": "--op-border-all", + "value": "0 0 0 var(--op-border-width)", + "category": "border", + "description": "border token: border-all" }, { - name: 'op-color-primary-on-plus-two', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 16%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 80%))', - category: 'color', - description: 'Text color for primary-plus-two backgrounds' + "name": "border-top", + "cssVar": "--op-border-top", + "value": "0 calc(-1 * var(--op-border-width)) 0 0", + "category": "border", + "description": "border token: border-top" }, { - name: 'op-color-primary-on-plus-two-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 6%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 92%))', - category: 'color', - description: 'Alt text color for primary-plus-two backgrounds' + "name": "border-right", + "cssVar": "--op-border-right", + "value": "var(--op-border-width) 0 0 0", + "category": "border", + "description": "border token: border-right" }, { - name: 'op-color-primary-on-plus-one', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 100%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 80%))', - category: 'color', - description: 'Text color for primary-plus-one backgrounds' + "name": "border-bottom", + "cssVar": "--op-border-bottom", + "value": "0 var(--op-border-width) 0 0", + "category": "border", + "description": "border token: border-bottom" }, { - name: 'op-color-primary-on-plus-one-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 95%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 98%))', - category: 'color', - description: 'Alt text color for primary-plus-one backgrounds' + "name": "border-left", + "cssVar": "--op-border-left", + "value": "calc(-1 * var(--op-border-width)) 0 0 0", + "category": "border", + "description": "border token: border-left" }, { - name: 'op-color-primary-on-base', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 100%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 100%))', - category: 'color', - description: 'Text color for primary-base backgrounds' + "name": "border-y", + "cssVar": "--op-border-y", + "value": "var(--op-border-top) var(--op-color-border), var(--op-border-bottom) var(--op-color-border)", + "category": "border", + "description": "border token: border-y" }, { - name: 'op-color-primary-on-base-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 88%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 84%))', - category: 'color', - description: 'Alt text color for primary-base backgrounds' + "name": "border-x", + "cssVar": "--op-border-x", + "value": "var(--op-border-left) var(--op-color-border), var(--op-border-right) var(--op-color-border)", + "category": "border", + "description": "border token: border-x" }, { - name: 'op-color-primary-on-minus-one', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 94%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 98%))', - category: 'color', - description: 'Text color for primary-minus-one backgrounds' + "name": "font-scale-unit", + "cssVar": "--op-font-scale-unit", + "value": "1rem", + "category": "typography", + "description": "typography token: font-scale-unit" }, { - name: 'op-color-primary-on-minus-one-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 82%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 90%))', - category: 'color', - description: 'Alt text color for primary-minus-one backgrounds' + "name": "font-2x-small", + "cssVar": "--op-font-2x-small", + "value": "calc(var(--op-font-scale-unit) * 1)", + "category": "typography", + "description": "typography token: font-2x-small" }, { - name: 'op-color-primary-on-minus-two', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 90%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 98%))', - category: 'color', - description: 'Text color for primary-minus-two backgrounds' + "name": "font-x-small", + "cssVar": "--op-font-x-small", + "value": "calc(var(--op-font-scale-unit) * 1.2)", + "category": "typography", + "description": "typography token: font-x-small" }, { - name: 'op-color-primary-on-minus-two-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 78%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 92%))', - category: 'color', - description: 'Alt text color for primary-minus-two backgrounds' + "name": "font-small", + "cssVar": "--op-font-small", + "value": "calc(var(--op-font-scale-unit) * 1.4)", + "category": "typography", + "description": "typography token: font-small" }, { - name: 'op-color-primary-on-minus-three', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 86%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 98%))', - category: 'color', - description: 'Text color for primary-minus-three backgrounds' + "name": "font-medium", + "cssVar": "--op-font-medium", + "value": "calc(var(--op-font-scale-unit) * 1.6)", + "category": "typography", + "description": "typography token: font-medium" }, { - name: 'op-color-primary-on-minus-three-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 74%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 96%))', - category: 'color', - description: 'Alt text color for primary-minus-three backgrounds' + "name": "font-large", + "cssVar": "--op-font-large", + "value": "calc(var(--op-font-scale-unit) * 1.8)", + "category": "typography", + "description": "typography token: font-large" }, { - name: 'op-color-primary-on-minus-four', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 84%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 2%))', - category: 'color', - description: 'Text color for primary-minus-four backgrounds' + "name": "font-x-large", + "cssVar": "--op-font-x-large", + "value": "calc(var(--op-font-scale-unit) * 2)", + "category": "typography", + "description": "typography token: font-x-large" }, { - name: 'op-color-primary-on-minus-four-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 72%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 2%))', - category: 'color', - description: 'Alt text color for primary-minus-four backgrounds' + "name": "font-2x-large", + "cssVar": "--op-font-2x-large", + "value": "calc(var(--op-font-scale-unit) * 2.4)", + "category": "typography", + "description": "typography token: font-2x-large" }, { - name: 'op-color-primary-on-minus-five', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 88%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 2%))', - category: 'color', - description: 'Text color for primary-minus-five backgrounds' + "name": "font-3x-large", + "cssVar": "--op-font-3x-large", + "value": "calc(var(--op-font-scale-unit) * 2.8)", + "category": "typography", + "description": "typography token: font-3x-large" }, { - name: 'op-color-primary-on-minus-five-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 78%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 20%))', - category: 'color', - description: 'Alt text color for primary-minus-five backgrounds' + "name": "font-4x-large", + "cssVar": "--op-font-4x-large", + "value": "calc(var(--op-font-scale-unit) * 3.2)", + "category": "typography", + "description": "typography token: font-4x-large" }, { - name: 'op-color-primary-on-minus-six', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 94%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 8%))', - category: 'color', - description: 'Text color for primary-minus-six backgrounds' + "name": "font-5x-large", + "cssVar": "--op-font-5x-large", + "value": "calc(var(--op-font-scale-unit) * 3.6)", + "category": "typography", + "description": "typography token: font-5x-large" }, { - name: 'op-color-primary-on-minus-six-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 82%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 26%))', - category: 'color', - description: 'Alt text color for primary-minus-six backgrounds' + "name": "font-6x-large", + "cssVar": "--op-font-6x-large", + "value": "calc(var(--op-font-scale-unit) * 4.8)", + "category": "typography", + "description": "typography token: font-6x-large" }, { - name: 'op-color-primary-on-minus-seven', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 96%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 8%))', - category: 'color', - description: 'Text color for primary-minus-seven backgrounds' + "name": "font-weight-thin", + "cssVar": "--op-font-weight-thin", + "value": "100", + "category": "typography", + "description": "typography token: font-weight-thin" }, { - name: 'op-color-primary-on-minus-seven-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 84%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 34%))', - category: 'color', - description: 'Alt text color for primary-minus-seven backgrounds' + "name": "font-weight-extra-light", + "cssVar": "--op-font-weight-extra-light", + "value": "200", + "category": "typography", + "description": "typography token: font-weight-extra-light" }, { - name: 'op-color-primary-on-minus-eight', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 98%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 4%))', - category: 'color', - description: 'Text color for primary-minus-eight backgrounds' + "name": "font-weight-light", + "cssVar": "--op-font-weight-light", + "value": "300", + "category": "typography", + "description": "typography token: font-weight-light" }, { - name: 'op-color-primary-on-minus-eight-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 86%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 38%))', - category: 'color', - description: 'Alt text color for primary-minus-eight backgrounds' + "name": "font-weight-normal", + "cssVar": "--op-font-weight-normal", + "value": "400", + "category": "typography", + "description": "typography token: font-weight-normal" }, { - name: 'op-color-primary-on-minus-max', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 100%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 0%))', - category: 'color', - description: 'Text color for primary-minus-max backgrounds' + "name": "font-weight-medium", + "cssVar": "--op-font-weight-medium", + "value": "500", + "category": "typography", + "description": "typography token: font-weight-medium" }, { - name: 'op-color-primary-on-minus-max-alt', - value: 'light-dark(hsl(var(--op-color-primary-h) var(--op-color-primary-s) 88%), hsl(var(--op-color-primary-h) var(--op-color-primary-s) 38%))', - category: 'color', - description: 'Alt text color for primary-minus-max backgrounds' + "name": "font-weight-semi-bold", + "cssVar": "--op-font-weight-semi-bold", + "value": "600", + "category": "typography", + "description": "typography token: font-weight-semi-bold" }, - - // Core semantic colors (most commonly used) { - name: 'op-color-white', - value: 'hsl(0deg 100% 100%)', - category: 'color', - description: 'Pure white' + "name": "font-weight-bold", + "cssVar": "--op-font-weight-bold", + "value": "700", + "category": "typography", + "description": "typography token: font-weight-bold" }, { - name: 'op-color-black', - value: 'hsl(0deg 0% 0%)', - category: 'color', - description: 'Pure black' + "name": "font-weight-extra-bold", + "cssVar": "--op-font-weight-extra-bold", + "value": "800", + "category": "typography", + "description": "typography token: font-weight-extra-bold" }, - - // Spacing Tokens { - name: 'op-space-scale-unit', - value: '1rem', - category: 'spacing', - description: 'Base unit for spacing scale (10px)' + "name": "font-weight-black", + "cssVar": "--op-font-weight-black", + "value": "900", + "category": "typography", + "description": "typography token: font-weight-black" }, { - name: 'op-space-3x-small', - value: 'calc(var(--op-space-scale-unit) * 0.2)', - category: 'spacing', - description: '2px spacing' + "name": "font-family", + "cssVar": "--op-font-family", + "value": "'Noto Sans', sans-serif", + "category": "typography", + "description": "typography token: font-family" }, { - name: 'op-space-2x-small', - value: 'calc(var(--op-space-scale-unit) * 0.4)', - category: 'spacing', - description: '4px spacing' + "name": "line-height-none", + "cssVar": "--op-line-height-none", + "value": "0", + "category": "sizing", + "description": "sizing token: line-height-none" }, { - name: 'op-space-x-small', - value: 'calc(var(--op-space-scale-unit) * 0.8)', - category: 'spacing', - description: '8px spacing' + "name": "line-height-densest", + "cssVar": "--op-line-height-densest", + "value": "1", + "category": "sizing", + "description": "sizing token: line-height-densest" }, { - name: 'op-space-small', - value: 'calc(var(--op-space-scale-unit) * 1.2)', - category: 'spacing', - description: '12px spacing' + "name": "line-height-denser", + "cssVar": "--op-line-height-denser", + "value": "1.15", + "category": "sizing", + "description": "sizing token: line-height-denser" }, { - name: 'op-space-medium', - value: 'calc(var(--op-space-scale-unit) * 1.6)', - category: 'spacing', - description: '16px spacing' + "name": "line-height-dense", + "cssVar": "--op-line-height-dense", + "value": "1.3", + "category": "sizing", + "description": "sizing token: line-height-dense" }, { - name: 'op-space-large', - value: 'calc(var(--op-space-scale-unit) * 2)', - category: 'spacing', - description: '20px spacing' + "name": "line-height-base", + "cssVar": "--op-line-height-base", + "value": "1.5", + "category": "sizing", + "description": "sizing token: line-height-base" }, { - name: 'op-space-x-large', - value: 'calc(var(--op-space-scale-unit) * 2.4)', - category: 'spacing', - description: '24px spacing' + "name": "line-height-loose", + "cssVar": "--op-line-height-loose", + "value": "1.6", + "category": "sizing", + "description": "sizing token: line-height-loose" }, { - name: 'op-space-2x-large', - value: 'calc(var(--op-space-scale-unit) * 2.8)', - category: 'spacing', - description: '28px spacing' + "name": "line-height-looser", + "cssVar": "--op-line-height-looser", + "value": "1.7", + "category": "sizing", + "description": "sizing token: line-height-looser" }, { - name: 'op-space-3x-large', - value: 'calc(var(--op-space-scale-unit) * 4)', - category: 'spacing', - description: '40px spacing' + "name": "line-height-loosest", + "cssVar": "--op-line-height-loosest", + "value": "1.8", + "category": "sizing", + "description": "sizing token: line-height-loosest" }, { - name: 'op-space-4x-large', - value: 'calc(var(--op-space-scale-unit) * 8)', - category: 'spacing', - description: '80px spacing' + "name": "letter-spacing-navigation", + "cssVar": "--op-letter-spacing-navigation", + "value": "0.01rem", + "category": "typography", + "description": "typography token: letter-spacing-navigation" }, - - // Typography Tokens - Font Family { - name: 'op-font-family', - value: "'Noto Sans', 'Noto Serif', sans-serif", - category: 'typography', - description: 'Font family for all text' - }, - - // Font Sizes - { - name: 'op-font-scale-unit', - value: '1rem', - category: 'typography', - description: 'Base unit for font scale (10px)' - }, - { - name: 'op-font-2x-small', - value: 'calc(var(--op-font-scale-unit) * 1)', - category: 'typography', - description: '10px font size' + "name": "letter-spacing-label", + "cssVar": "--op-letter-spacing-label", + "value": "0.04rem", + "category": "typography", + "description": "typography token: letter-spacing-label" }, { - name: 'op-font-x-small', - value: 'calc(var(--op-font-scale-unit) * 1.2)', - category: 'typography', - description: '12px font size' + "name": "transition-accordion", + "cssVar": "--op-transition-accordion", + "value": "rotate 120ms ease-in", + "category": "animation", + "description": "animation token: transition-accordion" }, { - name: 'op-font-small', - value: 'calc(var(--op-font-scale-unit) * 1.4)', - category: 'typography', - description: '14px font size' + "name": "transition-input", + "cssVar": "--op-transition-input", + "value": "all 120ms ease-in", + "category": "animation", + "description": "animation token: transition-input" }, { - name: 'op-font-medium', - value: 'calc(var(--op-font-scale-unit) * 1.6)', - category: 'typography', - description: '16px font size' + "name": "transition-sidebar", + "cssVar": "--op-transition-sidebar", + "value": "all 200ms ease-in-out", + "category": "animation", + "description": "animation token: transition-sidebar" }, { - name: 'op-font-large', - value: 'calc(var(--op-font-scale-unit) * 1.8)', - category: 'typography', - description: '18px font size' + "name": "transition-modal", + "cssVar": "--op-transition-modal", + "value": "all var(--op-transition-modal-time) ease-in", + "category": "animation", + "description": "animation token: transition-modal" }, { - name: 'op-font-x-large', - value: 'calc(var(--op-font-scale-unit) * 2)', - category: 'typography', - description: '20px font size' + "name": "transition-panel", + "cssVar": "--op-transition-panel", + "value": "right 400ms ease-in", + "category": "animation", + "description": "animation token: transition-panel" }, { - name: 'op-font-2x-large', - value: 'calc(var(--op-font-scale-unit) * 2.4)', - category: 'typography', - description: '24px font size' + "name": "transition-tooltip", + "cssVar": "--op-transition-tooltip", + "value": "all 300ms ease-in 300ms", + "category": "animation", + "description": "animation token: transition-tooltip" }, { - name: 'op-font-3x-large', - value: 'calc(var(--op-font-scale-unit) * 2.8)', - category: 'typography', - description: '28px font size' + "name": "animation-flash", + "cssVar": "--op-animation-flash", + "value": "rm-slide-in-out-flash 5s normal forwards", + "category": "animation", + "description": "animation token: animation-flash" }, { - name: 'op-font-4x-large', - value: 'calc(var(--op-font-scale-unit) * 3.2)', - category: 'typography', - description: '32px font size' + "name": "encoded-images-dropdown-arrow", + "cssVar": "--op-encoded-images-dropdown-arrow", + "value": "url('data", + "category": "encoded-image", + "description": "encoded-image token: encoded-images-dropdown-arrow" }, { - name: 'op-font-5x-large', - value: 'calc(var(--op-font-scale-unit) * 3.6)', - category: 'typography', - description: '36px font size' + "name": "size-unit", + "cssVar": "--op-size-unit", + "value": "0.4rem", + "category": "sizing", + "description": "sizing token: size-unit" }, { - name: 'op-font-6x-large', - value: 'calc(var(--op-font-scale-unit) * 4.8)', - category: 'typography', - description: '48px font size' + "name": "space-scale-unit", + "cssVar": "--op-space-scale-unit", + "value": "1rem", + "category": "spacing", + "description": "spacing token: space-scale-unit" }, - - // Font Weights { - name: 'op-font-weight-thin', - value: '100', - category: 'typography', - description: 'Thin font weight' + "name": "space-3x-small", + "cssVar": "--op-space-3x-small", + "value": "calc(var(--op-space-scale-unit) * 0.2)", + "category": "spacing", + "description": "spacing token: space-3x-small" }, { - name: 'op-font-weight-extra-light', - value: '200', - category: 'typography', - description: 'Extra light font weight' + "name": "space-2x-small", + "cssVar": "--op-space-2x-small", + "value": "calc(var(--op-space-scale-unit) * 0.4)", + "category": "spacing", + "description": "spacing token: space-2x-small" }, { - name: 'op-font-weight-light', - value: '300', - category: 'typography', - description: 'Light font weight' + "name": "space-x-small", + "cssVar": "--op-space-x-small", + "value": "calc(var(--op-space-scale-unit) * 0.8)", + "category": "spacing", + "description": "spacing token: space-x-small" }, { - name: 'op-font-weight-normal', - value: '400', - category: 'typography', - description: 'Normal font weight' + "name": "space-small", + "cssVar": "--op-space-small", + "value": "calc(var(--op-space-scale-unit) * 1.2)", + "category": "spacing", + "description": "spacing token: space-small" }, { - name: 'op-font-weight-medium', - value: '500', - category: 'typography', - description: 'Medium font weight' + "name": "space-medium", + "cssVar": "--op-space-medium", + "value": "calc(var(--op-space-scale-unit) * 1.6)", + "category": "spacing", + "description": "spacing token: space-medium" }, { - name: 'op-font-weight-semi-bold', - value: '600', - category: 'typography', - description: 'Semi-bold font weight' + "name": "space-large", + "cssVar": "--op-space-large", + "value": "calc(var(--op-space-scale-unit) * 2)", + "category": "spacing", + "description": "spacing token: space-large" }, { - name: 'op-font-weight-bold', - value: '700', - category: 'typography', - description: 'Bold font weight' + "name": "space-x-large", + "cssVar": "--op-space-x-large", + "value": "calc(var(--op-space-scale-unit) * 2.4)", + "category": "spacing", + "description": "spacing token: space-x-large" }, { - name: 'op-font-weight-extra-bold', - value: '800', - category: 'typography', - description: 'Extra bold font weight' + "name": "space-2x-large", + "cssVar": "--op-space-2x-large", + "value": "calc(var(--op-space-scale-unit) * 2.8)", + "category": "spacing", + "description": "spacing token: space-2x-large" }, { - name: 'op-font-weight-black', - value: '900', - category: 'typography', - description: 'Black font weight' + "name": "space-3x-large", + "cssVar": "--op-space-3x-large", + "value": "calc(var(--op-space-scale-unit) * 4)", + "category": "spacing", + "description": "spacing token: space-3x-large" }, - - // Line Heights { - name: 'op-line-height-none', - value: '0', - category: 'typography', - description: 'No line height' + "name": "space-4x-large", + "cssVar": "--op-space-4x-large", + "value": "calc(var(--op-space-scale-unit) * 8)", + "category": "spacing", + "description": "spacing token: space-4x-large" }, { - name: 'op-line-height-densest', - value: '1', - category: 'typography', - description: 'Densest line height' + "name": "shadow-x-small", + "cssVar": "--op-shadow-x-small", + "value": "0 1px 3px hsl(0deg 0% 0% / 15%), 0 1px 2px hsl(0deg 0% 0% / 30%)", + "category": "shadow", + "description": "shadow token: shadow-x-small" }, { - name: 'op-line-height-denser', - value: '1.15', - category: 'typography', - description: 'Denser line height' + "name": "shadow-small", + "cssVar": "--op-shadow-small", + "value": "0 2px 6px hsl(0deg 0% 0% / 15%), 0 1px 2px hsl(0deg 0% 0% / 30%)", + "category": "shadow", + "description": "shadow token: shadow-small" }, { - name: 'op-line-height-dense', - value: '1.3', - category: 'typography', - description: 'Dense line height' + "name": "shadow-medium", + "cssVar": "--op-shadow-medium", + "value": "0 4px 8px hsl(0deg 0% 0% / 15%), 0 1px 3px hsl(0deg 0% 0% / 30%)", + "category": "shadow", + "description": "shadow token: shadow-medium" }, { - name: 'op-line-height-base', - value: '1.5', - category: 'typography', - description: 'Base line height' + "name": "shadow-large", + "cssVar": "--op-shadow-large", + "value": "0 6px 10px hsl(0deg 0% 0% / 15%), 0 2px 3px hsl(0deg 0% 0% / 30%)", + "category": "shadow", + "description": "shadow token: shadow-large" }, { - name: 'op-line-height-loose', - value: '1.6', - category: 'typography', - description: 'Loose line height' + "name": "shadow-x-large", + "cssVar": "--op-shadow-x-large", + "value": "0 8px 12px hsl(0deg 0% 0% / 15%), 0 4px 4px hsl(0deg 0% 0% / 30%)", + "category": "shadow", + "description": "shadow token: shadow-x-large" }, { - name: 'op-line-height-looser', - value: '1.7', - category: 'typography', - description: 'Looser line height' + "name": "z-index-header", + "cssVar": "--op-z-index-header", + "value": "500", + "category": "z-index", + "description": "z-index token: z-index-header" }, { - name: 'op-line-height-loosest', - value: '1.8', - category: 'typography', - description: 'Loosest line height' + "name": "z-index-footer", + "cssVar": "--op-z-index-footer", + "value": "500", + "category": "z-index", + "description": "z-index token: z-index-footer" }, - - // Letter Spacing { - name: 'op-letter-spacing-navigation', - value: '0.01rem', - category: 'typography', - description: 'Letter spacing for navigation' + "name": "z-index-sidebar", + "cssVar": "--op-z-index-sidebar", + "value": "700", + "category": "z-index", + "description": "z-index token: z-index-sidebar" }, { - name: 'op-letter-spacing-label', - value: '0.04rem', - category: 'typography', - description: 'Letter spacing for labels' + "name": "z-index-dialog", + "cssVar": "--op-z-index-dialog", + "value": "800", + "category": "z-index", + "description": "z-index token: z-index-dialog" }, - - // Border Radius Tokens { - name: 'op-radius-small', - value: '2px', - category: 'border', - description: 'Small border radius' + "name": "z-index-dialog-backdrop", + "cssVar": "--op-z-index-dialog-backdrop", + "value": "801", + "category": "z-index", + "description": "z-index token: z-index-dialog-backdrop" }, { - name: 'op-radius-medium', - value: '4px', - category: 'border', - description: 'Medium border radius' + "name": "z-index-dialog-content", + "cssVar": "--op-z-index-dialog-content", + "value": "802", + "category": "z-index", + "description": "z-index token: z-index-dialog-content" }, { - name: 'op-radius-large', - value: '8px', - category: 'border', - description: 'Large border radius' + "name": "z-index-dropdown", + "cssVar": "--op-z-index-dropdown", + "value": "900", + "category": "z-index", + "description": "z-index token: z-index-dropdown" }, { - name: 'op-radius-x-large', - value: '12px', - category: 'border', - description: 'Extra large border radius' + "name": "z-index-alert-group", + "cssVar": "--op-z-index-alert-group", + "value": "950", + "category": "z-index", + "description": "z-index token: z-index-alert-group" }, { - name: 'op-radius-2x-large', - value: '16px', - category: 'border', - description: '2X large border radius' + "name": "z-index-tooltip", + "cssVar": "--op-z-index-tooltip", + "value": "1000", + "category": "z-index", + "description": "z-index token: z-index-tooltip" }, { - name: 'op-radius-circle', - value: '50%', - category: 'border', - description: 'Circular border radius' + "name": "input-height-small", + "cssVar": "--op-input-height-small", + "value": "2.8rem", + "category": "input", + "description": "input token: input-height-small" }, { - name: 'op-radius-pill', - value: '9999px', - category: 'border', - description: 'Pill-shaped border radius' - }, - - // Border Width Tokens - { - name: 'op-border-width', - value: '1px', - category: 'border', - description: 'Standard border width' - }, - { - name: 'op-border-width-large', - value: '2px', - category: 'border', - description: 'Large border width' - }, - { - name: 'op-border-width-x-large', - value: '4px', - category: 'border', - description: 'Extra large border width' - }, - - // Shadow Tokens - { - name: 'op-shadow-x-small', - value: '0 1px 2px hsl(0deg 0% 0% / 3%), 0 1px 3px hsl(0deg 0% 0% / 15%)', - category: 'shadow', - description: 'Extra small shadow' - }, - { - name: 'op-shadow-small', - value: '0 1px 2px hsl(0deg 0% 0% / 3%), 0 2px 6px hsl(0deg 0% 0% / 15%)', - category: 'shadow', - description: 'Small shadow' - }, - { - name: 'op-shadow-medium', - value: '0 4px 8px hsl(0deg 0% 0% / 15%), 0 1px 3px hsl(0deg 0% 0% / 3%)', - category: 'shadow', - description: 'Medium shadow' - }, - { - name: 'op-shadow-large', - value: '0 6px 10px hsl(0deg 0% 0% / 15%), 0 2px 3px hsl(0deg 0% 0% / 3%)', - category: 'shadow', - description: 'Large shadow' - }, - { - name: 'op-shadow-x-large', - value: '0 8px 12px hsl(0deg 0% 0% / 15%), 0 4px 4px hsl(0deg 0% 0% / 3%)', - category: 'shadow', - description: 'Extra large shadow' - }, - - // Opacity Tokens - { - name: 'op-opacity-none', - value: '0', - category: 'color', - description: 'No opacity' + "name": "input-height-medium", + "cssVar": "--op-input-height-medium", + "value": "3.6rem", + "category": "input", + "description": "input token: input-height-medium" }, { - name: 'op-opacity-overlay', - value: '0.2', - category: 'color', - description: 'Overlay opacity' + "name": "input-height-large", + "cssVar": "--op-input-height-large", + "value": "4rem", + "category": "input", + "description": "input token: input-height-large" }, { - name: 'op-opacity-disabled', - value: '0.4', - category: 'color', - description: 'Disabled opacity' + "name": "input-height-x-large", + "cssVar": "--op-input-height-x-large", + "value": "8.4rem", + "category": "input", + "description": "input token: input-height-x-large" }, { - name: 'op-opacity-half', - value: '0.5', - category: 'color', - description: 'Half opacity' + "name": "input-inner-focus", + "cssVar": "--op-input-inner-focus", + "value": "inset 0 0 0 var(--op-border-width-large)", + "category": "input", + "description": "input token: input-inner-focus" }, { - name: 'op-opacity-full', - value: '1', - category: 'color', - description: 'Full opacity' + "name": "input-outer-focus", + "cssVar": "--op-input-outer-focus", + "value": "0 0 0 var(--op-border-width-x-large)", + "category": "input", + "description": "input token: input-outer-focus" } ]; -/** - * Components - Reusable UI components with their design token usage - * Real components from the Optics Design System - */ -export const components: Component[] = [ - { - name: 'Accordion', - description: 'Collapsible content panel with expand/collapse animation', - tokens: [ - '--op-color-neutral-on-plus-max', - '--op-font-weight-semi-bold', - '--op-font-x-large', - '--op-font-x-small', - '--op-mso-optical-sizing', - '--op-size-unit', - '--op-space-2x-small', - '--op-transition-accordion' +export const cssPatterns: CSSPattern[] = [ + { + "name": "Accordion", + "description": "Accordion classes are built on the `details` and `summary` html elements. They provide consistent and composable styling for disclosure widgets.", + "className": "accordion", + "type": "component", + "modifiers": [ + "accordion--disable-animation" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Alert', - description: 'Notification component for displaying important messages (warning, danger, info, notice)', - tokens: [ - '--op-animation-flash', - '--op-border-all', - '--op-color-alerts-danger-base', - '--op-color-alerts-danger-on-base', - '--op-color-alerts-danger-on-base-alt', - '--op-color-alerts-danger-on-plus-eight', - '--op-color-alerts-danger-on-plus-eight-alt', - '--op-color-alerts-danger-on-plus-five', - '--op-color-alerts-danger-on-plus-five-alt', - '--op-color-alerts-danger-plus-eight', - '--op-color-alerts-danger-plus-five', - '--op-color-alerts-info-base', - '--op-color-alerts-info-on-base', - '--op-color-alerts-info-on-base-alt', - '--op-color-alerts-info-on-plus-eight', - '--op-color-alerts-info-on-plus-eight-alt', - '--op-color-alerts-info-on-plus-five', - '--op-color-alerts-info-on-plus-five-alt', - '--op-color-alerts-info-plus-eight', - '--op-color-alerts-info-plus-five', - '--op-color-alerts-notice-base', - '--op-color-alerts-notice-on-base', - '--op-color-alerts-notice-on-base-alt', - '--op-color-alerts-notice-on-plus-eight', - '--op-color-alerts-notice-on-plus-eight-alt', - '--op-color-alerts-notice-on-plus-five', - '--op-color-alerts-notice-on-plus-five-alt', - '--op-color-alerts-notice-plus-eight', - '--op-color-alerts-notice-plus-five', - '--op-color-alerts-warning-base', - '--op-color-alerts-warning-on-base', - '--op-color-alerts-warning-on-base-alt', - '--op-color-alerts-warning-on-plus-eight', - '--op-color-alerts-warning-on-plus-eight-alt', - '--op-color-alerts-warning-on-plus-five', - '--op-color-alerts-warning-on-plus-five-alt', - '--op-color-alerts-warning-plus-eight', - '--op-color-alerts-warning-plus-five', - '--op-font-medium', - '--op-font-small', - '--op-font-weight-medium', - '--op-line-height-dense', - '--op-radius-medium', - '--op-space-2x-small', - '--op-space-large', - '--op-space-medium', - '--op-space-small', - '--op-space-x-small', - '--op-z-index-alert-group' + "elements": [ + "accordion__label", + "accordion__marker" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Avatar', - description: 'User profile picture component with multiple sizes and states', - tokens: [ - '--op-border-width', - '--op-border-width-large', - '--op-color-neutral-base', - '--op-color-neutral-minus-max', - '--op-color-primary-base', - '--op-color-primary-plus-one', - '--op-opacity-disabled', - '--op-opacity-overlay', - '--op-radius-circle', - '--op-size-unit' + "exampleHtml": "
\n
...
\n
...
\n
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-accordion--docs" + }, + { + "name": "Alert", + "description": "Alert classes can be used to create a highlighted message or callout in your application.", + "className": "alert", + "type": "component", + "modifiers": [ + "alert--alert", + "alert--danger", + "alert--filled", + "alert--flash", + "alert--info", + "alert--muted", + "alert--notice", + "alert--warning" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Badge', - description: 'Small status indicator or label with multiple color variants', - tokens: [ - '--op-border-width-large', - '--op-color-alerts-danger-base', - '--op-color-alerts-danger-on-base', - '--op-color-alerts-info-base', - '--op-color-alerts-info-on-base', - '--op-color-alerts-notice-base', - '--op-color-alerts-notice-on-base', - '--op-color-alerts-warning-base', - '--op-color-alerts-warning-on-base', - '--op-color-neutral-base', - '--op-color-neutral-on-base', - '--op-color-neutral-plus-max', - '--op-color-primary-base', - '--op-color-primary-on-base', - '--op-font-small', - '--op-font-weight-bold', - '--op-font-x-small', - '--op-letter-spacing-label', - '--op-line-height-dense', - '--op-radius-medium', - '--op-radius-pill', - '--op-space-2x-small', - '--op-space-x-small' + "elements": [ + "alert__description", + "alert__icon", + "alert__messages", + "alert__title" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Breadcrumbs', - description: 'Navigation component showing the current page location in the site hierarchy', - tokens: [ - '--op-font-small', - '--op-font-weight-bold', - '--op-font-x-small', - '--op-space-x-small' + "exampleHtml": "
\n
...
\n
...
\n
...
\n
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-alert--docs" + }, + { + "name": "Avatar", + "description": "Avatar classes can be used on `a` or `div` html elements with an `img` within it. They provide consistent and composable styling for application avatars or profile pictures.", + "className": "avatar", + "type": "component", + "modifiers": [ + "avatar--disabled", + "avatar--large", + "avatar--medium", + "avatar--small" + ], + "elements": [], + "exampleHtml": "
...
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-avatar--docs" + }, + { + "name": "Badge", + "description": "The Badge component is similar to the Tag component, however it has a different semantic purpose. Badge is intended to be used for notification and information where Tag is intended to be used for interaction and input. See [Tag](?path=/docs/components-tag--docs) for details on its usage.", + "className": "badge", + "type": "component", + "modifiers": [ + "badge--danger", + "badge--info", + "badge--notice", + "badge--notification-left", + "badge--notification-right", + "badge--pill", + "badge--primary", + "badge--warning", + "btn--with-badge" + ], + "elements": [], + "exampleHtml": "
...
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-badge--docs" + }, + { + "name": "Breadcrumbs", + "description": "The breadcrumbs component is used to show the user's current location in a hierarchy of pages.", + "className": "breadcrumbs", + "type": "component", + "modifiers": [ + "breadcrumbs--large", + "breadcrumbs--small" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Button', - description: 'Interactive button component with multiple variants (primary, secondary, etc.) and states', - tokens: [ - '--op-border-all', - '--op-color-alerts-danger-base', - '--op-color-alerts-danger-minus-two', - '--op-color-alerts-danger-on-base', - '--op-color-alerts-danger-on-minus-two', - '--op-color-alerts-danger-on-plus-five', - '--op-color-alerts-danger-plus-five', - '--op-color-alerts-danger-plus-three', - '--op-color-alerts-warning-base', - '--op-color-alerts-warning-minus-two', - '--op-color-alerts-warning-on-base', - '--op-color-alerts-warning-on-minus-two', - '--op-color-alerts-warning-on-plus-five', - '--op-color-alerts-warning-plus-five', - '--op-color-alerts-warning-plus-three', - '--op-color-neutral-on-plus-eight', - '--op-color-neutral-plus-eight', - '--op-color-neutral-plus-four', - '--op-color-primary-base', - '--op-color-primary-minus-five', - '--op-color-primary-on-base', - '--op-color-primary-on-minus-five', - '--op-color-primary-on-plus-eight', - '--op-color-primary-on-plus-five', - '--op-color-primary-on-plus-max', - '--op-color-primary-on-plus-one', - '--op-color-primary-plus-eight', - '--op-color-primary-plus-five', - '--op-color-primary-plus-one', - '--op-color-primary-plus-three', - '--op-color-primary-plus-two', - '--op-font-small', - '--op-font-weight-normal', - '--op-font-x-small', - '--op-input-focus-danger', - '--op-input-focus-primary', - '--op-input-focus-warning', - '--op-input-height-large', - '--op-input-height-medium', - '--op-input-height-small', - '--op-opacity-disabled', - '--op-radius-medium', - '--op-radius-pill', - '--op-space-3x-small', - '--op-space-small', - '--op-space-x-small', - '--op-transition-input' + "elements": [ + "breadcrumbs__link", + "breadcrumbs__separator", + "breadcrumbs__text" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] + "exampleHtml": "
\n
...
\n
...
\n
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-breadcrumbs--docs" + }, + { + "name": "Button", + "description": "Button classes can be used on `button` or `a` html elements. They provide consistent and composable styling that should address most applications basic needs.", + "className": "btn", + "type": "component", + "modifiers": [ + "btn--active", + "btn--delete", + "btn--destructive", + "btn--disabled", + "btn--icon", + "btn--icon-with-label", + "btn--large", + "btn--medium", + "btn--no-border", + "btn--pill", + "btn--primary", + "btn--small", + "btn--warning", + "btn--with-badge" + ], + "elements": [], + "exampleHtml": "", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-button--docs" + }, + { + "name": "ButtonGroup", + "description": "ButtonGroup component", + "className": "btn-group", + "type": "component", + "modifiers": [], + "elements": [ + "btn-group-toolbar" + ], + "exampleHtml": "
...
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-buttongroup--docs" + }, + { + "name": "Card", + "description": "Card classes can be used to denote bordered sections of an application. They provide simple styles to create sections or \"cards\" for your interface. They can also be used as a starting point for \"row\" or list styles.", + "className": "card", + "type": "component", + "modifiers": [ + "card--condensed", + "card--padded", + "card--shadow-large", + "card--shadow-medium", + "card--shadow-small", + "card--shadow-x-large", + "card--shadow-x-small" + ], + "elements": [ + "card__body", + "card__footer", + "card__header" + ], + "exampleHtml": "
\n
...
\n
...
\n
...
\n
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-card--docs" }, { - name: 'ButtonGroup', - description: 'Container for grouping related buttons together', - tokens: [ - '--op-btn-group-active-z-index', - '--op-btn-group-focus-z-index', - '--op-btn-group-hover-z-index' + "name": "ConfirmDialog", + "description": "ConfirmDialog component", + "className": "confirm-dialog-wrapper", + "type": "component", + "modifiers": [ + "confirm-dialog-wrapper--active" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Card', - description: 'Container component for grouping related content with optional header, body, and footer', - tokens: [ - '--op-border-all', - '--op-color-background', - '--op-color-border', - '--op-color-on-background', - '--op-font-medium', - '--op-line-height-base', - '--op-radius-medium', - '--op-shadow-large', - '--op-shadow-medium', - '--op-shadow-small', - '--op-shadow-x-large', - '--op-shadow-x-small', - '--op-space-medium', - '--op-space-scale-unit' + "elements": [ + "confirm-dialog", + "confirm-dialog-wrapper__backdrop", + "confirm-dialog__body", + "confirm-dialog__footer", + "confirm-dialog__header" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'ConfirmDialog', - description: 'Modal dialog for confirming user actions', - tokens: [ - '--op-border-all', - '--op-color-background', - '--op-color-black', - '--op-color-border', - '--op-color-on-background', - '--op-font-large', - '--op-font-medium', - '--op-font-weight-semi-bold', - '--op-line-height-base', - '--op-opacity-full', - '--op-opacity-half', - '--op-opacity-none', - '--op-radius-medium', - '--op-size-unit', - '--op-space-medium', - '--op-transition-modal', - '--op-z-index-dialog', - '--op-z-index-dialog-backdrop', - '--op-z-index-dialog-content' + "exampleHtml": "
\n
...
\n
...
\n
...
\n
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-confirmdialog--docs" + }, + { + "name": "ContentHeader", + "description": "ContentHeader component", + "className": "content-header", + "type": "component", + "modifiers": [], + "elements": [ + "content-header__aside", + "content-header__context", + "content-header__details", + "content-header__subline", + "content-header__title", + "context-header__aside", + "context-header__context", + "context-header__details", + "context-header__subline" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Border', - description: 'Visual separator between content sections', - tokens: [ - '--op-border-width', - '--op-border-width-large', - '--op-border-width-x-large', - '--op-color-border', - '--op-space-2x-small', - '--op-space-large', - '--op-space-medium', - '--op-space-x-small' + "exampleHtml": "
\n
...
\n
...
\n
...
\n
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-contentheader--docs" + }, + { + "name": "Divider", + "description": "Divider classes can be used to create horizontal or vertical visual divides between content.", + "className": "divider", + "type": "component", + "modifiers": [ + "divider--large", + "divider--medium", + "divider--small", + "divider--spacing-large", + "divider--spacing-medium", + "divider--spacing-small", + "divider--vertical" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Form', - description: 'Form input components including text inputs, textareas, selects, and labels', - tokens: [ - '--op-border-all', - '--op-border-bottom', - '--op-color-alerts-danger-base', - '--op-color-alerts-danger-minus-three', - '--op-color-alerts-danger-minus-two', - '--op-color-alerts-danger-on-plus-eight', - '--op-color-alerts-danger-on-plus-seven', - '--op-color-alerts-danger-plus-eight', - '--op-color-alerts-danger-plus-seven', - '--op-color-neutral-on-plus-eight', - '--op-color-neutral-plus-eight', - '--op-color-neutral-plus-four', - '--op-color-on-background', - '--op-color-primary-base', - '--op-color-primary-on-plus-eight', - '--op-color-primary-on-plus-max', - '--op-color-primary-on-plus-seven', - '--op-color-primary-plus-eight', - '--op-color-primary-plus-seven', - '--op-color-primary-plus-three', - '--op-color-primary-plus-two', - '--op-encoded-images-dropdown-arrow', - '--op-encoded-images-dropdown-arrow-width', - '--op-font-medium', - '--op-font-small', - '--op-font-weight-bold', - '--op-font-weight-normal', - '--op-font-x-small', - '--op-input-focus-danger', - '--op-input-focus-primary', - '--op-input-height-large', - '--op-input-height-medium', - '--op-input-height-small', - '--op-letter-spacing-label', - '--op-line-height-base', - '--op-opacity-disabled', - '--op-radius-large', - '--op-radius-medium', - '--op-space-2x-small', - '--op-space-3x-large', - '--op-space-large', - '--op-space-medium', - '--op-space-small', - '--op-space-x-large', - '--op-space-x-small', - '--op-transition-input' + "elements": [], + "exampleHtml": "
...
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-divider--docs" + }, + { + "name": "Form", + "description": "Form classes can be used on a variety of `inputs` or `select` HTML elements.", + "className": "form-label", + "type": "component", + "modifiers": [ + "form-control--large", + "form-control--medium", + "form-control--no-border", + "form-control--small", + "form-group--error", + "form-group--inline" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Icon', - description: 'Material Symbols icon component', - tokens: [ - '--op-font-2x-large', - '--op-font-3x-large', - '--op-font-large', - '--op-font-medium', - '--op-font-small', - '--op-font-weight-bold', - '--op-font-weight-light', - '--op-font-weight-normal', - '--op-font-weight-semi-bold', - '--op-line-height-densest', - '--op-mso-fill', - '--op-mso-grade', - '--op-mso-optical-sizing', - '--op-mso-weight' + "elements": [ + "form-control", + "form-error", + "form-error-summary", + "form-group", + "form-hint" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Modal', - description: 'Dialog component for focused interactions and content overlays', - tokens: [ - '--op-border-all', - '--op-color-background', - '--op-color-black', - '--op-color-border', - '--op-color-on-background', - '--op-font-large', - '--op-font-medium', - '--op-font-weight-semi-bold', - '--op-line-height-base', - '--op-opacity-full', - '--op-opacity-half', - '--op-opacity-none', - '--op-radius-medium', - '--op-size-unit', - '--op-space-medium', - '--op-space-small', - '--op-transition-modal', - '--op-z-index-dialog', - '--op-z-index-dialog-backdrop', - '--op-z-index-dialog-content' + "exampleHtml": "
...
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-form--docs" + }, + { + "name": "Icon", + "description": "Icon classes are built on top of [Google's Material Symbols Icon Font](https://fonts.google.com/icons). They provide a way to integrate iconography into your application in a flexible and customizable way.", + "className": "ph", + "type": "component", + "modifiers": [ + "icon--filled", + "icon--high-emphasis", + "icon--large", + "icon--low-emphasis", + "icon--medium", + "icon--normal-emphasis", + "icon--outlined", + "icon--small", + "icon--weight-bold", + "icon--weight-light", + "icon--weight-normal", + "icon--weight-semi-bold", + "icon--weight-thin", + "icon--x-large" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Navbar', - description: 'Top navigation bar component', - tokens: [ - '--op-border-bottom', - '--op-color-neutral-on-plus-eight', - '--op-color-neutral-plus-eight', - '--op-color-neutral-plus-four', - '--op-color-primary-on-plus-six', - '--op-color-primary-plus-four', - '--op-color-primary-plus-six', - '--op-size-unit', - '--op-space-2x-small', - '--op-space-small', - '--op-space-x-large', - '--op-space-x-small' + "elements": [ + "fi", + "icon", + "li", + "ph-duotone", + "ti" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] + "exampleHtml": "
...
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-icon--docs" }, { - name: 'Pagination', - description: 'Navigation component for paginated content', - tokens: [ - '--op-space-x-small' + "name": "Modal", + "description": "The Modal classes can be used for styling a custom modal. This can be used alongside the Rails configuration and Javascript implemented by [RoleModel Rails Modal](https://github.com/RoleModel/rolemodel_rails/tree/master/lib/generators/rolemodel/modals)", + "className": "modal-wrapper", + "type": "component", + "modifiers": [ + "modal-wrapper--active" + ], + "elements": [ + "modal", + "modal-wrapper__backdrop", + "modal__body", + "modal__footer", + "modal__header" + ], + "exampleHtml": "
\n
...
\n
...
\n
...
\n
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-modal--docs" + }, + { + "name": "Navbar", + "description": "Navbar classes provide simple styling for a navigation header.", + "className": "navbar", + "type": "component", + "modifiers": [ + "navbar--primary", + "navbar__content--justify-center", + "navbar__content--justify-end", + "navbar__content--justify-start" + ], + "elements": [ + "navbar__brand", + "navbar__content", + "navbar__content--justify-center", + "navbar__content--justify-end", + "navbar__content--justify-start" + ], + "exampleHtml": "
\n
...
\n
...
\n
...
\n
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-navbar--docs" + }, + { + "name": "Pagination", + "description": "Pagination is used to navigate through a series of pages, typically when dealing with tabular data.", + "className": "pagination", + "type": "component", + "modifiers": [], + "elements": [ + "pagination__divider" + ], + "exampleHtml": "
\n
...
\n
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-pagination--docs" + }, + { + "name": "SegmentedControl", + "description": "Styles are built on css variables scoped to the segmented control.", + "className": "segmented-control", + "type": "component", + "modifiers": [ + "segmented-control--full-width", + "segmented-control--large", + "segmented-control--medium", + "segmented-control--small" + ], + "elements": [ + "segmented-control__input", + "segmented-control__label" + ], + "exampleHtml": "
\n
...
\n
...
\n
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-segmentedcontrol--docs" + }, + { + "name": "SidePanel", + "description": "Side Panel classes provide simple styling for a panel of sections with a scrollable body.", + "className": "side-panel", + "type": "component", + "modifiers": [ + "side-panel--border-left", + "side-panel--border-right", + "side-panel__footer--padded", + "side-panel__footer--padded-x", + "side-panel__footer--padded-y", + "side-panel__section--padded", + "side-panel__section--padded-x", + "side-panel__section--padded-y" + ], + "elements": [ + "side-panel__body", + "side-panel__body--padded", + "side-panel__body--padded-x", + "side-panel__body--padded-y", + "side-panel__footer", + "side-panel__footer--padded", + "side-panel__footer--padded-x", + "side-panel__footer--padded-y", + "side-panel__header", + "side-panel__header--padded", + "side-panel__header--padded-x", + "side-panel__header--padded-y", + "side-panel__section", + "side-panel__section--padded", + "side-panel__section--padded-x", + "side-panel__section--padded-y" + ], + "exampleHtml": "
\n
...
\n
...
\n
...
\n
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-sidepanel--docs" + }, + { + "name": "Sidebar", + "description": "Sidebar classes provide simple styling for a navigation sidebar drawer, compact, or rail.", + "className": "sidebar", + "type": "component", + "modifiers": [ + "sidebar--compact", + "sidebar--drawer", + "sidebar--padded", + "sidebar--primary", + "sidebar--rail", + "sidebar__content--center", + "sidebar__content--end", + "sidebar__content--start" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'SidePanel', - description: 'Sliding panel from the side of the screen', - tokens: [ - '--op-border-left', - '--op-border-right', - '--op-border-x', - '--op-color-background', - '--op-color-border', - '--op-color-on-background', - '--op-size-unit', - '--op-space-medium' + "elements": [ + "icon-with-label", + "sidebar__brand", + "sidebar__content", + "sidebar__content--center", + "sidebar__content--end", + "sidebar__content--start" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Sidebar', - description: 'Side navigation panel component', - tokens: [ - '--op-border-right', - '--op-color-neutral-on-plus-eight', - '--op-color-neutral-plus-eight', - '--op-color-neutral-plus-four', - '--op-color-primary-on-plus-six', - '--op-color-primary-plus-four', - '--op-color-primary-plus-six', - '--op-size-unit', - '--op-space-2x-large', - '--op-space-2x-small', - '--op-space-3x-small', - '--op-space-medium', - '--op-space-small', - '--op-space-x-small', - '--op-transition-sidebar' + "exampleHtml": "
\n
...
\n
...
\n
...
\n
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-sidebar--docs" + }, + { + "name": "Spinner", + "description": "Spinners are CSS loading indicators that should be shown when retrieving data or performing slow computations.", + "className": "spinner", + "type": "component", + "modifiers": [ + "spinner--large", + "spinner--medium", + "spinner--small", + "spinner--x-small" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Spinner', - description: 'Loading indicator component', - tokens: [ - '--op-border-width', - '--op-border-width-large', - '--op-border-width-x-large', - '--op-color-neutral-plus-four', - '--op-color-primary-base', - '--op-size-unit' + "elements": [], + "exampleHtml": "
...
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-spinner--docs" + }, + { + "name": "Switch", + "description": "Switch classes can be used to create a stylized checkbox or boolean input.", + "className": "switch", + "type": "component", + "modifiers": [ + "switch--large", + "switch--small" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Switch', - description: 'Toggle switch component', - tokens: [ - '--op-border-all', - '--op-border-width-large', - '--op-color-neutral-base', - '--op-color-neutral-plus-eight', - '--op-color-neutral-plus-five', - '--op-color-neutral-plus-three', - '--op-color-primary-base', - '--op-color-primary-minus-three', - '--op-color-primary-minus-two', - '--op-color-primary-plus-five', - '--op-color-primary-plus-six', - '--op-opacity-disabled', - '--op-radius-circle', - '--op-radius-pill', - '--op-size-unit', - '--op-space-2x-small', - '--op-space-x-small', - '--op-transition-input' + "elements": [], + "exampleHtml": "
...
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-switch--docs" + }, + { + "name": "Tab", + "description": "Tab classes provide simple styling for a tab group navigation.", + "className": "tab-group", + "type": "component", + "modifiers": [ + "tab--active", + "tab--disabled", + "tab--large", + "tab--small" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Tab', - description: 'Tabbed interface component', - tokens: [ - '--op-border-width-large', - '--op-border-width-x-large', - '--op-color-background', - '--op-color-on-background', - '--op-color-primary-base', - '--op-color-primary-on-plus-seven', - '--op-color-primary-plus-one', - '--op-color-primary-plus-seven', - '--op-font-small', - '--op-font-x-small', - '--op-input-focus-primary', - '--op-opacity-disabled', - '--op-space-2x-small', - '--op-space-3x-small', - '--op-space-medium', - '--op-space-small', - '--op-space-x-small' + "elements": [ + "tab" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Table', - description: 'Data table component for displaying structured information', - tokens: [ - '--op-border-all', - '--op-border-top', - '--op-color-alerts-danger-on-plus-seven', - '--op-color-alerts-danger-plus-seven', - '--op-color-border', - '--op-color-neutral-on-plus-eight', - '--op-color-neutral-on-plus-max', - '--op-color-neutral-on-plus-seven', - '--op-color-neutral-plus-eight', - '--op-color-neutral-plus-max', - '--op-color-neutral-plus-seven', - '--op-color-primary-on-plus-seven', - '--op-color-primary-plus-seven', - '--op-font-small', - '--op-font-weight-semi-bold', - '--op-radius-medium', - '--op-size-unit', - '--op-space-2x-small', - '--op-space-small' + "exampleHtml": "
...
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-tab--docs" + }, + { + "name": "Table", + "description": "Table classes provide simple styling for tables and their content.", + "className": "table", + "type": "component", + "modifiers": [ + "table--auto-layout", + "table--comfortable-density", + "table--compact-density", + "table--container", + "table--danger", + "table--default-density", + "table--even-striped", + "table--fixed-layout", + "table--odd-striped", + "table--primary", + "table--sticky-footer", + "table--sticky-header" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Tag', - description: 'Small label component for categorizing or tagging content', - tokens: [ - '--op-color-alerts-danger-base', - '--op-color-alerts-danger-minus-three', - '--op-color-alerts-danger-on-base', - '--op-color-alerts-danger-on-minus-three', - '--op-color-alerts-info-base', - '--op-color-alerts-info-minus-three', - '--op-color-alerts-info-on-base', - '--op-color-alerts-info-on-minus-three', - '--op-color-alerts-notice-base', - '--op-color-alerts-notice-minus-three', - '--op-color-alerts-notice-on-base', - '--op-color-alerts-notice-on-minus-three', - '--op-color-alerts-warning-base', - '--op-color-alerts-warning-minus-three', - '--op-color-alerts-warning-on-base', - '--op-color-alerts-warning-on-minus-three', - '--op-color-neutral-base', - '--op-color-neutral-minus-three', - '--op-color-neutral-on-base', - '--op-color-neutral-on-minus-three', - '--op-color-neutral-on-plus-four', - '--op-color-neutral-plus-four', - '--op-color-primary-base', - '--op-color-primary-minus-three', - '--op-color-primary-on-base', - '--op-color-primary-on-minus-three', - '--op-font-medium', - '--op-font-weight-bold', - '--op-font-x-small', - '--op-input-focus-danger', - '--op-input-focus-info', - '--op-input-focus-neutral', - '--op-input-focus-notice', - '--op-input-focus-primary', - '--op-input-focus-warning', - '--op-letter-spacing-label', - '--op-line-height-dense', - '--op-radius-pill', - '--op-space-2x-small' + "elements": [ + "table-container" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'TextPair', - description: 'Component for displaying label-value pairs', - tokens: [ - '--op-font-large', - '--op-font-medium', - '--op-font-small', - '--op-font-weight-normal', - '--op-font-weight-semi-bold', - '--op-line-height-dense', - '--op-space-x-small' + "exampleHtml": "
...
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-table--docs" + }, + { + "name": "Tag", + "description": "The tag component can be applied to an element with a button within it. The Tag component is similar to the Badge component, however it has a different semantic purpose. Tag is intended to be used for interaction and input where Badge is intended to be used for Notification and Information. See [Badge](?path=/docs/components-badge--docs) for details on its usage.", + "className": "tag", + "type": "component", + "modifiers": [ + "tag--danger", + "tag--info", + "tag--notice", + "tag--primary", + "tag--read-only", + "tag--warning" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] - }, - { - name: 'Tooltip', - description: 'Contextual information component that appears on hover or focus', - tokens: [ - '--op-color-neutral-minus-max', - '--op-color-neutral-on-minus-max', - '--op-font-family', - '--op-font-small', - '--op-opacity-full', - '--op-opacity-none', - '--op-radius-medium', - '--op-size-unit', - '--op-space-medium', - '--op-space-small', - '--op-space-x-small', - '--op-transition-tooltip', - '--op-z-index-tooltip' + "elements": [ + "tag__label" ], - usage: 'See https://docs.optics.rolemodel.design for component usage and examples', - examples: [] + "exampleHtml": "
\n
...
\n
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-tag--docs" + }, + { + "name": "TextPair", + "description": "TextPair component", + "className": "text-pair", + "type": "component", + "modifiers": [ + "text-pair--inline", + "text-pair__subtitle--large", + "text-pair__subtitle--medium", + "text-pair__subtitle--small", + "text-pair__title--large", + "text-pair__title--medium", + "text-pair__title--small" + ], + "elements": [ + "text-pair__subtitle", + "text-pair__subtitle--large", + "text-pair__subtitle--medium", + "text-pair__subtitle--small", + "text-pair__title", + "text-pair__title--large", + "text-pair__title--medium", + "text-pair__title--small" + ], + "exampleHtml": "
\n
...
\n
...
\n
...
\n
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-textpair--docs" + }, + { + "name": "Stack", + "description": "Layout utility: op-stack", + "className": "op-stack", + "type": "layout", + "modifiers": [], + "elements": [], + "exampleHtml": "
...
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/layout-stack--docs" + }, + { + "name": "Cluster", + "description": "Layout utility: op-cluster", + "className": "op-cluster", + "type": "layout", + "modifiers": [], + "elements": [], + "exampleHtml": "
...
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/layout-cluster--docs" + }, + { + "name": "Split", + "description": "Layout utility: op-split", + "className": "op-split", + "type": "layout", + "modifiers": [], + "elements": [], + "exampleHtml": "
...
", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/layout-split--docs" + }, + { + "name": "Tooltip", + "description": "CSS-only tooltip using data attributes", + "className": "[data-tooltip-text]", + "type": "component", + "modifiers": [ + "[data-tooltip-position=\"top\"]", + "[data-tooltip-position=\"bottom\"]", + "[data-tooltip-position=\"left\"]", + "[data-tooltip-position=\"right\"]" + ], + "elements": [], + "exampleHtml": "", + "docsUrl": "https://docs.optics.rolemodel.design/?path=/docs/components-tooltip--docs" } ]; -/** - * Documentation - Organized documentation sections - */ +// Backwards compatibility: components alias with extended interface +export const components: Component[] = cssPatterns.map(p => ({ + ...p, + tokens: p.modifiers, + usage: p.description, + examples: p.exampleHtml ? [p.exampleHtml] : [], +})); + export const documentation: Documentation[] = [ { - section: 'introduction', - title: 'Introduction to Optics', - content: 'Optics is a comprehensive design system that provides a consistent visual language and component library for building user interfaces. It includes design tokens, components, patterns, and guidelines to ensure consistency across all RoleModel products.', - tokens: [] - }, - { - section: 'getting-started', - title: 'Getting Started', - content: 'To get started with Optics, install the design system package and import the tokens and components you need. The system is built with modularity in mind, allowing you to use only what you need.', - tokens: [] - }, - { - section: 'design-tokens', - title: 'Design Tokens', - content: 'Design tokens are the visual design atoms of the design system — specifically, they are named entities that store visual design attributes. They are used in place of hard-coded values to ensure consistency and enable theming.', - tokens: designTokens.map(t => t.name) - }, - { - section: 'color-system', - title: 'Color System', - content: 'The Optics color system provides a comprehensive palette designed for accessibility and visual harmony. Use semantic color tokens (primary, secondary, success, danger, warning, info) rather than specific color values.', - tokens: designTokens.filter(t => t.category === 'color').map(t => t.name) - }, - { - section: 'spacing', - title: 'Spacing System', - content: 'Consistent spacing creates visual rhythm and helps users understand relationships between elements. Optics uses a base-8 spacing system with tokens ranging from xs (4px) to 2xl (48px).', - tokens: designTokens.filter(t => t.category === 'spacing').map(t => t.name) - }, - { - section: 'typography', - title: 'Typography', - content: 'Typography is crucial for creating clear information hierarchy and readability. Optics provides font family, size, weight, and line height tokens to ensure consistent text styling.', - tokens: designTokens.filter(t => t.category === 'typography').map(t => t.name) - }, - { - section: 'components', - title: 'Components', - content: 'Optics components are reusable UI elements built with design tokens. Each component follows accessibility best practices and includes comprehensive documentation on usage and token application.', - tokens: [] - }, - { - section: 'accessibility', - title: 'Accessibility Guidelines', - content: 'Accessibility is a core principle of Optics. All components meet WCAG 2.1 AA standards. Ensure proper color contrast, keyboard navigation, and screen reader support when using Optics components.', - tokens: [] + "section": "overview", + "title": "Optics Overview", + "content": "Optics is a CSS-only design system. It provides CSS custom properties (tokens) and utility classes - NOT JavaScript components. Use the provided CSS classes and tokens; do not write custom CSS for patterns that already exist.", + "tokens": [] + }, + { + "section": "color-pairing", + "title": "Color Pairing Rule", + "content": "CRITICAL: Background and text colors must ALWAYS be paired. Never use --op-color-{family}-{scale} without also setting color to --op-color-{family}-on-{scale}. The \"on\" tokens are calculated for proper contrast against their matching background.", + "tokens": [ + "color-white", + "color-black", + "color-primary-h", + "color-primary-s", + "color-primary-l", + "color-primary-original", + "color-neutral-h", + "color-neutral-s", + "color-neutral-l", + "color-neutral-original", + "color-alerts-warning-h", + "color-alerts-warning-s", + "color-alerts-warning-l", + "color-alerts-warning-original", + "color-alerts-danger-h", + "color-alerts-danger-s", + "color-alerts-danger-l", + "color-alerts-danger-original", + "color-alerts-info-h", + "color-alerts-info-s", + "color-alerts-info-l", + "color-alerts-info-original", + "color-alerts-notice-h", + "color-alerts-notice-s", + "color-alerts-notice-l", + "color-alerts-notice-original", + "color-border", + "color-background", + "color-on-background" + ] + }, + { + "section": "color-system", + "title": "HSL Color System", + "content": "Optics uses HSL-based colors defined by -h (hue), -s (saturation), -l (lightness) tokens. A full scale is generated from plus-max (lightest) to minus-max (darkest). Each scale step has a matching \"on-\" token for text.", + "tokens": [ + "color-white", + "color-black", + "color-primary-h", + "color-primary-s", + "color-primary-l", + "color-primary-original", + "color-neutral-h", + "color-neutral-s", + "color-neutral-l", + "color-neutral-original", + "color-alerts-warning-h", + "color-alerts-warning-s", + "color-alerts-warning-l", + "color-alerts-warning-original", + "color-alerts-danger-h", + "color-alerts-danger-s", + "color-alerts-danger-l", + "color-alerts-danger-original", + "color-alerts-info-h", + "color-alerts-info-s", + "color-alerts-info-l", + "color-alerts-info-original", + "color-alerts-notice-h", + "color-alerts-notice-s", + "color-alerts-notice-l", + "color-alerts-notice-original", + "color-border", + "color-background", + "color-on-background" + ] + }, + { + "section": "use-existing", + "title": "Use Existing Classes", + "content": "Don't write custom CSS for components that already exist. Use .btn for buttons, .card for cards, .op-stack/.op-cluster/.op-split for layouts. Only write custom CSS when truly extending the system.", + "tokens": [] } ]; From 254e0ea936f4cd94f7048ffbae973ed172133d65 Mon Sep 17 00:00:00 2001 From: Dallas Peters Date: Thu, 5 Feb 2026 21:59:22 +0000 Subject: [PATCH 4/6] Add sync-data npm script and dev dependencies - Add npm run sync-data command - Add @rolemodel/optics and ts-node as dev deps Co-Authored-By: Claude Opus 4.5 --- package-lock.json | 216 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 5 +- 2 files changed, 220 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index cc3e0e5..ddbdd5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,14 +16,29 @@ "optics-mcp": "dist/index.js" }, "devDependencies": { + "@rolemodel/optics": "^2.3.0", "@types/node": "^20.10.0", "esbuild": "^0.27.2", + "ts-node": "^10.9.2", "typescript": "^5.9.3" }, "engines": { "node": ">=24.13.0" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", @@ -478,6 +493,34 @@ "hono": "^4" } }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.26.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", @@ -517,12 +560,59 @@ } } }, + "node_modules/@rolemodel/optics": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@rolemodel/optics/-/optics-2.3.0.tgz", + "integrity": "sha512-retjCKOscYSvLAh9aWRDPGnZRmP6+pc2yZNivU2WRFrp2FeaLfcw9g5oKvw9Lv03yEXL1dT1yQ6bUXeFjp7jkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "modern-css-reset": "^1.4.0" + }, + "peerDependencies": { + "tom-select": "^2.0.0" + }, + "peerDependenciesMeta": { + "tom-select": { + "optional": true + } + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.31", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.31.tgz", "integrity": "sha512-5jsi0wpncvTD33Sh1UCgacK37FFwDn+EG7wCmEvs62fCvBL+n8/76cAYDok21NF6+jaVWIqKwCZyX7Vbu8eB3A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -539,6 +629,32 @@ "node": ">= 0.6" } }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -572,6 +688,13 @@ } } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", @@ -683,6 +806,13 @@ "url": "https://opencollective.com/express" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -721,6 +851,16 @@ "node": ">= 0.8" } }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1141,6 +1281,13 @@ "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", "license": "BSD-2-Clause" }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -1191,6 +1338,13 @@ "url": "https://opencollective.com/express" } }, + "node_modules/modern-css-reset": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/modern-css-reset/-/modern-css-reset-1.4.0.tgz", + "integrity": "sha512-0crZmSFmrxkI7159rvQWjpDhy0u4+Awg/iOycJdlVn0RSeft/a+6BrQHR3IqvmdK25sqt0o6Z5Ap7cWgUee2rw==", + "dev": true, + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1508,6 +1662,50 @@ "node": ">=0.6" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -1527,6 +1725,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -1550,6 +1749,13 @@ "node": ">= 0.8" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -1579,6 +1785,16 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index 10dc0ca..9d3a36e 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "watch": "npx tsc --watch", "start": "node dist/index.js", "test": "node dist/test.js", - "test:interactive": "npm run build && node dist/interactive-client.js" + "test:interactive": "npm run build && node dist/interactive-client.js", + "sync-data": "npx ts-node scripts/sync-optics-data.ts" }, "keywords": [ "mcp", @@ -42,8 +43,10 @@ "zod": "^3.25.76" }, "devDependencies": { + "@rolemodel/optics": "^2.3.0", "@types/node": "^20.10.0", "esbuild": "^0.27.2", + "ts-node": "^10.9.2", "typescript": "^5.9.3" }, "engines": { From e51693642fb1ac1ab95c7ea7c21e3e440099bbec Mon Sep 17 00:00:00 2001 From: Dallas Peters Date: Thu, 5 Feb 2026 21:59:40 +0000 Subject: [PATCH 5/6] Add critical color pairing and component usage docs Document the two most common AI mistakes: - Not pairing background/text colors - Writing custom CSS instead of using existing components Co-Authored-By: Claude Opus 4.5 --- src/resources/00-system-overview.md | 126 ++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/src/resources/00-system-overview.md b/src/resources/00-system-overview.md index c5f8fd0..80489c4 100644 --- a/src/resources/00-system-overview.md +++ b/src/resources/00-system-overview.md @@ -269,6 +269,132 @@ When you call `get_component_info` for "Button", you'll see tokens like: } ``` +## ⚠️ CRITICAL: Color Pairing Rule + +**This is the #1 rule AI gets wrong. Read carefully.** + +In Optics, background colors and text colors are ALWAYS paired. You cannot use one without the other. + +### The Rule + +| When you set... | You MUST also set... | +|-----------------|----------------------| +| `background-color: var(--op-color-primary-base)` | `color: var(--op-color-primary-on-base)` | +| `background-color: var(--op-color-danger-minus-1)` | `color: var(--op-color-danger-on-minus-1)` | +| `color: var(--op-color-neutral-on-plus-eight)` | `background-color: var(--op-color-neutral-plus-eight)` | + +### Why? + +The `-on-` tokens are calculated for proper contrast against their matching background. Using them separately: +- Breaks accessibility +- Creates unreadable text +- Defeats the purpose of the system + +### ❌ WRONG - Unpaired Colors + +```css +/* Missing the text color! */ +.card { + background-color: var(--op-color-primary-base); +} + +/* Missing the background! */ +.label { + color: var(--op-color-danger-on-base); +} + +/* Using mismatched tokens! */ +.badge { + background-color: var(--op-color-primary-base); + color: var(--op-color-danger-on-base); /* WRONG - mismatched family */ +} +``` + +### ✅ CORRECT - Paired Colors + +```css +.card { + background-color: var(--op-color-primary-base); + color: var(--op-color-primary-on-base); +} + +.label { + background-color: var(--op-color-danger-base); + color: var(--op-color-danger-on-base); +} + +.badge-light { + background-color: var(--op-color-primary-plus-five); + color: var(--op-color-primary-on-plus-five); +} +``` + +### The Pattern + +For ANY color usage: +1. Pick your background: `--op-color-{family}-{scale}` +2. Add matching text: `--op-color-{family}-on-{scale}` +3. For secondary text, use: `--op-color-{family}-on-{scale}-alt` + +**Never use background colors alone. Never use text colors alone. They are a pair.** + +--- + +## ⚠️ CRITICAL: Use Existing Components + +**Don't write CSS for things that already exist.** + +Optics has pre-built components with established class names. AI should use these, not create new ones. + +### ❌ WRONG - Writing New CSS + +```css +/* DON'T DO THIS - buttons already exist */ +.my-button { + padding: var(--op-space-small) var(--op-space-medium); + background-color: var(--op-color-primary-base); + color: var(--op-color-primary-on-base); + border-radius: var(--op-radius-medium); +} + +/* DON'T DO THIS - cards already exist */ +.custom-card { + padding: var(--op-space-large); + background: var(--op-color-neutral-plus-eight); + border-radius: var(--op-radius-large); + box-shadow: var(--op-shadow-medium); +} +``` + +### ✅ CORRECT - Use Existing Classes + +```html + + + + +
+
...
+
+``` + +### When to Write Custom CSS + +Only write custom CSS when: +1. **Extending** an existing component with a modifier (following BEM conventions) +2. Creating something that **truly doesn't exist** in Optics +3. Overriding specific tokens for **theming purposes** + +### Before Writing CSS, Ask: + +1. Does this component exist in Optics? → Check https://docs.optics.rolemodel.design +2. Can I use existing utility classes? → `.stack`, `.cluster`, `.split`, etc. +3. Am I just recreating something that exists? → Use the existing class + +**The whole point of a design system is to NOT write custom CSS for common patterns.** + +--- + ## 🚨 Common Mistakes ### Mistake 1: Looking for Simple Color Names From 8fa02deba40e6a824f99bb7c37e83814fd49b772 Mon Sep 17 00:00:00 2001 From: Dallas Peters Date: Thu, 5 Feb 2026 22:10:52 +0000 Subject: [PATCH 6/6] Fix DesignToken interface compatibility in generate-theme-tool Add missing cssVar property to generated tokens. Co-Authored-By: Claude Opus 4.5 --- src/tools/generate-theme-tool.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/tools/generate-theme-tool.ts b/src/tools/generate-theme-tool.ts index a0cafe6..bda7e1e 100644 --- a/src/tools/generate-theme-tool.ts +++ b/src/tools/generate-theme-tool.ts @@ -99,6 +99,7 @@ class GenerateThemeTool extends Tool { tokens.push({ name: `op-color-${family}-h`, + cssVar: `--op-color-${family}-h`, value: String(hsl.h), category: 'color', description: `${family} color hue (HSL) - drives all ${family} scale tokens` @@ -106,6 +107,7 @@ class GenerateThemeTool extends Tool { tokens.push({ name: `op-color-${family}-s`, + cssVar: `--op-color-${family}-s`, value: `${hsl.s}%`, category: 'color', description: `${family} color saturation (HSL)` @@ -113,6 +115,7 @@ class GenerateThemeTool extends Tool { tokens.push({ name: `op-color-${family}-l`, + cssVar: `--op-color-${family}-l`, value: `${hsl.l}%`, category: 'color', description: `${family} color lightness (HSL)`