diff --git a/mdx2md/.eslintrc.json b/mdx2md/.eslintrc.json new file mode 100644 index 0000000000..199afb7e2d --- /dev/null +++ b/mdx2md/.eslintrc.json @@ -0,0 +1,8 @@ +{ + "env": { + "node": true + }, + "rules": { + "compat/compat": "off" + } +} diff --git a/mdx2md/README.md b/mdx2md/README.md new file mode 100644 index 0000000000..034ba1674b --- /dev/null +++ b/mdx2md/README.md @@ -0,0 +1,151 @@ +# MDX to Markdown Converter (mdx2md) + +## Overview + +`mdx2md` is a utility that converts MDX (Markdown with JSX) files from Canvas documentation to standard Markdown format. This tool helps migrate documentation from a React-based MDX system to a more portable Markdown format while preserving as much of the original content structure and component documentation as possible. + +## Core Functionality + +The script performs several key operations: + +1. **MDX File Processing**: Scans directories recursively to find all MDX files and processes them into Markdown. +2. **Component Transformation**: Converts React components used in MDX to equivalent Markdown syntax. +3. **API Documentation Generation**: Extracts and formats component API documentation from a structured data source. +4. **Image Handling**: Copies and updates image references to maintain proper paths in the output. +5. **Code Example Inclusion**: Finds and includes referenced code examples from example files. +6. **Frontmatter Preservation**: Maintains YAML frontmatter with any necessary adjustments. +7. **Cross-referencing Resolution**: Resolves component cross-references and imports. +8. **LLMs Integration**: Generates a reference file (llms.txt) for AI tools and large language models. + +## Architecture + +The converter is organized into several specialized modules: + +### Core Modules +- **cli.js**: Handles command-line argument parsing +- **config.js**: Manages configuration settings +- **fileProcessor.js**: Implements the main file processing pipeline +- **fileUtils.js**: Provides file system operations and utilities +- **genLlmsTxt.js**: Generates the LLMs reference file +- **index.js**: Serves as the main entry point and orchestration +- **mdxParser.js**: Handles MDX parsing and transformation + +### Component Processors (in `/processors`) +- **apiComponents.js**: Processes API documentation components +- **basicComponents.js**: Handles common layout components +- **codeComponents.js**: Processes code examples and snippets +- **imageProcessor.js**: Manages image processing +- **tokenComponents.js**: Handles design token components like color grids and brand tokens + +## Component Processors + +The converter handles various React components including: + +- `PackageInfo` → Markdown tables +- `CKDocs` → Included documentation +- `InternalContent` → Conditionally included sections +- `TabPanel` → Section headers and content +- `SymbolDoc` → API documentation tables +- `ExampleCodeBlock` → Code blocks with actual code +- `Specifications` → Component specification info +- `LegacyPatternLink` → Standard Markdown links +- `Suggestion` → Formatted suggestion blocks +- `SideBySide` → Comparison sections +- `BrandTokens` → Brand token documentation +- `ColorGrid` → Color system documentation + +## Workflow + +When executed, the script follows this flow: + +1. **Initialization**: Parses command-line arguments and loads configuration +2. **Document Loading**: Loads component documentation data from the design system +3. **File Discovery**: Recursively finds all MDX files in the input directory +4. **Batch Processing**: Processes files in batches to manage memory usage +5. **For Each File**: + - Reads MDX content + - Processes frontmatter + - Handles imports and component references + - Transforms components to Markdown + - Generates API documentation + - Writes the resulting Markdown to the output directory +6. **LLMs File Generation**: Creates a reference file for AI tools +7. **Completion**: Reports processing statistics and completion status + +## Key Features + +- **Component Documentation**: Generates comprehensive API tables from component metadata +- **Code Examples**: Includes actual code for example components +- **Content Filtering**: Option to exclude internal documentation +- **Structure Preservation**: Maintains the original directory structure +- **Graceful Degradation**: Falls back to placeholder content when referenced elements can't be found +- **Memory Management**: Processes large documentation sets in batches to avoid memory issues +- **Cross-References**: Resolves relative and package imports +- **Image Handling**: Batched image copying for performance optimization +- **Type Formatting**: Sophisticated formatting of complex TypeScript types +- **Default Directories**: Uses sensible defaults if no directories are specified +- **Debug Mode**: Provides detailed logging with the `--debug` flag + +## Usage + +The script is executed from the command line with the following syntax: + +``` +node index.js [input-dir] [output-dir] [options] +``` + +Parameters: +- `input-dir`: The root directory containing MDX files to convert (defaults to './content') +- `output-dir`: The destination directory for generated Markdown files (defaults to './public/markdown') + +Options: +- `--include-internal`: Include content marked as internal +- `--base-url=URL`: Specify a base URL for documentation links (defaults to canvas.workdaydesign.com) +- `--debug`: Enable detailed debug output +- `--help` or `-h`: Display usage information + +Examples: +```bash +# Use default directories +node index.js + +# Specify custom directories +node index.js ./docs ./converted-docs + +# Include internal content with custom base URL +node index.js ./src/docs ./output --include-internal --base-url=https://canvas.workdaydesign.com + +# Run with debug output +node index.js --debug + +# Process a single file +node index.js ./content/components/button.mdx ./output/button.md +``` + +## Technical Implementation + +The converter is implemented as an ES Module and uses modern JavaScript features: + +- Factory pattern for creating utility modules with dependency injection +- Asynchronous file operations with `fs/promises` +- Regular expressions for content parsing +- Function composition for pipeline-style processing +- Caching mechanisms to avoid redundant processing +- Batched processing for performance optimization +- Careful error handling with appropriate logging +- Sensible defaults for easy execution + +## Output Format + +The generated Markdown files include: + +- YAML frontmatter with metadata about the source file +- Live URL linking to the original documentation +- Properly formatted component API tables +- Correctly indented code blocks with language specification +- Transformed interactive components as static Markdown +- Maintained image references with correct relative paths + +Additionally, the converter generates an `llms.txt` file that serves as a reference for AI tools and large language models, containing consolidated information about the documentation set. + +The converter creates a faithful representation of the original MDX documentation in standard Markdown format, making it compatible with a wide range of documentation systems and platforms. \ No newline at end of file diff --git a/mdx2md/cli.js b/mdx2md/cli.js new file mode 100644 index 0000000000..3bb3c1c1e6 --- /dev/null +++ b/mdx2md/cli.js @@ -0,0 +1,72 @@ +/** + * CLI Arguments Parser Module + * + * Handles command-line argument parsing and validation for the MDX to Markdown converter. + */ + +/** + * Parses command line arguments into a structured options object. + * + * @returns {Object} Parsed CLI options + */ +export function parseCliArgs() { + // Define default directories for canvas-kit + const DEFAULT_INPUT_DIR = './modules'; + const DEFAULT_OUTPUT_DIR = './docs/markdown'; + + // Extract arguments and handle defaults + const args = process.argv.slice(2); + const options = { + inputDir: DEFAULT_INPUT_DIR, + outputDir: DEFAULT_OUTPUT_DIR, + baseUrl: 'https://workday.github.io/canvas-kit', + showHelp: false, + debug: false, + }; + + // Handle positional arguments (input/output directories) + if (args.length >= 1 && !args[0].startsWith('--')) { + options.inputDir = args[0]; + } + + if (args.length >= 2 && !args[1].startsWith('--')) { + options.outputDir = args[1]; + } + + // Parse flag arguments + args.forEach(arg => { + if (arg.startsWith('--base-url=')) { + options.baseUrl = arg.split('=')[1]; + } else if (arg === '--help' || arg === '-h') { + options.showHelp = true; + } else if (arg === '--debug') { + options.debug = true; + } + }); + + return options; +} + +/** + * Displays help information for the CLI. + * + * @param {Object} options - Default options to show in the help text + */ +export function displayHelp(options) { + console.log( + `MDX to Markdown Converter + +Usage: node index.js [input-dir] [output-dir] [options] + +Options: + --base-url=URL Specify the base URL for live links + --debug Enable detailed debug output + --help, -h Display this help information + +Defaults: + - input-dir: ${options.inputDir} + - output-dir: ${options.outputDir} + - base-url: ${options.baseUrl} +` + ); +} diff --git a/mdx2md/config.js b/mdx2md/config.js new file mode 100644 index 0000000000..98c94138c8 --- /dev/null +++ b/mdx2md/config.js @@ -0,0 +1,79 @@ +/** + * Configuration Module + * + * Centralizes configuration for the MDX to Markdown conversion process. + * Provides default values and utility functions for working with the configuration. + */ + +import path from 'path'; + +/** + * Creates a configuration object with sensible defaults. + * + * @param {Object} options - Optional configuration overrides + * @returns {Object} Complete configuration object + */ +export function createConfig(options = {}) { + // Create the logger utility based on debug mode + const logger = { + // Debug mode setting + debug: options.debug || false, + + // Log message only in debug mode + log: function (message) { + if (this.debug) { + console.log(message); + } + }, + + // Always log error messages + error: function (message) { + console.error(message); + }, + + // Log warnings only in debug mode + warn: function (message) { + if (this.debug) { + console.warn(message); + } + }, + + // Always log success messages, but can be formatted differently + success: function (message) { + if (this.debug) { + console.log(message); + } else { + // In non-debug mode, we could customize success messages if needed + } + }, + }; + + return { + // Feature flags + debug: options.debug || false, + + // URLs and paths + baseUrl: options.baseUrl || 'https://workday.github.io/canvas-kit', + nodeModulesPath: options.nodeModulesPath || path.resolve('./') + '/node_modules', + + // Processing configuration + imageBatchSize: options.imageBatchSize || 20, + + // Cache and state + cache: new Map(), + canvasDocs: [], + pendingImages: [], + + // Logging utility + logger, + + // Cache key generators + cacheKeys: { + processedFile: path => `file:${path}`, + componentDoc: (name, attrs) => `component:${name}:${attrs || ''}`, + componentDesc: (name, fileName) => `desc:${name}:${fileName || ''}`, + fileContent: path => `content:${path}`, + exampleCode: name => `example:${name}`, + }, + }; +} diff --git a/mdx2md/fileProcessor.js b/mdx2md/fileProcessor.js new file mode 100644 index 0000000000..67caa9f759 --- /dev/null +++ b/mdx2md/fileProcessor.js @@ -0,0 +1,265 @@ +/** + * File Processor Module + * + * Core module for processing MDX files and converting them to Markdown. + * Handles the main processing pipeline and coordinates between various utilities. + */ + +import fs from 'fs/promises'; +import path from 'path'; +import {normalizeWhitespace} from './utils/markdownUtils.js'; + +/** + * Creates a file processor with the given dependencies. + * + * @param {Object} config - Configuration object + * @param {Object} fileUtils - File utilities + * @param {Object} mdxParser - MDX parser + * @param {Object} componentProcessors - Component processors + * @returns {Object} File processor object + */ +export function createFileProcessor(config, fileUtils, mdxParser, componentProcessors) { + return { + /** + * Process MDX File to Markdown + * + * Core function that reads an MDX file, processes its components, + * and generates a Markdown file with equivalent content. + * + * @param {string} mdxFilePath - Path to the source MDX file + * @param {string} outputPath - Destination path for the Markdown file + * @param {string} rootInputDir - Root input directory + * @param {string} rootOutputDir - Root output directory + * @param {boolean} isImported - Whether this file is being processed as an import + * @returns {Promise} - The processed Markdown content + * @throws {Error} - If there are issues with file processing + */ + async processMdxFile(mdxFilePath, outputPath, rootInputDir, rootOutputDir, isImported = false) { + // Check if this file has already been processed + const cacheKey = config.cacheKeys.processedFile(mdxFilePath); + if (config.cache.has(cacheKey)) { + return config.cache.get(cacheKey); + } + + const logger = config.logger; + + + try { + // Read file content + const mdxContent = await fs.readFile(mdxFilePath, 'utf-8'); + + // Check if it's a TSX file + if (mdxFilePath.endsWith('.tsx')) { + + // For TSX files, create a simple markdown file with just the code + const fileName = path.basename(mdxFilePath, '.tsx'); + let markdownContent = `--- +title: ${fileName} +source_file: ${path.relative(rootInputDir, mdxFilePath)} +--- + +# ${fileName} + +This file is a React component. + +\`\`\`tsx +${mdxContent} +\`\`\` +`; + // Write to file + if (!isImported && outputPath) { + await fs.mkdir(path.dirname(outputPath), {recursive: true}); + await fs.writeFile(outputPath, markdownContent, 'utf-8'); + } + + return markdownContent; + } + + // Parse frontmatter and content for MDX files + const {frontmatter, content} = mdxParser.parseFrontmatter(mdxContent); + + // Process imports and extract them + const {processedContent, imports} = mdxParser.processImports(content); + + // Process components in the content + const finalContent = await componentProcessors.processComponents( + processedContent, + imports, + mdxFilePath, + rootInputDir, + rootOutputDir + ); + + // Add document identifier and URL to frontmatter + frontmatter.source_file = path.relative(rootInputDir, mdxFilePath); + const relativePath = path.relative(rootInputDir, mdxFilePath); + const urlPath = relativePath.replace(/\.mdx$/, ''); + frontmatter.live_url = `${config.baseUrl}/${urlPath}`; + + // Build final markdown content + let markdownContent = mdxParser.generateFrontmatter(frontmatter); + markdownContent += finalContent; + + // Clean up whitespace + const cleanedContent = normalizeWhitespace(markdownContent); + + // Save to the cache + config.cache.set(cacheKey, cleanedContent); + + // Write to file if needed + if (!isImported && outputPath) { + try { + // Make sure we can write to this path (handles directory conflicts) + await fileUtils.ensureValidOutputPath(outputPath); + + // Create directory structure + const dirName = path.dirname(outputPath); + await fs.mkdir(dirName, {recursive: true}); + + // Write the file + await fs.writeFile(outputPath, cleanedContent, 'utf-8'); + + // Log success (less verbose) + logger.log(`Successfully wrote: ${outputPath} (${cleanedContent.length} chars)`); + } catch (error) { + logger.error(`Error writing file ${outputPath}:`, error); + } + } + + return cleanedContent; + } catch (error) { + logger.error(`Error processing file ${mdxFilePath}:`, error); + throw error; + } + }, + + /** + * Process MDX to Markdown Conversion + * + * Main entry point that orchestrates the conversion process. + * + * @param {Object} options - Processing options + * @returns {Promise} - Resolves when all files have been processed + * @throws {Error} - If there are issues with directory access or processing + */ + async processFiles(options) { + const {inputDir, outputDir} = options; + const logger = config.logger; + + try { + // Validate input directory + try { + await fs.access(inputDir); + } catch (err) { + logger.error(`Input directory not found: ${inputDir}`); + process.exit(1); + } + + // Check for node_modules existence + try { + await fs.access(config.nodeModulesPath); + } catch (err) { + logger.warn(`⚠️ node_modules not found at: ${config.nodeModulesPath}`); + } + + // Create output directory (only if outputDir is a directory, not a file) + const isInputFile = (await fs.stat(inputDir)).isFile(); + logger.log(`isInputFile: ${isInputFile}, inputDir: ${inputDir}, outputDir: ${outputDir}`); + if (!isInputFile) { + logger.log(`Creating output directory: ${outputDir}`); + await fs.mkdir(outputDir, {recursive: true}); + } else { + // For single file processing, ensure the output directory exists + const outputDirPath = path.dirname(outputDir); + logger.log(`Creating parent directory for single file: ${outputDirPath}`); + await fs.mkdir(outputDirPath, {recursive: true}); + } + + logger.log(`Starting MDX to Markdown conversion...`); + logger.log(`Input directory: ${inputDir}`); + logger.log(`Output directory: ${outputDir}`); + logger.log(`Base URL: ${config.baseUrl}`); + + // Load docs + const docsLoaded = await fileUtils.loadCKDocs(); + logger.log(`Docs loading ${docsLoaded ? 'succeeded' : 'failed'}`); + + let mdxFiles = []; + try { + // Check if inputDir is a file + const stats = await fs.stat(inputDir); + if (stats.isFile()) { + if (inputDir.endsWith('.mdx') || inputDir.endsWith('.tsx')) { + mdxFiles = [inputDir]; + logger.log(`Processing single file: ${inputDir}`); + } else { + logger.error(`Input file is not an MDX or TSX file: ${inputDir}`); + process.exit(1); + } + } else { + // Find all MDX files recursively + mdxFiles = await fileUtils.findMdxFiles(inputDir); + } + } catch (err) { + logger.error(`Error checking input path: ${err.message}`); + process.exit(1); + } + + logger.log(`Found ${mdxFiles.length} files to process.`); + + // Process files in batches to avoid memory issues with large repositories + const BATCH_SIZE = 10; + for (let i = 0; i < mdxFiles.length; i += BATCH_SIZE) { + const batch = mdxFiles.slice(i, i + BATCH_SIZE); + + // Setup directory structure first to avoid missing directories + const contentRootDir = isInputFile ? path.dirname(inputDir) : inputDir; + if (i === 0 && !isInputFile) { + // Only need to do this once, in the first batch, and only for directory processing + logger.log(`Setting up directory structure from ${contentRootDir} to ${outputDir}`); + await fileUtils.setupDirectoryStructure(contentRootDir, outputDir); + } + + // Process each file in the batch sequentially to avoid race conditions + for (const mdxFile of batch) { + // Handle single file case + let outputPath; + const isInputFile = (await fs.stat(inputDir)).isFile(); + + if (isInputFile) { + // If input is a file, use outputDir as the direct output path + outputPath = outputDir; + } else { + // Use the utility function to calculate the output path + outputPath = fileUtils.calculateOutputPath(contentRootDir, mdxFile, outputDir); + } + + logger.log(`Processing file: ${mdxFile} -> ${outputPath}`); + const rootInputDir = isInputFile ? path.dirname(inputDir) : inputDir; + const rootOutputDir = isInputFile ? path.dirname(outputDir) : outputDir; + await this.processMdxFile(mdxFile, outputPath, rootInputDir, rootOutputDir); + } + + // Process any queued images after each batch + await fileUtils.processImageBatch(); + + logger.log( + `Processed batch ${Math.min(i + BATCH_SIZE, mdxFiles.length)}/${mdxFiles.length}` + ); + } + + // Process any remaining images + await fileUtils.processImageBatch(); + + // Success message is handled in the main function + return true; + } catch (error) { + logger.error('Error during conversion:'); + if (config.debug) { + logger.error(error); + } + return false; + } + }, + }; +} diff --git a/mdx2md/fileUtils.js b/mdx2md/fileUtils.js new file mode 100644 index 0000000000..cae8d7fc08 --- /dev/null +++ b/mdx2md/fileUtils.js @@ -0,0 +1,451 @@ +/** + * FileUtils Module + * + * A utility module for handling file system operations related to MDX-to-Markdown conversion. + * This module uses a factory pattern to create utility functions that operate on the file system. + * + * The module provides functionality for: + * - Finding and reading MDX files recursively in a directory structure + * - Managing image file copying with batched processing for performance + * - Resolving import paths between files (both relative and module-based) + * - Loading and caching file contents to avoid redundant reads + * - Loading component documentation from Canvas Kit + * + * @module fileUtils + * @requires fs/promises - Node.js promise-based file system operations + * @requires path - Node.js path manipulation utilities + * + * @param {Object} config - Configuration object with the following properties: + * @param {string} config.baseUrl - Base URL for generated documentation + * @param {Map} config.cache - Cache for storing processed content + * @param {string} config.nodeModulesPath - Path to node_modules directory + * @param {Array} config.canvasDocs - Storage for loaded component documentation + * @param {number} config.imageBatchSize - Number of images to process in a batch + * @param {Array} config.pendingImages - Queue of images waiting to be processed + * + * @returns {Object} Collection of file utility functions + * @version 1.0.0 + * @since Node 14.21.3 + */ + +import fs from 'fs/promises'; +import path from 'path'; + +// We'll use a function factory pattern instead of shared state +export function createFileUtils(config) { + return { + /** + * Gets file stats for a given path + * + * @param {string} pathToStat - Path to get stats for + * @returns {Promise} - File stats or null if error + */ + async statPath(pathToStat) { + try { + return await fs.stat(pathToStat); + } catch (error) { + return null; + } + }, + + /** + * Recursively finds all MDX files in a given directory. + * Skips lib, spec, specs, examples, dist, visual-testing, node_modules, + * and any directories ending in css. + */ + async findMdxFiles(directory) { + try { + const entries = await fs.readdir(directory, {withFileTypes: true}); + const filePromises = entries.map(async entry => { + const fullPath = path.join(directory, entry.name); + + // Skip directories we don't want to process + if (entry.isDirectory()) { + const dirName = entry.name.toLowerCase(); + // Skip lib, spec, specs directories + if (dirName === 'lib' || dirName === 'spec' || dirName === 'specs') { + config.logger.log(`Skipping directory: ${fullPath}`); + return []; + } + // Skip all examples directories + if (dirName === 'examples') { + config.logger.log(`Skipping examples directory: ${fullPath}`); + return []; + } + // Skip dist, visual-testing, node_modules directories + if (dirName === 'dist' || dirName === 'visual-testing' || dirName === 'node_modules') { + config.logger.log(`Skipping directory: ${fullPath}`); + return []; + } + // Skip folders ending in css + if (dirName.endsWith('css')) { + config.logger.log(`Skipping directory: ${fullPath}`); + return []; + } + // Skip specific top-level directories + if (dirName === 'codemod' || dirName === 'mcp' || dirName === 'popup-stack' || dirName === 'styling-transform') { + config.logger.log(`Skipping directory: ${fullPath}`); + return []; + } + // Skip version directories + if (dirName === 'version') { + config.logger.log(`Skipping version directory: ${fullPath}`); + return []; + } + // Skip specific subdirectories + const normalizedPath = fullPath.replace(/\\/g, '/').toLowerCase(); + if (normalizedPath.includes('/docs/docgen') || + normalizedPath.includes('/docs/llm') || + normalizedPath.includes('/docs/llm-txt') || + normalizedPath.includes('/docs/mdx/images') || + normalizedPath.includes('/docs/utils') || + normalizedPath.includes('/docs/webpack')) { + config.logger.log(`Skipping directory: ${fullPath}`); + return []; + } + return this.findMdxFiles(fullPath); + } + + // Only process MDX files (not TSX files) + return entry.name.endsWith('.mdx') ? [fullPath] : []; + }); + + const files = await Promise.all(filePromises); + return files.flat(); + } catch (error) { + config.logger.error(`Error finding MDX files in ${directory}:`, error.message); + return []; + } + }, + + /** + * Adds an image to the pending queue for processing. + */ + queueImageCopy(imagePath, filePath, rootInputDir, rootOutputDir) { + const baseDir = path.dirname(filePath); + const fullImagePath = path.resolve(baseDir, imagePath); + + // Calculate output file path + const relativePath = path.relative(rootInputDir, filePath); + const outputFilePath = path.join(rootOutputDir, relativePath.replace(/\.mdx$/, '.md')); + const outputDir = path.dirname(outputFilePath); + + // Output path will be in the same directory as the markdown file + const imageName = path.basename(imagePath); + const outputImagePath = path.join(outputDir, imageName); + + // IMPORTANT: Validate the destination path is within the output directory + if (!outputImagePath.startsWith(rootOutputDir)) { + config.logger.warn( + `Skipping image with invalid destination path: ${outputImagePath} (not in output directory)` + ); + return imageName; // Still return the image name for the markdown reference + } + + // Add to pending queue + config.pendingImages.push({ + source: fullImagePath, + destination: outputImagePath, + }); + + // Process in batches if we've reached the batch size + if (config.pendingImages.length >= config.imageBatchSize) { + // Process async - don't await here to avoid blocking + this.processImageBatch(); + } + + // Return just the image filename for the markdown reference + return imageName; + }, + + /** + * Processes a batch of pending images. + */ + async processImageBatch() { + if (config.pendingImages.length === 0) return; + + const imagesToProcess = [...config.pendingImages]; + config.pendingImages = []; // Clear the queue + + // Group by directory to reduce mkdir calls + const dirSet = new Set(); + imagesToProcess.forEach(img => dirSet.add(path.dirname(img.destination))); + + // Create all directories first + await Promise.all( + [...dirSet].map(dir => + fs.mkdir(dir, {recursive: true}).catch(err => { + config.logger.warn(`Failed to create directory ${dir}: ${err.message}`); + }) + ) + ); + + // Process all images + await Promise.all( + imagesToProcess.map(async img => { + try { + await fs.copyFile(img.source, img.destination); + } catch (error) { + config.logger.warn(`Could not copy image ${img.source}: ${error.message}`); + } + }) + ); + }, + + /** + * Loads Canvas Kit component documentation from node_modules. + */ + async loadCKDocs() { + try { + const docsPath = path.join( + config.nodeModulesPath, + '@workday/canvas-kit-docs/dist/es6/lib/docs.js' + ); + + try { + // Import the docs module directly + const docsModule = await import(docsPath); + + if (docsModule.docs && Array.isArray(docsModule.docs)) { + config.canvasDocs = docsModule.docs; + config.logger.log( + `Successfully loaded ${config.canvasDocs.length} documentation items` + ); + return true; + } else { + config.logger.warn('docs export not found or not an array in the module'); + } + } catch (err) { + config.logger.warn(`Could not import Canvas Kit docs from: ${docsPath}`); + config.logger.warn(`Import error: ${err.message}`); + } + + return false; + } catch (error) { + config.logger.warn('Could not load docs module:', error.message); + return false; + } + }, + + /** + * Resolves an import path to an actual file path. + * + * @param {string} importPath - The import path to resolve + * @param {string} filePath - The path of the file containing the import + * @returns {Promise} - The resolved path or null if not found + */ + async resolveImportPath(importPath, filePath) { + let resolvedPath; + const baseDir = path.dirname(filePath); + + // Determine how to resolve the path based on import type + if (importPath.startsWith('@') || !importPath.startsWith('.')) { + // For package imports, resolve from node_modules + resolvedPath = path.resolve(config.nodeModulesPath, importPath); + } else { + // For relative imports, resolve relative to the current file + resolvedPath = path.resolve(baseDir, importPath); + } + + // Try the resolved path directly + try { + await fs.access(resolvedPath); + return resolvedPath; + } catch { + // If direct access fails, try with extensions + const extensions = ['.mdx', '.tsx', '.jsx', '.js']; + + // Only try extensions if the import doesn't already have one + if (!path.extname(importPath)) { + for (const ext of extensions) { + const pathWithExt = `${resolvedPath}${ext}`; + try { + await fs.access(pathWithExt); + return pathWithExt; + } catch { + // Continue to next extension + } + } + } + + // If no valid path found, return null + return null; + } + }, + + /** + * Loads a file's content with caching. + */ + async loadFileContent(filePath) { + const cacheKey = `content:${filePath}`; + + // Check cache first + if (config.cache.has(cacheKey)) { + return config.cache.get(cacheKey); + } + + try { + const content = await fs.readFile(filePath, 'utf-8'); + config.cache.set(cacheKey, content); + return content; + } catch (error) { + return null; + } + }, + + /** + * Ensures an output path is valid for writing a file by handling directory conflicts. + * If a directory exists with the same name as the target file, it is renamed. + * + * @param {string} outputPath - The path where a file will be written + */ + async ensureValidOutputPath(outputPath) { + try { + const stats = await fs.stat(outputPath); + if (stats.isDirectory()) { + // If a directory exists at the output path, rename it + const backupPath = `${outputPath}-dir`; + config.logger.warn(`${outputPath} is a directory, moving to ${backupPath}`); + await fs.rm(backupPath, {recursive: true, force: true}).catch(error => { + config.logger.error(error); + }); + await fs.rename(outputPath, backupPath); + } + } catch (err) { + // If the path doesn't exist, that's normal and expected + if (err.code !== 'ENOENT') { + config.logger.warn(`Checking output path ${outputPath}: ${err.message}`); + } + } + }, + + /** + * Sets up the output directory structure based on the input directory. + * Ensures all subdirectories exist before writing files. + * + * @param {string} inputDir - The base input directory to mirror + * @param {string} outputDir - The base output directory + */ + async setupDirectoryStructure(inputDir, outputDir) { + // Get all subdirectories from content folder + const directories = await this.findDirectories(inputDir); + + // Create all required directories in the output folder + for (const dir of directories) { + const relativePath = path.relative(inputDir, dir); + const targetDir = path.join(outputDir, relativePath); + + try { + await fs.mkdir(targetDir, {recursive: true}); + } catch (err) { + config.logger.warn(`Could not create directory ${targetDir}: ${err.message}`); + } + } + + }, + + /** + * Check if a directory exists + * + * @param {string} dir - Directory path to check + * @returns {Promise} - True if directory exists + */ + async directoryExists(dir) { + try { + const stats = await fs.stat(dir); + return stats.isDirectory(); + } catch (err) { + return false; + } + }, + + /** + * Recursively finds all directories in a given path. + * Skips lib, spec, specs, examples, dist, visual-testing, node_modules, + * and any directories ending in css. + * + * @param {string} dirPath - The directory to scan + * @returns {Promise} - Array of directory paths + */ + async findDirectories(dirPath) { + const result = []; + + try { + const entries = await fs.readdir(dirPath, {withFileTypes: true}); + + for (const entry of entries) { + if (entry.isDirectory()) { + const fullPath = path.join(dirPath, entry.name); + const dirName = entry.name.toLowerCase(); + + // Skip lib, spec, specs directories + if (dirName === 'lib' || dirName === 'spec' || dirName === 'specs') { + continue; + } + + // Skip all examples directories + if (dirName === 'examples') { + continue; + } + + // Skip dist, visual-testing, node_modules directories + if (dirName === 'dist' || dirName === 'visual-testing' || dirName === 'node_modules') { + continue; + } + + // Skip folders ending in css + if (dirName.endsWith('css')) { + continue; + } + + // Skip specific top-level directories + if (dirName === 'codemod' || dirName === 'mcp' || dirName === 'popup-stack' || dirName === 'styling-transform') { + continue; + } + + // Skip version directories + if (dirName === 'version') { + continue; + } + // Skip specific subdirectories + const normalizedPath = fullPath.replace(/\\/g, '/').toLowerCase(); + if (normalizedPath.includes('/docs/docgen') || + normalizedPath.includes('/docs/llm') || + normalizedPath.includes('/docs/llm-txt') || + normalizedPath.includes('/docs/mdx/images') || + normalizedPath.includes('/docs/utils') || + normalizedPath.includes('/docs/webpack')) { + continue; + } + + result.push(fullPath); + + // Recursively scan subdirectories + const subDirs = await this.findDirectories(fullPath); + result.push(...subDirs); + } + } + } catch (err) { + config.logger.warn(`Error scanning directory ${dirPath}: ${err.message}`); + } + + return result; + }, + + /** + * Calculates the output path for a given input file, preserving directory structure. + * + * @param {string} contentRootDir - The root content directory + * @param {string} inputFile - The input file path + * @param {string} outputDir - The output directory + * @returns {string} - The calculated output path + */ + calculateOutputPath(contentRootDir, inputFile, outputDir) { + // Calculate the proper relative path that preserves subdirectory structure + const relativePath = path.relative(contentRootDir, inputFile); + + // Create the output path in the corresponding location + return path.join(outputDir, relativePath.replace(/\.(mdx|tsx)$/, '.md')); + }, + }; +} diff --git a/mdx2md/genLlmsTxt.js b/mdx2md/genLlmsTxt.js new file mode 100644 index 0000000000..47823ad229 --- /dev/null +++ b/mdx2md/genLlmsTxt.js @@ -0,0 +1,94 @@ +// genLlmsTxt.js +import fs from 'fs/promises'; // Change to promises API +import path from 'path'; + +export async function genLlmsTxt(rootDir, outputFile, logger) { + // Make async + // Helper function to find all markdown files recursively + async function findMarkdownFiles(dir, fileList = []) { + // Make async + const files = await fs.readdir(dir); + + for (const file of files) { + // Use for...of for async operations + const filePath = path.join(dir, file); + const stat = await fs.stat(filePath); + + if (stat.isDirectory()) { + await findMarkdownFiles(filePath, fileList); + } else if (file.endsWith('.md')) { + fileList.push(filePath); + } + } + + return fileList; + } + + // Get all markdown files + const markdownFiles = await findMarkdownFiles(rootDir); + + // Organize files by section + const sections = { + Docs: [], + Guidelines: [], + 'Content Guidelines': [], + Styles: [], + 'Get Started': [], + Help: [], + Frameworks: [], + Patterns: [], + }; + + for (const file of markdownFiles) { + // Use for...of for async operations + const relativePath = path.relative(rootDir, file); + const fileContent = await fs.readFile(file, 'utf8'); + const titleMatch = fileContent.match(/^# (.+)$/m); + const title = titleMatch ? titleMatch[1] : path.basename(file, '.md'); + + const descMatch = fileContent.match(/^# .+\n\n(.+)$/m); + const description = descMatch ? descMatch[1] : ''; + + const entry = `- [${title}](markdown/${relativePath}): ${description}`; + + if (relativePath.startsWith('components/')) { + sections['Docs'].push(entry); + } else if ( + relativePath.startsWith('guidelines/accessibility/') || + relativePath.startsWith('guidelines/ai-guidance/') + ) { + sections['Guidelines'].push(entry); + } else if (relativePath.startsWith('guidelines/content/')) { + sections['Content Guidelines'].push(entry); + } else if (relativePath.startsWith('styles/')) { + sections['Styles'].push(entry); + } else if (relativePath.startsWith('get-started/')) { + sections['Get Started'].push(entry); + } else if (relativePath.startsWith('help/')) { + sections['Help'].push(entry); + } else if (relativePath.startsWith('frameworks/')) { + sections['Frameworks'].push(entry); + } else if (relativePath.startsWith('patterns/')) { + sections['Patterns'].push(entry); + } + } + + // Generate output content + let output = '# Markdown Docs\n\n'; + + for (const [section, entries] of Object.entries(sections)) { + if (entries.length > 0) { + output += `## ${section}\n\n`; + output += entries.join('\n'); + output += '\n\n'; + } + } + + // Write to output file + await fs.writeFile(outputFile, output); + + // Log output based on logger availability + if (logger) { + logger.log(`Generated reference file at ${outputFile}`); + } +} diff --git a/mdx2md/index.js b/mdx2md/index.js new file mode 100755 index 0000000000..15c0b90836 --- /dev/null +++ b/mdx2md/index.js @@ -0,0 +1,104 @@ +#!/usr/bin/env node +/* eslint-disable compat/compat */ + +/** + * MDX to Markdown Converter + * + * This script converts MDX files from a design system repository to Markdown format, + * handling imports, component references, image copying, and code compilation. + * + * Features: + * - Extracts and preserves YAML frontmatter + * - Processes component imports and references + * - Handles code examples and API documentation + * - Copies and relinks images + * + * Usage: + * node mdx2md.mjs [--base-url=URL] + * + * @example + * node mdx2md.mjs ./modules ./docs/markdown --base-url=https://workday.github.io/canvas-kit + */ + +import path from 'path'; +import {REGEX} from './utils/regexPatterns.js'; +import {createConfig} from './config.js'; +import {parseCliArgs, displayHelp} from './cli.js'; +import {createFileProcessor} from './fileProcessor.js'; +import {createFileUtils} from './fileUtils.js'; +import {createMdxParser} from './mdxParser.js'; +import {createComponentProcessors} from './processors/index.js'; +import {genLlmsTxt} from './genLlmsTxt.js'; + +// Parse CLI arguments +const cliOptions = parseCliArgs(); + +// Display help if requested +if (cliOptions.showHelp) { + displayHelp(cliOptions); + process.exit(0); +} + +// Create configuration with CLI options +const config = createConfig({ + baseUrl: cliOptions.baseUrl, + nodeModulesPath: path.resolve('./') + '/node_modules', + debug: cliOptions.debug, +}); + +// Initialize dependencies +const fileUtils = createFileUtils(config); +const mdxParser = createMdxParser(REGEX); +const componentProcessors = createComponentProcessors(config, REGEX, fileUtils, mdxParser); +const fileProcessor = createFileProcessor(config, fileUtils, mdxParser, componentProcessors); + +/** + * Main function to run the MDX to Markdown conversion + */ +async function main() { + try { + // Display start message based on debug mode + if (!config.debug) { + console.log('Converting MDX files to Markdown...'); + } + + // Process all files + const success = await fileProcessor.processFiles({ + inputDir: cliOptions.inputDir, + outputDir: cliOptions.outputDir, + }); + + // Generate the LLMs reference file + if (success) { + config.logger.log(`Generating LLMs reference file...`); + await genLlmsTxt( + cliOptions.outputDir, + path.join(cliOptions.outputDir, '..', 'llms.txt'), + config.logger + ); + + // Show completion message + if (config.debug) { + config.logger.log('Conversion completed successfully!'); + } else { + console.log(`✅ Conversion successful! Output written to: ${cliOptions.outputDir}`); + } + } else { + if (!config.debug) { + console.error('❌ Conversion failed. Use --debug for more information.'); + } + } + + // Exit with appropriate code + process.exit(success ? 0 : 1); + } catch (error) { + console.error('Fatal error:'); + if (config.debug) { + console.error(error); + } + process.exit(1); + } +} + +// Start the script +main(); diff --git a/mdx2md/mdxParser.js b/mdx2md/mdxParser.js new file mode 100644 index 0000000000..6e8286c775 --- /dev/null +++ b/mdx2md/mdxParser.js @@ -0,0 +1,237 @@ +/** + * MDX Parser Module + * + * This module provides functionality for parsing, processing, and transforming MDX content + * into standardized Markdown format. It handles extraction and manipulation of frontmatter, + * import statements, component attributes, and special MDX syntax patterns. + * + * The parser is designed with a factory pattern to allow dependency injection of regex + * patterns, making it more testable and modular. Each method focuses on a specific aspect + * of MDX parsing with careful handling of edge cases and special formatting requirements. + * + * Key Features: + * - YAML frontmatter extraction and generation + * - Import statement processing with support for default, named, and aliased imports + * - Component attribute parsing with support for various formats and types + * - JSDoc link conversion to Markdown format + * - Special handling for complex data structures in frontmatter + * + * @module mdxParser + * @requires REGEX - Regular expression patterns for parsing different MDX constructs + */ + +export function createMdxParser(REGEX) { + return { + /** + * Extracts and parses YAML frontmatter from MDX content. + * + * @param {string} content - MDX content including frontmatter + * @returns {Object} Object with frontmatter object and remaining content + */ + parseFrontmatter(content) { + const match = content.match(REGEX.frontmatter); + + if (!match) { + return {frontmatter: {}, content}; + } + + const frontmatterStr = match[1]; + const frontmatter = this.parseFrontmatterContent(frontmatterStr); + return { + frontmatter, + content: content.slice(match[0].length), + }; + }, + + /** + * Parses frontmatter content string into a structured object. + * Handles simple key-value pairs, booleans, and basic JSON structures. + * + * @param {string} content - Frontmatter content as a string + * @returns {Object} Parsed frontmatter object + */ + parseFrontmatterContent(content) { + const lines = content.split('\n'); + const frontmatter = {}; + + for (const line of lines) { + const colonIndex = line.indexOf(':'); + if (colonIndex <= 0) continue; + + const key = line.slice(0, colonIndex).trim(); + let value = line.slice(colonIndex + 1).trim(); + + // Handle boolean values + if (value === 'true') value = true; + if (value === 'false') value = false; + + // Handle arrays and objects + if (typeof value === 'string' && (value.startsWith('[') || value.startsWith('{'))) { + try { + value = JSON.parse(value); + } catch (e) { + // Keep as string if parsing fails + } + } + + frontmatter[key] = value; + } + + // Handle complex tab structures + if (frontmatter.tabs && typeof frontmatter.tabs === 'string') { + try { + frontmatter.tabs = frontmatter.tabs.split(/,\s*/).map(tab => { + if (tab.includes('{')) { + // Try to parse as JSON-like structure + const match = tab.match(/{name:\s*([^,}]+)(?:,\s*internal:\s*(true|false))?}/); + if (match) { + return { + name: match[1].trim(), + internal: match[2] === 'true', + }; + } + } + return tab.trim(); + }); + } catch (e) { + // Keep as is if parsing fails + } + } + + return frontmatter; + }, + + /** + * Generates frontmatter in YAML format from a frontmatter object. + * + * @param {Object} frontmatter - Frontmatter object + * @returns {string} Formatted frontmatter string + */ + generateFrontmatter(frontmatter) { + const lines = ['---']; + + for (const [key, value] of Object.entries(frontmatter)) { + const formattedValue = + typeof value === 'object' && value !== null ? JSON.stringify(value) : value; + lines.push(`${key}: ${formattedValue}`); + } + + lines.push('---', ''); + return lines.join('\n'); + }, + + /** + * Processes import statements in MDX content. + * Extracts import information and removes the statements from content. + * + * @param {string} content - MDX content with import statements + * @returns {Object} Object with processed content and import information + */ + processImports(content) { + let processedContent = content; + const imports = []; + + // Reset the regex to start from the beginning + REGEX.importStatement.lastIndex = 0; + + // Extract all imports + let match; + while ((match = REGEX.importStatement.exec(content)) !== null) { + imports.push({ + statement: match[0], // Includes the semicolon if present + names: this.parseImportNames(match[1].trim()), + path: match[2].trim(), + }); + } + + // Remove import statements from content + for (const importInfo of imports) { + processedContent = processedContent.replace(importInfo.statement, ''); + } + + // Clean up any standalone semicolons at the beginning of lines or with only whitespace before them + processedContent = processedContent.replace(/^\s*;/gm, ''); + + return {processedContent, imports}; + }, + + /** + * Parses import names from import statements. + * Handles default imports, named imports, and aliases. + * + * @param {string} importNames - The import names part of an import statement + * @returns {Array} Array of import name objects + */ + parseImportNames(importNames) { + const result = []; + + // Handle default import + if (importNames && !importNames.startsWith('{')) { + const parts = importNames.split(','); + result.push({default: parts[0].trim()}); + importNames = parts.slice(1).join(',').trim(); + } + + // Handle named imports + if (importNames && importNames.startsWith('{')) { + const namedImports = importNames + .slice(1, -1) + .split(',') + .map(name => name.trim()); + + for (const namedImport of namedImports) { + const [importName, alias] = namedImport.split(' as ').map(part => part.trim()); + result.push({name: importName, alias: alias || importName}); + } + } + + return result; + }, + + /** + * Parses component attributes string into an object. + * Handles quoted values, unquoted values, and boolean attributes. + * + * @param {string} attributesStr - String of component attributes + * @returns {Object} Parsed attributes object + */ + parseComponentAttributes(attributesStr) { + const attrs = {}; + const attrRegex = /\s*(\w+(?:-\w+)*)(?:=(?:"([^"]*)"|'([^']*)'|(\w+)))?/g; + let match; + + while ((match = attrRegex.exec(attributesStr)) !== null) { + const name = match[1]; + // Value can be in any of the capture groups 2, 3, or 4 depending on quotes + const value = + match[2] !== undefined + ? match[2] + : match[3] !== undefined + ? match[3] + : match[4] !== undefined + ? match[4] + : true; + attrs[name] = value; + } + + return attrs; + }, + + /** + * Converts JSDoc links to Markdown format. + * + * @param {string} text - Text containing JSDoc links + * @returns {string} Text with converted links + */ + convertJSDocLinks(text) { + // Convert JSDoc {@link ComponentName text} to markdown links + return text.replace( + /{@link ([a-z0-9.]+)( [a-z0-9. ]+)?}/gi, + (_, componentName, displayText) => { + const text = displayText ? displayText.trim() : componentName; + return `\`${text}\``; + } + ); + }, + }; +} diff --git a/mdx2md/package.json b/mdx2md/package.json new file mode 100644 index 0000000000..3e7c1ccf09 --- /dev/null +++ b/mdx2md/package.json @@ -0,0 +1,7 @@ +{ + "name": "mdx2md", + "version": "1.0.0", + "description": "MDX to Markdown converter", + "main": "mdx2md.js", + "type": "module" +} diff --git a/mdx2md/processors/apiComponents.js b/mdx2md/processors/apiComponents.js new file mode 100644 index 0000000000..26abddd072 --- /dev/null +++ b/mdx2md/processors/apiComponents.js @@ -0,0 +1,337 @@ +/** + * API Component Processors Module + * + * Handles components related to API documentation like SymbolDoc and SymbolDescription. + * Generates formatted Markdown tables for component props and metadata. + */ + +import {resetRegex} from '../utils/regexPatterns.js'; +import {formatPropType, formatDefaultValue} from '../utils/formatters.js'; +import {generateMarkdownTable} from '../utils/markdownUtils.js'; + +/** + * Creates API component processors with the given dependencies. + * + * @param {Object} config - Configuration object + * @param {Object} regex - Regular expression patterns + * @param {Object} mdxParser - MDX parser utilities + * @returns {Object} API component processor functions + */ +export function createApiComponentProcessors(config, regex, mdxParser) { + return { + /** + * Processes SymbolDoc components to generate API documentation. + * Caches processed documentation for better performance. + * + * @param {string} content - MDX content + * @returns {string} Processed content with SymbolDoc components converted + */ + processSymbolDocComponent(content) { + // Reset regex to ensure we start from the beginning + resetRegex(regex.symbolDoc); + + return content.replace(regex.symbolDoc, (_, componentName, attributes) => { + const cacheKey = config.cacheKeys.componentDoc(componentName, attributes); + + // Check if we've already processed this component + if (config.cache.has(cacheKey)) { + return config.cache.get(cacheKey); + } + + const attrs = mdxParser.parseComponentAttributes(attributes); + const fileName = attrs.fileName || ''; + + // Find the component in canvasDocs + const componentDoc = config.canvasDocs.find( + doc => doc.name === componentName && (fileName ? doc.fileName.includes(fileName) : true) + ); + + if (!componentDoc) { + return ``; + } + + const result = this.generateFullComponentDocumentation(componentDoc); + + // Cache the result for future use + config.cache.set(cacheKey, result); + + return result; + }); + }, + + /** + * Processes SymbolDescription components to include component descriptions. + * + * @param {string} content - MDX content + * @returns {string} Processed content with SymbolDescription components converted + */ + processSymbolDescription(content) { + // Reset regex to ensure we start from the beginning + resetRegex(regex.symbolDesc); + + return content.replace(regex.symbolDesc, (_, componentName, attributes) => { + const attrs = mdxParser.parseComponentAttributes(attributes); + const fileName = attrs.fileName || ''; + const cacheKey = config.cacheKeys.componentDesc(componentName, fileName); + + // Check cache first + if (config.cache.has(cacheKey)) { + return config.cache.get(cacheKey); + } + + // Find the component in canvasDocs + const componentDoc = config.canvasDocs.find( + doc => doc.name === componentName && (fileName ? doc.fileName.includes(fileName) : true) + ); + + if (!componentDoc || !componentDoc.description) { + return ``; + } + + const result = mdxParser.convertJSDocLinks(componentDoc.description); + config.cache.set(cacheKey, result); + return result; + }); + }, + + /** + * Generates full component documentation including description, metadata, and props. + * + * @param {Object} componentDoc - Component documentation object + * @returns {string} Complete Markdown documentation for the component + */ + generateFullComponentDocumentation(componentDoc) { + // Start with component name as heading + let markdown = `## ${componentDoc.name}\n\n`; + + // Add description + if (componentDoc.description) { + // Convert JSDoc links to markdown links + const description = mdxParser.convertJSDocLinks(componentDoc.description); + markdown += `${description}\n\n`; + } + + // Check component type + const componentType = componentDoc.type; + if (!componentType) { + return markdown + '\n'; + } + + // Add special metadata sections based on component type + markdown += this.generateComponentMetadata(componentType); + + // Generate props documentation + markdown += this.generatePropsDocumentation(componentDoc); + + // Generate subcomponents documentation + markdown += this.generateSubComponentsDocumentation(componentDoc); + + return markdown; + }, + + /** + * Generates component metadata documentation. + * + * @param {Object} componentType - Component type information + * @returns {string} Markdown documentation for component metadata + */ + generateComponentMetadata(componentType) { + let markdown = ''; + + // Add component type information + if (componentType.kind) { + markdown += `**Component Type:** \`${componentType.kind}\`\n\n`; + } + + // If it's an enhanced component, add display name + if (componentType.kind === 'enhancedComponent' && componentType.displayName) { + markdown += `**Display Name:** \`${componentType.displayName}\`\n\n`; + } + + // If it's a styled component, add style info + if (componentType.styleComponent) { + markdown += `**Style Component:** \`${ + componentType.styleComponent.name || 'CustomStyled' + }\`\n\n`; + } + + // If it has a base element, document it + if (componentType.baseElement) { + const baseElement = componentType.baseElement; + if (typeof baseElement === 'object' && baseElement.name) { + const elementLink = baseElement.url + ? `[${baseElement.name}](${baseElement.url})` + : baseElement.name; + markdown += `**Base Element:** ${elementLink}\n\n`; + } else if (typeof baseElement === 'string') { + markdown += `**Base Element:** \`${baseElement}\`\n\n`; + } + } + + // If it's an alias, show what it extends + if (componentType.kind === 'alias' && componentType.name) { + markdown += `**Extends:** \`${componentType.name}\`\n\n`; + } + + return markdown; + }, + + /** + * Generates props documentation for a component. + * + * @param {Object} componentDoc - Component documentation object + * @returns {string} Markdown documentation for component props + */ + generatePropsDocumentation(componentDoc) { + let markdown = ''; + const componentType = componentDoc.type; + + // Determine props based on component type + let props; + if (componentType.props) { + props = componentType.props; + } else if (componentType.kind === 'alias' && componentType.name) { + // For aliased types, try to find the original component + const aliasDoc = config.canvasDocs.find(doc => doc.name === componentType.name); + if (aliasDoc && aliasDoc.type && aliasDoc.type.props) { + props = aliasDoc.type.props; + } + } + + if (!props || props.length === 0) { + return markdown + '\n'; + } + + // Create a props table with more detailed information + markdown += `### Props\n\n`; + + // Format props into rows for the table + const headers = ['Name', 'Type', 'Default', 'Description']; + const rows = props.map(prop => { + // Name with formatting for required props + const name = prop.required ? `**${prop.name}**` : prop.name; + + // Format type with the utility + const type = formatPropType(prop.type); + + // Format default value with the utility + const defaultValue = formatDefaultValue(prop); + + // Format description + let description = prop.description || ''; + description = mdxParser.convertJSDocLinks(description); + + // Add deprecated notice + if (prop.tags && prop.tags.deprecated) { + description = `**Deprecated:** ${prop.tags.deprecated}\n\n${description}`; + } + + // Format for table + const formattedDescription = description.replace(/\n\n/g, '

