Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion modules/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,35 @@ Returns Canvas Kit upgrade guide documentation (v9 through v14) as resource link

Returns Canvas Kit design token documentation for migrating to `@workday/canvas-tokens-web`.

### `get-accessibility-guidelines`

Returns Canvas Kit accessibility guidance resource links for a component, scenario, or both. This is
documentation guidance only; it does not scan code or pages, run automated accessibility tests,
certify WCAG conformance, or guarantee compliance.

Parameters:

- `component` (optional) -- Canvas Kit component/story slug, such as `checkbox`, `table`, or
`modal`.
- `scenario` (optional) -- Accessibility scenario slug, such as `forms`, `tables`, `popups`, or
`page-structure`.

At least one of `component` or `scenario` is required.

Examples:

```json
{"scenario": "forms"}
```

```json
{"component": "table"}
```

```json
{"component": "checkbox", "scenario": "forms"}
```

### `fetch-component-documentation-example`

Renders an interactive Canvas Kit component story inline for the user. Accepts a `story` parameter
Expand All @@ -70,12 +99,23 @@ Markdown upgrade guides for Canvas Kit major versions (v9-v14).

Design token migration guides, color palette, roles, contrast, and scale documentation.

### `docs://accessibility/*`

Accessibility guidance documentation for Canvas Kit scenarios, including overview, forms, page
structure, tables, popups, ARIA live regions, headers, side panels, Windows High Contrast themes, and
color contrast.

### `docs://examples/{slug}`

Markdown documentation and inline code examples for each component. These are extracted from the MDX
story files at build time, with `ExampleCodeBlock` references replaced by the actual source code of
each example.

### `docs://examples/{slug}/accessibility`

Accessibility section extracted from a component's MDX documentation when that component has a
non-empty `## Accessibility` section.

### `ui://story/{slug}`

Interactive HTML previews of Canvas Kit components, served as MCP App resources
Expand All @@ -98,7 +138,8 @@ The build runs in stages via `npm run build`:
writes `lib/stories-config.json`
2. **`build:apps`** -- compiles each MDX story into a self-contained single-file HTML app using
Vite, bundling React, Emotion, Canvas Tokens CSS, and lightweight Storybook stubs
3. **`build:copy`** -- copies static resources (upgrade guides, token docs) into `dist/lib`
3. **`build:copy`** -- copies static resources (upgrade guides, token docs, accessibility docs) into
`dist/lib`
4. **`build:types`** -- generates TypeScript declarations
5. **`build:mcp`** -- bundles `lib/index.ts` and `lib/cli.ts` with esbuild

Expand Down
32 changes: 28 additions & 4 deletions modules/mcp/build/discover-stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface StoryEntry {
storybookUrl: string;
mdxPath: string;
mdxProse: string;
accessibilityProse: string;
}

