From c8a78ef7289998c1b3a3e7537847865bff914c9d Mon Sep 17 00:00:00 2001 From: dominicmacaulay Date: Thu, 5 Feb 2026 15:48:07 -0500 Subject: [PATCH 1/8] Create read tool file method --- src/_internal/resource-path.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/_internal/resource-path.ts b/src/_internal/resource-path.ts index 1393076..8cc1ca6 100644 --- a/src/_internal/resource-path.ts +++ b/src/_internal/resource-path.ts @@ -16,4 +16,11 @@ const readPromptFile = async (filename: string): Promise => { return readFileSync(filePath, 'utf-8') } -export { readResourceFile, readPromptFile } +const readToolFile = async (filename: string): Promise => { + const currentDir = dirname(fileURLToPath(import.meta.url)) + const filePath = join(currentDir, '..', 'tools', filename) + + return readFileSync(filePath, 'utf-8') +} + +export { readResourceFile, readPromptFile, readToolFile } From e99e11a648c9448ad5ae46e8505792150ce35a5a Mon Sep 17 00:00:00 2001 From: dominicmacaulay Date: Thu, 5 Feb 2026 16:05:06 -0500 Subject: [PATCH 2/8] extract the theme generator --- src/tools/generate-theme-instructions.md | 63 ++++ src/tools/generate-theme-output.md | 24 ++ src/tools/generate-theme-tool.ts | 287 +++++++++++++++++ src/tools/theme-generator.ts | 387 ----------------------- 4 files changed, 374 insertions(+), 387 deletions(-) create mode 100644 src/tools/generate-theme-instructions.md create mode 100644 src/tools/generate-theme-output.md create mode 100644 src/tools/generate-theme-tool.ts delete mode 100644 src/tools/theme-generator.ts diff --git a/src/tools/generate-theme-instructions.md b/src/tools/generate-theme-instructions.md new file mode 100644 index 0000000..ff891d2 --- /dev/null +++ b/src/tools/generate-theme-instructions.md @@ -0,0 +1,63 @@ +# {{themeName}} Theme + +## Overview +This theme was generated by Optics MCP and uses the Optics Design System tokens. + +## Step 1: Install Optics Design System + +### Option A: Via npm (Recommended) +```bash +npm install @rolemodel/optics +``` + +Then import in your CSS: +```css +@import "@rolemodel/optics/dist/optics.css"; +``` + +### Option B: Via CDN (jsDelivr) +```html + +``` + +### Option C: Via unpkg +```html + +``` + +## Step 2: Add Your Custom Theme Overrides + +Create a `theme.css` file with the custom HSL color values below and load it AFTER Optics: + +```html + + +``` + +## Token Summary + +{{tokenSummary}} + +## Step 3: Using Optics Components + +**IMPORTANT:** Optics has pre-built components with specific HTML structure and CSS classes. + +Do NOT make up CSS classes. Use the actual Optics components and their documentation: + +**Component Documentation:** https://docs.optics.rolemodel.design + +Your custom theme will automatically apply to all Optics components through the HSL base values. + +### Example: Using an Optics Button +Refer to the Optics documentation for the actual button HTML structure and CSS classes. +Your theme colors will apply automatically through the `--op-color-primary-*` variables. + +### Figma Variables +Import the `figma-variables.json` file into Figma: +1. Open your Figma file +2. Go to Variables panel +3. Import → Select `figma-variables.json` + +## Token Categories + +{{tokenCategories}} diff --git a/src/tools/generate-theme-output.md b/src/tools/generate-theme-output.md new file mode 100644 index 0000000..a34d95a --- /dev/null +++ b/src/tools/generate-theme-output.md @@ -0,0 +1,24 @@ +# {{brandName}} Theme Generated + +## CSS Variables + +```css +{{cssVariables}} +``` + +## Figma Variables + +Save this as `figma-variables.json`: + +```json +{{figmaVariables}} +``` + +## Summary + +- **Total tokens**: {{totalTokens}} +- **Colors**: {{colorTokens}} +- **Typography**: {{typographyTokens}} +- **Spacing**: {{spacingTokens}} + +{{documentation}} diff --git a/src/tools/generate-theme-tool.ts b/src/tools/generate-theme-tool.ts new file mode 100644 index 0000000..53e85bb --- /dev/null +++ b/src/tools/generate-theme-tool.ts @@ -0,0 +1,287 @@ +/** + * Theme generator tool + * Generates complete theme with CSS variables and Figma Variables JSON + * Uses actual Optics tokens, only customizing HSL color base values + */ + +import { z } from 'zod'; +import Tool, { type ToolInputSchema } from './tool.js'; +import { designTokens, type DesignToken } from '../optics-data.js'; +import { generateFigmaVariablesJSON } from '../utils/figma-tokens.js'; +import { readToolFile } from '../_internal/resource-path.js'; + +interface BrandColors { + primary?: string; // Main brand color (hex) - converted to --op-color-primary-h/s/l + neutral?: string; // Neutral color (hex) - converted to --op-color-neutral-h/s/l + 'alerts-warning'?: string; // Warning color (hex) + 'alerts-danger'?: string; // Error color (hex) + 'alerts-info'?: string; // Info color (hex) + 'alerts-notice'?: string; // Success color (hex) +} + +interface GeneratedTheme { + cssVariables: string; + figmaVariables: string; + tokens: DesignToken[]; + documentation: string; +} + +class GenerateThemeTool extends Tool { + name = 'generate_theme' + title = 'Generate Theme' + description = 'Generate a complete theme with CSS variables and Figma Variables JSON using Optics design tokens' + + inputSchema = { + brandName: z + .string() + .describe('The name of the brand/theme (e.g., "Acme Corp")'), + primary: z + .string() + .describe('Primary brand color (hex, e.g., "#FF5733")'), + neutral: z + .string() + .optional() + .describe('Neutral color (hex, optional)') + } + + async handler(args: ToolInputSchema): Promise { + const brandColors: BrandColors = { + primary: args.primary, + neutral: args.neutral + }; + const theme = await this.generateTheme(args.brandName, brandColors); + + // Load markdown template and replace placeholders + let output = await readToolFile('generate-theme-output.md'); + + output = output + .replace('{{brandName}}', args.brandName) + .replace('{{cssVariables}}', theme.cssVariables) + .replace('{{figmaVariables}}', theme.figmaVariables) + .replace('{{totalTokens}}', String(theme.tokens.length)) + .replace('{{colorTokens}}', String(theme.tokens.filter(t => t.category === 'color').length)) + .replace('{{typographyTokens}}', String(theme.tokens.filter(t => t.category === 'typography').length)) + .replace('{{spacingTokens}}', String(theme.tokens.filter(t => t.category === 'spacing').length)) + .replace('{{documentation}}', theme.documentation); + + return output; + } + + /** + * Optics color families that can be themed + * Each accepts a hex color that will be converted to HSL base values + */ + + /** + * Generate Optics HSL color tokens from brand colors + * Each color family gets h/s/l base values that drive the scale system + */ + private generateColorTokens(brandColors: BrandColors): DesignToken[] { + const tokens: DesignToken[] = []; + + // Default Optics colors if not provided + const defaults: BrandColors = { + primary: '#2D6FDB', // Optics default primary + neutral: '#757882', // Optics default neutral + 'alerts-warning': '#FFD93D', + 'alerts-danger': '#FF6B94', + 'alerts-info': '#2D6FDB', + 'alerts-notice': '#6ACF71' + }; + + const colors = { ...defaults, ...brandColors }; + + // Generate HSL base values for each color family + for (const [family, hex] of Object.entries(colors)) { + if (!hex) continue; + + const hsl = this.hexToHSL(hex); + const familyName = family === 'primary' || family === 'neutral' ? family : family; + + tokens.push({ + name: `op-color-${familyName}-h`, + value: String(hsl.h), + category: 'color', + description: `${familyName} color hue (HSL) - drives all ${familyName} scale tokens` + }); + + tokens.push({ + name: `op-color-${familyName}-s`, + value: `${hsl.s}%`, + category: 'color', + description: `${familyName} color saturation (HSL)` + }); + + tokens.push({ + name: `op-color-${familyName}-l`, + value: `${hsl.l}%`, + category: 'color', + description: `${familyName} color lightness (HSL)` + }); + } + + return tokens; + } + + /** + * Convert hex to HSL + */ + private hexToHSL(hex: string): { h: number; s: number; l: number } { + // Remove # if present + hex = hex.replace('#', ''); + + // Convert to RGB first + const r = parseInt(hex.substring(0, 2), 16) / 255; + const g = parseInt(hex.substring(2, 4), 16) / 255; + const b = parseInt(hex.substring(4, 6), 16) / 255; + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + let h = 0; + let s = 0; + const l = (max + min) / 2; + + if (max !== min) { + const d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + + switch (max) { + case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break; + case g: h = ((b - r) / d + 2) / 6; break; + case b: h = ((r - g) / d + 4) / 6; break; + } + } + + return { + h: Math.round(h * 360), + s: Math.round(s * 100), + l: Math.round(l * 100) + }; + } + + /** + * Generate CSS variables from tokens using HSL for colors + */ + private generateCSSVariables(tokens: DesignToken[], themeName: string = 'default'): string { + const lines: string[] = [ + `/* ${themeName} Theme - Generated by Optics MCP */`, + `/* HSL tokens for easy theming */`, + `:root {` + ]; + + // Group tokens by category + const grouped: Record = {}; + tokens.forEach(token => { + if (!grouped[token.category]) grouped[token.category] = []; + grouped[token.category].push(token); + }); + + // Output color tokens - they're already in HSL format + if (grouped['color']) { + lines.push(` /* Colors (HSL) */`); + for (const token of grouped['color']) { + // Tokens are already properly formatted (either HSL base values or full color values) + lines.push(` --${token.name}: ${token.value};`); + } + lines.push(''); + } + + // Output non-color tokens normally + for (const [category, categoryTokens] of Object.entries(grouped)) { + if (category === 'color') continue; // Already handled + + lines.push(` /* ${category.charAt(0).toUpperCase() + category.slice(1)} */`); + for (const token of categoryTokens) { + lines.push(` --${token.name}: ${token.value};`); + } + lines.push(''); + } + + lines.push('}'); + + return lines.join('\n'); + } + + /** + * Generate theme documentation + */ + private async generateDocumentation(themeName: string, tokens: DesignToken[]): Promise { + // Load documentation template + let documentation = await readToolFile('generate-theme-instructions.md'); + + // Generate token summary + const stats = tokens.reduce((acc, token) => { + acc[token.category] = (acc[token.category] || 0) + 1; + return acc; + }, {} as Record); + + const tokenSummary = Object.entries(stats) + .map(([category, count]) => `- **${category}**: ${count} tokens`) + .join('\n'); + + // Generate token categories table + const grouped: Record = {}; + tokens.forEach(token => { + if (!grouped[token.category]) grouped[token.category] = []; + grouped[token.category].push(token); + }); + + const tokenCategories = Object.entries(grouped) + .map(([category, categoryTokens]) => { + const lines = [ + `### ${category.charAt(0).toUpperCase() + category.slice(1)}`, + '', + '| Token Name | Value | Description |', + '|------------|-------|-------------|' + ]; + + for (const token of categoryTokens) { + lines.push(`| \`${token.name}\` | \`${token.value}\` | ${token.description || ''} |`); + } + + return lines.join('\n'); + }) + .join('\n\n'); + + // Replace placeholders + documentation = documentation + .replace('{{themeName}}', themeName) + .replace('{{tokenSummary}}', tokenSummary) + .replace('{{tokenCategories}}', tokenCategories); + + return documentation; + } + + /** + * Main theme generation function + * Uses all standard Optics tokens, only customizing the HSL color base values + */ + private async generateTheme(brandName: string, brandColors: BrandColors): Promise { + // Start with all standard Optics tokens + let tokens: DesignToken[] = [...designTokens]; + + // Override HSL color base values if custom colors provided + if (Object.keys(brandColors).length > 0) { + const customColorTokens = this.generateColorTokens(brandColors); + + // Replace the HSL base tokens with custom ones + tokens = tokens.map(token => { + const customToken = customColorTokens.find(ct => ct.name === token.name); + return customToken || token; + }); + } + + const cssVariables = this.generateCSSVariables(tokens, brandName); + const figmaVariables = generateFigmaVariablesJSON(tokens, { collectionName: `${brandName} Design System` }); + const documentation = await this.generateDocumentation(brandName, tokens); + + return { + cssVariables, + figmaVariables, + tokens, + documentation + }; + } +} + +export default GenerateThemeTool; diff --git a/src/tools/theme-generator.ts b/src/tools/theme-generator.ts deleted file mode 100644 index 6330266..0000000 --- a/src/tools/theme-generator.ts +++ /dev/null @@ -1,387 +0,0 @@ -/** - * Theme generator tool - * Generates complete theme with CSS variables and Figma Variables JSON - * Uses actual Optics tokens, only customizing HSL color base values - */ - -import { DesignToken, designTokens } from '../optics-data.js'; -import { generateFigmaVariablesJSON } from '../utils/figma-tokens.js'; - -/** - * Optics color families that can be themed - * Each accepts a hex color that will be converted to HSL base values - */ -export interface BrandColors { - primary?: string; // Main brand color (hex) - converted to --op-color-primary-h/s/l - neutral?: string; // Neutral/gray color (hex) - converted to --op-color-neutral-h/s/l - 'alerts-warning'?: string; // Warning color (hex) - 'alerts-danger'?: string; // Error color (hex) - 'alerts-info'?: string; // Info color (hex) - 'alerts-notice'?: string; // Success color (hex) -} - -export interface ThemeOptions { - includeDarkMode?: boolean; - includeSemanticColors?: boolean; - includeTypography?: boolean; - includeSpacing?: boolean; - includeBorders?: boolean; - includeShadows?: boolean; -} - -export interface GeneratedTheme { - cssVariables: string; - figmaVariables: string; - tokens: DesignToken[]; - documentation: string; -} - -/** - * Generate spacing tokens using base-8 system - */ -function generateSpacingTokens(): DesignToken[] { - const spacingScale = [ - { name: 'xs', value: '4px' }, - { name: 'sm', value: '8px' }, - { name: 'md', value: '16px' }, - { name: 'lg', value: '24px' }, - { name: 'xl', value: '32px' }, - { name: '2xl', value: '48px' } - ]; - - return spacingScale.map(({ name, value }) => ({ - name: `spacing-${name}`, - value, - category: 'spacing', - description: `Spacing ${name} - ${value}` - })); -} - -/** - * Generate typography tokens - */ -function generateTypographyTokens(): DesignToken[] { - return [ - { - name: 'font-family-base', - value: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', - category: 'typography', - description: 'Base font family for body text' - }, - { - name: 'font-family-heading', - value: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', - category: 'typography', - description: 'Font family for headings' - }, - { - name: 'font-family-mono', - value: '"SF Mono", "Monaco", "Consolas", monospace', - category: 'typography', - description: 'Monospace font family for code' - }, - { name: 'font-size-xs', value: '12px', category: 'typography', description: 'Extra small font size' }, - { name: 'font-size-sm', value: '14px', category: 'typography', description: 'Small font size' }, - { name: 'font-size-md', value: '16px', category: 'typography', description: 'Medium font size (base)' }, - { name: 'font-size-lg', value: '18px', category: 'typography', description: 'Large font size' }, - { name: 'font-size-xl', value: '20px', category: 'typography', description: 'Extra large font size' }, - { name: 'font-size-2xl', value: '24px', category: 'typography', description: '2X large font size' }, - { name: 'font-size-3xl', value: '32px', category: 'typography', description: '3X large font size' }, - { name: 'font-weight-normal', value: '400', category: 'typography', description: 'Normal font weight' }, - { name: 'font-weight-medium', value: '500', category: 'typography', description: 'Medium font weight' }, - { name: 'font-weight-semibold', value: '600', category: 'typography', description: 'Semibold font weight' }, - { name: 'font-weight-bold', value: '700', category: 'typography', description: 'Bold font weight' }, - { name: 'line-height-tight', value: '1.25', category: 'typography', description: 'Tight line height for headings' }, - { name: 'line-height-normal', value: '1.5', category: 'typography', description: 'Normal line height for body text' }, - { name: 'line-height-relaxed', value: '1.75', category: 'typography', description: 'Relaxed line height for long-form content' } - ]; -} - -/** - * Generate border tokens - */ -function generateBorderTokens(): DesignToken[] { - return [ - { name: 'border-radius-sm', value: '4px', category: 'border', description: 'Small border radius' }, - { name: 'border-radius-md', value: '8px', category: 'border', description: 'Medium border radius' }, - { name: 'border-radius-lg', value: '12px', category: 'border', description: 'Large border radius' }, - { name: 'border-radius-full', value: '9999px', category: 'border', description: 'Full border radius for circular elements' } - ]; -} - -/** - * Generate shadow tokens - */ -function generateShadowTokens(): DesignToken[] { - return [ - { name: 'shadow-sm', value: '0 1px 2px 0 rgba(0, 0, 0, 0.05)', category: 'shadow', description: 'Small shadow for subtle elevation' }, - { name: 'shadow-md', value: '0 4px 6px -1px rgba(0, 0, 0, 0.1)', category: 'shadow', description: 'Medium shadow for cards and panels' }, - { name: 'shadow-lg', value: '0 10px 15px -3px rgba(0, 0, 0, 0.1)', category: 'shadow', description: 'Large shadow for modals and popovers' } - ]; -} - -/** - * Generate Optics HSL color tokens from brand colors - * Each color family gets h/s/l base values that drive the scale system - */ -function generateColorTokens(brandColors: BrandColors): DesignToken[] { - const tokens: DesignToken[] = []; - - // Default Optics colors if not provided - const defaults: BrandColors = { - primary: '#2D6FDB', // Optics default primary - neutral: '#757882', // Optics default neutral - 'alerts-warning': '#FFD93D', - 'alerts-danger': '#FF6B94', - 'alerts-info': '#2D6FDB', - 'alerts-notice': '#6ACF71' - }; - - const colors = { ...defaults, ...brandColors }; - - // Generate HSL base values for each color family - for (const [family, hex] of Object.entries(colors)) { - if (!hex) continue; - - const hsl = hexToHSL(hex); - const familyName = family === 'primary' || family === 'neutral' ? family : family; - - tokens.push({ - name: `op-color-${familyName}-h`, - value: String(hsl.h), - category: 'color', - description: `${familyName} color hue (HSL) - drives all ${familyName} scale tokens` - }); - - tokens.push({ - name: `op-color-${familyName}-s`, - value: `${hsl.s}%`, - category: 'color', - description: `${familyName} color saturation (HSL)` - }); - - tokens.push({ - name: `op-color-${familyName}-l`, - value: `${hsl.l}%`, - category: 'color', - description: `${familyName} color lightness (HSL)` - }); - } - - return tokens; -} - -/** - * Convert hex to HSL - */ -function hexToHSL(hex: string): { h: number; s: number; l: number } { - // Remove # if present - hex = hex.replace('#', ''); - - // Convert to RGB first - const r = parseInt(hex.substring(0, 2), 16) / 255; - const g = parseInt(hex.substring(2, 4), 16) / 255; - const b = parseInt(hex.substring(4, 6), 16) / 255; - - const max = Math.max(r, g, b); - const min = Math.min(r, g, b); - let h = 0; - let s = 0; - const l = (max + min) / 2; - - if (max !== min) { - const d = max - min; - s = l > 0.5 ? d / (2 - max - min) : d / (max + min); - - switch (max) { - case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break; - case g: h = ((b - r) / d + 2) / 6; break; - case b: h = ((r - g) / d + 4) / 6; break; - } - } - - return { - h: Math.round(h * 360), - s: Math.round(s * 100), - l: Math.round(l * 100) - }; -} - -/** - * Generate CSS variables from tokens using HSL for colors - */ -function generateCSSVariables(tokens: DesignToken[], themeName: string = 'default'): string { - const lines: string[] = [ - `/* ${themeName} Theme - Generated by Optics MCP */`, - `/* HSL tokens for easy theming */`, - `:root {` - ]; - - // Group tokens by category - const grouped: Record = {}; - tokens.forEach(token => { - if (!grouped[token.category]) grouped[token.category] = []; - grouped[token.category].push(token); - }); - - // Output color tokens - they're already in HSL format - if (grouped['color']) { - lines.push(` /* Colors (HSL) */`); - for (const token of grouped['color']) { - // Tokens are already properly formatted (either HSL base values or full color values) - lines.push(` --${token.name}: ${token.value};`); - } - lines.push(''); - } - - // Output non-color tokens normally - for (const [category, categoryTokens] of Object.entries(grouped)) { - if (category === 'color') continue; // Already handled - - lines.push(` /* ${category.charAt(0).toUpperCase() + category.slice(1)} */`); - for (const token of categoryTokens) { - lines.push(` --${token.name}: ${token.value};`); - } - lines.push(''); - } - - lines.push('}'); - - return lines.join('\n'); -} - -/** - * Generate theme documentation - */ -function generateDocumentation(themeName: string, tokens: DesignToken[]): string { - const stats = tokens.reduce((acc, token) => { - acc[token.category] = (acc[token.category] || 0) + 1; - return acc; - }, {} as Record); - - const lines: string[] = [ - `# ${themeName} Theme`, - '', - '## Overview', - 'This theme was generated by Optics MCP and uses the Optics Design System tokens.', - '', - '## Step 1: Install Optics Design System', - '', - '### Option A: Via npm (Recommended)', - '```bash', - 'npm install @rolemodel/optics', - '```', - '', - 'Then import in your CSS:', - '```css', - '@import "@rolemodel/optics/dist/optics.css";', - '```', - '', - '### Option B: Via CDN (jsDelivr)', - '```html', - '', - '```', - '', - '### Option C: Via unpkg', - '```html', - '', - '```', - '', - '## Step 2: Add Your Custom Theme Overrides', - '', - 'Create a `theme.css` file with the custom HSL color values below and load it AFTER Optics:', - '', - '```html', - '', - '', - '```', - '', - '## Token Summary', - '' - ]; - - for (const [category, count] of Object.entries(stats)) { - lines.push(`- **${category}**: ${count} tokens`); - } - - lines.push(''); - lines.push('## Step 3: Using Optics Components'); - lines.push(''); - lines.push('**IMPORTANT:** Optics has pre-built components with specific HTML structure and CSS classes.'); - lines.push(''); - lines.push('Do NOT make up CSS classes. Use the actual Optics components and their documentation:'); - lines.push(''); - lines.push('**Component Documentation:** https://docs.optics.rolemodel.design'); - lines.push(''); - lines.push('Your custom theme will automatically apply to all Optics components through the HSL base values.'); - lines.push(''); - lines.push('### Example: Using an Optics Button'); - lines.push('Refer to the Optics documentation for the actual button HTML structure and CSS classes.'); - lines.push('Your theme colors will apply automatically through the `--op-color-primary-*` variables.'); - lines.push(''); - lines.push('### Figma Variables'); - lines.push('Import the `figma-variables.json` file into Figma:'); - lines.push('1. Open your Figma file'); - lines.push('2. Go to Variables panel'); - lines.push('3. Import → Select `figma-variables.json`'); - lines.push(''); - lines.push('## Token Categories'); - lines.push(''); - - // Group tokens by category for documentation - const grouped: Record = {}; - tokens.forEach(token => { - if (!grouped[token.category]) grouped[token.category] = []; - grouped[token.category].push(token); - }); - - for (const [category, categoryTokens] of Object.entries(grouped)) { - lines.push(`### ${category.charAt(0).toUpperCase() + category.slice(1)}`); - lines.push(''); - lines.push('| Token Name | Value | Description |'); - lines.push('|------------|-------|-------------|'); - for (const token of categoryTokens) { - lines.push(`| \`${token.name}\` | \`${token.value}\` | ${token.description || ''} |`); - } - lines.push(''); - } - - return lines.join('\n'); -} - -/** - * Main theme generation function - * Uses all standard Optics tokens, only customizing the HSL color base values - */ -export function generateTheme( - brandName: string, - brandColors: BrandColors, - options: ThemeOptions = {} -): GeneratedTheme { - // Start with all standard Optics tokens - let tokens: DesignToken[] = [...designTokens]; - - // Override HSL color base values if custom colors provided - if (Object.keys(brandColors).length > 0) { - const customColorTokens = generateColorTokens(brandColors); - - // Replace the HSL base tokens with custom ones - tokens = tokens.map(token => { - const customToken = customColorTokens.find(ct => ct.name === token.name); - return customToken || token; - }); - } - - const cssVariables = generateCSSVariables(tokens, brandName); - const figmaVariables = generateFigmaVariablesJSON(tokens, { - collectionName: `${brandName} Design System` - }); - const documentation = generateDocumentation(brandName, tokens); - - return { - cssVariables, - figmaVariables, - tokens, - documentation - }; -} From 498ab897a58f18a570fe273e45c7e7b0701545a4 Mon Sep 17 00:00:00 2001 From: dominicmacaulay Date: Thu, 5 Feb 2026 16:13:57 -0500 Subject: [PATCH 3/8] Finish tool class extractions # Conflicts: # src/index.ts # src/tools/replace-hard-coded-values-tool.ts --- src/index.ts | 209 +------------ src/tools/accessibility.ts | 148 --------- src/tools/check-contrast-tool.ts | 148 +++++++++ src/tools/generate-component-scaffold-tool.ts | 237 +++++++++++++++ ...heet.ts => generate-sticker-sheet-tool.ts} | 284 +++++++++--------- src/tools/migration.ts | 149 --------- src/tools/scaffold.ts | 208 ------------- src/tools/suggest-token-migration-tool.ts | 172 +++++++++++ src/tools/validate-token-usage-tool.ts | 142 +++++++++ src/tools/validate.ts | 121 -------- 10 files changed, 858 insertions(+), 960 deletions(-) delete mode 100644 src/tools/accessibility.ts create mode 100644 src/tools/check-contrast-tool.ts create mode 100644 src/tools/generate-component-scaffold-tool.ts rename src/tools/{sticker-sheet.ts => generate-sticker-sheet-tool.ts} (71%) delete mode 100644 src/tools/migration.ts delete mode 100644 src/tools/scaffold.ts create mode 100644 src/tools/suggest-token-migration-tool.ts create mode 100644 src/tools/validate-token-usage-tool.ts delete mode 100644 src/tools/validate.ts diff --git a/src/index.ts b/src/index.ts index d713d6a..623480f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,13 +12,6 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import type { ListPromptsRequestSchema, GetPromptRequestSchema } from "@modelcontextprotocol/sdk/types" import { z } from 'zod'; import { designTokens } from './optics-data.js'; -import { generateTheme } from './tools/theme-generator.js'; -import { validateTokenUsage, formatValidationReport } from './tools/validate.js'; - -import { checkTokenContrast, formatContrastResult } from './tools/accessibility.js'; -import { suggestTokenMigration, formatMigrationSuggestions } from './tools/migration.js'; -import { generateComponentScaffold, formatScaffoldOutput } from './tools/scaffold.js'; -import { generateStickerSheet, formatStickerSheet } from './tools/sticker-sheet.js'; // Resources import * as systemOverview from './resources/system-overview.js'; @@ -43,7 +36,13 @@ import ListComponentsTool from './tools/list-components-tool.js' import GetComponentInfoTool from './tools/get-component-info-tool.js'; import GetComponentTokensTool from './tools/get-component-tokens-tool.js'; import SearchDocumentationTool from './tools/search-documentation-tool.js'; +import GenerateThemeTool from './tools/generate-theme-tool.js'; +import ValidateTokenUsageTool from './tools/validate-token-usage-tool.js'; import ReplaceHardCodedValuesTool from './tools/replace-hard-coded-values-tool.js'; +import CheckContrastTool from './tools/check-contrast-tool.js'; +import SuggestTokenMigrationTool from './tools/suggest-token-migration-tool.js'; +import GenerateComponentScaffoldTool from './tools/generate-component-scaffold-tool.js'; +import GenerateStickerSheetTool from './tools/generate-sticker-sheet-tool.js'; /** * Create and configure the MCP server @@ -189,7 +188,13 @@ const tools = [ new GetComponentInfoTool(), new GetComponentTokensTool(), new SearchDocumentationTool(), - new ReplaceHardCodedValuesTool() + new GenerateThemeTool(), + new ValidateTokenUsageTool(), + new ReplaceHardCodedValuesTool(), + new CheckContrastTool(), + new SuggestTokenMigrationTool(), + new GenerateComponentScaffoldTool(), + new GenerateStickerSheetTool() ] tools.forEach((tool) => { @@ -215,194 +220,6 @@ tools.forEach((tool) => { ) }) -/** - * Tool: Generate Theme - */ -server.registerTool( - 'generate_theme', - { - title: 'Generate Theme', - description: 'Generate a custom Optics theme with CSS variable overrides', - inputSchema: { - brandName: z.string().describe('Name of the brand/theme (e.g., "Acme Corp")'), - primary: z.string().describe('Primary brand color (hex, e.g., "#0066CC")'), - secondary: z.string().optional().describe('Secondary color (hex, optional)'), - }, - }, - async ({ brandName, primary, secondary }) => { - const brandColors = { - primary, - secondary, - }; - - const theme = generateTheme(brandName, brandColors); - - return { - content: [ - { - type: 'text', - text: `# ${brandName} Theme Generated\n\n## CSS Variables\n\n\`\`\`css\n${theme.cssVariables}\n\`\`\`\n\n## Figma Variables\n\nSave this as \`figma-variables.json\`:\n\n\`\`\`json\n${theme.figmaVariables}\n\`\`\`\n\n## Summary\n\n- **Total tokens**: ${theme.tokens.length}\n- **Colors**: ${theme.tokens.filter(t => t.category === 'color').length}\n- **Typography**: ${theme.tokens.filter(t => t.category === 'typography').length}\n- **Spacing**: ${theme.tokens.filter(t => t.category === 'spacing').length}\n\n${theme.documentation}`, - }, - ], - }; - } -); - -/** - * Tool: Validate Token Usage - */ -server.registerTool( - 'validate_token_usage', - { - title: 'Validate Token Usage', - description: 'Validate code for hard-coded values that should use design tokens', - inputSchema: { - code: z.string().describe('CSS or component code to validate'), - }, - }, - async ({ code }) => { - const report = validateTokenUsage(code, designTokens); - const formatted = formatValidationReport(report); - - return { - content: [ - { - type: 'text', - text: formatted, - }, - ], - }; - } -); - - - -/** - * Tool: Check Contrast - */ -server.registerTool( - 'check_contrast', - { - title: 'Check Contrast', - description: 'Check WCAG contrast ratio between two color tokens', - inputSchema: { - foregroundToken: z.string().describe('Foreground color token name'), - backgroundToken: z.string().describe('Background color token name'), - }, - }, - async ({ foregroundToken, backgroundToken }) => { - const result = checkTokenContrast(foregroundToken, backgroundToken, designTokens); - const formatted = formatContrastResult(result); - - return { - content: [ - { - type: 'text', - text: formatted, - }, - ], - }; - } -); - -/** - * Tool: Suggest Token Migration - */ -server.registerTool( - 'suggest_token_migration', - { - title: 'Suggest Token Migration', - description: 'Suggest design tokens for a hard-coded value', - inputSchema: { - value: z.string().describe('Hard-coded value to find tokens for (e.g., "#0066CC", "16px")'), - category: z.string().optional().describe('Optional category filter (color, spacing, typography)'), - }, - }, - async ({ value, category }) => { - const suggestion = suggestTokenMigration(value, designTokens, category); - const formatted = formatMigrationSuggestions(suggestion); - - return { - content: [ - { - type: 'text', - text: formatted, - }, - ], - }; - } -); - -/** - * Tool: Generate Component Scaffold - */ -server.registerTool( - 'generate_component_scaffold', - { - title: 'Generate Component Scaffold', - description: 'Generate a React component scaffold with proper token usage', - inputSchema: { - componentName: z.string().describe('Name of the component (e.g., "Alert", "Card")'), - description: z.string().describe('Brief description of the component'), - tokens: z.array(z.string()).describe('List of token names the component should use'), - }, - }, - async ({ componentName, description, tokens }) => { - const scaffold = generateComponentScaffold( - componentName, - description, - tokens, - designTokens - ); - const formatted = formatScaffoldOutput(scaffold); - - return { - content: [ - { - type: 'text', - text: formatted, - }, - ], - }; - } -); - -/** - * Tool: Generate Sticker Sheet - */ -server.registerTool( - 'generate_sticker_sheet', - { - title: 'Generate Sticker Sheet', - description: 'Generate a visual style guide with color swatches and component examples', - inputSchema: { - framework: z.enum(['react', 'vue', 'svelte', 'html']).optional().describe('Target framework (default: react)'), - includeColors: z.boolean().optional().describe('Include color swatches (default: true)'), - includeTypography: z.boolean().optional().describe('Include typography specimens (default: true)'), - includeComponents: z.boolean().optional().describe('Include component examples (default: true)'), - }, - }, - async ({ framework, includeColors, includeTypography, includeComponents }) => { - const options = { - framework: framework ?? 'react', - includeColors: includeColors ?? true, - includeTypography: includeTypography ?? true, - includeComponents: includeComponents ?? true, - }; - const sheet = generateStickerSheet(designTokens, components, options); - const formatted = formatStickerSheet(sheet); - - return { - content: [ - { - type: 'text', - text: formatted, - }, - ], - }; - } -); - /** * Start the server */ diff --git a/src/tools/accessibility.ts b/src/tools/accessibility.ts deleted file mode 100644 index 5a44602..0000000 --- a/src/tools/accessibility.ts +++ /dev/null @@ -1,148 +0,0 @@ -/** - * Accessibility checker tool - * Validates WCAG contrast ratios for token combinations - */ - -import { DesignToken } from '../optics-data.js'; -import { checkContrast, ContrastResult } from '../utils/color.js'; - -export interface ContrastCheckResult { - foregroundToken: string; - backgroundToken: string; - foregroundValue: string; - backgroundValue: string; - contrast: ContrastResult | null; - passes: boolean; - recommendation?: string; -} - -/** - * Check contrast between two tokens - */ -export function checkTokenContrast( - foregroundToken: string, - backgroundToken: string, - tokens: DesignToken[] -): ContrastCheckResult { - const fgToken = tokens.find(t => t.name === foregroundToken); - const bgToken = tokens.find(t => t.name === backgroundToken); - - if (!fgToken || !bgToken) { - return { - foregroundToken, - backgroundToken, - foregroundValue: '', - backgroundValue: '', - contrast: null, - passes: false, - recommendation: 'Token not found' - }; - } - - const contrast = checkContrast(fgToken.value, bgToken.value); - - if (!contrast) { - return { - foregroundToken, - backgroundToken, - foregroundValue: fgToken.value, - backgroundValue: bgToken.value, - contrast: null, - passes: false, - recommendation: 'Unable to calculate contrast (non-color tokens?)' - }; - } - - const passes = contrast.wcagAA; - let recommendation = ''; - - if (!passes) { - recommendation = findBetterTokenCombination(fgToken, tokens, bgToken.value); - } - - return { - foregroundToken, - backgroundToken, - foregroundValue: fgToken.value, - backgroundValue: bgToken.value, - contrast, - passes, - recommendation - }; -} - -/** - * Find better token combination with sufficient contrast - */ -function findBetterTokenCombination( - currentToken: DesignToken, - allTokens: DesignToken[], - backgroundValue: string -): string { - const colorTokens = allTokens.filter(t => t.category === 'color'); - - for (const token of colorTokens) { - const contrast = checkContrast(token.value, backgroundValue); - if (contrast && contrast.wcagAA) { - return `Try using ${token.name} (${token.value}) for better contrast`; - } - } - - return 'No alternative tokens found with sufficient contrast'; -} - -/** - * Format contrast check result - */ -export function formatContrastResult(result: ContrastCheckResult): string { - const lines: string[] = [ - '# Contrast Check Result', - '', - `**Foreground**: ${result.foregroundToken} (\`${result.foregroundValue}\`)`, - `**Background**: ${result.backgroundToken} (\`${result.backgroundValue}\`)`, - '' - ]; - - if (result.contrast) { - lines.push(`**Contrast Ratio**: ${result.contrast.ratio}:1`); - lines.push(`**WCAG AA**: ${result.contrast.wcagAA ? '✓ Pass' : '✗ Fail'}`); - lines.push(`**WCAG AAA**: ${result.contrast.wcagAAA ? '✓ Pass' : '✗ Fail'}`); - lines.push(`**Score**: ${result.contrast.score}`); - lines.push(''); - - if (!result.passes && result.recommendation) { - lines.push('## Recommendation'); - lines.push(result.recommendation); - } - } else { - lines.push('✗ Unable to calculate contrast'); - if (result.recommendation) { - lines.push(`**Reason**: ${result.recommendation}`); - } - } - - return lines.join('\n'); -} - -/** - * Check all color token combinations for a given background - */ -export function checkAllCombinations( - backgroundToken: string, - tokens: DesignToken[] -): ContrastCheckResult[] { - const bgToken = tokens.find(t => t.name === backgroundToken); - if (!bgToken) return []; - - const colorTokens = tokens.filter(t => t.category === 'color' && t.name !== backgroundToken); - const results: ContrastCheckResult[] = []; - - for (const fgToken of colorTokens) { - results.push(checkTokenContrast(fgToken.name, backgroundToken, tokens)); - } - - return results.sort((a, b) => { - if (!a.contrast || !b.contrast) return 0; - return b.contrast.ratio - a.contrast.ratio; - }); -} diff --git a/src/tools/check-contrast-tool.ts b/src/tools/check-contrast-tool.ts new file mode 100644 index 0000000..13c7b18 --- /dev/null +++ b/src/tools/check-contrast-tool.ts @@ -0,0 +1,148 @@ +/** + * Check Contrast Tool + * Validates WCAG contrast ratios for token combinations + */ + +import { z } from 'zod'; +import Tool from './tool.js'; +import { DesignToken, designTokens } from '../optics-data.js'; +import { checkContrast, ContrastResult } from '../utils/color.js'; + +export interface ContrastCheckResult { + foregroundToken: string; + backgroundToken: string; + foregroundValue: string; + backgroundValue: string; + contrast: ContrastResult | null; + passes: boolean; + recommendation?: string; +} + +class CheckContrastTool extends Tool { + name = 'check_contrast'; + title = 'Check Contrast'; + description = 'Check WCAG contrast ratio between two color tokens'; + + inputSchema = { + foregroundToken: z.string().describe('Foreground color token name'), + backgroundToken: z.string().describe('Background color token name'), + }; + + async handler(args: any): Promise { + const { foregroundToken, backgroundToken } = args; + const result = this.checkTokenContrast(foregroundToken, backgroundToken, designTokens); + const formatted = this.formatContrastResult(result); + + return formatted; + } + + /** + * Check contrast between two tokens + */ + private checkTokenContrast( + foregroundToken: string, + backgroundToken: string, + tokens: DesignToken[] + ): ContrastCheckResult { + const fgToken = tokens.find(t => t.name === foregroundToken); + const bgToken = tokens.find(t => t.name === backgroundToken); + + if (!fgToken || !bgToken) { + return { + foregroundToken, + backgroundToken, + foregroundValue: '', + backgroundValue: '', + contrast: null, + passes: false, + recommendation: 'Token not found' + }; + } + + const contrast = checkContrast(fgToken.value, bgToken.value); + + if (!contrast) { + return { + foregroundToken, + backgroundToken, + foregroundValue: fgToken.value, + backgroundValue: bgToken.value, + contrast: null, + passes: false, + recommendation: 'Unable to calculate contrast (non-color tokens?)' + }; + } + + const passes = contrast.wcagAA; + let recommendation = ''; + + if (!passes) { + recommendation = this.findBetterTokenCombination(fgToken, tokens, bgToken.value); + } + + return { + foregroundToken, + backgroundToken, + foregroundValue: fgToken.value, + backgroundValue: bgToken.value, + contrast, + passes, + recommendation + }; + } + + /** + * Find better token combination with sufficient contrast + */ + private findBetterTokenCombination( + currentToken: DesignToken, + allTokens: DesignToken[], + backgroundValue: string + ): string { + const colorTokens = allTokens.filter(t => t.category === 'color'); + + for (const token of colorTokens) { + const contrast = checkContrast(token.value, backgroundValue); + if (contrast && contrast.wcagAA) { + return `Try using ${token.name} (${token.value}) for better contrast`; + } + } + + return 'No alternative tokens found with sufficient contrast'; + } + + /** + * Format contrast check result + */ + private formatContrastResult(result: ContrastCheckResult): string { + const lines: string[] = [ + '# Contrast Check Result', + '', + `**Foreground**: ${result.foregroundToken} (\`${result.foregroundValue}\`)`, + `**Background**: ${result.backgroundToken} (\`${result.backgroundValue}\`)`, + '' + ]; + + if (result.contrast) { + lines.push(`**Contrast Ratio**: ${result.contrast.ratio}:1`); + lines.push(`**WCAG AA**: ${result.contrast.wcagAA ? '✓ Pass' : '✗ Fail'}`); + lines.push(`**WCAG AAA**: ${result.contrast.wcagAAA ? '✓ Pass' : '✗ Fail'}`); + lines.push(`**Score**: ${result.contrast.score}`); + lines.push(''); + + if (!result.passes && result.recommendation) { + lines.push('## Recommendation'); + lines.push(result.recommendation); + } + } else { + lines.push('✗ Unable to calculate contrast'); + if (result.recommendation) { + lines.push(`**Reason**: ${result.recommendation}`); + } + } + + return lines.join('\n'); + } +} + +export default CheckContrastTool; diff --git a/src/tools/generate-component-scaffold-tool.ts b/src/tools/generate-component-scaffold-tool.ts new file mode 100644 index 0000000..f32eb8a --- /dev/null +++ b/src/tools/generate-component-scaffold-tool.ts @@ -0,0 +1,237 @@ +/** + * Generate Component Scaffold Tool + * Generates component templates with proper token usage + */ + +import { z } from 'zod'; +import Tool from './tool.js'; +import { DesignToken, designTokens } from '../optics-data.js'; + +export interface ComponentScaffold { + name: string; + typescript: string; + css: string; + usage: string; +} + +class GenerateComponentScaffoldTool extends Tool { + name = 'generate_component_scaffold'; + title = 'Generate Component Scaffold'; + description = 'Generate a React component scaffold with proper token usage'; + + inputSchema = { + componentName: z.string().describe('Name of the component (e.g., "Alert", "Card")'), + description: z.string().describe('Brief description of the component'), + tokens: z.array(z.string()).describe('List of token names the component should use'), + }; + + async handler(args: any): Promise { + const { componentName, description, tokens } = args; + const scaffold = this.generateComponentScaffold( + componentName, + description, + tokens, + designTokens + ); + const formatted = this.formatScaffoldOutput(scaffold); + + return formatted; + } + + /** + * Generate component scaffold + */ + private generateComponentScaffold( + componentName: string, + description: string, + requiredTokens: string[], + allTokens: DesignToken[] + ): ComponentScaffold { + const validTokens = requiredTokens.filter(tokenName => + allTokens.some(t => t.name === tokenName) + ); + + const typescript = this.generateTypeScriptComponent(componentName, description, validTokens); + const css = this.generateCSSModule(componentName, validTokens, allTokens); + const usage = this.generateUsageExample(componentName); + + return { + name: componentName, + typescript, + css, + usage + }; + } + + /** + * Generate TypeScript component + */ + private generateTypeScriptComponent( + name: string, + description: string, + tokens: string[] + ): string { + const lines: string[] = [ + `/**`, + ` * ${name} Component`, + ` * ${description}`, + ` * `, + ` * Design tokens used:`, + ...tokens.map(t => ` * - ${t}`), + ` */`, + ``, + `import React from 'react';`, + `import styles from './${name}.module.css';`, + ``, + `export interface ${name}Props {`, + ` children: React.ReactNode;`, + ` className?: string;`, + `}`, + ``, + `export const ${name}: React.FC<${name}Props> = ({ children, className }) => {`, + ` return (`, + `
`, + ` {children}`, + `
`, + ` );`, + `};` + ]; + + return lines.join('\n'); + } + + /** + * Generate CSS module + */ + private generateCSSModule( + name: string, + tokenNames: string[], + allTokens: DesignToken[] + ): string { + const lines: string[] = [ + `/**`, + ` * ${name} Component Styles`, + ` * Uses Optics design tokens for consistent styling`, + ` */`, + ``, + `.${name.toLowerCase()} {` + ]; + + // Group tokens by category + const colorTokens = tokenNames.filter(t => allTokens.find(token => token.name === t && token.category === 'color')); + const spacingTokens = tokenNames.filter(t => allTokens.find(token => token.name === t && token.category === 'spacing')); + const typographyTokens = tokenNames.filter(t => allTokens.find(token => token.name === t && token.category === 'typography')); + const borderTokens = tokenNames.filter(t => allTokens.find(token => token.name === t && token.category === 'border')); + const shadowTokens = tokenNames.filter(t => allTokens.find(token => token.name === t && token.category === 'shadow')); + + // Add color properties + if (colorTokens.length > 0) { + lines.push(` /* Colors */`); + if (colorTokens.some(t => t.includes('background'))) { + const bgToken = colorTokens.find(t => t.includes('background')); + lines.push(` background-color: var(--${bgToken});`); + } + if (colorTokens.some(t => t.includes('text') || t.includes('color-primary'))) { + const textToken = colorTokens.find(t => t.includes('text')) || colorTokens[0]; + lines.push(` color: var(--${textToken});`); + } + } + + // Add spacing + if (spacingTokens.length > 0) { + lines.push(` /* Spacing */`); + const paddingToken = spacingTokens[0]; + lines.push(` padding: var(--${paddingToken});`); + } + + // Add typography + if (typographyTokens.length > 0) { + lines.push(` /* Typography */`); + typographyTokens.forEach(token => { + if (token.includes('font-size')) { + lines.push(` font-size: var(--${token});`); + } else if (token.includes('font-weight')) { + lines.push(` font-weight: var(--${token});`); + } else if (token.includes('line-height')) { + lines.push(` line-height: var(--${token});`); + } else if (token.includes('font-family')) { + lines.push(` font-family: var(--${token});`); + } + }); + } + + // Add borders + if (borderTokens.length > 0) { + lines.push(` /* Borders */`); + lines.push(` border-radius: var(--${borderTokens[0]});`); + } + + // Add shadows + if (shadowTokens.length > 0) { + lines.push(` /* Elevation */`); + lines.push(` box-shadow: var(--${shadowTokens[0]});`); + } + + lines.push(`}`); + lines.push(``); + + return lines.join('\n'); + } + + /** + * Generate usage example + */ + private generateUsageExample(name: string): string { + const lines: string[] = [ + `# ${name} Usage`, + ``, + `## Import`, + `\`\`\`typescript`, + `import { ${name} } from './components/${name}';`, + `\`\`\``, + ``, + `## Basic Usage`, + `\`\`\`tsx`, + `<${name}>`, + ` Your content here`, + ``, + `\`\`\``, + ``, + `## With Custom ClassName`, + `\`\`\`tsx`, + `<${name} className="custom-class">`, + ` Your content here`, + ``, + `\`\`\``, + `` + ]; + + return lines.join('\n'); + } + + /** + * Format scaffold output + */ + private formatScaffoldOutput(scaffold: ComponentScaffold): string { + const lines: string[] = [ + `# ${scaffold.name} Component Scaffold`, + ``, + `## TypeScript Component`, + `\`\`\`typescript`, + scaffold.typescript, + `\`\`\``, + ``, + `## CSS Module`, + `\`\`\`css`, + scaffold.css, + `\`\`\``, + ``, + `## Usage`, + scaffold.usage + ]; + + return lines.join('\n'); + } +} + +export default GenerateComponentScaffoldTool; diff --git a/src/tools/sticker-sheet.ts b/src/tools/generate-sticker-sheet-tool.ts similarity index 71% rename from src/tools/sticker-sheet.ts rename to src/tools/generate-sticker-sheet-tool.ts index 7f1e3c5..2b94450 100644 --- a/src/tools/sticker-sheet.ts +++ b/src/tools/generate-sticker-sheet-tool.ts @@ -1,9 +1,11 @@ /** - * Sticker sheet generator + * Generate Sticker Sheet Tool * Generates visual style guide with color swatches and component examples */ -import { DesignToken, Component } from '../optics-data.js'; +import { z } from 'zod'; +import Tool from './tool.js'; +import { DesignToken, Component, designTokens, components } from '../optics-data.js'; export type FrameworkType = 'react' | 'vue' | 'svelte' | 'html'; @@ -11,7 +13,6 @@ export interface StickerSheetOptions { framework?: FrameworkType; includeColors?: boolean; includeTypography?: boolean; - includeSpacing?: boolean; includeComponents?: boolean; } @@ -22,32 +23,58 @@ export interface StickerSheet { instructions: string; } -/** - * Generate color swatch component - */ -function generateColorSwatches(tokens: DesignToken[], framework: FrameworkType): string { - const colors = tokens.filter(t => t.category === 'color'); - - const swatchesData = colors.map(token => ({ - name: token.name, - value: token.value, - hsl: token.name.startsWith('color-') ? `var(--op-${token.name.replace('color-', '')}-h) var(--op-${token.name.replace('color-', '')}-s) var(--op-${token.name.replace('color-', '')}-l)` : token.value - })); - - switch (framework) { - case 'react': - return ` +class GenerateStickerSheetTool extends Tool { + name = 'generate_sticker_sheet'; + title = 'Generate Sticker Sheet'; + description = 'Generate a visual style guide with color swatches and component examples'; + + inputSchema = { + framework: z.enum(['react', 'vue', 'svelte', 'html']).optional().describe('Target framework (default: react)'), + includeColors: z.boolean().optional().describe('Include color swatches (default: true)'), + includeTypography: z.boolean().optional().describe('Include typography specimens (default: true)'), + includeComponents: z.boolean().optional().describe('Include component examples (default: true)'), + }; + + async handler(args: any): Promise { + const { framework, includeColors, includeTypography, includeComponents } = args; + const options = { + framework: framework ?? 'react', + includeColors: includeColors ?? true, + includeTypography: includeTypography ?? true, + includeComponents: includeComponents ?? true, + }; + const sheet = this.generateStickerSheet(designTokens, components, options); + const formatted = this.formatStickerSheet(sheet); + + return formatted; + } + + /** + * Generate color swatch component + */ + private generateColorSwatches(tokens: DesignToken[], framework: FrameworkType): string { + const colors = tokens.filter(t => t.category === 'color'); + + const swatchesData = colors.map(token => ({ + name: token.name, + value: token.value, + hsl: token.name.startsWith('color-') ? `var(--op-${token.name.replace('color-', '')}-h) var(--op-${token.name.replace('color-', '')}-s) var(--op-${token.name.replace('color-', '')}-l)` : token.value + })); + + switch (framework) { + case 'react': + return ` export function ColorSwatches() { const colors = ${JSON.stringify(swatchesData, null, 2)}; - + return (

Color Palette

{colors.map(color => (
-
@@ -61,15 +88,15 @@ export function ColorSwatches() { ); }`; - case 'vue': - return ` + case 'vue': + return `