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 } 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-error.md b/src/tools/check-contrast-error.md new file mode 100644 index 0000000..fbaecf2 --- /dev/null +++ b/src/tools/check-contrast-error.md @@ -0,0 +1,7 @@ +# Contrast Check Result + +**Foreground**: {{foregroundToken}} (`{{foregroundValue}}`) +**Background**: {{backgroundToken}} (`{{backgroundValue}}`) + +✗ Unable to calculate contrast +**Reason**: {{reason}} diff --git a/src/tools/check-contrast-result.md b/src/tools/check-contrast-result.md new file mode 100644 index 0000000..06f3fab --- /dev/null +++ b/src/tools/check-contrast-result.md @@ -0,0 +1,9 @@ +# Contrast Check Result + +**Foreground**: {{foregroundToken}} (`{{foregroundValue}}`) +**Background**: {{backgroundToken}} (`{{backgroundValue}}`) + +**Contrast Ratio**: {{contrastRatio}}:1 +**WCAG AA**: {{wcagAA}} +**WCAG AAA**: {{wcagAAA}} +**Score**: {{score}} diff --git a/src/tools/check-contrast-tool.ts b/src/tools/check-contrast-tool.ts new file mode 100644 index 0000000..40f9db2 --- /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, { type ToolInputSchema } from './tool.js'; +import { designTokens, type DesignToken } from '../optics-data.js'; +import { checkContrast, type ContrastResult } from '../utils/color.js'; +import { readToolFile } from '../_internal/resource-path.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: ToolInputSchema): Promise { + const { foregroundToken, backgroundToken } = args; + const result = this.checkTokenContrast(foregroundToken, backgroundToken, designTokens); + const formatted = await 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 async formatContrastResult(result: ContrastCheckResult): Promise { + if (result.contrast) { + const template = await readToolFile('check-contrast-result.md'); + let output = template + .replace('{{foregroundToken}}', result.foregroundToken) + .replace('{{foregroundValue}}', result.foregroundValue) + .replace('{{backgroundToken}}', result.backgroundToken) + .replace('{{backgroundValue}}', result.backgroundValue) + .replace('{{contrastRatio}}', result.contrast.ratio.toString()) + .replace('{{wcagAA}}', result.contrast.wcagAA ? '✓ Pass' : '✗ Fail') + .replace('{{wcagAAA}}', result.contrast.wcagAAA ? '✓ Pass' : '✗ Fail') + .replace('{{score}}', result.contrast.score); + + if (!result.passes && result.recommendation) { + output += '\n\n## Recommendation\n' + result.recommendation; + } + + return output; + } else { + const template = await readToolFile('check-contrast-error.md'); + return template + .replace('{{foregroundToken}}', result.foregroundToken) + .replace('{{foregroundValue}}', result.foregroundValue) + .replace('{{backgroundToken}}', result.backgroundToken) + .replace('{{backgroundValue}}', result.backgroundValue) + .replace('{{reason}}', result.recommendation || 'Unknown error'); + } + } +} + +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..2aae030 --- /dev/null +++ b/src/tools/generate-component-scaffold-tool.ts @@ -0,0 +1,216 @@ +/** + * Generate Component Scaffold Tool + * Generates component templates with proper token usage + */ + +import { z } from 'zod'; +import Tool, { type ToolInputSchema } from './tool.js'; +import { designTokens, type DesignToken } from '../optics-data.js'; +import { readToolFile } from '../_internal/resource-path.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: ToolInputSchema): Promise { + const { componentName, description, tokens } = args; + const scaffold = await this.generateComponentScaffold( + componentName, + description, + tokens, + designTokens + ); + const formatted = this.formatScaffoldOutput(scaffold); + + return formatted; + } + + /** + * Generate component scaffold + */ + private async generateComponentScaffold( + componentName: string, + description: string, + requiredTokens: string[], + allTokens: DesignToken[] + ): Promise { + 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 = await 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 async generateUsageExample(name: string): Promise { + const template = await readToolFile('generate-component-scaffold-usage.md'); + + return template.replace(/{{componentName}}/g, name); + } + + /** + * 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/generate-component-scaffold-usage.md b/src/tools/generate-component-scaffold-usage.md new file mode 100644 index 0000000..8c8abec --- /dev/null +++ b/src/tools/generate-component-scaffold-usage.md @@ -0,0 +1,23 @@ +# {{componentName}} Usage + +## Import + +```typescript +import { {{componentName}} } from './components/{{componentName}}'; +``` + +## Basic Usage + +```tsx +<{{componentName}}> + Your content here + +``` + +## With Custom ClassName + +```tsx +<{{componentName}} className="custom-class"> + Your content here + +``` diff --git a/src/tools/generate-sticker-sheet-instructions.md b/src/tools/generate-sticker-sheet-instructions.md new file mode 100644 index 0000000..f3dfd40 --- /dev/null +++ b/src/tools/generate-sticker-sheet-instructions.md @@ -0,0 +1,22 @@ +# Optics Sticker Sheet - {{framework}} + +This sticker sheet provides a visual reference for all Optics design tokens and components. + +## Usage + +1. Copy the component code below into your {{framework}} project +2. Copy the CSS styles into your stylesheet +3. Import and use the components in your app +4. Replace placeholders with actual component implementations + +## Files Generated + +- **Component Code**: Ready-to-use {{framework}} components +- **Styles**: CSS using Optics design tokens +- **Examples**: Visual specimens of colors, typography, and components + +## Next Steps + +- Customize the examples to match your specific components +- Add interactive states (hover, focus, disabled) +- Include additional token categories as needed diff --git a/src/tools/sticker-sheet.ts b/src/tools/generate-sticker-sheet-tool.ts similarity index 67% rename from src/tools/sticker-sheet.ts rename to src/tools/generate-sticker-sheet-tool.ts index 7f1e3c5..723b67c 100644 --- a/src/tools/sticker-sheet.ts +++ b/src/tools/generate-sticker-sheet-tool.ts @@ -1,9 +1,12 @@ /** - * 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, { type ToolInputSchema } from './tool.js'; +import { designTokens, components, type DesignToken, type Component } from '../optics-data.js'; +import { readToolFile } from '../_internal/resource-path.js'; export type FrameworkType = 'react' | 'vue' | 'svelte' | 'html'; @@ -11,7 +14,6 @@ export interface StickerSheetOptions { framework?: FrameworkType; includeColors?: boolean; includeTypography?: boolean; - includeSpacing?: boolean; includeComponents?: boolean; } @@ -22,32 +24,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: ToolInputSchema): Promise { + const { framework, includeColors, includeTypography, includeComponents } = args; + const options = { + framework: framework ?? 'react', + includeColors: includeColors ?? true, + includeTypography: includeTypography ?? true, + includeComponents: includeComponents ?? true, + }; + const sheet = await 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 +89,15 @@ export function ColorSwatches() { ); }`; - case 'vue': - return ` + case 'vue': + return `