').replace(/\n/g, ' '); + + return { + name, + type, + defaultValue, + description: formattedDescription, + }; + }); + + // Generate the table + markdown += generateMarkdownTable(headers, rows, [ + 'name', + 'type', + 'defaultValue', + 'description', + ]); + + return markdown; + }, + + /** + * Generates documentation for subcomponents. + * + * @param {Object} componentDoc - Component documentation object + * @returns {string} Markdown documentation for subcomponents + */ + generateSubComponentsDocumentation(componentDoc) { + let markdown = ''; + const componentType = componentDoc.type; + + // Check if component has subcomponents + if (!componentType.subComponents || componentType.subComponents.length === 0) { + return markdown; + } + + markdown += `## Subcomponents\n\n`; + + // Process each subcomponent + for (const subComponent of componentType.subComponents) { + const subComponentName = `${componentDoc.name}.${subComponent.name}`; + + markdown += `### ${subComponentName}\n\n`; + + // Add subcomponent description + if (subComponent.description) { + const description = mdxParser.convertJSDocLinks(subComponent.description); + markdown += `${description}\n\n`; + } + + // Find the full documentation for this subcomponent by its symbol name + const subComponentDoc = config.canvasDocs.find( + doc => doc.name === subComponent.symbol + ); + + if (subComponentDoc && subComponentDoc.type && subComponentDoc.type.props) { + // Generate props table for the subcomponent + markdown += `#### Props\n\n`; + + const headers = ['Name', 'Type', 'Default', 'Description']; + const rows = subComponentDoc.type.props.map(prop => { + // Name with formatting for required props + const name = prop.required ? `**${prop.name}**` : prop.name; + + // Format type with the utility + const type = formatPropType(prop.type); + + // Format default value with the utility + const defaultValue = formatDefaultValue(prop); + + // Format description + let description = prop.description || ''; + description = mdxParser.convertJSDocLinks(description); + + // Add deprecated notice + if (prop.tags && prop.tags.deprecated) { + description = `**Deprecated:** ${prop.tags.deprecated}\n\n${description}`; + } + + // Format for table + const formattedDescription = description.replace(/\n\n/g, '