function titleToStorybookPath(title: string): string {
Expand Down Expand Up @@ -68,6 +69,25 @@ function extractMdxProse(mdxFilePath: string, exampleSources: Record<string, str
return prose.replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n');
}

function extractAccessibilitySection(markdown: string): string {
const lines = markdown.split('\n');
const start = lines.findIndex(line => /^#{1,6}\s+Accessibility\b/i.test(line.trim()));

if (start === -1) {
return '';
}

const headingLevel = lines[start].trim().match(/^(#{1,6})\s+Accessibility\b/i)?.[1].length || 2;
const nextSameOrHigherHeading = new RegExp(`^#{1,${headingLevel}}\\s+`);
const end = lines.findIndex(
(line, index) => index > start && nextSameOrHigherHeading.test(line.trim())
);
return lines
.slice(start, end === -1 ? undefined : end)
.join('\n')
.trim();
}

function findExampleSources(mdxFilePath: string): Record<string, string> {
const mdxDir = path.dirname(mdxFilePath);
const examplesDir = path.join(mdxDir, 'examples');
Expand All @@ -76,7 +96,9 @@ function findExampleSources(mdxFilePath: string): Record<string, string> {
}

const sources: Record<string, string> = {};
const entries = fs.readdirSync(examplesDir).filter(f => f.endsWith('.tsx') || f.endsWith('.ts'));
const entries = fs
.readdirSync(examplesDir)
.filter((f: string) => f.endsWith('.tsx') || f.endsWith('.ts'));

for (const entry of entries) {
const name = entry.replace(/\.(tsx?|ts)$/, '');
Expand All @@ -89,14 +111,14 @@ function findExampleSources(mdxFilePath: string): Record<string, string> {
function findMdxFile(storyFilePath: string): string | null {
const dir = path.dirname(storyFilePath);
const entries = fs.readdirSync(dir);
const mdxFiles = entries.filter(e => e.endsWith('.mdx'));
const mdxFiles = entries.filter((e: string) => e.endsWith('.mdx'));

if (mdxFiles.length === 0) {
return null;
}

const storyBaseName = path.basename(storyFilePath).replace(/\.stories\.(ts|tsx)$/, '');
const exactMatch = mdxFiles.find(f => f.replace('.mdx', '') === storyBaseName);
const exactMatch = mdxFiles.find((f: string) => f.replace('.mdx', '') === storyBaseName);
if (exactMatch) {
return path.join(dir, exactMatch);
}
Expand Down Expand Up @@ -165,11 +187,13 @@ async function main() {
const repoRoot = path.resolve(__dirname, '../../..');
const absoluteMdxPath = path.resolve(repoRoot, candidate.mdxPath);
const exampleSources = findExampleSources(absoluteMdxPath);
const mdxProse = extractMdxProse(absoluteMdxPath, exampleSources);
stories[slug] = {
title: candidate.title,
storybookUrl: `${STORYBOOK_BASE_URL}?path=/docs/${storybookPath}--docs`,
mdxPath: candidate.mdxPath,
mdxProse: extractMdxProse(absoluteMdxPath, exampleSources),
mdxProse,
accessibilityProse: extractAccessibilitySection(mdxProse),
};
}

Expand Down
31 changes: 24 additions & 7 deletions modules/mcp/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,21 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const llmSourceDir = path.resolve(__dirname, '../../docs/llm');
const accessibilitySourceDir = path.resolve(__dirname, '../../docs/mdx');
const targetDir = path.resolve(__dirname, '../dist/lib');

type AccessibilityFileEntry =
| string
| {
source: string;
slug: string;
};

/**
* Copy a specific file from source to destination, creating directories as needed
*/
function copyFile(relativePath: string): void {
// All files are now in the llm source directory
const srcPath = path.resolve(llmSourceDir, relativePath);
function copyFile(sourceDir: string, relativePath: string): void {
const srcPath = path.resolve(sourceDir, relativePath);
const destPath = path.resolve(targetDir, relativePath);

// Check if source file exists
Expand All @@ -34,14 +41,24 @@ function copyFile(relativePath: string): void {
fs.copyFileSync(srcPath, destPath);
}

// Get file list from index.json and copy only those files
// Combine upgradeGuideFiles and tokenFiles, removing duplicates
const allFiles = [...new Set([...index.upgradeGuideFiles, ...index.tokenFiles])];
const accessibilityFiles = [
...new Map(
(index.accessibilityFiles as AccessibilityFileEntry[]).map(file => {
const source = typeof file === 'string' ? file : file.source;
return [source, {source}] as const;
})
).values(),
];

console.log(`Found ${allFiles.length} files to copy:`);
console.log(`Found ${allFiles.length + accessibilityFiles.length} files to copy:`);
allFiles.forEach(file => console.log(` - ${file}`));
accessibilityFiles.forEach(file => console.log(` - ${file.source}`));

allFiles.forEach(file => copyFile(file));
allFiles.forEach(file => copyFile(llmSourceDir, file));
accessibilityFiles.forEach(file => {
copyFile(accessibilitySourceDir, file.source);
});

// story-viewer.html is now built by build-story-apps.ts through Vite (not copied raw).

Expand Down
176 changes: 176 additions & 0 deletions modules/mcp/lib/accessibility-enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
export const ACCESSIBILITY_SCENARIOS = [
'overview',
'page-structure',
'tables',
'popups',
'aria-live',
'headers',
'side-panel',
'windows-high-contrast',
'forms',
'color-contrast',
] as const;

export type AccessibilityScenario = (typeof ACCESSIBILITY_SCENARIOS)[number];

export const ACCESSIBILITY_COMPONENTS = [
'action-bar',
'ai-ingress-button-(ai)',
'avatar-(promoted)',
'banner',
'body-text',
'box',
'breadcrumbs',
'buttons',
'card',
'checkbox',
'color-input',
'color-picker',
'color-preview',
'countbadge',
'dialog',
'divider',
'expandable',
'flex',
'form-field',
'grid',
'heading',
'hyperlink',
'information-highlight',
'loading-dots',
'loading-sparkles-(ai)',
'menu',
'modal',
'multi-select',
'pagination',
'pill',
'popper',
'popup',
'radio',
'radio-(deprecated)',
'segmented-control',
'select',
'side-panel-(deprecated)',
'side-panel-(new)',
'skeleton',
'status-indicator',
'status-indicator-(deprecated)',
'subtext',
'switch-(new)',
'switch-(deprecated)',
'table',
'tabs',
'text',
'text-area',
'text-input',
'title',
'toast',
'toolbar',
'tooltip',
] as const;

export type AccessibilityComponent = (typeof ACCESSIBILITY_COMPONENTS)[number];

const FORM_COMPONENTS = new Set<string>([
'checkbox',
'color-input',
'color-picker',
'color-preview',
'form-field',
'multi-select',
'radio',
'radio-(deprecated)',
'segmented-control',
'select',
'switch-(new)',
'switch-(deprecated)',
'text-area',
'text-input',
]);

const POPUP_COMPONENTS = new Set<string>(['dialog', 'menu', 'modal', 'popper', 'popup', 'tooltip']);

const STRUCTURE_COMPONENTS = new Set<string>([
'box',
'breadcrumbs',
'card',
'divider',
'expandable',
'flex',
'grid',
'heading',
'hyperlink',
'pagination',
'side-panel-(deprecated)',
'side-panel-(new)',
'tabs',
]);

const STATUS_COMPONENTS = new Set<string>([
'ai-ingress-button-(ai)',
'avatar-(promoted)',
'banner',
'body-text',
'countbadge',
'information-highlight',
'loading-dots',
'loading-sparkles-(ai)',
'pill',
'skeleton',
'status-indicator',
'status-indicator-(deprecated)',
'subtext',
'text',
'title',
'toast',
'toolbar',
'action-bar',
]);

export function getAccessibilityScenarioSlugsForComponent(
component: string
): AccessibilityScenario[] {
if (FORM_COMPONENTS.has(component)) {
return ['forms', 'overview'];
}

if (POPUP_COMPONENTS.has(component)) {
return ['popups', 'overview'];
}

if (component === 'table') {
return ['tables', 'overview'];
}

if (STRUCTURE_COMPONENTS.has(component)) {
return ['page-structure', 'overview'];
}

if (STATUS_COMPONENTS.has(component)) {
return ['aria-live', 'color-contrast', 'overview'];
}

return ['overview'];
}

export function resolveAccessibilityScenarioSlugs({
component,
scenario,
}: {
component?: string;
scenario?: AccessibilityScenario;
}): AccessibilityScenario[] {
if (component && scenario) {
return [...new Set([...getAccessibilityScenarioSlugsForComponent(component), scenario])];
}

if (component) {
return getAccessibilityScenarioSlugsForComponent(component);
}

if (scenario) {
return [scenario];
}

return [];
}
Loading
Loading