diff --git a/2nd-gen/packages/swc/.storybook/scripts/generate-llm-docs.mjs b/2nd-gen/packages/swc/.storybook/scripts/generate-llm-docs.mjs new file mode 100644 index 0000000000..266ac0846b --- /dev/null +++ b/2nd-gen/packages/swc/.storybook/scripts/generate-llm-docs.mjs @@ -0,0 +1,278 @@ +/** + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/** + * Generates static Markdown (with rendered HTML examples) from component docs.mdx files. + * + * Requires Storybook to be reachable (default http://localhost:6006). Examples are captured + * from story iframes via Playwright, matching what users see in the docs canvases. + * + * Usage (from 2nd-gen/packages/swc): + * yarn analyze + * yarn storybook # separate terminal, or use --start-storybook + * yarn generate:llm-docs + * yarn generate:llm-docs -- --component badge + */ + +import { glob } from 'glob'; +import { execSync, spawn } from 'node:child_process'; +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { setTimeout as sleep } from 'node:timers/promises'; +import { fileURLToPath } from 'node:url'; +import { chromium } from 'playwright'; + +import { renderApiMarkdown } from './llm-docs/api-markdown.mjs'; +import { renderGettingStartedMarkdown } from './llm-docs/getting-started.mjs'; +import { parseDocsMdx, parseStoriesMeta } from './llm-docs/parse-docs-mdx.mjs'; +import { + buildStoryId, + captureStoryHtml, + exportNameToStorySlug, + htmlExampleBlock, +} from './llm-docs/story-html.mjs'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const SWC_DIR = resolve(__dirname, '../..'); +const CEM_PATH = resolve(SWC_DIR, '.storybook/custom-elements.json'); +const DEFAULT_STORYBOOK_URL = 'http://localhost:6006'; + +/** + * Converts a CSF export name to a human-readable story label. + * + * @param {string} exportName - CSF export name (e.g. "SemanticVariants") + * @returns {string} Title-cased label (e.g. "Semantic Variants") + */ +function humanizeExportName(exportName) { + return exportNameToStorySlug(exportName) + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); +} + +/** + * Checks whether Storybook responds on the given URL. + * + * @param {string} url - Storybook base URL + * @returns {Promise} True when the server returns a successful response + */ +async function isStorybookUp(url) { + try { + const response = await fetch(url, { method: 'GET' }); + return response.ok; + } catch { + return false; + } +} + +/** + * Starts a local Storybook dev server and waits until it is reachable. + * + * @param {string} url - Storybook base URL to poll + * @returns {Promise} Process handle for the dev server + */ +async function launchStorybook(url) { + console.log('Starting Storybook (this may take a minute)…'); + + const child = spawn( + 'yarn', + ['exec', 'storybook', 'dev', '-p', '6006', '--no-open'], + { + cwd: SWC_DIR, + stdio: ['ignore', 'pipe', 'pipe'], + env: { ...process.env, BROWSER: 'none' }, + } + ); + + child.stdout?.on('data', (chunk) => { + const text = chunk.toString(); + if (text.includes('error') || text.includes('Error')) { + process.stderr.write(text); + } + }); + child.stderr?.on('data', (chunk) => process.stderr.write(chunk)); + + for (let attempt = 0; attempt < 180; attempt++) { + if (await isStorybookUp(url)) { + console.log('Storybook is ready.'); + return child; + } + await sleep(1000); + } + + child.kill(); + throw new Error(`Storybook did not become ready at ${url} within 3 minutes.`); +} + +/** + * Builds the LLM markdown document for a single component. + * + * @param {string} componentDir - Absolute path to the component folder + * @param {import('playwright').Page} page - Playwright page for story iframe capture + * @param {string} storybookUrl - Storybook base URL + * @returns {Promise} Generated markdown content + */ +async function generateComponentMarkdown(componentDir, page, storybookUrl) { + const componentSlug = componentDir.split('/').pop(); + const docsMdxPath = join(componentDir, 'docs.mdx'); + const storiesPath = join( + componentDir, + 'stories', + `${componentSlug}.stories.ts` + ); + + if (!existsSync(storiesPath)) { + throw new Error(`Missing stories file: ${storiesPath}`); + } + + const mdxSource = readFileSync(docsMdxPath, 'utf8'); + const storiesSource = readFileSync(storiesPath, 'utf8'); + const { segments, subtitle } = parseDocsMdx(mdxSource); + const meta = parseStoriesMeta(storiesSource); + + const cem = JSON.parse(readFileSync(CEM_PATH, 'utf8')); + const tagName = meta.component; + + if (!tagName) { + throw new Error(`Could not read component tag from ${storiesPath}`); + } + + /** @type {string[]} */ + const parts = [ + '---', + `component: ${tagName}`, + `title: ${meta.title}`, + `source: components/${componentSlug}/docs.mdx`, + `generated: ${new Date().toISOString()}`, + '---', + '', + `# ${meta.title}`, + '', + ]; + + if (subtitle) { + parts.push(`_${subtitle}_`, ''); + } + + for (const segment of segments) { + if (segment.type === 'markdown') { + parts.push(segment.content); + continue; + } + + if (segment.type === 'getting-started') { + const kind = meta.migrated ? 'migrated' : 'utility'; + parts.push(renderGettingStartedMarkdown(meta.title, kind), ''); + continue; + } + + if (segment.type === 'api-table') { + parts.push(renderApiMarkdown(cem, tagName), ''); + continue; + } + + if (segment.type === 'story') { + const storyId = buildStoryId(componentSlug, segment.exportName); + const label = humanizeExportName(segment.exportName); + + console.log(` Capturing ${storyId}…`); + + const html = await captureStoryHtml(page, storyId, tagName, storybookUrl); + parts.push(htmlExampleBlock(html, label), ''); + } + } + + return ( + parts + .join('\n') + .replace(/\n{3,}/g, '\n\n') + .trimEnd() + '\n' + ); +} + +async function main() { + const args = process.argv.slice(2); + const componentArgIndex = args.indexOf('--component'); + const componentFilter = + componentArgIndex >= 0 ? args[componentArgIndex + 1] : null; + const shouldStartStorybook = args.includes('--start-storybook'); + const storybookUrl = process.env.STORYBOOK_URL ?? DEFAULT_STORYBOOK_URL; + + if (!existsSync(CEM_PATH)) { + console.log('Running yarn analyze…'); + execSync('yarn analyze', { cwd: SWC_DIR, stdio: 'inherit' }); + } + + const docsPattern = componentFilter + ? `components/${componentFilter}/docs.mdx` + : 'components/*/docs.mdx'; + + const docsFiles = await glob(docsPattern, { cwd: SWC_DIR, absolute: true }); + + if (docsFiles.length === 0) { + console.error( + componentFilter + ? `No docs.mdx found for component "${componentFilter}".` + : 'No components/*/docs.mdx files found.' + ); + process.exit(1); + } + + /** @type {import('node:child_process').ChildProcess | null} */ + let storybookProcess = null; + + if (!(await isStorybookUp(storybookUrl))) { + if (shouldStartStorybook) { + storybookProcess = await launchStorybook(storybookUrl); + } else { + console.error( + `Storybook is not running at ${storybookUrl}.\n` + + 'Start it with `yarn storybook` or pass --start-storybook.' + ); + process.exit(1); + } + } + + const browser = await chromium.launch({ headless: true }); + const page = await browser.newPage(); + + try { + for (const docsMdxPath of docsFiles) { + const componentDir = dirname(docsMdxPath); + const componentSlug = componentDir.split('/').pop(); + const outputPath = join(componentDir, 'docs.llm.md'); + + console.log(`\nGenerating ${componentSlug} → docs.llm.md`); + + const markdown = await generateComponentMarkdown( + componentDir, + page, + storybookUrl + ); + + writeFileSync(outputPath, markdown, 'utf8'); + console.log(` Wrote ${outputPath}`); + } + } finally { + await browser.close(); + if (storybookProcess) { + storybookProcess.kill(); + } + } + + console.log('\nDone.'); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/2nd-gen/packages/swc/.storybook/scripts/llm-docs/api-markdown.mjs b/2nd-gen/packages/swc/.storybook/scripts/llm-docs/api-markdown.mjs new file mode 100644 index 0000000000..631afdbd82 --- /dev/null +++ b/2nd-gen/packages/swc/.storybook/scripts/llm-docs/api-markdown.mjs @@ -0,0 +1,171 @@ +/** + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/** + * Escapes a table cell value for markdown pipe tables. + * + * @param {string} value - Raw cell value + * @returns {string} Escaped cell value + */ +function escapeCell(value) { + return String(value ?? '') + .replace(/\|/g, '\\|') + .replace(/\n/g, ' '); +} + +/** + * Formats a markdown pipe table. + * + * @param {string[]} headers - Column headings + * @param {string[][]} rows - Table body rows + * @returns {string} Markdown table, or an empty string when there are no rows + */ +function markdownTable(headers, rows) { + if (rows.length === 0) { + return ''; + } + + const headerLine = `| ${headers.join(' | ')} |`; + const separator = `| ${headers.map(() => '---').join(' | ')} |`; + const body = rows + .map((row) => `| ${row.map(escapeCell).join(' | ')} |`) + .join('\n'); + + return `${headerLine}\n${separator}\n${body}`; +} + +/** + * Finds a custom element declaration in the CEM package. + * + * @param {import('custom-elements-manifest/schema').Package} cem - Custom elements manifest + * @param {string} tagName - Custom element tag name + * @returns {import('custom-elements-manifest/schema').Declaration | undefined} Matching declaration, if any + */ +function findComponent(cem, tagName) { + for (const mod of cem.modules ?? []) { + for (const decl of mod.declarations ?? []) { + if ('tagName' in decl && decl.tagName === tagName) { + return decl; + } + } + } + return undefined; +} + +/** + * Renders API reference tables from the custom elements manifest. + * + * @param {import('custom-elements-manifest/schema').Package} cem - Custom elements manifest + * @param {string} tagName - Custom element tag name + * @returns {string} Markdown API section + */ +export function renderApiMarkdown(cem, tagName) { + const component = findComponent(cem, tagName); + + if (!component) { + return `_No API data found for \`${tagName}\` in custom-elements.json. Run \`yarn analyze\` first._\n`; + } + + const sections = []; + + const members = (component.members ?? []).filter( + (m) => + m.kind === 'field' && + m.privacy !== 'private' && + m.privacy !== 'protected' && + !m.static + ); + + const attrByField = new Map(); + for (const attr of component.attributes ?? []) { + if (attr.fieldName) { + attrByField.set(attr.fieldName, attr); + } + } + + if (members.length > 0) { + const rows = members.map((prop) => { + const attr = attrByField.get(prop.name); + const attribute = attr + ? `\`${attr.name}\`${prop.reflects ? ' (reflects)' : ''}` + : '-'; + const type = prop.type?.text ? `\`${prop.type.text}\`` : '-'; + const defaultValue = prop.default != null ? `\`${prop.default}\`` : '-'; + + return [ + `\`${prop.name}\``, + attribute, + type, + defaultValue, + prop.description ?? '', + ]; + }); + + sections.push( + '### Properties\n\n' + + markdownTable( + ['Property', 'Attribute', 'Type', 'Default', 'Description'], + rows + ) + ); + } + + const slots = component.slots ?? []; + if (slots.length > 0) { + const rows = slots.map((slot) => [ + `\`${slot.name || '(default)'}\``, + slot.description ?? '', + ]); + sections.push( + '### Slots\n\n' + markdownTable(['Name', 'Description'], rows) + ); + } + + const events = component.events ?? []; + if (events.length > 0) { + const rows = events.map((event) => [ + `\`${event.name}\``, + event.description ?? '', + ]); + sections.push( + '### Events\n\n' + markdownTable(['Name', 'Description'], rows) + ); + } + + const cssProps = component.cssProperties ?? []; + if (cssProps.length > 0) { + const rows = cssProps.map((prop) => [ + `\`${prop.name}\``, + prop.default != null ? `\`${prop.default}\`` : '-', + prop.description ?? '', + ]); + sections.push( + '### CSS custom properties\n\n' + + markdownTable(['Name', 'Default', 'Description'], rows) + ); + } + + const cssParts = component.cssParts ?? []; + if (cssParts.length > 0) { + const rows = cssParts.map((part) => [ + `\`${part.name}\``, + part.description ?? '', + ]); + sections.push( + '### CSS parts\n\n' + markdownTable(['Name', 'Description'], rows) + ); + } + + return sections.length > 0 + ? `${sections.join('\n\n')}\n` + : '_No public API members documented in the manifest._\n'; +} diff --git a/2nd-gen/packages/swc/.storybook/scripts/llm-docs/getting-started.mjs b/2nd-gen/packages/swc/.storybook/scripts/llm-docs/getting-started.mjs new file mode 100644 index 0000000000..d714cac34a --- /dev/null +++ b/2nd-gen/packages/swc/.storybook/scripts/llm-docs/getting-started.mjs @@ -0,0 +1,69 @@ +/** + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/** + * Renders installation and import instructions for a component docs page. + * + * @param {string} title - Storybook meta title (e.g. "Badge") + * @param {'migrated' | 'controller' | 'utility'} [kind] - Component category for template selection + * @returns {string} Getting started markdown section + */ +export function renderGettingStartedMarkdown(title, kind = 'migrated') { + const packageName = title + .split('/') + .pop() + ?.toLowerCase() + .replace(/\s+/g, '-'); + const baseClassName = packageName + ?.split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(''); + const tagName = `swc-${packageName}`; + + if (kind === 'controller') { + return `## Getting started + +Controllers are not published as packages. Instead, they are imported directly from the core package. + +\`\`\`typescript +import { ${baseClassName} } from '@adobe/spectrum-wc/components/core/controllers/${packageName}.js'; +\`\`\` +`; + } + + if (kind === 'utility') { + return ''; + } + + return `## Getting started + +Add the package to your project: + +\`\`\`zsh +yarn add @adobe/spectrum-wc +\`\`\` + +Import the side effectful registration of \`<${tagName}>\` via: + +\`\`\`typescript +import '@adobe/spectrum-wc/components/${packageName}/${tagName}.js'; +\`\`\` + +To reference the \`${baseClassName}\` type, import it as a type-only import: + +\`\`\`typescript +import type { ${baseClassName} } from '@adobe/spectrum-wc/components/${packageName}'; +\`\`\` + +> The class is exposed primarily for type purposes. Extending it is possible, but the internal shape is not part of the public API; if you choose to subclass, you do so at your own risk and may need to adjust your code between releases. +`; +} diff --git a/2nd-gen/packages/swc/.storybook/scripts/llm-docs/parse-docs-mdx.mjs b/2nd-gen/packages/swc/.storybook/scripts/llm-docs/parse-docs-mdx.mjs new file mode 100644 index 0000000000..7244b48532 --- /dev/null +++ b/2nd-gen/packages/swc/.storybook/scripts/llm-docs/parse-docs-mdx.mjs @@ -0,0 +1,134 @@ +/** + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +const SKIP_SINGLE_LINE = + /^\s*(||)/; +const SUBTITLE_OPEN = /\s*/; +const SUBTITLE_CLOSE = /\s*<\/Subtitle>/; +const CANVAS_RE = //; +const OVERVIEW_STORY_RE = //; +const GETTING_STARTED_RE = //; +const API_TABLE_RE = //; + +/** + * Extracts Storybook meta fields from a CSF stories file. + * + * @param {string} storiesSource - Raw stories file contents + * @returns {{ title: string, component: string, migrated: boolean }} Parsed meta fields + */ +export function parseStoriesMeta(storiesSource) { + const titleMatch = storiesSource.match(/title:\s*['"]([^'"]+)['"]/); + const componentMatch = storiesSource.match(/component:\s*['"]([^'"]+)['"]/); + const migrated = /tags:\s*\[[^\]]*['"]migrated['"]/.test(storiesSource); + + return { + title: titleMatch?.[1] ?? 'Component', + component: componentMatch?.[1] ?? '', + migrated, + }; +} + +/** + * Parses a component docs.mdx file into structured segments. + * + * @param {string} mdxSource - Raw docs.mdx contents + * @returns {{ segments: import('./types.js').DocsSegment[]; subtitle: string }} Parsed segments and subtitle + */ +export function parseDocsMdx(mdxSource) { + /** @type {import('./types.js').DocsSegment[]} */ + const segments = []; + let subtitle = ''; + let inSubtitle = false; + let inImport = false; + /** @type {string[]} */ + let markdownBuffer = []; + + const flushMarkdown = () => { + const text = markdownBuffer.join('\n').trim(); + if (text) { + segments.push({ type: 'markdown', content: `${text}\n\n` }); + } + markdownBuffer = []; + }; + + for (const line of mdxSource.split('\n')) { + if (inImport) { + if (/from\s+['"]/.test(line)) { + inImport = false; + } + continue; + } + + if (/^\s*import\b/.test(line)) { + inImport = true; + if (/from\s+['"]/.test(line)) { + inImport = false; + } + continue; + } + + if (SUBTITLE_OPEN.test(line)) { + inSubtitle = true; + subtitle = line + .replace(SUBTITLE_OPEN, '') + .replace(SUBTITLE_CLOSE, '') + .trim(); + if (SUBTITLE_CLOSE.test(line)) { + inSubtitle = false; + } + continue; + } + + if (inSubtitle) { + subtitle += ` ${line.replace(SUBTITLE_CLOSE, '').trim()}`; + if (SUBTITLE_CLOSE.test(line)) { + inSubtitle = false; + } + continue; + } + + if (SKIP_SINGLE_LINE.test(line.trim())) { + continue; + } + + if (OVERVIEW_STORY_RE.test(line)) { + flushMarkdown(); + segments.push({ type: 'story', exportName: 'Overview' }); + continue; + } + + if (GETTING_STARTED_RE.test(line)) { + flushMarkdown(); + segments.push({ type: 'getting-started' }); + continue; + } + + if (API_TABLE_RE.test(line)) { + flushMarkdown(); + segments.push({ type: 'api-table' }); + continue; + } + + const canvasMatch = line.match(CANVAS_RE); + if (canvasMatch) { + flushMarkdown(); + segments.push({ type: 'story', exportName: canvasMatch[1] }); + continue; + } + + markdownBuffer.push(line); + } + + flushMarkdown(); + + return { segments, subtitle: subtitle.trim() }; +} diff --git a/2nd-gen/packages/swc/.storybook/scripts/llm-docs/story-html.mjs b/2nd-gen/packages/swc/.storybook/scripts/llm-docs/story-html.mjs new file mode 100644 index 0000000000..b0b171096d --- /dev/null +++ b/2nd-gen/packages/swc/.storybook/scripts/llm-docs/story-html.mjs @@ -0,0 +1,138 @@ +/** + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import prettier from 'prettier'; + +/** + * Converts a CSF export name to a Storybook story slug segment. + * + * @param {string} exportName - CSF export name (e.g. "SemanticVariants") + * @returns {string} Kebab-case slug (e.g. "semantic-variants") + */ +export function exportNameToStorySlug(exportName) { + return exportName + .replace(/([a-z0-9])([A-Z])/g, '$1-$2') + .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2') + .toLowerCase(); +} + +/** + * Builds the Storybook story id used in iframe URLs. + * + * @param {string} componentSlug - Kebab-case folder name (e.g. "badge") + * @param {string} exportName - CSF export name + * @returns {string} Story id (e.g. "components-badge--anatomy") + */ +export function buildStoryId(componentSlug, exportName) { + return `components-${componentSlug}--${exportNameToStorySlug(exportName)}`; +} + +/** + * Captures rendered story markup from a Storybook iframe. + * + * @param {import('playwright').Page} page - Playwright page + * @param {string} storyId - Storybook story id + * @param {string} tagName - Primary custom element tag to wait for + * @param {string} baseUrl - Storybook base URL + * @returns {Promise} Prettified HTML for the story content + */ +export async function captureStoryHtml(page, storyId, tagName, baseUrl) { + const url = `${baseUrl.replace(/\/$/, '')}/iframe.html?id=${storyId}&viewMode=story`; + + await page.goto(url, { waitUntil: 'domcontentloaded' }); + await page.waitForLoadState('networkidle'); + + await page.waitForFunction( + (tag) => customElements.get(tag) !== undefined, + tagName, + { timeout: 30_000 } + ); + + await page.waitForFunction(() => { + const root = document.querySelector('#storybook-root'); + return root && root.children.length > 0; + }); + + const rawHtml = await page.evaluate(() => { + const root = document.querySelector('#storybook-root'); + if (!root) { + return ''; + } + + /** @type {HTMLElement} */ + let container = + root.querySelector('#root-inner') ?? /** @type {HTMLElement} */ (root); + + const isFlexDecorator = (element) => { + if (!(element instanceof HTMLElement) || element.tagName !== 'DIV') { + return false; + } + + const display = + element.style.display || getComputedStyle(element).display; + return display === 'flex'; + }; + + // Strip Storybook shells: single-child flex wrappers from withFlexLayout. + while ( + container.children.length === 1 && + isFlexDecorator(container.children[0]) + ) { + container = /** @type {HTMLElement} */ (container.children[0]); + } + + if (container.children.length === 0) { + return container.outerHTML; + } + + return Array.from(container.children) + .map((child) => child.outerHTML) + .join('\n'); + }); + + const cleaned = cleanCapturedHtml(rawHtml); + + try { + return await prettier.format(cleaned, { + parser: 'html', + printWidth: 100, + }); + } catch { + return cleaned; + } +} + +/** + * Removes Lit and Storybook artifacts from captured HTML. + * + * @param {string} html - Raw captured HTML + * @returns {string} Cleaned HTML + */ +function cleanCapturedHtml(html) { + return html + .replace(//g, '') + .replace(//g, '') + .replace(/\s+$/gm, '') + .replace(/\n{3,}/g, '\n\n'); +} + +/** + * Wraps HTML in a fenced code block with an optional example heading. + * + * @param {string} html - Example HTML + * @param {string} [label] - Optional human-readable example label + * @returns {string} Markdown code block section + */ +export function htmlExampleBlock(html, label) { + const heading = label ? `#### Example: ${label}\n\n` : ''; + return `${heading}\`\`\`html\n${html.trim()}\n\`\`\`\n`; +} diff --git a/2nd-gen/packages/swc/.storybook/scripts/llm-docs/types.js b/2nd-gen/packages/swc/.storybook/scripts/llm-docs/types.js new file mode 100644 index 0000000000..68da3b5c43 --- /dev/null +++ b/2nd-gen/packages/swc/.storybook/scripts/llm-docs/types.js @@ -0,0 +1,39 @@ +/** + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/** + * @typedef {object} MarkdownDocsSegment + * @property {'markdown'} type + * @property {string} content + */ + +/** + * @typedef {object} GettingStartedDocsSegment + * @property {'getting-started'} type + */ + +/** + * @typedef {object} ApiTableDocsSegment + * @property {'api-table'} type + */ + +/** + * @typedef {object} StoryDocsSegment + * @property {'story'} type + * @property {string} exportName + */ + +/** + * @typedef {MarkdownDocsSegment|GettingStartedDocsSegment|ApiTableDocsSegment|StoryDocsSegment} DocsSegment + */ + +export {}; diff --git a/2nd-gen/packages/swc/components/badge/docs.llm.md b/2nd-gen/packages/swc/components/badge/docs.llm.md new file mode 100644 index 0000000000..b629a06206 --- /dev/null +++ b/2nd-gen/packages/swc/components/badge/docs.llm.md @@ -0,0 +1,506 @@ +--- +component: swc-badge +title: Badge +source: components/badge/docs.mdx +generated: 2026-05-19T10:33:45.201Z +--- + +# Badge + +_Display small amounts of color-categorized metadata to get a user's attention._ + +Similar to [status lights](/docs/components-status-light--docs), badges use color and text to convey status or category information. + +Badges come in three styles: bold fill (default), subtle fill, and outline. Choose one style consistently within a product; `outline` and `subtle` fill draw similar attention levels. Reserve bold fill for high-attention badging only. + +#### Example: Overview + +```html + Active +``` + +## Getting started + +Add the package to your project: + +```zsh +yarn add @adobe/spectrum-wc +``` + +Import the side effectful registration of `` via: + +```typescript +import '@adobe/spectrum-wc/components/badge/swc-badge.js'; +``` + +To reference the `Badge` type, import it as a type-only import: + +```typescript +import type { Badge } from '@adobe/spectrum-wc/components/badge'; +``` + +> The class is exposed primarily for type purposes. Extending it is possible, but the internal shape is not part of the public API; if you choose to subclass, you do so at your own risk and may need to adjust your code between releases. + +## Anatomy + +A badge consists of: + +1. **Container** - Colored background with rounded corners +2. **Label** - Text content describing the status or category (required) +3. **Icon** (optional) - Visual indicator positioned before the label + +### Content + +- **Default slot**: Text content describing the status or category (required for accessibility) +- **icon slot**: (optional) - Visual indicator positioned before the label + +#### Example: Anatomy + +```html + Label only + + + + + + Icon and label + +``` + +## Upcoming features + +### Notification and indicator badge types + +- **Notification**: Displays a numeric count to signal unread or pending items, such as a message counter on an icon +- **Indicator**: A dot-only badge that signals activity or updated content without showing a count + +## Options + +### Sizes + +Badges come in four sizes to fit various contexts: + +- **Small (`s`)**: Default size; compact spaces, inline with text, or in tables +- **Medium (`m`)**: Common usage when slightly more emphasis is needed +- **Large (`l`)**: Increased emphasis in cards or content areas +- **Extra-large (`xl`)**: Maximum visibility for primary status indicators + +The `s` size is the default. Use larger sizes sparingly to create a hierarchy of importance on a page. + +#### Example: Sizes + +```html +
+ + + Small + + + + Medium + + + + Large + + + + Extra-large + +
+
+ Small + Medium + Large + Extra-large +
+
+ + + + + + + + + + + + +
+``` + +### Semantic variants + +Semantic variants provide meaning through color and should be used when status has specific significance. These variants align consistently with other design system components that use the same semantic meanings. + +Use these variants for the following statuses: + +- **accent**: New, beta, prototype, draft +- **informative**: Active, in use, live, published +- **neutral**: Archived, deleted, paused, not started, ended +- **positive**: Approved, complete, success, purchased, licensed +- **notice**: Pending, expiring soon, limited, deprecated +- **negative**: Rejected, error, alert, failed + +#### Example: Semantic Variants + +```html + New + Active + Archived + Approved + Pending approval + Rejected +``` + +### Non-semantic variants + +Non-semantic variants use distinctive colors for visual categorization without inherent meaning. These are ideal for color-coding categories, teams, or projects, especially when there are 8 categories or fewer. + +Use non-semantic variants when: + +- Categories don't have universal status meanings +- Visual distinction matters more than semantic meaning +- Creating department, team, or project color schemes + +> **Note**: 2nd-gen adds `pink`, `turquoise`, `brown`, `cinnamon`, and `silver` variants. + +#### Example: Non Semantic Variants + +```html + Marketing + Engineering + Design + Product + Support + Busy + Available + Sales + Research + Quality + Documentation + Legal + Analytics + Security + Creative + Training + Facilities + Compliance + Version 1.2.10 +``` + +### Outline + +The `outline` style provides a bordered appearance with a transparent background. This style reduces visual weight while maintaining semantic meaning. + +**Important**: The outline style is only valid for semantic variants (`accent`, `informative`, `neutral`, `positive`, `notice`, `negative`). Attempting to use `outline` with non-semantic color variants will not apply the style. + +#### Example: Outline + +```html + New + Active + Archived + Approved + Pending approval + Rejected +``` + +### Subtle + +The `subtle` style reduces visual prominence with a softer background fill. Unlike outline, subtle is available for **all** variants (semantic and non-semantic). + +Use subtle style when: + +- Multiple badges appear together and need less visual competition +- Status is secondary to main content +- Maintaining design system color palette while reducing emphasis + +#### Example: Subtle + +```html + New + Active + Archived + Approved + Pending approval + Rejected + Marketing + Engineering + Design + Product + Support + Busy + Available + Sales + Research + Quality + Documentation + Legal + Analytics + Security + Creative + Training + Facilities + Compliance + Version 1.2.10 +``` + +### Fixed + +The `fixed` attribute adjusts border radius based on edge positioning, creating the appearance that the badge is "fixed" to a UI edge. + +Fixed positioning options: + +- **block-start**: Top edge (removes top-left and top-right border radius) +- **block-end**: Bottom edge (removes bottom-left and bottom-right border radius) +- **inline-start**: Left edge (removes top-left and bottom-left border radius) +- **inline-end**: Right edge (removes top-right and bottom-right border radius) + +This is purely visual styling; actual positioning must be handled separately with CSS. + +#### Example: Fixed + +```html + Block start + Block end + Inline start + Inline end +``` + +## Behaviors + +### Text wrapping + +When a badge's label is too long for the available horizontal space, it wraps to form multiple lines. Text wrapping can be controlled by applying a `max-inline-size` constraint to the badge. + +This ensures badges remain readable even with longer status messages or category names. + +#### Example: Text Wrapping + +```html + + Document review pending approval from manager + +``` + +### Inline + +Badges flow naturally within prose text to annotate inline content such as headings, labels, list items, or table cells. + +Because `` renders as `inline-flex`, it participates in the normal text flow without any extra wrapper styling required. Use small (`s`) badges in most inline contexts to avoid disrupting line height. + +#### Example: Inline + +```html +

+ Design system components + Beta +

+

+ API documentation + Stable +

+

+ Legacy components + Deprecated +

+``` + +## Accessibility + +### Features + +The `` element implements several accessibility features: + +#### Color meaning + +- Colors are used in combination with text labels and/or icons to ensure that status information is not conveyed through color alone +- Users with color vision deficiencies can understand badge meaning through text content +- High contrast mode is supported with appropriate color overrides + +#### Non-interactive element + +- Badges have no interactive behavior and are not focusable +- Screen readers will announce the badge content as static text +- No keyboard interaction is required or expected + +> Important: In focus mode, only interactive elements and their associated labels/descriptions are announced. If content is not a label or description for a focusable element, it will not be read. For non-interactive content, screen reader users must [switch to Browse mode](https://swcpreviews.z13.web.core.windows.net/pr-6122/docs/second-gen-storybook/?path=/docs/guides-accessibility-guides-screen-reader-testing--readme#screen-reader-modes). This is expected behavior, not a bug; ensure you test both modes when evaluating component accessibility. + +### Text label + +Badges with visible text are announced directly by screen readers. The text in the default slot is the accessible name. + +### Icon + text + +When an icon accompanies a text label, the icon is decorative and should be hidden from assistive technology. Apply `aria-hidden="true"` to the `` so screen readers only announce the label text. + +### Icon only + +When space is limited and no visible label is shown, the badge **must** have an accessible name. Set `role="img"` and `aria-label` directly on the `` element to describe the badge's meaning. `role="img"` is required because custom elements have no implicit ARIA role; without it, `aria-label` is not permitted by the ARIA specification and will fail automated accessibility checks. Without both attributes, the badge has no accessible name and fails WCAG 1.1.1. + +### Best practices + +- Use semantic variants (`positive`, `negative`, `notice`, `informative`, `neutral`, `accent`) when the status has specific meaning +- Include clear, descriptive labels that explain the status without relying on color alone +- For icon-only badges, always set `role="img"` and `aria-label` on `swc-badge` +- Ensure sufficient color contrast between the badge and its background +- Badges are not interactive elements; for interactive status indicators, consider using buttons, tags, or links instead +- When using multiple badges together, ensure they're clearly associated with their related content +- Use consistent badge variants across your application for the same statuses +- Test with screen readers to verify badge content is announced in context +- Consider placement carefully; badges should be close to the content they describe + +#### Example: Accessibility + +```html + Approved + Rejected + Pending approval + Active + Archived + Documentation + Busy + Version 1.2.10 + + + Approved + + + + +``` + +## API + +### Properties + +| Property | Attribute | Type | Default | Description | +| --- | --- | --- | --- | --- | +| `variant` | `variant` (reflects) | `BadgeVariant` | `'neutral'` | | +| `size` | `size` | `ElementSize` | `s` | The size of the badge. | +| `fixed` | `fixed` (reflects) | `FixedValues \| undefined` | - | The fixed position of the badge. | +| `subtle` | `subtle` (reflects) | `boolean` | `false` | Whether the badge is subtle. | +| `outline` | `outline` (reflects) | `boolean` | `false` | Whether the badge is outlined. Can only be used with semantic variants. | + +### Slots + +| Name | Description | +| --- | --- | +| `(default)` | Text label of the badge. | +| `icon` | Optional icon that appears to the left of the label | + +### CSS custom properties + +| Name | Default | Description | +| --- | --- | --- | +| `--swc-badge-height` | - | Minimum block size of the badge. | +| `--swc-badge-corner-radius` | - | Corner radius of the badge. | +| `--swc-badge-gap` | - | Gap between the icon and label. | +| `--swc-badge-padding-block` | - | Block padding. | +| `--swc-badge-padding-inline` | - | Inline padding. | +| `--swc-badge-padding-inline-start` | - | Inline-start padding; overrides the start side of `--swc-badge-padding-inline`. | +| `--swc-badge-font-size` | - | Font size of the label. | +| `--swc-badge-line-height` | - | Line height of the label. | +| `--swc-badge-icon-size` | - | Size of the icon in the icon slot. | +| `--swc-badge-label-icon-color` | - | Color of the label text and icon. | +| `--swc-badge-background-color` | - | Background color of the badge. | +| `--swc-badge-border-color` | - | Border color; visible on the outline variant. | +| `--swc-badge-with-icon-padding-inline` | - | Inline padding when the badge has both an icon and a label. | +| `--swc-badge-with-icon-only-padding-inline` | - | Inline padding for icon-only badges. | +| `--swc-badge-with-icon-only-padding-block` | - | Block padding for icon-only badges. | +| `--swc-badge-outline-background-color` | - | Background color override for the outline variant. | +| `--swc-badge-outline-label-icon-color` | - | Label and icon color override for the outline variant. | + +## Feedback + +Have feedback or questions? [Open an issue](https://github.com/adobe/spectrum-web-components/issues/new/choose). diff --git a/2nd-gen/packages/swc/components/badge/docs.mdx b/2nd-gen/packages/swc/components/badge/docs.mdx new file mode 100644 index 0000000000..14192325d1 --- /dev/null +++ b/2nd-gen/packages/swc/components/badge/docs.mdx @@ -0,0 +1,198 @@ +import { Meta, Title, Subtitle, Canvas } from '@storybook/addon-docs/blocks'; +import { + ApiTable, + GettingStarted, + OverviewStory, + StatusBadge, +} from '../../.storybook/blocks'; +import * as BadgeStories from './stories/badge.stories'; + + + + +<StatusBadge /> + +<Subtitle> + Display small amounts of color-categorized metadata to get a user's attention. +</Subtitle> + +Similar to [status lights](/docs/components-status-light--docs), badges use color and text to convey status or category information. + +Badges come in three styles: bold fill (default), subtle fill, and outline. Choose one style consistently within a product; `outline` and `subtle` fill draw similar attention levels. Reserve bold fill for high-attention badging only. + +<OverviewStory /> + +<GettingStarted /> + +## Anatomy + +A badge consists of: + +1. **Container** - Colored background with rounded corners +2. **Label** - Text content describing the status or category (required) +3. **Icon** (optional) - Visual indicator positioned before the label + +### Content + +- **Default slot**: Text content describing the status or category (required for accessibility) +- **icon slot**: (optional) - Visual indicator positioned before the label + +<Canvas of={BadgeStories.Anatomy} /> + +## Upcoming features + +### Notification and indicator badge types + +- **Notification**: Displays a numeric count to signal unread or pending items, such as a message counter on an icon +- **Indicator**: A dot-only badge that signals activity or updated content without showing a count + +## Options + +### Sizes + +Badges come in four sizes to fit various contexts: + +- **Small (`s`)**: Default size; compact spaces, inline with text, or in tables +- **Medium (`m`)**: Common usage when slightly more emphasis is needed +- **Large (`l`)**: Increased emphasis in cards or content areas +- **Extra-large (`xl`)**: Maximum visibility for primary status indicators + +The `s` size is the default. Use larger sizes sparingly to create a hierarchy of importance on a page. + +<Canvas of={BadgeStories.Sizes} /> + +### Semantic variants + +Semantic variants provide meaning through color and should be used when status has specific significance. These variants align consistently with other design system components that use the same semantic meanings. + +Use these variants for the following statuses: + +- **accent**: New, beta, prototype, draft +- **informative**: Active, in use, live, published +- **neutral**: Archived, deleted, paused, not started, ended +- **positive**: Approved, complete, success, purchased, licensed +- **notice**: Pending, expiring soon, limited, deprecated +- **negative**: Rejected, error, alert, failed + +<Canvas of={BadgeStories.SemanticVariants} /> + +### Non-semantic variants + +Non-semantic variants use distinctive colors for visual categorization without inherent meaning. These are ideal for color-coding categories, teams, or projects, especially when there are 8 categories or fewer. + +Use non-semantic variants when: + +- Categories don't have universal status meanings +- Visual distinction matters more than semantic meaning +- Creating department, team, or project color schemes + +> **Note**: 2nd-gen adds `pink`, `turquoise`, `brown`, `cinnamon`, and `silver` variants. + +<Canvas of={BadgeStories.NonSemanticVariants} /> + +### Outline + +The `outline` style provides a bordered appearance with a transparent background. This style reduces visual weight while maintaining semantic meaning. + +**Important**: The outline style is only valid for semantic variants (`accent`, `informative`, `neutral`, `positive`, `notice`, `negative`). Attempting to use `outline` with non-semantic color variants will not apply the style. + +<Canvas of={BadgeStories.Outline} /> + +### Subtle + +The `subtle` style reduces visual prominence with a softer background fill. Unlike outline, subtle is available for **all** variants (semantic and non-semantic). + +Use subtle style when: + +- Multiple badges appear together and need less visual competition +- Status is secondary to main content +- Maintaining design system color palette while reducing emphasis + +<Canvas of={BadgeStories.Subtle} /> + +### Fixed + +The `fixed` attribute adjusts border radius based on edge positioning, creating the appearance that the badge is "fixed" to a UI edge. + +Fixed positioning options: + +- **block-start**: Top edge (removes top-left and top-right border radius) +- **block-end**: Bottom edge (removes bottom-left and bottom-right border radius) +- **inline-start**: Left edge (removes top-left and bottom-left border radius) +- **inline-end**: Right edge (removes top-right and bottom-right border radius) + +This is purely visual styling; actual positioning must be handled separately with CSS. + +<Canvas of={BadgeStories.Fixed} /> + +## Behaviors + +### Text wrapping + +When a badge's label is too long for the available horizontal space, it wraps to form multiple lines. Text wrapping can be controlled by applying a `max-inline-size` constraint to the badge. + +This ensures badges remain readable even with longer status messages or category names. + +<Canvas of={BadgeStories.TextWrapping} /> + +### Inline + +Badges flow naturally within prose text to annotate inline content such as headings, labels, list items, or table cells. + +Because `<swc-badge>` renders as `inline-flex`, it participates in the normal text flow without any extra wrapper styling required. Use small (`s`) badges in most inline contexts to avoid disrupting line height. + +<Canvas of={BadgeStories.Inline} /> + +## Accessibility + +### Features + +The `<swc-badge>` element implements several accessibility features: + +#### Color meaning + +- Colors are used in combination with text labels and/or icons to ensure that status information is not conveyed through color alone +- Users with color vision deficiencies can understand badge meaning through text content +- High contrast mode is supported with appropriate color overrides + +#### Non-interactive element + +- Badges have no interactive behavior and are not focusable +- Screen readers will announce the badge content as static text +- No keyboard interaction is required or expected + +> Important: In focus mode, only interactive elements and their associated labels/descriptions are announced. If content is not a label or description for a focusable element, it will not be read. For non-interactive content, screen reader users must [switch to Browse mode](https://swcpreviews.z13.web.core.windows.net/pr-6122/docs/second-gen-storybook/?path=/docs/guides-accessibility-guides-screen-reader-testing--readme#screen-reader-modes). This is expected behavior, not a bug; ensure you test both modes when evaluating component accessibility. + +### Text label + +Badges with visible text are announced directly by screen readers. The text in the default slot is the accessible name. + +### Icon + text + +When an icon accompanies a text label, the icon is decorative and should be hidden from assistive technology. Apply `aria-hidden="true"` to the `<swc-icon>` so screen readers only announce the label text. + +### Icon only + +When space is limited and no visible label is shown, the badge **must** have an accessible name. Set `role="img"` and `aria-label` directly on the `<swc-badge>` element to describe the badge's meaning. `role="img"` is required because custom elements have no implicit ARIA role; without it, `aria-label` is not permitted by the ARIA specification and will fail automated accessibility checks. Without both attributes, the badge has no accessible name and fails WCAG 1.1.1. + +### Best practices + +- Use semantic variants (`positive`, `negative`, `notice`, `informative`, `neutral`, `accent`) when the status has specific meaning +- Include clear, descriptive labels that explain the status without relying on color alone +- For icon-only badges, always set `role="img"` and `aria-label` on `swc-badge` +- Ensure sufficient color contrast between the badge and its background +- Badges are not interactive elements; for interactive status indicators, consider using buttons, tags, or links instead +- When using multiple badges together, ensure they're clearly associated with their related content +- Use consistent badge variants across your application for the same statuses +- Test with screen readers to verify badge content is announced in context +- Consider placement carefully; badges should be close to the content they describe + +<Canvas of={BadgeStories.Accessibility} /> + +## API + +<ApiTable /> + +## Feedback + +Have feedback or questions? [Open an issue](https://github.com/adobe/spectrum-web-components/issues/new/choose). diff --git a/2nd-gen/packages/swc/components/badge/stories/badge.stories.ts b/2nd-gen/packages/swc/components/badge/stories/badge.stories.ts index 8d025f9eda..3b859a85e6 100644 --- a/2nd-gen/packages/swc/components/badge/stories/badge.stories.ts +++ b/2nd-gen/packages/swc/components/badge/stories/badge.stories.ts @@ -212,7 +212,7 @@ export const Playground: Story = { 'default-slot': 'Active', 'icon-slot': undefined, }, - tags: ['autodocs', 'dev'], + tags: ['dev'], }; // ────────────────────────────── diff --git a/2nd-gen/packages/swc/package.json b/2nd-gen/packages/swc/package.json index dc0f0167ab..263a6939f9 100644 --- a/2nd-gen/packages/swc/package.json +++ b/2nd-gen/packages/swc/package.json @@ -63,6 +63,7 @@ "dev": "vite build --watch", "dev:core": "yarn workspace @spectrum-web-components/core dev", "generate:contributor-docs": "node .storybook/scripts/generate-contributor-docs.mjs", + "generate:llm-docs": "node .storybook/scripts/generate-llm-docs.mjs", "storybook": "yarn generate:contributor-docs && yarn analyze && storybook dev -p 6006", "storybook:build": "yarn generate:contributor-docs && yarn analyze && storybook build", "stylesheet:tokens": "swc-tokens --outputType tokens --out ./stylesheets/tokens.css --prefix swc",