').replace(/\n/g, ' '); + + return { + name, + type, + defaultValue, + description: formattedDescription, + }; + }); + + // Generate the table + markdown += generateMarkdownTable(headers, rows, [ + 'name', + 'type', + 'defaultValue', + 'description', + ]); + + markdown += '\n'; + } else { + markdown += `\n\n`; + } + } + + return markdown; + }, + }; +} diff --git a/mdx2md/processors/basicComponents.js b/mdx2md/processors/basicComponents.js new file mode 100644 index 0000000000..a710edfc43 --- /dev/null +++ b/mdx2md/processors/basicComponents.js @@ -0,0 +1,119 @@ +/** + * Basic Component Processors Module + * + * Provides processors for simple MDX components that don't require complex operations. + * Handles components like PackageInfo, InternalContent, TabPanel, and Specifications. + */ + +import {resetRegex} from '../utils/regexPatterns.js'; + +/** + * Creates basic component processors with the given dependencies. + * + * @param {Object} config - Configuration object + * @param {Object} regex - Regular expression patterns + * @param {Object} mdxParser - MDX parser utilities + * @returns {Object} Basic component processor functions + */ +export function createBasicComponentProcessors(config, regex, mdxParser) { + return { + /** + * Processes PackageInfo components and converts them to Markdown tables. + * + * @param {string} content - MDX content + * @returns {string} Processed content with PackageInfo components converted + */ + processPackageInfoComponent(content) { + // Reset regex to ensure we start from the beginning + resetRegex(regex.packageInfo); + + return content.replace(regex.packageInfo, attributes => { + const attrs = mdxParser.parseComponentAttributes(attributes); + let table = '| Package Information | |\n| --- | --- |\n'; + + for (const [key, value] of Object.entries(attrs)) { + if (key.startsWith('data-')) continue; + table += `| ${key} | ${value} |\n`; + } + + return table; + }); + }, + + /** + * Processes InternalContent components - always includes the content. + * + * @param {string} content - MDX content + * @returns {string} Processed content with internal content included + */ + processInternalContentComponent(content) { + // Reset regex to ensure we start from the beginning + resetRegex(regex.internalContent); + + // Always include the internal content without the wrapper + return content.replace(regex.internalContent, (_, internalContent) => internalContent); + }, + + /** + * Processes ExternalContent components based on configuration. + * + * @param {string} content - MDX content + * @returns {string} Processed content with external content handled + */ + processExternalContentComponent(content) { + // Reset regex to ensure we start from the beginning + resetRegex(regex.externalContent); + + // Always include the external content without the wrapper + // This is appropriate content for both internal and external modes + return content.replace(regex.externalContent, (_, externalContent) => externalContent); + }, + + /** + * Processes TabPanel components and converts them to Markdown headings. + * + * @param {string} content - MDX content + * @returns {string} Processed content with TabPanel components converted + */ + processTabPanelComponent(content) { + // Reset regex to ensure we start from the beginning + resetRegex(regex.tabPanel); + + return content.replace(regex.tabPanel, (_, attributes, tabContent) => { + const attrs = mdxParser.parseComponentAttributes(attributes); + + // Get the tab ID (data-id) or name + const tabTitle = attrs['data-id'] || attrs.name || 'Tab'; + + // Return the tab content with a header + return `\n## ${tabTitle}\n\n${tabContent}\n`; + }); + }, + + /** + * Processes Specifications components to include specifications info. + * + * @param {string} content - MDX content + * @returns {string} Processed content with Specifications components converted + */ + processSpecificationsComponent(content) { + // Reset regex to ensure we start from the beginning + resetRegex(regex.specification); + + return content.replace(regex.specification, (_, file, name) => { + return `### Specifications for ${name}\n\n\n`; + }); + }, + + /** + * Processes figma links - no filtering needed for open source. + * + * @param {string} content - MDX content + * @returns {string} Processed content (unchanged) + */ + processFigmaLinks(content) { + // No filtering needed for open source repository + return content; + }, + }; +} diff --git a/mdx2md/processors/codeComponents.js b/mdx2md/processors/codeComponents.js new file mode 100644 index 0000000000..2c802c73be --- /dev/null +++ b/mdx2md/processors/codeComponents.js @@ -0,0 +1,442 @@ +/** + * Code Components Processor Module + * + * Handles code-related MDX components such as ExampleCodeBlock, CKDocs, and TSX imports. + * Processes and formats code examples for Markdown output. + */ + +import path from 'path'; +import {resetRegex} from '../utils/regexPatterns.js'; +import {codeBlock} from '../utils/markdownUtils.js'; + +/** + * Creates code component processors with the given dependencies. + * + * @param {Object} config - Configuration object + * @param {Object} regex - Regular expression patterns + * @param {Object} fileUtils - File utilities + * @param {Object} apiProcessors - API processors for SymbolDoc handling + * @returns {Object} Code component processor functions + */ +export function createCodeComponentProcessors(config, regex, fileUtils, apiProcessors, mdxParser) { + return { + /** + * Processes CKDocs components by resolving and including the referenced documentation. + * + * @param {string} content - MDX content + * @param {Array} imports - Array of import information + * @param {string} filePath - Path to the current MDX file + * @param {string} rootInputDir - Root input directory + * @param {string} rootOutputDir - Root output directory + * @returns {Promise} Processed content with CKDocs components resolved + */ + async processCKDocsComponent(content, imports, filePath, rootInputDir, rootOutputDir) { + if (!regex.ckDocs.test(content)) { + return content; // Skip if no CKDocs component present + } + + // Reset regex to ensure we start from the beginning + resetRegex(regex.ckDocs); + + // Find the import for CKDocs + const ckDocsImport = imports.find(imp => + imp.names.some(name => name.default === 'CKDocs' || name.name === 'CKDocs') + ); + + if (!ckDocsImport) { + return content.replace(regex.ckDocs, ''); + } + + const importPath = ckDocsImport.path; + const resolvedPath = await fileUtils.resolveImportPath(importPath, filePath); + + if (!resolvedPath) { + return content.replace( + regex.ckDocs, + `` + ); + } + + // Store the CKDocs resolved path for later use in processing example code blocks + config.ckDocsPath = resolvedPath; + + if (config.debug) { + config.logger.log(`Found CKDocs file at: ${resolvedPath}`); + } + + try { + // This function will be defined in fileProcessor.js, so we need to call it from there + // We need to pass this function in as a parameter + const processMdxFile = async ( + mdxFilePath, + outputPath, + rootInputDir, + rootOutputDir, + isImported + ) => { + try { + const cacheKey = config.cacheKeys.processedFile(mdxFilePath); + if (config.cache.has(cacheKey)) { + return config.cache.get(cacheKey); + } + + // Read file content + const content = await fileUtils.loadFileContent(mdxFilePath); + if (!content) { + return ``; + } + + return content; + } catch (error) { + console.error(`Error processing imported MDX file ${mdxFilePath}:`, error.message); + return ``; + } + }; + + // Process the imported MDX file + let importedContent = await processMdxFile( + resolvedPath, + '', // No output path for imported content + rootInputDir, + rootOutputDir, + true + ); + + // Process any imported components in the content + importedContent = await this.processImportedComponents(importedContent, resolvedPath); + + // Process SymbolDoc components in the imported content + if (apiProcessors) { + importedContent = apiProcessors.processSymbolDocComponent(importedContent); + importedContent = apiProcessors.processSymbolDescription(importedContent); + } + + // Process the example code blocks in the imported content + // Note: We need to parse imports from the imported content for this + const {processedContent: _, imports: importedImports} = mdxParser.processImports(importedContent); + importedContent = await this.processExampleCodeBlocks( + importedContent, + importedImports, + resolvedPath, + rootInputDir, + rootOutputDir + ); + + return content.replace(regex.ckDocs, importedContent); + } catch (error) { + console.error(`Error processing CKDocs from ${resolvedPath}:`, error.message); + return content.replace( + regex.ckDocs, + `` + ); + } + }, + + /** + * Processes imported components that need special handling. + * + * @param {string} content - MDX content + * @param {string} filePath - Path to the MDX file + * @returns {Promise} Processed content with imported components converted + */ + async processImportedComponents(content, filePath) { + // Define component patterns and their replacements + const componentPatterns = [ + { + regex: //g, + replacer: (_, file, name) => + `\n## Specifications for ${name}\n\n\n`, + }, + { + regex: /([^<]+)<\/LegacyPatternLink>/g, + replacer: (_, href, text) => `[${text}](${href})`, + }, + { + regex: /]*)>([^<]*)<\/Suggestion>/g, + replacer: (_, attrs, content) => { + // This would need the mdxParser to be passed in + const attributes = {status: '', guidance: ''}; // Simplified + return `\n**${attributes.status ? attributes.status.toUpperCase() + ': ' : ''}${ + attributes.guidance + }**\n\n${content}\n`; + }, + }, + { + regex: /([^]*?)<\/SideBySide>/g, + replacer: (_, content) => `\n## Side by Side Comparison\n\n${content}\n`, + }, + ]; + + // Apply each pattern + return componentPatterns.reduce( + (currentContent, pattern) => currentContent.replace(pattern.regex, pattern.replacer), + content + ); + }, + + /** + * Processes ExampleCodeBlock components to include example code snippets. + * + * @param {string} content - MDX content + * @param {Array} imports - Array of import information from the MDX parser + * @param {string} filePath - Path to the current MDX file + * @param {string} rootInputDir - Root input directory + * @param {string} rootOutputDir - Root output directory + * @returns {Promise} Processed content with example code blocks included + */ + async processExampleCodeBlocks(content, imports, filePath, rootInputDir, rootOutputDir) { + // Reset regex to ensure we start from the beginning + resetRegex(regex.exampleCodeBlock); + + // Build an imports map from the parsed imports array + const importsMap = {}; + for (const importInfo of imports) { + const importPath = importInfo.path; + for (const nameInfo of importInfo.names) { + const componentName = nameInfo.default || nameInfo.alias || nameInfo.name; + if (componentName) { + importsMap[componentName] = { path: importPath, originalName: nameInfo.name || componentName }; + } + } + } + + if (config.debug) { + config.logger.log(`Found imports: ${JSON.stringify(Object.keys(importsMap))}`); + } + + let processedContent = content; + let match; + + // Find all ExampleCodeBlock components + while ((match = regex.exampleCodeBlock.exec(content)) !== null) { + const fullMatch = match[0]; + const componentName = match[1].trim(); + // Include file path in cache key to avoid conflicts between different components with same name + const cacheKey = `${config.cacheKeys.exampleCode(componentName)}:${filePath}`; + + // Check cache first + let exampleContent, foundPath; + if (config.cache.has(cacheKey)) { + exampleContent = config.cache.get(cacheKey); + foundPath = config.cache.get(`${cacheKey}:path`) || ''; + } else { + // Get the directory of the current file + const dirPath = path.dirname(filePath); + + // First approach: Use the import information to resolve the file directly + if (importsMap[componentName]) { + const importInfo = importsMap[componentName]; + const importPath = importInfo.path; + + if (config.debug) { + config.logger.log(`Found import for ${componentName}: ${importPath}`); + } + + // Resolve the path correctly based on import type + let resolvedBasePath; + + if (importPath.startsWith('.')) { + // For relative imports, resolve relative to the current file + resolvedBasePath = path.resolve(dirPath, importPath); + } else if (importPath.startsWith('@') || !importPath.startsWith('/')) { + // For package imports, resolve from node_modules + resolvedBasePath = path.resolve(config.nodeModulesPath, importPath); + } else { + // For absolute paths + resolvedBasePath = importPath; + } + + // Check if the resolved path exists + // First try the path as is (in case it's a direct file path) + exampleContent = await fileUtils.loadFileContent(resolvedBasePath); + if (exampleContent) { + foundPath = resolvedBasePath; + } else { + // If direct load fails, try adding extensions + const extensions = ['.tsx', '.jsx', '.ts', '.js']; + for (const ext of extensions) { + const pathWithExt = `${resolvedBasePath}${ext}`; + exampleContent = await fileUtils.loadFileContent(pathWithExt); + if (exampleContent) { + foundPath = pathWithExt; + break; + } + } + } + } + + // Second approach: If the file is from CKDocs, check its examples directory + if (!exampleContent && config.ckDocsPath) { + const ckDocsDir = path.dirname(config.ckDocsPath); + if (config.debug) { + config.logger.log(`Looking for examples in CKDocs directory: ${ckDocsDir}`); + } + + // Try the examples directory in the CKDocs path + const examplesPath = path.join(ckDocsDir, 'examples', componentName); + const extensions = ['.tsx', '.jsx', '.js']; + + for (const ext of extensions) { + try { + const pathWithExt = `${examplesPath}${ext}`; + exampleContent = await fileUtils.loadFileContent(pathWithExt); + if (exampleContent) { + foundPath = pathWithExt; + if (config.debug) { + config.logger.log(`Found example in CKDocs directory: ${foundPath}`); + } + break; + } + } catch (error) { + // Continue to next extension + } + } + } + + // Third approach: Try the examples directory in the current file's path + if (!exampleContent) { + const examplesPath = path.join(dirPath, 'examples', componentName); + const extensions = ['.tsx', '.jsx', '.js']; + + for (const ext of extensions) { + try { + const pathWithExt = `${examplesPath}${ext}`; + exampleContent = await fileUtils.loadFileContent(pathWithExt); + if (exampleContent) { + foundPath = pathWithExt; + if (config.debug) { + config.logger.log(`Found example in current directory: ${foundPath}`); + } + break; + } + } catch (error) { + // Continue to next extension + } + } + } + + // Cache the result if found + if (exampleContent) { + config.cache.set(cacheKey, exampleContent); + config.cache.set(`${cacheKey}:path`, foundPath); + if (config.debug) { + config.logger.log(`Found example code for ${componentName} at ${foundPath}`); + } + } else if (config.debug) { + config.logger.log(`Could not find example code for ${componentName}`); + } + } + + if (exampleContent) { + // Determine the language from the file extension + const extension = path.extname(foundPath).slice(1) || 'tsx'; + + // Replace the ExampleCodeBlock with the code block + processedContent = processedContent.replace( + fullMatch, + codeBlock(exampleContent, extension) + ); + } else { + processedContent = processedContent.replace( + fullMatch, + `` + ); + } + } + + return processedContent; + }, + + /** + * Processes TSX imports to include code blocks. + * + * @param {string} content - MDX content + * @param {Array} imports - Array of import information + * @param {string} filePath - Path to the current MDX file + * @param {string} rootInputDir - Root input directory + * @returns {Promise} Processed content with TSX imports converted to code blocks + */ + async processTsxImports(content, imports, filePath, rootInputDir) { + // Find import statements for TSX files + const tsxImports = imports.filter( + imp => + imp.path.endsWith('.tsx') || imp.path.endsWith('.jsx') || imp.path.includes('/examples/') + ); + + if (tsxImports.length === 0) { + return content; + } + + let processedContent = content; + const dirPath = path.dirname(filePath); + + for (const tsxImport of tsxImports) { + let importPath = tsxImport.path; + let resolvedPath; + + // Resolve the path correctly based on import type + if (importPath.startsWith('.')) { + // For relative imports, resolve relative to the current file + resolvedPath = path.resolve(dirPath, importPath); + } else if (importPath.startsWith('@') || !importPath.startsWith('/')) { + // For package imports, resolve from node_modules + resolvedPath = path.resolve(config.nodeModulesPath, importPath); + } else { + // For absolute paths + resolvedPath = importPath; + } + + // Try to load the file content + let tsxContent; + try { + tsxContent = await fileUtils.loadFileContent(resolvedPath); + } catch (error) { + // If direct loading fails, try to resolve with extensions + const extensions = ['.tsx', '.jsx', '.js']; + for (const ext of extensions) { + if (!resolvedPath.endsWith(ext)) { + try { + const pathWithExt = `${resolvedPath}${ext}`; + tsxContent = await fileUtils.loadFileContent(pathWithExt); + if (tsxContent) { + resolvedPath = pathWithExt; + break; + } + } catch (error) { + // Continue to next extension + } + } + } + } + + // Skip if we couldn't load the content + if (!tsxContent) { + if (config.debug) { + config.logger.log(`Could not load TSX content from ${resolvedPath}`); + } + continue; + } + + // For each imported component, replace its usage with a code block + for (const name of tsxImport.names) { + const componentName = name.default || name.name; + const componentRegex = new RegExp( + `<${componentName}\\s*\\/?>|<${componentName}([^>]*)>([\\s\\S]*?)<\\/${componentName}>`, + 'g' + ); + + const extension = path.extname(resolvedPath).slice(1) || 'tsx'; + if (config.debug) { + config.logger.log(`Replacing component ${componentName} with code block from ${resolvedPath}`); + } + + processedContent = processedContent.replace(componentRegex, () => + codeBlock(tsxContent, extension) + ); + } + } + + return processedContent; + }, + }; +} diff --git a/mdx2md/processors/imageProcessor.js b/mdx2md/processors/imageProcessor.js new file mode 100644 index 0000000000..24ed0a68e4 --- /dev/null +++ b/mdx2md/processors/imageProcessor.js @@ -0,0 +1,84 @@ +/** + * Image Processor Module + * + * Handles image references in MDX content, copying images to the output location + * and updating references to maintain correct paths. + */ + +import path from 'path'; +import {resetRegex} from '../utils/regexPatterns.js'; + +/** + * Creates an image processor with the given dependencies. + * + * @param {Object} config - Configuration object + * @param {Object} regex - Regular expression patterns + * @param {Object} fileUtils - File utilities + * @returns {Object} Image processor functions + */ +export function createImageProcessor(config, regex, fileUtils) { + return { + /** + * Processes image references in content. + * Copies images to the markdown file location and updates references. + * + * @param {string} content - MDX content + * @param {string} filePath - Path to the current MDX file + * @param {string} rootInputDir - Root input directory + * @param {string} rootOutputDir - Root output directory + * @returns {Promise} Processed content with updated image references + */ + async processImages(content, filePath, rootInputDir, rootOutputDir) { + // Reset regex to ensure we start from the beginning + resetRegex(regex.image); + + let match; + let processedContent = content; + const replacements = []; + + // Calculate output file path + const relativePath = path.relative(rootInputDir, filePath); + const outputFilePath = path.join(rootOutputDir, relativePath.replace(/\.mdx$/, '.md')); + const outputDir = path.dirname(outputFilePath); + + // Collect all image references + while ((match = regex.image.exec(content)) !== null) { + const [fullMatch, altText, imagePath] = match; + + // Skip URLs + if (imagePath.startsWith('http')) { + continue; + } + + // Get original image path + const baseDir = path.dirname(filePath); + const fullImagePath = path.resolve(baseDir, imagePath); + + // Get image filename + const imageName = path.basename(imagePath); + + // Destination will be in same directory as markdown file + const destImagePath = path.join(outputDir, imageName); + + // Queue the image for copying + config.pendingImages.push({ + source: fullImagePath, + destination: destImagePath, + }); + + // Use just the filename in the new markdown reference + replacements.push({ + original: fullMatch, + replacement: `![${altText}](${imageName})`, + }); + } + + // Update all image references in the content + for (const {original, replacement} of replacements) { + processedContent = processedContent.replace(original, replacement); + } + + return processedContent; + }, + }; +} diff --git a/mdx2md/processors/index.js b/mdx2md/processors/index.js new file mode 100644 index 0000000000..0442656c7b --- /dev/null +++ b/mdx2md/processors/index.js @@ -0,0 +1,111 @@ +/** + * Component Processors Module + * + * This module consolidates all the component processors used in the MDX to Markdown conversion. + * It serves as a central place to initialize and expose processor functions. + */ + +import { createBasicComponentProcessors } from './basicComponents.js'; +import { createApiComponentProcessors } from './apiComponents.js'; +import { createCodeComponentProcessors } from './codeComponents.js'; +import { createImageProcessor } from './imageProcessor.js'; +import { createTokenComponentProcessors } from './tokenComponents.js'; + +/** + * Creates and initializes all component processors. + * + * @param {Object} config - Configuration object + * @param {Object} regex - Regular expression patterns + * @param {Object} fileUtils - File utilities + * @param {Object} mdxParser - MDX parser utilities + * @returns {Object} Object containing all component processor functions + */ +export function createComponentProcessors(config, regex, fileUtils, mdxParser) { + // Initialize all processor categories + const basicProcessors = createBasicComponentProcessors(config, regex, mdxParser); + const apiProcessors = createApiComponentProcessors(config, regex, mdxParser); + const codeProcessors = createCodeComponentProcessors(config, regex, fileUtils, apiProcessors, mdxParser); + const imageProcessor = createImageProcessor(config, regex, fileUtils); + const tokenProcessors = createTokenComponentProcessors(config, regex, fileUtils); + + return { + /** + * Main method to process all components in MDX content. + * Orchestrates the various component processors in the correct order. + * + * @param {string} content - MDX content to process + * @param {Array} imports - Array of import information + * @param {string} filePath - Path to the current MDX file + * @param {string} rootInputDir - Root input directory + * @param {string} rootOutputDir - Root output directory + * @returns {Promise} Processed content with components converted to Markdown + */ + async processComponents(content, imports, filePath, rootInputDir, rootOutputDir) { + // Process CKDocs component first (can import other content) + let processedContent = await codeProcessors.processCKDocsComponent( + content, + imports, + filePath, + rootInputDir, + rootOutputDir + ); + + // Process other specialized components + processedContent = basicProcessors.processPackageInfoComponent(processedContent); + processedContent = basicProcessors.processInternalContentComponent(processedContent); + processedContent = basicProcessors.processExternalContentComponent(processedContent); + processedContent = basicProcessors.processTabPanelComponent(processedContent); + processedContent = apiProcessors.processSymbolDocComponent(processedContent); + processedContent = apiProcessors.processSymbolDescription(processedContent); + processedContent = basicProcessors.processSpecificationsComponent(processedContent); + processedContent = await tokenProcessors.processBrandTokensComponent( + processedContent, + filePath, + rootInputDir + ); + + processedContent = await tokenProcessors.processColorGridComponent( + processedContent, + filePath, + rootInputDir + ); + + // Process example code blocks + processedContent = await codeProcessors.processExampleCodeBlocks( + processedContent, + imports, + filePath, + rootInputDir, + rootOutputDir + ); + + // Process any TSX imports as code blocks + processedContent = await codeProcessors.processTsxImports( + processedContent, + imports, + filePath, + rootInputDir + ); + + // Process images + processedContent = await imageProcessor.processImages( + processedContent, + filePath, + rootInputDir, + rootOutputDir + ); + + // Filter figma links (should be done last to catch any remaining figma links) + processedContent = basicProcessors.processFigmaLinks(processedContent); + + return processedContent; + }, + + // Export individual processors for direct access if needed + ...basicProcessors, + ...apiProcessors, + ...codeProcessors, + ...imageProcessor, + ...tokenProcessors + }; +} \ No newline at end of file diff --git a/mdx2md/processors/tokenComponents.js b/mdx2md/processors/tokenComponents.js new file mode 100644 index 0000000000..9074329f7e --- /dev/null +++ b/mdx2md/processors/tokenComponents.js @@ -0,0 +1,444 @@ +/** + * Token Components Processor Module + * + * Handles design token-related MDX components such as BrandTokens and ColorGrid. + * Processes color token information and formats it for Markdown output. + */ + +import fs from 'fs/promises'; +import path from 'path'; +import {resetRegex} from '../utils/regexPatterns.js'; +import {generateMarkdownTable} from '../utils/markdownUtils.js'; + +/** + * Creates token component processors with the given dependencies. + * + * @param {Object} config - Configuration object + * @param {Object} regex - Regular expression patterns + * @param {Object} fileUtils - File utilities + * @returns {Object} Token component processor functions + */ +export function createTokenComponentProcessors(config, regex, fileUtils) { + /** + * Utility function - Converts RGB values to a hex color code. + * + * @param {number} r - Red value (0-255) + * @param {number} g - Green value (0-255) + * @param {number} b - Blue value (0-255) + * @returns {string} - Hex color code + */ + function rgbToHex(r, g, b) { + return ( + '#' + + [r, g, b] + .map(x => { + const hex = x.toString(16); + return hex.length === 1 ? '0' + hex : hex; + }) + .join('') + ); + } + + /** + * Gets the color value for a token. + * This will try to find the actual color value by analyzing CSS files. + * + * @param {string} category - Token category (primary, error, etc.) + * @param {string} key - Token key (base, light, etc.) + * @param {Object} colorValues - Color values object + * @returns {string} Color value or CSS variable reference + */ + function getColorValueForToken(category, key, colorValues) { + // If we have actual color values, use them + if (colorValues && colorValues[category] && colorValues[category][key]) { + return colorValues[category][key]; + } + + // Otherwise, return the CSS variable reference + return `var(--cnvs-brand-${category}-${key})`; + } + + /** + * Gets the table headings from the ColorGrid component. + * + * @param {string} rootInputDir - Root input directory + * @returns {Promise} Array of table headings + */ + async function getColorGridHeadings(rootInputDir) { + try { + // Path to the TokensColorsGrid component + const colorsGridPath = path.join( + rootInputDir, + '../src/components/content/tokens/TokensColorsGrid.tsx' + ); + + // Default headings in case we can't read the file + let headings = ['Swatch', 'CSS Variable', 'JS Variable', 'Value']; + + try { + // Read the file content + const fileContent = await fs.readFile(colorsGridPath, 'utf-8'); + + // Extract the headings from the ColorGrid component + const headingsMatch = /headings=\{(?:\s*)\[(.*?)\](?:\s*)\}/s.exec(fileContent); + if (headingsMatch && headingsMatch[1]) { + // Parse the headings array + const headingsStr = headingsMatch[1].replace(/'/g, '"'); + headings = headingsStr + .split(',') + .map(h => h.trim().replace(/["']/g, '')) + .filter(h => h); // Remove empty entries + } + } catch (error) { + config.logger.warn('Could not read TokensColorsGrid.tsx, using default headings'); + } + + return headings; + } catch (error) { + console.error('Error getting color grid headings:', error); + return ['Swatch', 'CSS Variable', 'JS Variable', 'Value']; + } + } + + return { + /** + * Processes BrandTokens components and dynamically generates markdown tables. + * + * @param {string} content - MDX content + * @param {string} filePath - Path to the current MDX file + * @param {string} rootInputDir - Root input directory + * @returns {Promise} Processed content with BrandTokens components converted + */ + async processBrandTokensComponent(content, filePath, rootInputDir) { + // Skip if no BrandTokens components + if (!regex.brandTokens.test(content)) { + return content; + } + + // Reset regex to ensure we start from the beginning + resetRegex(regex.brandTokens); + + try { + // Initialize brand tokens structure + let brandTokens = {}; + const nodeModulesPath = path.resolve(process.cwd(), 'node_modules'); + + // Paths to Canvas tokens CSS files + const brandCssPath = path.join( + nodeModulesPath, + '@workday/canvas-tokens-web/css/brand/_variables.css' + ); + const baseCssPath = path.join( + nodeModulesPath, + '@workday/canvas-tokens-web/css/base/_variables.css' + ); + + // Cache for color values from CSS files + const colorValues = {}; + + // Get table headings from TokensColorsGrid component + const tableHeadings = await getColorGridHeadings(rootInputDir); + + // Try to load the CSS files to get the color values + try { + // Read the CSS files + const brandCssContent = await fs.readFile(brandCssPath, 'utf-8'); + const baseCssContent = await fs.readFile(baseCssPath, 'utf-8'); + + // Parse the base CSS variables to get color values + const baseVars = {}; + const baseVarRegex = + /--cnvs-base-palette-([a-z-]+)-(\d+):\s*rgba\((\d+),(\d+),(\d+),\d+\);/g; + let baseMatch; + + while ((baseMatch = baseVarRegex.exec(baseCssContent)) !== null) { + const [, palette, shade, r, g, b] = baseMatch; + const varName = `--cnvs-base-palette-${palette}-${shade}`; + // Convert RGB to hex + const hex = rgbToHex(parseInt(r), parseInt(g), parseInt(b)); + baseVars[varName] = hex; + } + + config.logger.log( + `Successfully loaded ${Object.keys(baseVars).length} base color values` + ); + + // Parse the brand CSS variables + const brandVarRegex = + /--cnvs-brand-([a-z-]+)-([a-z-]+):\s*(?:var\(([^)]+)\)|rgba\(([^)]+)\));/g; + let brandMatch; + + while ((brandMatch = brandVarRegex.exec(brandCssContent)) !== null) { + const [, category, key, varRef, rgbaVal] = brandMatch; + + // Create category in the structure if it doesn't exist + if (!brandTokens[category]) { + brandTokens[category] = {}; + colorValues[category] = {}; + } + + // Store the CSS variable name + const cssVar = `--cnvs-brand-${category}-${key}`; + brandTokens[category][key] = cssVar; + + // Try to determine the actual color value + if (varRef && baseVars[varRef]) { + // It's a reference to a base variable and we have the value + colorValues[category][key] = baseVars[varRef]; + } else if (rgbaVal) { + // It's an RGBA value + const rgbaParts = rgbaVal.split(',').map(p => parseInt(p.trim())); + if (rgbaParts.length >= 3) { + const [r, g, b] = rgbaParts; + colorValues[category][key] = rgbToHex(r, g, b); + } + } + // If we couldn't determine the value, we'll use a fallback in getColorValueForToken + } + + // Handle special case for gradients + const gradientMatch = brandCssContent.match(/--cnvs-brand-gradient-primary:[^;]+;/); + if (gradientMatch) { + if (!brandTokens.gradient) { + brandTokens.gradient = {}; + colorValues.gradient = {}; + } + brandTokens.gradient.primary = '--cnvs-brand-gradient-primary'; + colorValues.gradient.primary = + 'linear-gradient(90deg, var(--cnvs-brand-primary-base), var(--cnvs-brand-primary-dark))'; + } + + config.logger.log( + `Successfully loaded brand token structure with ${ + Object.keys(brandTokens).length + } categories` + ); + } catch (error) { + console.warn( + 'Could not load Canvas token CSS files, will use variable references.', + error.message + ); + + // Create a basic structure with common brand token categories + const categories = [ + 'primary', + 'accent', + 'error', + 'alert', + 'success', + 'neutral', + 'gradient', + ]; + const keys = ['lightest', 'light', 'base', 'dark', 'darkest', 'accent']; + + categories.forEach(category => { + brandTokens[category] = {}; + keys.forEach(key => { + brandTokens[category][key] = `--cnvs-brand-${category}-${key}`; + }); + }); + + // Special case for gradient + if (brandTokens.gradient) { + brandTokens.gradient = { + primary: '--cnvs-brand-gradient-primary', + }; + } + } + + // Now replace the BrandTokens component with the markdown tables + return content.replace(regex.brandTokens, () => { + let markdown = ''; + + // Generate a markdown table for each brand token category + for (const [category, shades] of Object.entries(brandTokens)) { + if (Object.keys(shades).length === 0) continue; + + const categoryName = category.charAt(0).toUpperCase() + category.slice(1); + markdown += `\n### ${categoryName}\n\n`; + + // Filter out the "Swatch" column if it exists + const filteredHeadings = tableHeadings.filter(heading => heading !== 'Swatch'); + + // Sort token keys alphabetically + const sortedKeys = Object.keys(shades).sort(); + + // Prepare rows for the table + const rows = sortedKeys.map(key => { + // The CSS variable name is stored in the brandTokens object + const cssVar = shades[key]; + const jsVar = `brand.${category}.${key}`; + + // Get the color value from our parsed data + const value = getColorValueForToken(category, key, colorValues); + + return { + cssVar, + jsVar, + value, + }; + }); + + // Generate the table + markdown += generateMarkdownTable(filteredHeadings, rows, ['cssVar', 'jsVar', 'value']); + markdown += '\n'; + } + + return markdown; + }); + } catch (error) { + console.error('Error processing BrandTokens component:', error); + // Return a placeholder in case of error + return content.replace( + regex.brandTokens, + '' + ); + } + }, + + /** + * Processes ColorGrid components and converts them to markdown tables. + * + * @param {string} content - MDX content + * @param {string} filePath - Path to the current MDX file + * @param {string} rootInputDir - Root input directory + * @returns {Promise} Processed content with ColorGrid components converted + */ + async processColorGridComponent(content, filePath, rootInputDir) { + // Skip if no ColorGrid components + if (!regex.colorGrid.test(content)) { + return content; + } + + // Reset regex to ensure we start from the beginning + resetRegex(regex.colorGrid); + + try { + // Get table headings dynamically + const tableHeadings = await getColorGridHeadings(rootInputDir); + + // Load base palette colors from CSS + const nodeModulesPath = path.resolve(process.cwd(), 'node_modules'); + const baseCssPath = path.join( + nodeModulesPath, + '@workday/canvas-tokens-web/css/base/_variables.css' + ); + const baseColorMap = {}; + const colorFamilies = new Set(); // Use a Set to track unique color families + + try { + // Read the base CSS content + const baseCssContent = await fs.readFile(baseCssPath, 'utf-8'); + + // Parse the base CSS variables to get color values + const baseVarRegex = + /--cnvs-base-palette-([a-z-]+)-(\d+):\s*rgba\((\d+),(\d+),(\d+),\d+\);/g; + let baseMatch; + + while ((baseMatch = baseVarRegex.exec(baseCssContent)) !== null) { + const [, palette, shade, r, g, b] = baseMatch; + + // Add the palette to our unique color families + colorFamilies.add(palette); + + const colorName = `${palette}${shade}`; + // Convert RGB to hex + const hex = rgbToHex(parseInt(r), parseInt(g), parseInt(b)); + baseColorMap[colorName] = { + name: `${palette} ${shade}`, + cssVar: `--cnvs-base-palette-${palette}-${shade}`, + jsVar: `base.palette.${palette}.${shade}`, + value: hex, + }; + } + + config.logger.log( + `Successfully loaded ${ + Object.keys(baseColorMap).length + } base color values for ColorGrid` + ); + config.logger.log(`Found ${colorFamilies.size} unique color families`); + } catch (error) { + config.logger.warn('Could not load base colors for ColorGrid, using placeholders'); + } + + // Process each ColorGrid component + return content.replace(regex.colorGrid, (match, colorsVariable) => { + // Parse the colorsVariable to determine what to do + const isJsonVariable = colorsVariable.trim().endsWith('Json'); + + if (isJsonVariable) { + // This is likely using imported JSON (e.g., colorsJson) + // Create a descriptive table with placeholder + let markdown = '## All Colors\n\n'; + markdown += + 'The color palette contains a comprehensive set of colors organized by family.\n'; + markdown += 'Each color includes multiple shades from lightest to darkest.\n\n'; + + // Convert the Set to an array and sort alphabetically + const sortedFamilies = Array.from(colorFamilies).sort(); + + // If we didn't find any color families, use a small default set + if (sortedFamilies.length === 0) { + sortedFamilies.push('blueberry', 'cantaloupe', 'cinnamon', 'soap', 'french-vanilla'); + } + + // Create a sample table for each color family + for (const familyKey of sortedFamilies) { + // Format the family name for display (capitalize, replace hyphens with spaces) + const familyName = familyKey + .split('-') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + + // Get all the shades for this family + const familyShades = Object.keys(baseColorMap) + .filter(key => key.startsWith(familyKey)) + .map(key => key.replace(familyKey, '')) + .sort((a, b) => parseInt(a) - parseInt(b)); + + // Skip if no shades were found (shouldn't happen, but just in case) + if (familyShades.length === 0) continue; + + markdown += `### ${familyName}\n\n`; + // Filter out the "Swatch" column if it exists + const filteredHeadings = tableHeadings.filter(heading => heading !== 'Swatch'); + + // Display all shades for this color family + const rows = familyShades.map(shade => { + const colorKey = `${familyKey}${shade}`; + const color = baseColorMap[colorKey] || { + cssVar: `--cnvs-base-palette-${familyKey}-${shade}`, + jsVar: `base.palette.${familyKey}.${shade}`, + value: '#CCCCCC', // Placeholder color + }; + + return { + cssVar: color.cssVar, + jsVar: color.jsVar, + value: color.value, + }; + }); + + // Generate the table + markdown += generateMarkdownTable(filteredHeadings, rows, [ + 'cssVar', + 'jsVar', + 'value', + ]); + markdown += '\n'; + } + + return markdown; + } else { + // It's a different variable, use a generic placeholder + return '## Color Grid\n\nA collection of colors organized in a grid format. Refer to the Canvas API documentation for details on available colors.\n'; + } + }); + } catch (error) { + console.error('Error processing ColorGrid component:', error); + return content.replace(regex.colorGrid, ''); + } + }, + }; +} diff --git a/mdx2md/tsconfig.json b/mdx2md/tsconfig.json new file mode 100644 index 0000000000..031a6cbc85 --- /dev/null +++ b/mdx2md/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "node", + "esModuleInterop": true, + "allowJs": true, + "checkJs": false, + "noEmit": true + }, + "include": ["*.js"], + "exclude": ["node_modules"] +} diff --git a/mdx2md/utils/formatters.js b/mdx2md/utils/formatters.js new file mode 100644 index 0000000000..50b8d286aa --- /dev/null +++ b/mdx2md/utils/formatters.js @@ -0,0 +1,128 @@ +/** + * Formatters Module + * + * Provides utility functions for formatting different types of data + * for Markdown output, especially for API documentation. + */ + +/** + * Formats a prop type for display in Markdown. + * + * @param {Object} type - Prop type object + * @returns {string} Formatted type string + */ +export function formatPropType(type) { + if (!type) return 'unknown'; + + // Enhanced type formatting based on the original implementation + switch (type.kind) { + case 'primitive': { + return `\`${type.value}\``; + } + case 'string': { + return `\`"${type.value}"\``; + } + case 'boolean': { + return `\`${type.value}\``; + } + case 'number': { + return `\`${type.value}\``; + } + case 'symbol': { + return `\`${type.name}\``; + } + case 'union': { + if (type.value && Array.isArray(type.value)) { + return type.value.map(t => formatPropType(t)).join(' | '); + } + return '`union`'; + } + case 'array': { + if (type.value) { + return `${formatPropType(type.value)}[]`; + } + return '`Array`'; + } + case 'function': { + let fnSignature = '`function`'; + if (type.parameters && type.returnType) { + // Format parameters + const params = type.parameters + .map(p => `${p.name}${p.required ? '' : '?'}: ${formatPropType(p.type)}`) + .join(', '); + + // Format return type + const returnType = formatPropType(type.returnType); + + fnSignature = `\`(${params}) => ${returnType}\``; + } + return fnSignature; + } + case 'external': { + return type.url ? `[\`${type.name}\`](${type.url})` : `\`${type.name}\``; + } + case 'intersection': { + if (type.value && Array.isArray(type.value)) { + return type.value.map(t => formatPropType(t)).join(' & '); + } + return '`intersection`'; + } + case 'generic': { + let genericType = `\`${type.name}\``; + if (type.typeParameters && type.typeParameters.length) { + const typeParams = type.typeParameters.map(t => formatPropType(t)).join(', '); + genericType = `\`${type.name}<${typeParams}>\``; + } + return genericType; + } + case 'parenthesis': { + // Handle parenthesized types (typically function signatures wrapped in parentheses) + if (type.value) { + return formatPropType(type.value); + } + return '`parenthesis`'; + } + case 'typeParameter': { + return `\`${type.name}\``; + } + default: { + return `\`${type.kind}\``; + } + } +} + +/** + * Formats a default value for display in Markdown. + * + * @param {Object} prop - Prop object + * @returns {string} Formatted default value string + */ +export function formatDefaultValue(prop) { + // No default value provided + if (!prop.defaultValue) { + // Check if there's a default in tags + return prop.tags && prop.tags.default ? `\`${prop.tags.default}\`` : ''; + } + + // Format based on the default value type + switch (prop.defaultValue.kind) { + case 'string': + return `\`"${prop.defaultValue.value}"\``; + + case 'boolean': + case 'number': + return `\`${prop.defaultValue.value}\``; + + case 'symbol': + return `\`${prop.defaultValue.value}\``; + + case 'function': + return '`() => {...}`'; + + case 'external': + return `\`${prop.defaultValue.name}\``; + + default: + return `\`${prop.defaultValue.kind}\``; + } +} diff --git a/mdx2md/utils/markdownUtils.js b/mdx2md/utils/markdownUtils.js new file mode 100644 index 0000000000..aeac06f045 --- /dev/null +++ b/mdx2md/utils/markdownUtils.js @@ -0,0 +1,59 @@ +/** + * Markdown Utilities Module + * + * Provides utility functions for working with Markdown content. + */ + +/** + * Normalize Whitespace in Markdown Content + * + * Cleans up and standardizes whitespace in the generated Markdown content + * to ensure consistent formatting and readability. + * + * @param {string} markdown - The raw Markdown content to be normalized + * @returns {string} The normalized Markdown content with standardized whitespace + */ +export function normalizeWhitespace(markdown) { + return ( + markdown + // Replace 3+ consecutive newlines with 2 newlines + .replace(/\n{3,}/g, '\n\n') + // Remove trailing whitespace on each line + .replace(/[ \t]+$/gm, '') + // Ensure single newline after frontmatter + .replace(/---\n\n+/, '---\n\n') + // Ensure exactly one newline at the end of file + .replace(/\n+$/, '\n') + ); +} + +/** + * Generates a Markdown table from data + * + * @param {string[]} headers - Table headers + * @param {Array} rows - Array of objects representing rows + * @param {string[]} fields - Fields to include from each object + * @returns {string} Formatted Markdown table + */ +export function generateMarkdownTable(headers, rows, fields) { + let table = `| ${headers.join(' | ')} |\n`; + table += `| ${headers.map(() => '---').join(' | ')} |\n`; + + rows.forEach(row => { + const values = fields.map(field => row[field] || ''); + table += `| ${values.join(' | ')} |\n`; + }); + + return table; +} + +/** + * Creates a Markdown code block + * + * @param {string} code - The code content + * @param {string} language - Code language for syntax highlighting + * @returns {string} Formatted code block + */ +export function codeBlock(code, language = '') { + return `\`\`\`${language}\n${code}\n\`\`\``; +} diff --git a/mdx2md/utils/regexPatterns.js b/mdx2md/utils/regexPatterns.js new file mode 100644 index 0000000000..172182e50b --- /dev/null +++ b/mdx2md/utils/regexPatterns.js @@ -0,0 +1,44 @@ +/** + * Regex Patterns Module + * + * Centralizes regex patterns used throughout the MDX to Markdown conversion process. + * Pre-compiles frequently used patterns for better performance. + */ + +export const REGEX = { + // Document structure + frontmatter: /^---\n([\s\S]*?)\n---\n/, + importStatement: /import\s+([^;]*?)\s+from\s+['"]([^'"]+)['"](\s*;)?/g, + + // Components + exampleCodeBlock: //g, + symbolDoc: /]*)\/>/g, + symbolDesc: /]*)\/>/g, + packageInfo: /]*)\/>/g, + internalContent: /([\s\S]*?)<\/InternalContent>/g, + externalContent: /([\s\S]*?)<\/ExternalContent>/g, + tabPanel: /]*)>([\s\S]*?)<\/TabPanel>/g, + specification: //g, + + // Resources and references + image: /!\[(.*?)\]\((.*?)\)/g, + ckDocs: //g, + + // Design tokens + brandTokens: //g, + colorGrid: //g, +}; + +// Utility function to reset regex lastIndex properties +export function resetRegex(regex) { + if (regex) regex.lastIndex = 0; +} + +// Export reset function for all patterns at once +export function resetAllRegex() { + Object.values(REGEX).forEach(regex => { + if (regex && typeof regex.lastIndex !== 'undefined') { + regex.lastIndex = 0; + } + }); +} diff --git a/modules/docs/llm/tokens/color-contrast.md b/modules/docs/llm/tokens/color-contrast.md new file mode 100644 index 0000000000..cf5cccfbdb --- /dev/null +++ b/modules/docs/llm/tokens/color-contrast.md @@ -0,0 +1,125 @@ +--- +title: Color Contrast +preamble: true +platform: web, ios, android +description: +tags: color, contrast, accessibility +rank: 6 +source_file: guidelines/color/color-contrast.mdx +live_url: https://canvas.workdaydesign.com/guidelines/color/color-contrast +--- + +Color pairings should pass contrast requirements to ensure content is readable for everyone. Colors +in the palette are built with contrast baked in to make it simple to create accessible color +combinations. + +Canvas believes that designing for accessible experience benefits everyone. Improved contrast +improves readability in bright sunlight, low-quality displays, and for users experiencing temporary +vision impairment. Using the color system can help reduce the guesswork in choosing colors and +ensure contrast ratios when applied. + +## Understanding Contrast + +Contrast measures the difference in brightness between two colors, ensuring that content is +perceivable and readable against backgrounds. WCAG 2.1 guidelines specify a minimum contrast ratio +of 4.5:1 for text, 3:1 for interactive elements, and 7:1 for high contrast. The +[global palette](/guidelines/color/color-palette) and [tonal scale](/guidelines/color/color-scale) +are designed with target contrast ratios baked in, making it straightforward to achieve compliant +color combinations without a separate calculator. + +![Color contrast demonstration showing different step differences and their compliance levels](color-contrast-overview.png) + +![Text examples showing AA compliance (4.5:1) with black text on light gray, and AAA compliance (7:1) with white text on dark blue backgrounds](color-contrast-text-on-white.png) + +## Usage Guidance + +1. **Use [color roles](/guidelines/color/color-roles)** to guarantee accessibility through design + tokens. +2. **Use the contrast framework**: If you need to choose colors from directly from the palette, make + sure to choose accessible color combinations. Use the + [contrast framework](#accessible-color-combinations) to make it easy to select accessible pairs + just from the step number. +3. **Check your designs** against color-blindness and low-vision simulators to get a feel for what + your design might look like in different scenarios. +4. **Avoid the use of color alone** to communicate information. + +![Color role examples showing bg-primary and text-primary automatically maintaining proper contrast ratios across light and dark themes](color-contrast-color-blind.png) + +## Accessible Color Combinations + +Colors are graded using a 15 step tonal scale. Each step is assigned to a number that represents the +lightness of that color relative to other colors in the scale. For example, 0 is the lightest color +in the scale (white), and 1000 is the darkest color (black). A 500 color would have a lightness +value between the two, with lighter and darker variations on both sides. To determine if a color +will pass contrast, compare step numbers and the difference between the two. + +Regular text requires a 500+ step difference to achieve the 4.5:1 ratio needed for AA compliance. +For enhanced accessibility, text can use a 700+ step difference to achieve the 7:1 ratio that +exceeds AAA compliance standards. For non-text contrast, a 400+ step difference is needed (if both +colors have step numbers greater than 200+|) + +### Overview + +| **Content** | **WCAG Level** | **Target Ratio** | **Step Difference** | **Example** | +| ------------ | -------------- | ---------------- | ------------------- | -------------------------- | +| **Text** | AA | 4.5:1 | 500+ | `slate-100` on `slate-600` | +| **Text** | AAA | 7:1 | 700+ | `slate-100` on `slate-800` | +| **Non-text** | AA | 3:1 | 400+ (>200) | `blue-600` on `blue-100` | +| **Non-text** | AAA | 4.5:1 | 500+ | `blue-100` on `blue-600` | + +### Text Contrast + +Normal sized text should have at least at 4.5:1 contrast to meet Level AA compliance. A difference +of 500 or more between steps guarentees it passes text contrast guidelines. + +![Step difference examples showing 500+ differences guarantee 4.5:1 text contrast across various background and text color combinations](color-contrast-text-between.png) + +| **Background** | **Foreground Step** | **Step Difference** | **Compliance Level** | +| -------------- | ------------------- | ------------------- | -------------------- | +| 0(white) | 600 | 600 | AA Text | +| 100 | 600 | 500 | AA Text | +| 200 | 700 | 500 | AA Text | +| 300 | 800 | 500 | AA Text | +| 400 | 900 | 500 | AA Text | +| 500 | 1000 (black) | 500 | AA Text | + +### Non-text Contrast + +Interactive elements and non-decorative visuals (icons) should have a contrast of 3:1 to meet Level +AA compliance. + +A difference of 400 or more between steps guarantees a contrast of 3:1 for steps greater than 200. + +![Interactive elements showing buttons, form inputs, and icons meeting 3:1 contrast requirements with 400+ step differences](color-contrast-nontext-between.png) + +| **Background** | **Foreground Step** | **Step Difference** | **Compliance Level** | +| -------------- | ------------------- | ------------------- | -------------------- | +| 0(white) | 500 | 500 | AA Non-text | +| 25 | 500 | 475 | AA Non-text | +| 200 | 600 | 400 | AA Non-text | +| 300 | 700 | 400 | AA Non-text | +| 400 | 800 | 400 | AA Non-text | +| 500 | 900 | 400 | AA Non-text | +| 600 | 1000 (black) | 400 | AA Non-text | + +### High Contrast (> 7:1) + +Level AAA contrast should be targeted when you are designing for low vision or colorblindness. For +both text and non-text contrast, this means the target difference is increased (text increases to +700+, non-text increases to 500+). + +![High contrast interface examples showing AAA compliance with 7:1+ ratios for enhanced accessibility and low vision support](color-contrast-nontext-on-white.png) + +| **Background** | **AA Contrast** | **AAA Contrast** | **AA Difference** | **AAA Difference** | +| -------------- | --------------- | ---------------- | ----------------- | ------------------ | +| 0(white) | 700 | 500 | 700 | 500 | +| 100 | 800 | 600 | 700 | 500 | +| 200 | 900 | 700 | 700 | 500 | +| 300 | 1000 (black) | 800 | 700 | 500 | +| 400 | 1000 (black) | 900 | 600 | 500 | +| 500 | 0(white) | 0(white) | 500 | 500 | +| 600 | 0(white) | 0(white) | 600 | 600 | +| 700 | 0(white) | 100 | 700 | 600 | +| 800 | 100 | 200 | 700 | 600 | +| 900 | 200 | 300 | 700 | 600 | +| 1000 (black) | 300 | 400 | 700 | 600 | diff --git a/modules/docs/llm/tokens/color-palette.md b/modules/docs/llm/tokens/color-palette.md new file mode 100644 index 0000000000..91579e9383 --- /dev/null +++ b/modules/docs/llm/tokens/color-palette.md @@ -0,0 +1,150 @@ +--- +title: Color Palette +tags: hue, base, tokens, palette, global +platform: web, ios, android +preamble: true +description: +rank: 2 +source_file: guidelines/color/color-palette.mdx +live_url: https://canvas.workdaydesign.com/guidelines/color/color-palette +--- + +## Global Palette + +The Canvas palette is Workday's shared color palette meant for use across all products and +platforms. It includes 11 colors and 2 neutrals, each with 13 shades. + +Colors are designed in the okLCH (lightness, chroma, hue) color space to feel perceptually balanced, +meaning that colors with the same step value appear similar in brightness. This uniformity makes +accessible contrast ratios more predictable between steps and helps create smooth transitions when +switching between different contexts, such as themes and modes. + +The global palette extends Workday's brand colors to create functional tones and tints meant for use +in interface and product design. Colors are organized into color families ('blue', 'red', 'green') +and a tonal scale from lightest (0) to darkest (1000), with each step serving a specific role in the +interface. + +Color scales follow a gradual progression in vibrancy, peaking at the midpoint, before decreasing +again. This progression creates softer surface colors at the tailends, and more vibrant accents in +the middle. + +### Accent Colors + +Accent colors are saturated colors - like blue, red, orange, purple. Accent colors are used to draw +attention towards them and should be used sparingly, and for a specific purpose. + + + +- Use accent colors to highlight important information +- Use existing color roles to inform selection +- Use accent colors sparingly +- for important actions, alerts, or states + + + + + +- Avoid decorative use of accent colors +- Avoid applying accent colors to secondary or supporting content + + + +### Neutral Colors + +Neutrals are greyscale colors, used for backgrounds, foregrounds, or as a base for alpha colors. The +palette includes 2 neutrals meant for different purposes. + +- Slate is a tinted neutral built from Workday's brand blue. It's meant for secondary backgrounds, + borders, and text for subtle styling that pairs well with brand accents. +- Neutral is an achromatic greyscale with no hue or chroma, from white to black. It's meant for + consistent contrast against backgrounds, such as prose text. + + + +- Use Neutral for prose and structural content - Use Slate for secondary UI elements, borders, and + text + + + + + +- Use Neutral and Slate interchangeably + + + +### Alpha Colors + +Alpha colors are colors with transparency that adaptive dynamically to background colors. This +creates a natural layering effect on the background without requiring new color definitions for each +background. + + + +- Use alpha colors when elements need to adapt to different backgrounds - Use alpha colors in a + consistent way throughout the experience - Ensure sufficient contrast for underlying content + + + +## Brand Palette + +Brand colors represent Workday's core brand and identity. Reserve use for brand moments, marketing +materials, and other use cases specific to the Workday brand. + +For more information, see +[Workday's brand guidelines](https://brand.workday.com/document/89#/-/workday-brand-guide). + +## Product Palette + +The product palette is a subset of the global palette meant for use when designing Workday features +and interfaces. Colors in the product palette are assigned +[color roles](/guidelines/color/color-roles) through the use of +[design tokens](/styles/tokens/color). + +### Color Roles + +Color roles assign a purpose to colors in the palette, specifying when and how to use that color. +When building Workday interfaces, use system design tokens to ensure that color roles are +consistently applied throughout the admin. For example, the `positive` color role is used to +indicate success and task completion. + +To learn more about color roles, see [Color Roles](/guidelines/color/color-roles). + +### Design Tokens + +Design tokens represent a design decision as structured data. Tokens store visual properties like +color and spacing in a platform-agnostic format, such as JSON. Those decisions are then transformed +into platform specific pacakges in a format ready to be consumed by each platform. + +Defining colors as tokens removes the need to redefine colors for each platform. Instead, colors are +defined in a single location and distributed to each platform, enabling more consistent, +maintainable, and systemic change over time. + +#### Base Tokens + +Base tokens are static colors whose values do not change. All colors in the +[global palette](#global-palette) are represented as base tokens. Base colors are organized using a +tonal scale, from lightest to darkest. + +#### Brand Tokens + +Brand tokens are used to enable brand/tenant level theming. This means that colors are subject to +change depending on the customer's brand. + +They are not connected to system tokens, which are meant for application-wide use. + +For a full list of brand tokens, see [Brand Tokens](/styles/tokens/color#brand-color-tokens). + +#### System Tokens + +System tokens applies a role to a color. For example, `sys.color.bg.critical.default` can be read as +"the default background color for critical elements". + +System colors are mapped to Base colors. For example, `sys.color.bg.critical.default` is mapped to +`base.palette.red.600`. This is how theming is applied. Creating a new theme means that the value of +the system color is swapping with another color. + +Approaching color this way makes the process part of a larger system instead of an isolated choice. +Instead of asking "what color should this be?", the question becomes "what role does this element +play in the interface?". + +For a full list of system tokens, see [System Tokens](/styles/tokens/color#system-color-tokens). diff --git a/modules/docs/llm/tokens/color-roles.md b/modules/docs/llm/tokens/color-roles.md new file mode 100644 index 0000000000..7bad4f0273 --- /dev/null +++ b/modules/docs/llm/tokens/color-roles.md @@ -0,0 +1,529 @@ +--- +title: Color Roles +tags: color, roles, accessibility +platform: web, iOS, Android +preamble: true +description: +rank: 3 +source_file: guidelines/color/color-roles.mdx +live_url: https://canvas.workdaydesign.com/guidelines/color/color-roles +--- + +Color roles provide a systematic approach to applying color across interfaces. Each role serves a +specific purpose and includes modifiers to control visual emphasis and hierarchy. + +![Color roles system overview showing all semantic roles mapped to interface components with proper contrast relationships](color-roles-system-overview.png) + +Canvas components use standardized color roles for visual consistency. Whether building standard or +custom components, follow these role definitions for predictable results. + +Color roles guarantee accessible combinations—each pairing meets WCAG 2.1 AA standards. Roles are +implemented as system design tokens, creating a shared language between design and development teams +that enables theming and customization. + +## How to Apply Colors + +Use color roles instead of raw palette colors to ensure consistency and accessibility at scale. +Color roles map semantic meaning to specific colors through system tokens. + +The product palette assigns roles to these color families: `red` (critical), `blue` (primary), +`green` (positive), `amber` (caution), `slate`, and `neutral`. + +## Design Tokens + +Design tokens like `bg.primary.soft` shift the question from "what color should this be?" to "what +role does this element fill?" This prevents isolated color decisions and ensures color is being +applied as large of a larger system. + +When possible, always use system tokens to apply colors. Avoid raw palette colors to prevent +inconsistencies in the app and potential accessibility violations. + +System tokens follow a three-part naming structure: `{property}-{role}-{modifier}` + +![System token naming convention breakdown showing property-role-modifier structure](color-roles-token-naming-convention.png) + +- **Property**: What the color applies to (`bg`, `fg`, `text`, `border`) +- **Role**: How to use the color (`primary`, `positive`, `caution`) +- **Modifier**: Intensity or variation (`default`, `strong`, `stronger`) + +For example, `bg-primary-soft` can be read as a "soft primary background color". + +## Usage Guidance + +When choosing colors, consider: + +- **What** will the color be applied to (property) +- **Why** color should be used in the first place (role) +- **How** much emphasis is needed for the color (modifier) + +## Properties + +![Token naming convention showing property highlighted in bg-primary-default structure](color-roles-property-highlighted.png) + +Properties indicate what the color should be applied to, like a background or icon. `Static` and +`brand` tokens may be applied to any property. + +| **Property** | **Description** | +| ------------ | ------------------------------------------------------------------------------- | +| `bg` | Background colors for surfaces and containers | +| `fg` | Foreground colors that exist on top of backgrounds, inclusive of text and icons | +| `text` | Used for text elements that need differentiation from icon colors | +| `icon` | Used for icons that need differentiation from text colors | +| `border` | Used for borders and dividers | +| `shadow` | Used for box-shadows | +| `static` | Static colors are non-dynamic colors that don't change | +| `brand` | Brand indicates colors that are tenant themeable | + +### Background Colors + +Background colors are used to create the foundation for other elements. Use these for page +backgrounds, card surfaces, and container fills. All content, such as text, buttons, forms, and +icons should be on top of a background. + +### Foreground Colors + +Foreground colors are applied to elements that appear on top of backgrounds, inclusive of both text +and icons. + +### Text Colors + +Text colors should be used for standalone text, for example, headings, body text, and labels. Use +these colors intead of foreground colors when text colors may need to be different than the icon +color. + +### Icon Colors + +Icon colors may be used for System Icons if text elements are not used. Use these for interface +icons, indicators, and symbols. + +### Border Colors + +Border colors are used to define boundaries against a background, or to separate content areas. + +### Shadow Colors + +Shadow colors are used in Depth tokens to create a sense of depth and hierarchy between surfaces. + +### Static Colors + +Static colors maintain consistent colors regardless of tenant theming. + +### Brand Colors + +Brand colors are tenant-themeable colors that are subject to change. These colors are used to match +a tenant's brand identity. + +## Roles + +![Token naming convention showing role highlighted in bg-primary-default structure](color-roles-role-highlighted.png) + +Roles define colors by assigning semantic purpose to colors rather than categorizing them by +appearance. Each role is a collection of tokens that specify when and how to use colors in the +interface. Choose roles based on purpose, not preference. + +Role can be grouped into the following categories: + +- **Interactive**: `primary`, `focus` — guide user actions +- **Status**: `positive`, `caution`, `critical` — communicate system feedback +- **Hierarchy**: `alt`, `muted`, `contrast` — level of emphasis +- **Functional**: `disabled`, `translucent`, `overlay` — serve specific interface needs + +Some roles like `default` include tokens for all properties. Others like `overlay` include only +tokens relevant to a specific use case. + +| **Role** | **Description** | +| ------------- | ------------------------------------------------------------------- | +| `default` | Baseline color for that property or role, like `bg.primary.default` | +| `primary` | Brand color for main actions and interactive elements | +| `alt` | Secondary surfaces and alternate states | +| `muted` | Subtle elements with reduced emphasis | +| `positive` | Success states and completion feedback | +| `caution` | Warning messages and non-blocking alerts | +| `critical` | Error states and destructive actions | +| `inverse` | High contrast text on dark backgrounds | +| `contrast` | Enhanced contrast on default backgrounds | +| `hint` | Placeholder and helper text | +| `disabled` | Non-interactive and unavailable elements | +| `static` | Theme-independent colors | +| `error` | Blocking errors and failed states | +| `info` | Informational messages and notifications | +| `ai` | AI-powered features and content | +| `focus` | Keyboard navigation indicators | +| `transparent` | Invisible elements for spacing | +| `translucent` | Semi-transparent overlays | +| `overlay` | Modal scrims and background dimming | + +### Primary Colors + +Primary brand color for main actions and interactions. Use sparingly for maximum impact. + +![Primary Button component showing primary background and text colors](color-roles-primary-role.png) + + + +- Apply to brand elements and main actions + + + + + +- Use for secondary UI or informational content +- Apply to decorative elements + + + +### Positive Colors + +Success states and completion feedback. Provides immediate confirmation when tasks are completed. + +![Success Alert component with positive background and text colors](color-roles-positive-role.png) + + + +- Use for success messages and completed states +- Apply to confirmation feedback + + + +### Alt Colors + +Alternative styling for secondary elements that need visual distinction from primary elements. + + + +- Use to create subtle differences against the default background color + + + + + +- Use on foregrounds, text or icons +- Use on main actions + + + +### Muted Colors + +Subtle elements with reduced emphasis. Present but not prominent. + + + +- Use on interactive secondary UI borders + + + + + +- Use for main actions +- Apply to non-interactive surfaces + + + +### Caution Colors + +Non-blocking warnings and alerts. Informs the user of potential risk without blocking them. + +![Warning Alert component and TextInput with caution border showing validation error](color-roles-caution-role.png) + + + +- Use for alerts in forms +- Use for non-critical messaging + + + + + +- Use for blocking errors or destructive actions + + + +### Critical Colors + +Blocking errors and destructive actions. Reserved for situations that call for immediate attention. + +![Error Alert component and Destructive Button with critical background and text colors](color-roles-critical-role.png) + + + +- Use for blocking errors and to confirm destructive actions +- Apply to system failures + + + + + +- Use for warnings or informational messages +- Apply to non-destructive actions + + + +### Info Colors + +System announcements and informational messages that provide context or status updates. + + + +- Use for system announcements and status updates +- Apply to informational banners and notices +- Use for contextual information about features or changes + + + + + +- Use for errors, warnings, or success messages +- Apply to instructional or helper content +- Use for placeholder text or form guidance + + + +### AI Colors + +Highlights AI-powered content and features. Use consistently for all AI functionality. + +![AI Badge and Button components with AI-specific background and accent colors](color-roles-ai-role.png) + + + +- Use for AI-driven features and ML-powered content +- Apply to intelligent assistance widgets + + + + + +- Use for standard automation or manual processes +- Apply to features not powered by AI + + + +### Focus Colors + +Used for focus indicators. Shows the interactive element currently has focus. + +![Button and TextInput components with focus ring outlines for keyboard navigation](color-roles-focus-role.png) + + + +- Use for focus rings and keyboard navigation indicators + + + + + +- Use for active or selected states + + + +### Transparent Colors + +Colors that are fully or slightly transparent. Consistent behavior across dynamic backgrounds. + + + +- Use for invisible borders and spacing elements +- Use when backgrounds are unpredictable/dynamic + + + + + +- Apply to content that should be visible + + + +### Inverse Colors + +High contrast text on dark backgrounds. May be used when backgrounds use a color greater than 600. + +![Dark Card component with inverse text color for high contrast on dark backgrounds](color-roles-inverse-role.png) + + + +- Use for text on dark backgrounds (step 600+) +- Apply when high contrast is required + + + + + +- Use on backgrounds lighter than 600 +- Apply solely as a means to draw attention + + + +### Contrast Colors + +Contrast tokens are used to stand out strongly against the default page or surface background. Use +them for situations that require high contrast. + + + +- Use for strong contrast on the default page background + + + + + +- Use for standard prose + + + +### Input Colors + +Form field borders ensuring 3:1 contrast against surface backgrounds. + + + +- Use for form field borders and input outlines +- Apply to interactive form elements + + + + + +- Use on non-interactive surfaces + + + +### Container Colors + +Surfaces that exist on top of the page background. + + + +- Use for cards, side panels, and dialogs, popups +- Add a border or some other treatment (e.g. Depth) to visually separate from page backgrounds + + + + + +- Use for page sections or content areas + + + +### Divider Colors + +Content separators that organize information between sections. + + + +- Use to create visual separation between page sections or content areas +- Apply consistently between sections in a group + + + + + +- Apply to Containers + + + +### Overlay Colors + +Semi-transparent scrims that dim the background to focus attention on a modal view. + +![Modal Dialog with semi-transparent overlay background dimming page content](color-roles-overlay-role.png) + + + +- Use to focus attention on modals +- Apply consistent opacity (`opacity.overlay`) for all scrims + + + + + +- Use for non-modal views or when content visibility is a priority +- Stack overlays on top of each other + + + +### Disabled Colors + +Non-interactive elements that are unavailable. Some disabled elements use opacity instead. + +![Disabled Button and TextInput components with reduced opacity disabled colors](color-roles-disabled-role.png) + + + +- Use to indicate disabled elements, like Buttons or FormFields + + + + + +- Use for interactive elements +- Apply when opacity works better + + + +### Translucent Colors + +Slightly opaque overlays for dynamic surfaces. More opaque than transparent. + + + +- Use on dynamic surfaces like video players +- Apply to tooltips and overlays needing background visibility + + + + + +- Use when full transparency is needed +- Apply to static surfaces + + + +### Hint Colors + +Helper text and instructional content that guides users through tasks and interactions. + + + +- Use for placeholder text in form fields +- Apply to instructional content and step-by-step guidance +- Use for tooltips and contextual help +- Apply to field descriptions and input formatting requirements + + + + + +- Use for system announcements or status updates +- Apply to error or critical messages +- Use for informational banners or notices + + + +## Modifiers + +Modifiers specify the intensity or variation of a color role, creating a hierarchy through different +emphasis levels. + +![Modifier scale showing bg-primary from softest (most subtle) to strongest (most intense) with visual progression of intensity](color-roles-modifier-scale.png) + +| **Modifier** | **Description** | +| ------------ | --------------------------------------------------------- | +| `softest` | Minimal intensity for subtle backgrounds | +| `softer` | Low intensity for reduced emphasis | +| `soft` | Gentle intensity for understated applications | +| `default` | Standard color for most use cases | +| `strong` | Increased intensity for more emphasis | +| `stronger` | High intensity for important elements | +| `strongest` | Maximum intensity for active states and critical elements | + +Modifiers are named to clearly differentiate between levels of emphasis, but flexible enough to +accomodate a number of use cases, like interaction states (hover, active), and levels in typography +(heading, body text, captions). The table below shows examples of applying emphasis modifiers to +functional use cases. + +| Use Case | Design Token | +| ----------------------------------- | ----------------------- | +| PrimaryButton Background Hover | `bg.primary.strong` | +| Inverse Secondary Background Active | `bg.transparent.strong` | +| Heading Text | `bg.primary.strong` | +| TextInput Border Hover | `border.input.strong` | diff --git a/modules/docs/llm/tokens/color-scale.md b/modules/docs/llm/tokens/color-scale.md new file mode 100644 index 0000000000..54f9c4e48a --- /dev/null +++ b/modules/docs/llm/tokens/color-scale.md @@ -0,0 +1,286 @@ +--- +title: Color Scale +preamble: true +platform: web, ios, android +tags: color, tonal-scale, palette, lightness, contrast +description: +rank: 4 +source_file: guidelines/color/color-scale.mdx +live_url: https://canvas.workdaydesign.com/guidelines/color/color-scale +--- + +Colors are organized into scales with steps from 0 (lightest) to 1000 (darkest). Colors are designed +to be perceptually balanced, ensuring consistent lightness across color families. Colors with the +same step number serve similar purposes, regardless of hue. + +![Complete color scale showing 0-1000 progression across multiple color families demonstrating consistent lightness relationships](color-scale-overview.png) + +Scales are designed using the okLCH color space to create perceptually balanced colors. Color peak +in vividness at the midpoint (500) and decrease at the tail-ends to create softer surface colors and +more vibrant accents. +![Vividness curve diagram showing how color intensity peaks at step 500 and decreases toward extremes](color-scale-vividness-curve.png) + +**Amber Exception:** Due to amber's natural color properties, its chroma peaks at lighter values +(around step 300) rather than 500. This means amber uses step 400 for primary colors to maintain +accessibility compliance while preserving vibrancy. + +![Amber vividness exception showing higher chroma around step 300–400](color-scale-400-amber.png) + +![Perceptual uniformity demonstration showing how step 500 appears equally bright across all color families](color-scale-500.png) + + + +- Apply consistent steps for similar UI elements across all color families +- When choosing color combinations, use the [contrast framework](/guidelines/color/color-contrast) + to ensure accessible contrast ratios +- Use steps to create visual hierarchy + + + + + +- Use steps for different purposes +- Mix steps randomly, always consider visual hierarchy +- Ignore contrast requirements when choosing steps + + + +![Visual hierarchy example showing how step 100 applies consistently to secondary buttons across blue, green, and red color families](color-scale-100-to-300.png) + +## Step Guidelines + +Choose the right step for your UI elements: + +![Step usage examples showing how different steps apply to common interface elements](color-scale-step-examples.png) + +| **Step** | **Primary Use Case** | **Description** | +| -------- | --------------------- | ----------------------------------------------------------------- | +| 0 | Page backgrounds | Lightest color (white in light mode, black in dark mode) | +| 25 | Input backgrounds | Subtle differentiation that maintains text contrast | +| 50 | Subtle backgrounds | Light surface backgrounds that don't compete with primary content | +| 100 | Secondary backgrounds | Clear hierarchy between subtle and primary content | +| 200 | Divider borders | Gentle separation, less harsh than stronger borders | +| 300 | Container borders | Structural definition while remaining lightweight | +| 400 | Disabled states | Reduced contrast for disabled input borders and text | +| 500 | Input borders | Meets 3:1 contrast for interactive boundaries | +| 600 | Accent text | Balances vividness with readability on white backgrounds | +| 700 | Hover states | Increased weight provides interaction feedback | +| 800 | Body text | Comfortable contrast for extended reading | +| 900 | Heading text | Strong contrast creates content hierarchy | +| 950 | Display text | Maximum contrast demands immediate attention | +| 975 | Dark mode backgrounds | Reserved for dark mode page backgrounds | +| 1000 | Maximum contrast | Strongest emphasis for overlays, rarely used | + +### Steps 0–100 - Page Backgrounds + +![Interface showing default white page background (step 0) and subtle gray alternative background (step 50) in side-by-side layouts](color-scale-page-backgrounds.png) + +Use the default page background for most use cases, especially if colors will be used on top of it. + +A secondary option is needed when subtle differentiaton is needed against the background. + + + +- Use default background for most page layouts +- Choose subtle alternatives when background differentiation is needed +- Apply very light backgrounds for disabled and error states + + + + + +- Use subtle backgrounds when color overlays will be present +- Apply alternative backgrounds without clear purpose + + + +| **Color** | **Usage** | +| ---------- | ---------------------------------- | +| `base.0` | Default background color for pages | +| `slate.50` | Subtle background color for pages | + +### Step 50 - Subtle Backgrounds + +Light surfaces that don't compete with primary content. + +![Step 50 subtle background examples](color-scale-50.png) + + + +- Use for low emphasis status indicators +- Create subtle content zones that don't compete with primary content +- Apply when minimal visual presence is desired + + + + + +- Use when content needs to stand out or grab attention +- Apply to elements requiring clear visibility + + + +### Step 100 - Secondary Backgrounds + +Clear hierarchy between subtle and primary content. + +![Step 100 secondary background examples](color-scale-100-to-300.png) + + + +- Use for secondary button backgrounds +- Apply to hover states for light content +- Choose for UI elements needing moderate prominence + + + + + +- Use for primary actions or main content +- Apply when subtle emphasis is sufficient + + + +### Step 200, 300 - Surface Borders + +Visual structures for non-interactive surfaces. + + + +![Step 200 divider borders examples](color-scale-200-to-300-borders.png) + + + + + +![Step 300 container borders examples](color-scale-300.png) + + + + + +- Use lighter borders for list dividers and lightweight boundaries +- Apply stronger borders for container definition and modular sections +- Choose based on visual separation needs + + + + + +- Use for interactive elements requiring higher contrast +- Apply to elements needing accessibility compliance + + + +### Step 400, 500 - Interactive Elements + + + +![Step 400 disabled foregrounds/borders examples](color-scale-400.png) + + + + + +![Step 500 input borders examples](color-scale-500.png) + + + + + +- Use reduced contrast for disabled states and non-interactive elements +- Apply accessible contrast for input borders and interactive elements +- Meet minimum WCAG 2.1 AA compliance requirements +- Leverage peak saturation for vibrant accent colors + + + + + +- Use disabled state colors for interactive elements +- Apply interactive colors below minimum contrast requirements +- Use low contrast colors for accessibility-critical elements + + + +### Step 600, 700 - Accent Backgrounds + +Vibrant interactive elements with guarenteed readability with inverse text or top of default +backgrounds (4.5:1 contrast). + +![Step 600–800 accents and 700 hover examples](color-scale-600-800.png) +![Step 700 hover examples](color-scale-700-hover.png) + + + +- Use for interactive accent colors, like PrimaryButton backgrounds +- Apply to link text, error text, and hint text on default backgrounds +- Use the next step (700) for hover states and 800 for active states + + + + + +- Apply to secondary UI or prose text - Use when something more subtle is more appropriate + + + +### Step 800, 900, 950 - Text Hierarchy + +Text contrast levels for content hierarchy and readability. + +![Step 800–950 text hierarchy examples](color-scale-800-950.png) + +**Step 800** - Comfortable contrast for extended reading and body text. **Step 900** - Strong +contrast for headings and important text. **Step 950** - Maximum contrast for display text and hero +headlines. + + + +- Use on prose content +- Match level of contrast to typographic hierarchy + + + + + +- Use for secondary or instructional text (use 600 instead) +- Use for accent text, like links (use 600 intead) + + + +### Step 975 - Dark Mode Backgrounds + +Reserved for dark mode page backgrounds to maintain consistent color relationships. + +![Step 975 dark mode backgrounds](color-scale-975.png) + + + +- Use exclusively for dark mode page backgrounds + + + + + +- Use in light mode interfaces + + + +### Step 1000 - Overlay Backgrounds + +Strongest emphasis color for alpha overlays and modal dialogs. + +![Step 1000 overlay backgrounds](color-scale-1000.png) + + + +- Use for alpha overlays and modal dialog scrims + + + + + +- Use as solid color in interfaces + + diff --git a/modules/docs/llm/tokens/color-tokens.md b/modules/docs/llm/tokens/color-tokens.md new file mode 100644 index 0000000000..200f547c34 --- /dev/null +++ b/modules/docs/llm/tokens/color-tokens.md @@ -0,0 +1,139 @@ +--- +title: Color Tokens +preamble: true +platform: web, ios, android +tags: color, scale, palette, lightness, contrast, semantic, system, tokens +description: Color tokens assign color roles through design tokens +rank: 5 +source_file: guidelines/color/color-tokens.mdx +live_url: https://canvas.workdaydesign.com/guidelines/color/color-tokens +--- + +Design tokens represent design decisions as reusable, named values that drive consistency across +product teams. Tokens work as a shared language between design and development teams, replacing +isolated color decisions with systematic choices that carry meaning and adapt to different contexts. + +![Design token system diagram showing base palette mapping to system tokens across different platforms](color-tokens-dev-handoff.png) + +## Token Types + +| **Token Type** | **Example Name** | **Purpose** | +| -------------- | ------------------------------------ | ---------------------- | +| Base Palette | `blue-50`, `gray-100` | Raw color values | +| System Tokens | `bg.default`, `text.primary.default` | Intent-driven UI usage | + +### Base Palette + +Raw `oklch` colors from the [global palette](/guidelines/color/color-palette), categorized in color +families using the [tonal scale](/guidelines/color/color-scale) from 0 to 1000. Each number +represents lightness, creating perceptual uniformity across families. + +### System Colors + +Purpose-driven names that describe intended use rather than appearance. Follow the +`[property].[role].[modifier]` pattern and map to base tokens based on theme. + +## Token Naming System + +**`[property].[role].[modifier]`** + +![Token naming structure diagram showing element, role, and state components with examples](color-tokens-naming-structure.png) + +1. **Property**: What gets colored +2. **Role**: Semantic purpose +3. **Modifier**: Intensity or state (optional) + +### Property Types + +| **Property** | **Description** | **Examples** | +| ------------ | --------------------------------------------- | ---------------------------------------- | +| `bg` | Background colors for surfaces and containers | `bg.default`, `bg.primary.default` | +| `text` | Text and content colors | `text.default`, `text.primary.default` | +| `border` | Border and divider colors | `border.input.default`, `border.divider` | +| `icon` | Icon-specific colors when different from text | `icon.default`, `icon.primary.default` | +| `fg` | Foreground colors for content elements | `fg.default`, `fg.primary.default` | +| `shadow` | Shadow colors for depth and elevation | `shadow.default`, `shadow.1` | +| `static` | Static colors that don't change with themes | `static.blue.default`, `static.white` | + +### Role Types + +| **Role** | **Usage** | **Examples** | +| ------------- | --------------------------------------------- | ---------------------------------------------- | +| `default` | Main content and standard elements | `bg.default`, `text.default` | +| `primary` | Main brand and primary actions | `bg.primary.default`, `text.primary.default` | +| `positive` | Success states and positive feedback | `bg.positive.default`, `text.positive.default` | +| `critical` | Error states and destructive actions | `bg.critical.default`, `text.critical.default` | +| `caution` | Warning states and caution messages | `bg.caution.default`, `text.caution.default` | +| `info` | Informational messages and neutral feedback | `bg.info.default`, `text.info.default` | +| `alt` | Secondary surfaces and alternate states | `bg.alt.default`, `bg.alt.soft` | +| `muted` | Secondary content and subtle elements | `bg.muted.default`, `fg.muted.default` | +| `contrast` | High contrast elements | `bg.contrast.default`, `fg.contrast.default` | +| `ai` | AI-powered features and content | `bg.ai.default`, `text.ai` | +| `focus` | Keyboard navigation indicators | `border.primary.default` (focus) | +| `disabled` | Non-interactive and unavailable elements | `text.disabled`, `icon.disabled` | +| `inverse` | High contrast text on dark backgrounds | `text.inverse`, `icon.inverse` | +| `hint` | Placeholder and helper text | `text.hint` | +| `input` | Form field borders and interactive elements | `border.input.default` | +| `container` | Surfaces that exist on top of page background | `border.container` | +| `divider` | Content separators between sections | `border.divider` | +| `transparent` | Invisible elements for spacing | `bg.transparent.default` | +| `translucent` | Semi-transparent overlays | `bg.translucent` | +| `overlay` | Modal scrims and background dimming | `bg.overlay` | + +### Modifier Variations + +| **Modifier** | **Usage** | **Examples** | +| ------------ | ---------------------------------------- | ----------------------- | +| `default` | Standard intensity or base state | `bg.primary.default` | +| `soft` | Reduced intensity or subtle appearance | `bg.primary.soft` | +| `softer` | More reduced intensity | `bg.primary.softer` | +| `softest` | Minimum intensity or lightest appearance | `bg.primary.softest` | +| `strong` | Increased intensity or emphasis | `bg.primary.strong` | +| `stronger` | Higher intensity or more emphasis | `bg.primary.stronger` | +| `strongest` | Maximum intensity or strongest emphasis | `static.blue.strongest` | +| `disabled` | Disabled element states | `text.disabled` | +| `inverse` | Inverted color for contrast | `text.inverse` | + +## Theming and Adaptation + +Tokens enable flexible theming by separating purpose from appearance. System tokens maintain +consistent names across themes while colors adapt to match visual style. + +![Light and dark theme comparison showing same tokens with different color values](color-tokens-theming.png) + +Create multiple theme variations—light/dark modes, brand customizations, high-contrast +themes—without changing design logic or token names. + +## Benefits of Tokens + +**Consistency:** Clear relationships between colors and purposes make interfaces predictable. + +**Efficiency:** Centralized decisions enable systematic updates across all platforms automatically. + +**Collaboration:** Teams focus on semantic meaning rather than debating specific values. + +## Working with Tokens + +### Figma + +Canvas tokens sync to Figma Libraries as organized color variables. Designers search by name, apply +to designs, and receive automatic updates when values change. + +![Figma color variables showing organized system tokens with search functionality](color-tokens-figma.png) + +### Design-to-Development Handoff + +Shared vocabulary between teams. Designers apply variables to their designs, developers view those +designs in Figma's Developer Mode. Tokens can be viewed as either CSS variables, SwiftUI view +modifiers, or Jetpack Compose theme properties - making it easy for developers to choose correct +colors for the platform they build for. + +![Design-to-development workflow showing Figma design with token annotations matching React component code implementation](color-tokens-dev-handoff.png) + +### Brand Customization + +Brand tokens let you change the brand expression by modifying those token values without changing +semantic structure. This enables white-label solutions, reskinning UI without needing to completely +redesign the interfaces. + +![Brand customization showing same interface with different brand color tokens applied](color-tokens-customization.png) diff --git a/modules/docs/llm/tokens/token-migration.md b/modules/docs/llm/tokens/token-migration.md new file mode 100644 index 0000000000..30042cff29 --- /dev/null +++ b/modules/docs/llm/tokens/token-migration.md @@ -0,0 +1,234 @@ +# Design Token Migration: v2 to v3 + +## Overview + +This document outlines the migration from design tokens v2 to v3. Many color tokens from previous +versions have been deprecated or replaced in v3 to align with the new color system and improve +consistency, accessibility, and brand alignment. + +### Important Notes + +- Deprecated tokens are mapped to new palette values or system tokens where possible +- New color values may not be exact visual matches due to the switch to OKLCH color space and + expanded palette scale +- We recommend updating usage to system tokens for best results and future compatibility +- Tokens marked with "withWhiteText: true" indicate sufficient contrast for white text overlay + +## Deprecated Base Palette Tokens + +| Token Name | Old Value | New Value | System Token Replacement | +| ------------------------------------ | --------- | ------------------------- | ----------------------------------------------------------------------------------------------- | +| base.palette.cinnamon.100 | #ffefee | base.palette.red.50 | sys.color.bg.critical.softer | +| base.palette.cinnamon.200 | #FCC9C5 | base.palette.red.100 | sys.color.bg.critical.soft | +| base.palette.cinnamon.300 | #ff867d | base.palette.red.300 | - | +| base.palette.cinnamon.400 | #ff5347 | base.palette.red.400 | sys.color.fg.critical.soft | +| base.palette.cinnamon.500 | #de2e21 | base.palette.red.600 | sys.color.bg.critical.default, sys.color.fg.critical.default, sys.color.border.critical.default | +| base.palette.cinnamon.600 | #a31b12 | base.palette.red.700 | sys.color.bg.critical.strong, sys.color.fg.critical.strong | +| base.palette.peach.100 | #fff3f0 | base.palette.coral.50 | - | +| base.palette.peach.200 | #ffc2b3 | base.palette.coral.200 | - | +| base.palette.peach.300 | #ff957a | base.palette.coral.300 | - | +| base.palette.peach.400 | #ff643d | base.palette.red.400 | - | +| base.palette.peach.500 | #de4721 | base.palette.coral.600 | - | +| base.palette.peach.600 | #b53413 | base.palette.coral.700 | - | +| base.palette.chili-mango.100 | #ffe6d9 | base.palette.coral.25 | - | +| base.palette.chili-mango.200 | #ffc7ab | base.palette.coral.200 | - | +| base.palette.chili-mango.300 | #ff9b69 | base.palette.coral.300 | - | +| base.palette.chili-mango.400 | #ff671b | base.palette.orange.500 | - | +| base.palette.chili-mango.500 | #e04b00 | base.palette.orange.500 | - | +| base.palette.chili-mango.600 | #a33600 | base.palette.orange.700 | - | +| base.palette.cantaloupe.100 | #ffeed9 | base.palette.amber.50 | sys.color.bg.caution.softer | +| base.palette.cantaloupe.200 | #fcd49f | base.palette.amber.200 | sys.color.bg.caution.soft, sys.color.fg.caution.softer | +| base.palette.cantaloupe.300 | #ffbc63 | base.palette.amber.300 | | +| base.palette.cantaloupe.400 | #ffa126 | base.palette.amber.400 | sys.color.bg.caution.default, sys.color.border.caution.default | +| base.palette.cantaloupe.500 | #f38b00 | base.palette.amber.500 | sys.color.bg.caution.strong, sys.color.fg.caution.softer, | +| base.palette.cantaloupe.600 | #c06c00 | base.palette.amber.600 | sys.color.bg.caution.stronger, sys.color.border.caution.strong | +| base.palette.sour-lemon.100 | #fff9e6 | base.palette.amber.25 | - | +| base.palette.sour-lemon.200 | #ffecab | base.palette.amber.100 | - | +| base.palette.sour-lemon.300 | #ffda61 | base.palette.amber.200 | - | +| base.palette.sour-lemon.400 | #ffc629 | base.palette.amber.300 | - | +| base.palette.sour-lemon.500 | #ebb400 | base.palette.amber.300 | - | +| base.palette.sour-lemon.600 | #bd9100 | base.palette.amber.500 | - | +| base.palette.juicy-pear.100 | #f7fae6 | base.palette.amber.25 | - | +| base.palette.juicy-pear.200 | #e2f391 | base.palette.amber.100 | - | +| base.palette.juicy-pear.300 | #c4de40 | base.palette.amber.200 | - | +| base.palette.juicy-pear.400 | #a8c224 | base.palette.amber.200 | - | +| base.palette.juicy-pear.500 | #8ea618 | base.palette.green.500 | - | +| base.palette.juicy-pear.600 | #687818 | base.palette.green.700 | - | +| base.palette.kiwi.100 | #ecfcd7 | base.palette.green.50 | - | +| base.palette.kiwi.200 | #caf593 | base.palette.green.100 | - | +| base.palette.kiwi.300 | #a7e05c | base.palette.green.200 | - | +| base.palette.kiwi.400 | #77bc1f | base.palette.green.500 | - | +| base.palette.kiwi.500 | #609915 | base.palette.green.500 | - | +| base.palette.kiwi.600 | #537824 | base.palette.green.700 | - | +| base.palette.green-apple.100 | #ebfff0 | base.palette.green.50 | sys.color.bg.positive.softer | +| base.palette.green-apple.200 | #acf5be | base.palette.green.100 | sys.color.bg.positive.soft | +| base.palette.green-apple.300 | #5fe380 | base.palette.green.200 | sys.color.fg.positive.soft | +| base.palette.green-apple.400 | #43c463 | base.palette.green.600 | sys.color.bg.positive.default, sys.color.fg.positive.default | +| base.palette.green-apple.500 | #319c4c | base.palette.green.700 | sys.color.bg.positive.strong, sys.color.fg.positive.strong | +| base.palette.green-apple.600 | #217a37 | base.palette.green.800 | sys.color.bg.positive.stronger, sys.color.fg.positive.stronger | +| base.palette.watermelon.100 | #ebfdf8 | base.palette.neutral.50 | - | +| base.palette.watermelon.200 | #b7edde | base.palette.neutral.100 | - | +| base.palette.watermelon.300 | #65ccaf | base.palette.green.100 | - | +| base.palette.watermelon.400 | #12a67c | base.palette.green.600 | - | +| base.palette.watermelon.500 | #0b7a5c | base.palette.green.700 | - | +| base.palette.watermelon.600 | #08513d | base.palette.green.900 | - | +| base.palette.jewel.100 | #ebfdff | base.palette.teal.25 | - | +| base.palette.jewel.200 | #acecf3 | base.palette.teal.200 | - | +| base.palette.jewel.300 | #44c8d7 | base.palette.teal.400 | - | +| base.palette.jewel.400 | #1ea4b3 | base.palette.teal.500 | - | +| base.palette.jewel.500 | #1a818c | base.palette.teal.600 | - | +| base.palette.jewel.600 | #156973 | base.palette.teal.700 | - | +| base.palette.toothpaste.100 | #d7f1fc | base.palette.azure.50 | - | +| base.palette.toothpaste.200 | #99e0ff | base.palette.azure.200 | - | +| base.palette.toothpaste.300 | #40b4e5 | base.palette.azure.300 | - | +| base.palette.toothpaste.400 | #1894c9 | base.palette.azure.400 | - | +| base.palette.toothpaste.500 | #0271a1 | base.palette.azure.700 | - | +| base.palette.toothpaste.600 | #005B82 | base.palette.azure.800 | - | +| base.palette.blueberry.100 | #D7EAFC | base.palette.blue.25 | - | +| base.palette.blueberry.200 | #A6D2FF | base.palette.blue.100 | sys.color.bg.primary.soft | +| base.palette.blueberry.300 | #40A0FF | base.palette.blue.400 | sys.color.fg.primary.soft | +| base.palette.blueberry.400 | #0875E1 | base.palette.blue.600 | sys.color.bg.primary.default, sys.color.fg.primary.default, sys.color.border.primary.default | +| base.palette.blueberry.500 | #005cb9 | base.palette.blue.700 | sys.color.bg.primary.strong, sys.color.fg.primary.strong | +| base.palette.blueberry.600 | #004387 | base.palette.blue.800 | sys.color.bg.primary.stronger, sys.color.text.primary.stronger | +| base.palette.plum.100 | #e6f1ff | base.palette.blue.100 | - | +| base.palette.plum.200 | #A6CDFF | base.palette.blue.200 | - | +| base.palette.plum.300 | #529bfa | base.palette.blue.400 | - | +| base.palette.plum.400 | #3881E1 | base.palette.blue.600 | - | +| base.palette.plum.500 | #3166ab | base.palette.blue.700 | - | +| base.palette.plum.600 | #264a7a | base.palette.blue.800 | - | +| base.palette.berry-smoothie.100 | #e8edff | base.palette.indigo.50 | - | +| base.palette.berry-smoothie.200 | #c2cfff | base.palette.indigo.200 | - | +| base.palette.berry-smoothie.300 | #7891FF | base.palette.indigo.400 | - | +| base.palette.berry-smoothie.400 | #5E77E6 | base.palette.blue.500 | - | +| base.palette.berry-smoothie.500 | #4b5eb3 | base.palette.blue.700 | - | +| base.palette.berry-smoothie.600 | #3b4987 | base.palette.blue.800 | - | +| base.palette.blackberry.100 | #f0f0ff | base.palette.indigo.25 | - | +| base.palette.blackberry.200 | #c3c2ff | base.palette.indigo.200 | - | +| base.palette.blackberry.300 | #8483e6 | base.palette.indigo.400 | - | +| base.palette.blackberry.400 | #5c59e6 | base.palette.indigo.500 | - | +| base.palette.blackberry.500 | #413fcc | base.palette.indigo.700 | - | +| base.palette.blackberry.600 | #2e2d91 | base.palette.indigo.900 | - | +| base.palette.island-punch.100 | #f5f0ff | base.palette.purple.25 | - | +| base.palette.island-punch.200 | #d2befa | base.palette.purple.200 | - | +| base.palette.island-punch.300 | #a88ae6 | base.palette.indigo.400 | - | +| base.palette.island-punch.400 | #8660d1 | base.palette.purple.500 | - | +| base.palette.island-punch.500 | #6345a1 | base.palette.purple.700 | - | +| base.palette.island-punch.600 | #503882 | base.palette.purple.800 | - | +| base.palette.grape-soda.100 | #feebff | base.palette.magenta.50 | - | +| base.palette.grape-soda.200 | #fac0ff | base.palette.magenta.200 | - | +| base.palette.grape-soda.300 | #de8ae6 | base.palette.purple.400 | - | +| base.palette.grape-soda.400 | #c860d1 | base.palette.purple.500 | - | +| base.palette.grape-soda.500 | #97499e | base.palette.purple.600 | - | +| base.palette.grape-soda.600 | #7C3882 | base.palette.purple.800 | - | +| base.palette.pomegranate.100 | #ffebf3 | base.palette.magenta.50 | - | +| base.palette.pomegranate.200 | #ffbdd6 | base.palette.magenta.100 | - | +| base.palette.pomegranate.300 | #ff5c9a | base.palette.magenta.500 | - | +| base.palette.pomegranate.400 | #f31167 | base.palette.red.400 | - | +| base.palette.pomegranate.500 | #c70550 | base.palette.red.700 | - | +| base.palette.pomegranate.600 | #99003a | base.palette.red.800 | - | +| base.palette.fruit-punch.100 | #FFEEEE | base.palette.red.25 | - | +| base.palette.fruit-punch.200 | #ffbdbd | base.palette.red.200 | - | +| base.palette.fruit-punch.300 | #FF7E7E | base.palette.red.300 | - | +| base.palette.fruit-punch.400 | #ff4c4c | base.palette.red.400 | - | +| base.palette.fruit-punch.500 | #e12f2f | base.palette.red.400 | - | +| base.palette.fruit-punch.600 | #b82828 | base.palette.red.700 | - | +| base.palette.root-beer.100 | #faf3f0 | base.palette.coral.25 | - | +| base.palette.root-beer.200 | #EBD7CF | base.palette.coral.100 | - | +| base.palette.root-beer.300 | #dcbbad | base.palette.coral.200 | - | +| base.palette.root-beer.400 | #ba9a8c | base.palette.coral.200 | - | +| base.palette.root-beer.500 | #8C7266 | base.palette.coral.200 | - | +| base.palette.root-beer.600 | #664d42 | base.palette.amber.950 | - | +| base.palette.toasted-marshmallow.100 | #fdf6e6 | base.palette.amber.25 | - | +| base.palette.toasted-marshmallow.200 | #ebd6a9 | base.palette.orange.100 | - | +| base.palette.toasted-marshmallow.300 | #e6bf6c | base.palette.orange.200 | - | +| base.palette.toasted-marshmallow.400 | #CC9E3B | base.palette.orange.300 | - | +| base.palette.toasted-marshmallow.500 | #b37f10 | base.palette.amber.500 | - | +| base.palette.toasted-marshmallow.600 | #8C6000 | base.palette.amber.600 | - | +| base.palette.licorice.100 | #A1AAB3 | base.palette.slate.400 | sys.color.bg.muted.softer, sys.color.fg.disabled, sys.color.border.input.disabled | +| base.palette.licorice.200 | #7b858f | base.palette.slate.500 | sys.color.bg.muted.soft, sys.color.fg.muted.soft, sys.color.border.input.default | +| base.palette.licorice.300 | #5E6A75 | base.palette.slate.600 | sys.color.bg.muted.default, sys.color.fg.muted.default, sys.color.text.hint | +| base.palette.licorice.400 | #4a5561 | base.palette.slate.700 | sys.color.fg.muted.strong | +| base.palette.licorice.500 | #333d47 | base.palette.slate.800 | sys.color.bg.muted.strong, sys.color.fg.muted.stronger, sys.color.border.input.strong | +| base.palette.licorice.600 | #1f262e | base.palette.slate.900 | - | +| base.palette.soap.100 | #f6f7f8 | base.palette.slate.25 | sys.color.bg.alt.softer | +| base.palette.soap.200 | #F0F1F2 | base.palette.slate.50 | sys.color.bg.alt.soft | +| base.palette.soap.300 | #e8ebed | base.palette.slate.100 | sys.color.bg.alt.default, sys.color.border.input.inverse | +| base.palette.soap.400 | #DFE2E6 | base.palette.slate.200 | sys.color.bg.alt.strong, sys.color.border.divider | +| base.palette.soap.500 | #ced3d9 | base.palette.slate.300 | sys.color.bg.alt.stronger, sys.color.border.container | +| base.palette.soap.600 | #B9C0C7 | base.palette.slate.600 | - | +| base.palette.french-vanilla.100 | #ffffff | base.palette.neutral.0 | sys.color.bg.default, sys.color.fg.inverse, sys.color.border.inverse | +| base.palette.french-vanilla.200 | #ebebeb | base.palette.neutral.50 | - | +| base.palette.french-vanilla.300 | #d4d4d4 | base.palette.neutral.100 | - | +| base.palette.french-vanilla.400 | #bdbdbd | base.palette.neutral.200 | - | +| base.palette.french-vanilla.500 | #a6a6a6 | base.palette.neutral.300 | - | +| base.palette.french-vanilla.600 | #8f8f8f | base.palette.neutral.400 | - | +| base.palette.black-pepper.100 | #787878 | base.palette.neutral.500 | - | +| base.palette.black-pepper.200 | #616161 | base.palette.neutral.700 | - | +| base.palette.black-pepper.300 | #494949 | base.palette.neutral.900 | sys.color.fg.default | +| base.palette.black-pepper.400 | #333333 | base.palette.neutral.950 | sys.color.bg.contrast.default, sys.color.fg.strong, sys.color.border.contrast.default | +| base.palette.black-pepper.500 | #1e1e1e | base.palette.neutral.975 | sys.color.bg.contrast.strong, sys.color.fg.stronger, sys.color.border.contrast.strong | +| base.palette.black-pepper.600 | #000000 | base.palette.neutral.1000 | - | +| base.palette.coconut.100 | #F0EEEE | DEPRECATED | - | +| base.palette.coconut.200 | #e3dfdf | DEPRECATED | - | +| base.palette.coconut.300 | #d1cbcc | DEPRECATED | - | +| base.palette.coconut.400 | #b3acac | DEPRECATED | - | +| base.palette.coconut.500 | #9e9595 | DEPRECATED | - | +| base.palette.coconut.600 | #8F8687 | DEPRECATED | - | +| base.palette.cappuccino.100 | #7A7374 | DEPRECATED | - | +| base.palette.cappuccino.200 | #706869 | DEPRECATED | - | +| base.palette.cappuccino.300 | #5E5757 | DEPRECATED | - | +| base.palette.cappuccino.400 | #4A4242 | DEPRECATED | - | +| base.palette.cappuccino.500 | #352f2f | DEPRECATED | - | +| base.palette.cappuccino.600 | #231f20 | DEPRECATED | - | +| base.palette.dragon-fruit.100 | #FBF1FF | base.palette.purple.25 | - | +| base.palette.dragon-fruit.200 | #EFD3FF | base.palette.purple.100 | - | +| base.palette.dragon-fruit.300 | #BE61F6 | base.palette.indigo.500 | sys.color.bg.ai.default | +| base.palette.dragon-fruit.400 | #8C17D2 | base.palette.indigo.600 | sys.color.bg.ai.strong | +| base.palette.dragon-fruit.500 | #6B11A3 | base.palette.indigo.700 | sys.color.bg.ai.stronger | +| base.palette.dragon-fruit.600 | #4A0D71 | base.palette.indigo.900 | sys.color.bg.ai.strongest, sys.color.border.ai, sys.color.text.ai | + +## Migration Strategy + +### Phase 1: Assessment + +1. Audit existing token usage in your codebase +2. Identify deprecated tokens that need replacement +3. Map current usage to appropriate system tokens + +### Phase 2: Replacement + +1. Replace deprecated base palette tokens with System Token Replacement values +2. If system value is not provided uses new base palette value +3. Update brand tokens to new references +4. Migrate to system tokens where possible for semantic clarity + +### Phase 3: Testing + +1. Verify color contrast ratios meet WCAG guidelines +2. Test with assistive technologies +3. Validate visual consistency across components +4. Test color perception for color-blind users + +### Phase 4: Documentation + +1. Update design system documentation +2. Create migration guides for teams +3. Establish guidelines for future token usage + +## Accessibility Considerations + +- **Contrast Ratios**: New OKLCH color space may affect contrast. Verify all text meets WCAG AA + standards (4.5:1 for normal text, 3:1 for large text) +- **Color Blindness**: Test with color blindness simulators, especially for status indicators +- **System Tokens**: Prefer semantic system tokens over direct palette references for better + accessibility support +- **White Text Overlay**: Tokens marked "withWhiteText" have sufficient contrast for white text +- **Focus Indicators**: Ensure focus states remain visible with new color values + +## Best Practices + +1. **Use System Tokens First**: Always prefer sys.color._ tokens over base.palette._ tokens +2. **Semantic Meaning**: Choose tokens based on semantic meaning rather than visual appearance +3. **Consistent Patterns**: Use consistent token patterns across similar UI elements +4. **Future-Proof**: System tokens will adapt better to future theme changes +5. **Test Thoroughly**: Colors may appear different due to OKLCH color space changes diff --git a/modules/mcp/build/index.ts b/modules/mcp/build/index.ts index e92fd5ba7b..9a6acec98d 100644 --- a/modules/mcp/build/index.ts +++ b/modules/mcp/build/index.ts @@ -21,8 +21,11 @@ function copyFile(relativePath: string): void { // For llm-txt files, use llm-txt source directory and remove the llm-txt/ prefix const fileName = relativePath.replace('llm-txt/', ''); srcPath = path.resolve(llmTxtSourceDir, fileName); + } else if (relativePath.startsWith('tokens/') || relativePath.startsWith('upgrade-guides/')) { + // For tokens and upgrade-guides files, use llm source directory + srcPath = path.resolve(llmSourceDir, relativePath); } else { - // For upgrade-guides files, use llm source directory + // Default to llm source directory srcPath = path.resolve(llmSourceDir, relativePath); } const destPath = path.resolve(targetDir, relativePath); @@ -43,11 +46,12 @@ function copyFile(relativePath: string): void { } // Get file list from index.json and copy only those files -const filesToCopy = index.upgradeGuideFiles; +// Combine upgradeGuideFiles and tokenFiles, removing duplicates +const allFiles = [...new Set([...index.upgradeGuideFiles, ...index.tokenFiles])]; -console.log(`Found ${filesToCopy.length} files to copy:`); -filesToCopy.forEach(file => console.log(` - ${file}`)); +console.log(`Found ${allFiles.length} files to copy:`); +allFiles.forEach(file => console.log(` - ${file}`)); -filesToCopy.forEach(file => copyFile(file)); +allFiles.forEach(file => copyFile(file)); console.log('\nCopy completed successfully!'); diff --git a/modules/mcp/lib/config.json b/modules/mcp/lib/config.json index 24f08eddc0..8319156aab 100644 --- a/modules/mcp/lib/config.json +++ b/modules/mcp/lib/config.json @@ -5,5 +5,14 @@ "upgrade-guides/14.0-UPGRADE-GUIDE.md", "llm-txt/llm-token-migration-14.txt", "llm-txt/llm-style-props-migration.txt" + ], + "tokenFiles": [ + "tokens/token-migration.md", + "tokens/color-palette.md", + "tokens/color-tokens.md", + "tokens/color-contrast.md", + "tokens/color-roles.md", + "tokens/color-scale.md", + "llm-txt/llm-token-migration-14.txt" ] } diff --git a/modules/mcp/lib/index.ts b/modules/mcp/lib/index.ts index 2a0369a9d3..e536f84a40 100644 --- a/modules/mcp/lib/index.ts +++ b/modules/mcp/lib/index.ts @@ -168,5 +168,194 @@ In this release, we: }; } ); + /** + * Metadata for agents about the token documentation files. + */ + function getTokenResource(fileName: string) { + switch (fileName) { + case 'tokens/token-migration.md': + return { + title: 'Canvas Kit Token Migration v2 to v3', + description: `# Design Token Migration: v2 to v3 +Comprehensive mapping of deprecated tokens to new values. Includes: +- Old fruit-named palette tokens (cinnamon, cantaloupe, blueberry, etc.) mapped to new color family names (red, amber, blue, etc.) +- System token replacements for semantic color usage (sys.color.bg.*, sys.color.fg.*, sys.color.border.*) +- Migration phases: Assessment, Replacement, Testing, Documentation +- Accessibility considerations for OKLCH color space changes`, + mimeType: 'text/markdown', + uri: 'docs://tokens/token-migration', + contents: fs.readFileSync(path.resolve(__dirname, 'lib', fileName), 'utf8'), + }; + case 'tokens/color-palette.md': + return { + title: 'Canvas Kit Color Palette', + description: `# Canvas Kit Color Palette +Overview of Workday's shared color palette. Includes: +- Global palette with 11 colors and 2 neutrals (Slate, Neutral), each with 13 shades +- OKLCH color space for perceptual balance +- Accent colors, neutral colors, and alpha colors guidance +- Token hierarchy: Base tokens (raw values), Brand tokens (tenant theming), System tokens (semantic roles) +- Color roles mapping (primary, positive, caution, critical, etc.)`, + mimeType: 'text/markdown', + uri: 'docs://tokens/color-palette', + contents: fs.readFileSync(path.resolve(__dirname, 'lib', fileName), 'utf8'), + }; + case 'tokens/color-tokens.md': + return { + title: 'Canvas Kit Color Tokens', + description: `# Canvas Kit Color Tokens +Design tokens naming system and usage guide. Includes: +- Token naming pattern: [property].[role].[modifier] +- Properties: bg, fg, text, border, icon, shadow, static +- Roles: default, primary, positive, critical, caution, info, alt, muted, contrast, ai, focus, disabled, inverse, hint, input, container, divider, transparent, translucent, overlay +- Modifiers: softest, softer, soft, default, strong, stronger, strongest +- Theming and platform adaptation guidance`, + mimeType: 'text/markdown', + uri: 'docs://tokens/color-tokens', + contents: fs.readFileSync(path.resolve(__dirname, 'lib', fileName), 'utf8'), + }; + case 'tokens/color-contrast.md': + return { + title: 'Canvas Kit Color Contrast', + description: `# Canvas Kit Color Contrast +Accessibility contrast guidelines for color pairings. Includes: +- WCAG 2.1 compliance requirements (4.5:1 for text, 3:1 for non-text, 7:1 for AAA) +- Step difference framework: 500+ for AA text, 400+ for AA non-text, 700+ for AAA +- Practical contrast tables for background/foreground combinations +- High contrast (7:1+) guidelines for low vision support`, + mimeType: 'text/markdown', + uri: 'docs://tokens/color-contrast', + contents: fs.readFileSync(path.resolve(__dirname, 'lib', fileName), 'utf8'), + }; + case 'tokens/color-roles.md': + return { + title: 'Canvas Kit Color Roles', + description: `# Canvas Kit Color Roles +Semantic color role system for consistent UI styling. Includes: +- Role categories: Interactive (primary, focus), Status (positive, caution, critical), Hierarchy (alt, muted, contrast), Functional (disabled, translucent, overlay) +- Property types: bg, fg, text, icon, border, shadow, static, brand +- Usage guidance with Do's and Don'ts for each role +- Modifier scale: softest → softer → soft → default → strong → stronger → strongest`, + mimeType: 'text/markdown', + uri: 'docs://tokens/color-roles', + contents: fs.readFileSync(path.resolve(__dirname, 'lib', fileName), 'utf8'), + }; + case 'tokens/color-scale.md': + return { + title: 'Canvas Kit Color Scale', + description: `# Canvas Kit Color Scale +Tonal scale system from 0 (lightest) to 1000 (darkest). Includes: +- Step guidelines: 0 (page bg), 50-100 (subtle bg), 200-300 (borders), 400-500 (interactive), 600-700 (accents), 800-950 (text), 975-1000 (dark mode) +- Perceptual uniformity across color families using OKLCH +- Amber exception: chroma peaks at 300-400 instead of 500 +- Practical examples for each step range`, + mimeType: 'text/markdown', + uri: 'docs://tokens/color-scale', + contents: fs.readFileSync(path.resolve(__dirname, 'lib', fileName), 'utf8'), + }; + case 'llm-txt/llm-token-migration-14.txt': + return { + title: 'Canvas Kit v14 Token Migration Guide', + description: `# Canvas Kit v14 Token Migration Guide +Complete migration guide from @workday/canvas-kit-react/tokens to @workday/canvas-tokens-web. Includes: +- Installation and CSS variable imports setup +- Color token mapping tables (old fruit names → new base tokens → system tokens) +- Brand color migration from Emotion theme to CSS variables +- Spacing tokens (space.s → system.space.x4), shape tokens (borderRadius → system.shape) +- Typography tokens (type.levels → system.type), depth tokens (depth → system.depth) +- Complete before/after code examples for cards, forms, and buttons +- Best practices and common pitfalls`, + mimeType: 'text/plain', + uri: 'docs://llm-txt/llm-token-migration-14', + contents: fs.readFileSync(path.resolve(__dirname, 'lib', fileName), 'utf8'), + }; + default: + throw new Error(`${fileName} is not a valid token resource`); + } + } + + fileNames.tokenFiles.forEach(fileName => { + const resource = getTokenResource(fileName); + if (!resource || !resource.contents) { + throw new Error(`Resource ${fileName} not found`); + } + server.registerResource( + resource.title, + resource.uri, + { + title: resource.title, + description: resource.description, + mimeType: resource.mimeType, + }, + async (uri: URL) => ({ + contents: [ + { + uri: uri.href, + text: resource.contents, + }, + ], + }) + ); + }); + + server.registerTool( + 'get-canvas-kit-tokens', + { + title: 'Get Canvas Kit Tokens', + description: `Retrieve Canvas Kit design token documentation for migrating from old tokens to the new @workday/canvas-tokens-web system. + +Use this tool when: +- Migrating from @workday/canvas-kit-react/tokens to @workday/canvas-tokens-web +- Converting old fruit-named colors (cinnamon, blueberry, cantaloupe) to new token system +- Understanding the token hierarchy: base tokens, system tokens, and brand tokens +- Finding the correct system token replacement (sys.color.bg.*, sys.color.fg.*, sys.color.border.*) +- Learning the token naming pattern: [property].[role].[modifier] +- Understanding color roles (primary, positive, caution, critical, muted, etc.) +- Migrating spacing (space.s → system.space.x4), shape, typography, or depth tokens +- Ensuring WCAG accessibility compliance with color contrast requirements + +Returns links to token documentation resources including migration guides, color palettes, color roles, contrast guidelines, and complete v14 migration examples.`, + annotations: { + readOnlyHint: true, + }, + }, + async () => { + const output = { + count: fileNames.tokenFiles.length, + files: fileNames.tokenFiles.map(fileName => { + const resource = getTokenResource(fileName); + if (!resource) { + throw new Error(`Resource ${fileName} not found`); + } + return { + name: resource.title, + uri: resource.uri, + }; + }), + }; + return { + content: [ + {type: 'text', text: JSON.stringify(output)}, + ...fileNames.tokenFiles.map(fileName => { + const resource = getTokenResource(fileName); + if (!resource) { + throw new Error(`Resource ${fileName} not found`); + } + return { + type: 'resource_link' as const, + uri: resource.uri, + name: resource.title, + mimeType: resource.mimeType, + description: resource.description, + annotations: { + audience: ['user', 'assistant'], + }, + }; + }), + ], + structuredContent: output, + }; + } + ); return server; }