From 753c60eb0493847d2cb241bf7426544fa9ae7e66 Mon Sep 17 00:00:00 2001 From: Kyle Shepherd Date: Tue, 14 Jul 2026 22:57:29 -0500 Subject: [PATCH 1/5] Serve AI-friendly markdown docs (llms.txt) from the docs site --- .gitignore | 1 + .prettierignore | 3 + .storybook/main.js | 6 + README.md | 10 + package.json | 6 +- tools/build-llms-docs.mjs | 351 +++++++++++++++++++++++++++++++++++ tools/llms-resolve-hooks.mjs | 13 ++ yarn.lock | 268 +++++++++++++++++++++++++- 8 files changed, 652 insertions(+), 6 deletions(-) create mode 100644 tools/build-llms-docs.mjs create mode 100644 tools/llms-resolve-hooks.mjs diff --git a/.gitignore b/.gitignore index ce112c65..d78fa651 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ node_modules/ # Distribution files dist +llms-docs public/design-tokens.source.json *storybook.log diff --git a/.prettierignore b/.prettierignore index 9b921454..cd740aeb 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,3 +2,6 @@ tools/templates .yarn/* optics-design-system.code-workspace + +# Generated docs +llms-docs diff --git a/.storybook/main.js b/.storybook/main.js index 643d9fb0..029ea58d 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -19,6 +19,12 @@ const config = { from: './assets', to: '/public', }, + // AI-friendly markdown docs (llms.txt + per-page markdown), generated by + // `yarn build:llms` which runs as part of the storybook scripts. + { + from: '../llms-docs', + to: '/', + }, ], core: { diff --git a/README.md b/README.md index dd55d7e8..a9c3e175 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,16 @@ npm run generate The visual graphic found on the Selective Imports page in the documentation is generated from the `tools/generate-graph.dot` file. You can preview and export a newer svg version of the graphic by using the `tintinweb.graphviz-interactive-preview` VSCode extension. +### AI-Friendly Documentation (llms.txt) + +The docs site also serves the full documentation as plain markdown for AI tools: + +- [llms.txt](https://docs.optics.rolemodel.design/llms.txt) — index of every docs page +- [llms-full.txt](https://docs.optics.rolemodel.design/llms-full.txt) — the entire documentation in one file +- `https://docs.optics.rolemodel.design/llms/.md` — one markdown file per page + +These are generated from the Storybook MDX docs by `yarn build:llms` (run automatically by the `storybook` and `build-storybook` scripts). Each `` embed is replaced with the story's actual rendered HTML, and each token doc block with a table parsed from the token CSS. + ## License [MIT](LICENSE) diff --git a/package.json b/package.json index e544e706..260f303f 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,9 @@ "build:css-min": "postcss 'src/optics*.css' -d dist/css --ext .min.css --env=minify", "build:tokens": "node build_token_json --source=src/core/tokens --output=dist/tokens/tokens.json", "build:files": "mkdir -p dist/css/addons; cp LICENSE README.md package.json dist/; cp -rL src/addons src/core src/components dist/css", - "storybook": "storybook dev -p 6006 --docs", - "build-storybook": "storybook build --docs", + "build:llms": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON tools/build-llms-docs.mjs", + "storybook": "yarn build:llms && storybook dev -p 6006 --docs", + "build-storybook": "yarn build:llms && storybook build --docs", "lint": "yarn lint:js && yarn lint:css", "lint-fix": "yarn lint:js --fix && yarn lint:css --fix", "lint:js": "eslint 'src/stories/**/*.js'", @@ -56,6 +57,7 @@ "eslint-plugin-storybook": "^10.4.6", "generate-template-files": "^3.2.1", "globals": "^17.7.0", + "jsdom": "^29.1.1", "postcss": "^8.5.16", "postcss-cli": "^11.0.1", "postcss-import": "^16.1.1", diff --git a/tools/build-llms-docs.mjs b/tools/build-llms-docs.mjs new file mode 100644 index 00000000..cc783948 --- /dev/null +++ b/tools/build-llms-docs.mjs @@ -0,0 +1,351 @@ +/** + * Generates AI-friendly documentation (llms.txt + per-page markdown) from the + * Storybook MDX docs. Each `` is replaced with the + * story's actual rendered HTML, and each `` with a + * table of the tokens parsed from the token CSS files. + * + * Output (served from the root of the docs site via a Storybook staticDir): + * llms-docs/llms.txt — index of all pages + * llms-docs/llms-full.txt — every page concatenated + * llms-docs/llms/.md — one markdown file per docs page + */ +import fs from 'node:fs' +import path from 'node:path' +import { register } from 'node:module' +import { fileURLToPath, pathToFileURL } from 'node:url' +import { JSDOM } from 'jsdom' +import prettier from 'prettier' + +register('./llms-resolve-hooks.mjs', import.meta.url) + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const STORIES_DIR = path.join(ROOT, 'src/stories') +const TOKENS_DIR = path.join(ROOT, 'src/core/tokens') +const OUT_DIR = path.join(ROOT, 'llms-docs') +const SITE_URL = 'https://docs.optics.rolemodel.design' +const SOURCE_URL = 'https://github.com/RoleModel/optics/blob/main/src' + +// The story render functions build real DOM nodes. +const dom = new JSDOM('') +global.window = dom.window +global.document = dom.window.document +for (const key of ['HTMLElement', 'Node', 'CustomEvent', 'DocumentFragment']) { + global[key] = dom.window[key] +} +// jsdom does not implement innerText, which the story render functions use +Object.defineProperty(dom.window.HTMLElement.prototype, 'innerText', { + get() { + return this.textContent + }, + set(value) { + this.textContent = value + }, +}) + +const warnings = [] +const warn = (file, message) => warnings.push(`${path.relative(ROOT, file)}: ${message}`) + +// --------------------------------------------------------------------------- +// Design tokens, parsed from the `@tokens ` annotations in the CSS +// --------------------------------------------------------------------------- + +const parseTokenCategories = () => { + const categories = {} + for (const file of fs.readdirSync(TOKENS_DIR)) { + if (!file.endsWith('.css')) continue + const css = fs.readFileSync(path.join(TOKENS_DIR, file), 'utf8') + // Split on doc comments; a `@tokens` comment owns the tokens that follow it. + const segments = css.split(/\/\*\*/) + for (const segment of segments) { + const categoryMatch = segment.match(/@tokens\s+([^\n*]+)/) + if (!categoryMatch) continue + const name = categoryMatch[1].trim() + const body = segment.slice(segment.indexOf('*/') + 2) + const tokens = [] + for (const tokenMatch of body.matchAll(/(--[\w-]+)\s*:\s*([^;]+);/g)) { + tokens.push({ name: tokenMatch[1], value: tokenMatch[2].replace(/\s+/g, ' ').trim() }) + } + categories[name] = (categories[name] || []).concat(tokens) + } + } + return categories +} + +const tokenCategories = parseTokenCategories() + +const escapeCell = (text) => text.replaceAll('|', '\\|') + +const tokenTable = (categoryName, file) => { + const tokens = tokenCategories[categoryName] + if (!tokens || tokens.length === 0) { + warn(file, `no tokens found for category "${categoryName}"`) + return `_No tokens found for category "${categoryName}"._` + } + const rows = tokens.map((token) => `| \`${token.name}\` | \`${escapeCell(token.value)}\` |`) + return ['| Token | Value |', '| --- | --- |', ...rows].join('\n') +} + +// --------------------------------------------------------------------------- +// Story rendering +// --------------------------------------------------------------------------- + +const renderStory = async (storiesModule, storyName, file) => { + const meta = storiesModule.default || {} + const story = storiesModule[storyName] + if (!story) { + warn(file, `story "${storyName}" not found`) + return null + } + const render = story.render || meta.render + if (!render) { + warn(file, `story "${storyName}" has no render function`) + return null + } + const args = { ...meta.args, ...story.args } + let result + try { + result = render(args, { args }) + } catch (error) { + warn(file, `story "${storyName}" failed to render: ${error.message}`) + return null + } + let html = typeof result === 'string' ? result : result?.outerHTML + // A few stories interpolate optional args into class names (e.g. + // `form-control--undefined`); drop those so they don't read as real classes. + html = html?.replace(/ ?[\w-]+--undefined/g, '') + if (!html) { + warn(file, `story "${storyName}" rendered nothing`) + return null + } + try { + return (await prettier.format(html, { parser: 'html', printWidth: 110 })).trimEnd() + } catch { + return html + } +} + +const controlsTable = (storiesModule, storyName) => { + const meta = storiesModule.default || {} + const story = storiesModule[storyName] || {} + const argTypes = meta.argTypes || {} + const args = { ...meta.args, ...story.args } + const names = [...new Set([...Object.keys(argTypes), ...Object.keys(args)])] + if (names.length === 0) return null + const formatValue = (value) => (value === undefined ? '' : `\`${escapeCell(JSON.stringify(value))}\``) + const rows = names.map((name) => { + const argType = argTypes[name] || {} + const options = (argType.options || argType.control?.options || []).map((option) => `\`${option}\``).join(', ') + const description = escapeCell(argType.description || '').replace(/\s+/g, ' ') + return `| \`${name}\` | ${formatValue(args[name])} | ${escapeCell(options)} | ${description} |` + }) + return ['| Arg | Default | Options | Description |', '| --- | --- | --- | --- |', ...rows].join('\n') +} + +// --------------------------------------------------------------------------- +// MDX -> markdown +// --------------------------------------------------------------------------- + +const storybookSlug = (title) => + title + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + +const walk = (dir) => + fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const full = path.join(dir, entry.name) + if (entry.isDirectory()) return walk(full) + return entry.name.endsWith('.mdx') ? [full] : [] + }) + +const replaceAsync = async (text, regex, replacer) => { + const parts = [] + let lastIndex = 0 + for (const match of text.matchAll(regex)) { + parts.push(text.slice(lastIndex, match.index), await replacer(...match)) + lastIndex = match.index + match[0].length + } + parts.push(text.slice(lastIndex)) + return parts.join('') +} + +const convertMdx = async (file) => { + let text = fs.readFileSync(file, 'utf8') + + // Story module namespace imports (`import * as FooStories from './Foo.stories'`) + const storyModules = {} + for (const match of text.matchAll(/^import \* as (\w+) from '([^']+)'/gm)) { + let modulePath = path.resolve(path.dirname(file), match[2]) + if (!fs.existsSync(modulePath)) modulePath += '.js' + storyModules[match[1]] = await import(pathToFileURL(modulePath).href) + } + + // Page title: from `` or the story module's meta + let title = text.match(/\n?/g, '') + text = text.replace(/\{\/\*[\s\S]*?\*\/\}\n?/g, '') + + // Source-code breadcrumb helper -> plain link + text = text.replace( + //g, + (_, link) => `[Source Code](${SOURCE_URL}/${link})` + ) + + // Alert helper -> blockquote. The argument is a plain object literal; + // evaluate it to cope with any key order, template literals, or HTML in + // the description. + text = text.replace( + //g, + (whole, argsText) => { + let alertArgs + try { + alertArgs = new Function(`return (${argsText})`)() + } catch (error) { + warn(file, `could not parse createAlert args: ${error.message}`) + return '' + } + const scratch = document.createElement('div') + scratch.innerHTML = (alertArgs.description || '').replaceAll('', '\n') + const description = scratch.textContent + .split('\n') + .map((line) => line.replace(/\s+/g, ' ').trim()) + .filter(Boolean) + .join('\n> ') + return `> **${alertArgs.title || 'Note'}:** ${description}` + } + ) + + // -> rendered HTML + text = await replaceAsync(text, //g, async (whole, ns, storyName) => { + const storiesModule = storyModules[ns] + if (!storiesModule) { + warn(file, `unknown story module "${ns}"`) + return '' + } + const html = await renderStory(storiesModule, storyName, file) + return html ? `\`\`\`html\n${html}\n\`\`\`` : '' + }) + + // -> args table + text = text.replace(//g, (whole, ns, storyName) => { + const storiesModule = storyModules[ns] + if (!storiesModule) return '' + return controlsTable(storiesModule, storyName) || '' + }) + + // -> token table + text = text.replace(//g, (_, category) => + tokenTable(category, file) + ) + + // Color palette blocks (ColorScale.mdx) render swatches from JS at runtime; + // point readers at the token source instead. + text = text.replace( + /[\s\S]*?<\/ColorPalette>/g, + `_Full scale definitions: [scale_color_tokens.css](${SOURCE_URL}/core/tokens/scale_color_tokens.css)_` + ) + + // Storybook links (`(?path=/docs/--docs#anchor)`) -> markdown page links + text = text.replace(/\(\?path=\/docs\/([a-z0-9-]+?)--[a-z0-9-]+(#[^)]*)?\)/g, '($1.md$2)') + + // Inline JSX demos -> plain HTML + text = text.replace(/className=/g, 'class=') + + // Leftover JSX we do not handle: drop and warn + text = text.replace(/^\s*<[A-Z][\s\S]*?\/>\n?/gm, (block) => { + warn(file, `dropped unhandled JSX: ${block.trim().split('\n')[0]}`) + return '' + }) + if (/dangerouslySetInnerHTML/.test(text)) { + warn(file, 'unhandled dangerouslySetInnerHTML block left in output') + } + + text = text.replace(/\n{3,}/g, '\n\n').trim() + + // First prose paragraph after the H1 = page description for the index + const isProse = (line) => line && !/^[#[>`|<{\-*_]/.test(line) + const lines = text.split('\n').map((line) => line.trim()) + const firstProseIndex = lines.findIndex(isProse) + let description = '' + if (firstProseIndex !== -1) { + const paragraph = [] + for (let i = firstProseIndex; i < lines.length && isProse(lines[i]); i++) paragraph.push(lines[i]) + description = paragraph.join(' ') + } + + return { file, title, slug: storybookSlug(title), description, markdown: `${text}\n` } +} + +// --------------------------------------------------------------------------- +// Build +// --------------------------------------------------------------------------- + +const pages = [] +for (const file of walk(STORIES_DIR).sort()) { + const page = await convertMdx(file) + if (page) pages.push(page) +} + +const SECTION_ORDER = ['Introduction', 'Overview', 'Tokens', 'Utilities', 'Components', 'Recipes'] +const sectionOf = (page) => { + const section = page.title.split('/')[0] + return SECTION_ORDER.includes(section) ? section : 'Other' +} +const sectionRank = (name) => { + const index = SECTION_ORDER.indexOf(name) + return index === -1 ? SECTION_ORDER.length : index +} +pages.sort((a, b) => sectionRank(sectionOf(a)) - sectionRank(sectionOf(b)) || a.title.localeCompare(b.title)) + +fs.rmSync(OUT_DIR, { recursive: true, force: true }) +fs.mkdirSync(path.join(OUT_DIR, 'llms'), { recursive: true }) + +for (const page of pages) { + fs.writeFileSync(path.join(OUT_DIR, 'llms', `${page.slug}.md`), page.markdown) +} + +const version = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version + +const indexLines = [ + '# Optics Design System', + '', + `> Optics is a pure-CSS design system by RoleModel Software (npm: \`@rolemodel/optics\`, v${version}). It provides design tokens (CSS custom properties prefixed \`--op-\`), base styles, utilities, and BEM-style components (\`.btn\`, \`.card\`, ...) that are customized per project via token overrides.`, + '', + `Every page below is plain markdown. Component pages include the actual HTML markup for each variant, the component's CSS variable API, and customization patterns. The full documentation in one file: [llms-full.txt](${SITE_URL}/llms-full.txt). All design tokens as JSON: https://unpkg.com/@rolemodel/optics/dist/tokens/tokens.json`, +] + +let currentSection = null +for (const page of pages) { + const section = sectionOf(page) + if (section !== currentSection) { + indexLines.push('', `## ${section}`, '') + currentSection = section + } + const shortDescription = page.description.length > 160 ? `${page.description.slice(0, 157)}...` : page.description + indexLines.push( + `- [${page.title}](${SITE_URL}/llms/${page.slug}.md)${shortDescription ? `: ${shortDescription}` : ''}` + ) +} +fs.writeFileSync(path.join(OUT_DIR, 'llms.txt'), `${indexLines.join('\n')}\n`) + +const fullDoc = pages + .map((page) => `\n\n${page.markdown}`) + .join('\n---\n\n') +fs.writeFileSync(path.join(OUT_DIR, 'llms-full.txt'), `${indexLines.join('\n')}\n\n---\n\n${fullDoc}`) + +console.log(`llms-docs: wrote ${pages.length} pages to ${path.relative(ROOT, OUT_DIR)}/`) +if (warnings.length > 0) { + console.log(`llms-docs: ${warnings.length} warnings`) + for (const warning of warnings) console.log(` - ${warning}`) +} diff --git a/tools/llms-resolve-hooks.mjs b/tools/llms-resolve-hooks.mjs new file mode 100644 index 00000000..0fba18b7 --- /dev/null +++ b/tools/llms-resolve-hooks.mjs @@ -0,0 +1,13 @@ +// Resolve hook so Node can import the story files directly, which use +// extensionless relative imports (e.g. `from '../Button/Button'`) that Vite +// normally resolves for Storybook. +export async function resolve(specifier, context, nextResolve) { + try { + return await nextResolve(specifier, context) + } catch (error) { + if (error?.code === 'ERR_MODULE_NOT_FOUND' && specifier.startsWith('.') && !specifier.endsWith('.js')) { + return nextResolve(`${specifier}.js`, context) + } + throw error + } +} diff --git a/yarn.lock b/yarn.lock index c3f92963..a06aee6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,6 +12,46 @@ __metadata: languageName: node linkType: hard +"@asamuzakjp/css-color@npm:^5.1.11": + version: 5.1.11 + resolution: "@asamuzakjp/css-color@npm:5.1.11" + dependencies: + "@asamuzakjp/generational-cache": "npm:^1.0.1" + "@csstools/css-calc": "npm:^3.2.0" + "@csstools/css-color-parser": "npm:^4.1.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + checksum: 10c0/32720bdff8daea6a8847aba6cdfae55baa3b4a2690b51d21db7f0382bbd183f3d9f2d5126df50afd889062635684b2819e47113629ee2e80c99389e75f48d060 + languageName: node + linkType: hard + +"@asamuzakjp/dom-selector@npm:^7.1.1": + version: 7.1.1 + resolution: "@asamuzakjp/dom-selector@npm:7.1.1" + dependencies: + "@asamuzakjp/generational-cache": "npm:^1.0.1" + "@asamuzakjp/nwsapi": "npm:^2.3.9" + bidi-js: "npm:^1.0.3" + css-tree: "npm:^3.2.1" + is-potential-custom-element-name: "npm:^1.0.1" + checksum: 10c0/8cec1c618781c94de5836a215bbe5aafb4d8b835b18c51faf8547f4574afa39f92def3951e40123860062467613dd825f1e1600ff32e8045cc099a91796dcfb8 + languageName: node + linkType: hard + +"@asamuzakjp/generational-cache@npm:^1.0.1": + version: 1.0.1 + resolution: "@asamuzakjp/generational-cache@npm:1.0.1" + checksum: 10c0/1de62de43764e13fca3b9a31b7ea9b1bf0780fe053d266e40378a19ff8c66b543e011e6a0df02d410cd59bf981126706f176cdbb938985165202c4a079fe1057 + languageName: node + linkType: hard + +"@asamuzakjp/nwsapi@npm:^2.3.9": + version: 2.3.9 + resolution: "@asamuzakjp/nwsapi@npm:2.3.9" + checksum: 10c0/869b81382e775499c96c45c6dbe0d0766a6da04bcf0abb79f5333535c4e19946851acaa43398f896e2ecc5a1de9cf3db7cf8c4b1afac1ee3d15e21584546d74d + languageName: node + linkType: hard + "@babel/code-frame@npm:^7.0.0": version: 7.29.0 resolution: "@babel/code-frame@npm:7.29.0" @@ -1063,6 +1103,17 @@ __metadata: languageName: node linkType: hard +"@bramus/specificity@npm:^2.4.2": + version: 2.4.2 + resolution: "@bramus/specificity@npm:2.4.2" + dependencies: + css-tree: "npm:^3.0.0" + bin: + specificity: bin/cli.js + checksum: 10c0/c5f4e04e0bca0d2202598207a5eb0733c8109d12a68a329caa26373bec598d99db5bb785b8865fefa00fc01b08c6068138807ceb11a948fe15e904ed6cf4ba72 + languageName: node + linkType: hard + "@cacheable/memory@npm:^2.0.8": version: 2.0.9 resolution: "@cacheable/memory@npm:2.0.9" @@ -1092,7 +1143,14 @@ __metadata: languageName: node linkType: hard -"@csstools/css-calc@npm:^3.2.1": +"@csstools/color-helpers@npm:^6.1.0": + version: 6.1.0 + resolution: "@csstools/color-helpers@npm:6.1.0" + checksum: 10c0/ebb71eebcd6dde16a89d687c30d4c7f315f9079ec803497ebac0528d0a9ef09847eaf167b4565c4919f156e32e7ca2167199ac2d2020f184f7888551a3b4712b + languageName: node + linkType: hard + +"@csstools/css-calc@npm:^3.2.0, @csstools/css-calc@npm:^3.2.1": version: 3.2.1 resolution: "@csstools/css-calc@npm:3.2.1" peerDependencies: @@ -1102,6 +1160,19 @@ __metadata: languageName: node linkType: hard +"@csstools/css-color-parser@npm:^4.1.0": + version: 4.1.9 + resolution: "@csstools/css-color-parser@npm:4.1.9" + dependencies: + "@csstools/color-helpers": "npm:^6.1.0" + "@csstools/css-calc": "npm:^3.2.1" + peerDependencies: + "@csstools/css-parser-algorithms": ^4.0.0 + "@csstools/css-tokenizer": ^4.0.0 + checksum: 10c0/d9330a1d5b207230e97a522c9b73c1b625dbfbca7a91ac8888e05917efb01c6aa4075153e5af6ce028c3848660d3f24cf7b0f0760a080361089e1a3841e586c7 + languageName: node + linkType: hard + "@csstools/css-parser-algorithms@npm:^4.0.0": version: 4.0.0 resolution: "@csstools/css-parser-algorithms@npm:4.0.0" @@ -1111,7 +1182,7 @@ __metadata: languageName: node linkType: hard -"@csstools/css-syntax-patches-for-csstree@npm:^1.1.5": +"@csstools/css-syntax-patches-for-csstree@npm:^1.1.3, @csstools/css-syntax-patches-for-csstree@npm:^1.1.5": version: 1.1.6 resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.1.6" peerDependencies: @@ -1491,6 +1562,18 @@ __metadata: languageName: node linkType: hard +"@exodus/bytes@npm:^1.11.0, @exodus/bytes@npm:^1.15.0, @exodus/bytes@npm:^1.6.0": + version: 1.15.1 + resolution: "@exodus/bytes@npm:1.15.1" + peerDependencies: + "@noble/hashes": ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + "@noble/hashes": + optional: true + checksum: 10c0/333056a6953bbf875d9f3b86c32314de29458d842e5f56f6ef8034b18c2d9660184550093d1bae5de0064043d5e23f54cc03148798d9d29cf5167ac03f2e9f8c + languageName: node + linkType: hard + "@humanfs/core@npm:^0.19.1": version: 0.19.1 resolution: "@humanfs/core@npm:0.19.1" @@ -1988,6 +2071,7 @@ __metadata: eslint-plugin-storybook: "npm:^10.4.6" generate-template-files: "npm:^3.2.1" globals: "npm:^17.7.0" + jsdom: "npm:^29.1.1" modern-css-reset: "npm:^1.4.0" postcss: "npm:^8.5.16" postcss-cli: "npm:^11.0.1" @@ -2786,6 +2870,15 @@ __metadata: languageName: node linkType: hard +"bidi-js@npm:^1.0.3": + version: 1.0.3 + resolution: "bidi-js@npm:1.0.3" + dependencies: + require-from-string: "npm:^2.0.2" + checksum: 10c0/fdddea4aa4120a34285486f2267526cd9298b6e8b773ad25e765d4f104b6d7437ab4ba542e6939e3ac834a7570bcf121ee2cf6d3ae7cd7082c4b5bedc8f271e1 + languageName: node + linkType: hard + "big.js@npm:^5.2.2": version: 5.2.2 resolution: "big.js@npm:5.2.2" @@ -3120,7 +3213,7 @@ __metadata: languageName: node linkType: hard -"css-tree@npm:^3.0.1, css-tree@npm:^3.2.1": +"css-tree@npm:^3.0.0, css-tree@npm:^3.0.1, css-tree@npm:^3.2.1": version: 3.2.1 resolution: "css-tree@npm:3.2.1" dependencies: @@ -3252,6 +3345,16 @@ __metadata: languageName: node linkType: hard +"data-urls@npm:^7.0.0": + version: 7.0.0 + resolution: "data-urls@npm:7.0.0" + dependencies: + whatwg-mimetype: "npm:^5.0.0" + whatwg-url: "npm:^16.0.0" + checksum: 10c0/08d88ef50d8966a070ffdaa703e1e4b29f01bb2da364dfbc1612b1c2a4caa8045802c9532d81347b21781100132addb36a585071c8323b12cce97973961dee9f + languageName: node + linkType: hard + "debug@npm:4, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.4.3": version: 4.4.3 resolution: "debug@npm:4.4.3" @@ -3271,6 +3374,13 @@ __metadata: languageName: node linkType: hard +"decimal.js@npm:^10.6.0": + version: 10.6.0 + resolution: "decimal.js@npm:10.6.0" + checksum: 10c0/07d69fbcc54167a340d2d97de95f546f9ff1f69d2b45a02fd7a5292412df3cd9eb7e23065e532a318f5474a2e1bccf8392fdf0443ef467f97f3bf8cb0477e5aa + languageName: node + linkType: hard + "deep-eql@npm:^5.0.1": version: 5.0.2 resolution: "deep-eql@npm:5.0.2" @@ -3462,6 +3572,13 @@ __metadata: languageName: node linkType: hard +"entities@npm:^8.0.0": + version: 8.0.0 + resolution: "entities@npm:8.0.0" + checksum: 10c0/938e631664c19451823344a351aeeafd74fae2d5fa51e4d5b6ff635afaefd4bacf0f609989888c04c42733f46ffdac15211608267ebb02488005891a4793e94d + languageName: node + linkType: hard + "env-paths@npm:^2.2.0, env-paths@npm:^2.2.1": version: 2.2.1 resolution: "env-paths@npm:2.2.1" @@ -4268,6 +4385,15 @@ __metadata: languageName: node linkType: hard +"html-encoding-sniffer@npm:^6.0.0": + version: 6.0.0 + resolution: "html-encoding-sniffer@npm:6.0.0" + dependencies: + "@exodus/bytes": "npm:^1.6.0" + checksum: 10c0/66dc3f6f5539cc3beb814fcbfae7eacf4ec38cf824d6e1425b72039b51a40f4456bd8541ba66f4f4fe09cdf885ab5cd5bae6ec6339d6895a930b2fdb83c53025 + languageName: node + linkType: hard + "html-tags@npm:^5.1.0": version: 5.1.0 resolution: "html-tags@npm:5.1.0" @@ -4614,6 +4740,40 @@ __metadata: languageName: node linkType: hard +"jsdom@npm:^29.1.1": + version: 29.1.1 + resolution: "jsdom@npm:29.1.1" + dependencies: + "@asamuzakjp/css-color": "npm:^5.1.11" + "@asamuzakjp/dom-selector": "npm:^7.1.1" + "@bramus/specificity": "npm:^2.4.2" + "@csstools/css-syntax-patches-for-csstree": "npm:^1.1.3" + "@exodus/bytes": "npm:^1.15.0" + css-tree: "npm:^3.2.1" + data-urls: "npm:^7.0.0" + decimal.js: "npm:^10.6.0" + html-encoding-sniffer: "npm:^6.0.0" + is-potential-custom-element-name: "npm:^1.0.1" + lru-cache: "npm:^11.3.5" + parse5: "npm:^8.0.1" + saxes: "npm:^6.0.0" + symbol-tree: "npm:^3.2.4" + tough-cookie: "npm:^6.0.1" + undici: "npm:^7.25.0" + w3c-xmlserializer: "npm:^5.0.0" + webidl-conversions: "npm:^8.0.1" + whatwg-mimetype: "npm:^5.0.0" + whatwg-url: "npm:^16.0.1" + xml-name-validator: "npm:^5.0.0" + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + checksum: 10c0/20e2174b09d9d06393cb48e1392b7a1cb7191d6656a6f7b3b8fbf9853b4ab0ef60b4a42c2c55f71b55ca5da50ffa75bcdc6986210963182e7993c6f9cd4f499b + languageName: node + linkType: hard + "jsesc@npm:^3.0.2, jsesc@npm:~3.1.0": version: 3.1.0 resolution: "jsesc@npm:3.1.0" @@ -4911,6 +5071,13 @@ __metadata: languageName: node linkType: hard +"lru-cache@npm:^11.3.5": + version: 11.5.2 + resolution: "lru-cache@npm:11.5.2" + checksum: 10c0/ece1ad731f5b655e85d67047d04bfc13823dc77aa61c5454924a9869ba600a0104e39cc33d726021feef812bc347ca34a5608a5eb1972a5dd0870b7ecd42c3f2 + languageName: node + linkType: hard + "make-fetch-happen@npm:^13.0.0": version: 13.0.1 resolution: "make-fetch-happen@npm:13.0.1" @@ -5512,6 +5679,15 @@ __metadata: languageName: node linkType: hard +"parse5@npm:^8.0.1": + version: 8.0.1 + resolution: "parse5@npm:8.0.1" + dependencies: + entities: "npm:^8.0.0" + checksum: 10c0/c3c1c5aab55f6e4be5245599790e56e64be7764a4a0edd7f98db4fe3bb380f63add752fa047dff0496446c25f4104f0c7c1967723de640bde92306a7bb67ed2f + languageName: node + linkType: hard + "path-exists@npm:4.0.0, path-exists@npm:^4.0.0": version: 4.0.0 resolution: "path-exists@npm:4.0.0" @@ -6115,7 +6291,7 @@ __metadata: languageName: node linkType: hard -"punycode@npm:^2.1.0, punycode@npm:^2.1.1, punycode@npm:^2.3.0": +"punycode@npm:^2.1.0, punycode@npm:^2.1.1, punycode@npm:^2.3.0, punycode@npm:^2.3.1": version: 2.3.1 resolution: "punycode@npm:2.3.1" checksum: 10c0/14f76a8206bc3464f794fb2e3d3cc665ae416c01893ad7a02b23766eb07159144ee612ad67af5e84fa4479ccfe67678c4feb126b0485651b302babf66f04f9e9 @@ -6999,6 +7175,24 @@ __metadata: languageName: node linkType: hard +"tldts-core@npm:^7.4.8": + version: 7.4.8 + resolution: "tldts-core@npm:7.4.8" + checksum: 10c0/94edee2a06cb1929fae4a26045628b01dd865ee8e28661df29128228522fb8dcf29c2f68ffa7286ee41a2a2c98da3e6814dbf49a1f6509de3fe2c9448e0db1b7 + languageName: node + linkType: hard + +"tldts@npm:^7.0.5": + version: 7.4.8 + resolution: "tldts@npm:7.4.8" + dependencies: + tldts-core: "npm:^7.4.8" + bin: + tldts: bin/cli.js + checksum: 10c0/438b379a43b105359311427b8ab9d73572244996b4f6dd1fbe9112bd274c308d4ffb33b6f644302e79c8bc60f2e2539463b00dd91ee1063b92d9fc8b10882b6c + languageName: node + linkType: hard + "to-regex-range@npm:^5.0.1": version: 5.0.1 resolution: "to-regex-range@npm:5.0.1" @@ -7020,6 +7214,15 @@ __metadata: languageName: node linkType: hard +"tough-cookie@npm:^6.0.1": + version: 6.0.2 + resolution: "tough-cookie@npm:6.0.2" + dependencies: + tldts: "npm:^7.0.5" + checksum: 10c0/5ff521a476a3c540821352125a5d481c8d2fe16035de7e0efda4df120f290c95500a0e9b51ea0aa56343955be482e014c30f5ba73e04b93ba138e4e855cb9e89 + languageName: node + linkType: hard + "tr46@npm:^4.1.1": version: 4.1.1 resolution: "tr46@npm:4.1.1" @@ -7029,6 +7232,15 @@ __metadata: languageName: node linkType: hard +"tr46@npm:^6.0.0": + version: 6.0.0 + resolution: "tr46@npm:6.0.0" + dependencies: + punycode: "npm:^2.3.1" + checksum: 10c0/83130df2f649228aa91c17754b66248030a3af34911d713b5ea417066fa338aa4bc8668d06bd98aa21a2210f43fc0a3db8b9099e7747fb5830e40e39a6a1058e + languageName: node + linkType: hard + "ts-api-utils@npm:^2.4.0": version: 2.4.0 resolution: "ts-api-utils@npm:2.4.0" @@ -7061,6 +7273,13 @@ __metadata: languageName: node linkType: hard +"undici@npm:^7.25.0": + version: 7.28.0 + resolution: "undici@npm:7.28.0" + checksum: 10c0/fe781983a26098795e99bb1f64906cbb7d0bcaa029a26baade007b53ea67f2631d189b8f9671a31f4c8d0cb3773b7559608628ba54452fef51fec90e7c78bb0d + languageName: node + linkType: hard + "unicode-canonical-property-names-ecmascript@npm:^2.0.0": version: 2.0.0 resolution: "unicode-canonical-property-names-ecmascript@npm:2.0.0" @@ -7258,6 +7477,15 @@ __metadata: languageName: node linkType: hard +"w3c-xmlserializer@npm:^5.0.0": + version: 5.0.0 + resolution: "w3c-xmlserializer@npm:5.0.0" + dependencies: + xml-name-validator: "npm:^5.0.0" + checksum: 10c0/8712774c1aeb62dec22928bf1cdfd11426c2c9383a1a63f2bcae18db87ca574165a0fbe96b312b73652149167ac6c7f4cf5409f2eb101d9c805efe0e4bae798b + languageName: node + linkType: hard + "webidl-conversions@npm:^7.0.0": version: 7.0.0 resolution: "webidl-conversions@npm:7.0.0" @@ -7265,6 +7493,13 @@ __metadata: languageName: node linkType: hard +"webidl-conversions@npm:^8.0.1": + version: 8.0.1 + resolution: "webidl-conversions@npm:8.0.1" + checksum: 10c0/3f6f327ca5fa0c065ed8ed0ef3b72f33623376e68f958e9b7bd0df49fdb0b908139ac2338d19fb45bd0e05595bda96cb6d1622222a8b413daa38a17aacc4dd46 + languageName: node + linkType: hard + "webpack-virtual-modules@npm:^0.6.2": version: 0.6.2 resolution: "webpack-virtual-modules@npm:0.6.2" @@ -7288,6 +7523,13 @@ __metadata: languageName: node linkType: hard +"whatwg-mimetype@npm:^5.0.0": + version: 5.0.0 + resolution: "whatwg-mimetype@npm:5.0.0" + checksum: 10c0/eead164fe73a00dd82f817af6fc0bd22e9c273e1d55bf4bc6bdf2da7ad8127fca82ef00ea6a37892f5f5641f8e34128e09508f92126086baba126b9e0d57feb4 + languageName: node + linkType: hard + "whatwg-url@npm:^12.0.0, whatwg-url@npm:^12.0.1": version: 12.0.1 resolution: "whatwg-url@npm:12.0.1" @@ -7298,6 +7540,17 @@ __metadata: languageName: node linkType: hard +"whatwg-url@npm:^16.0.0, whatwg-url@npm:^16.0.1": + version: 16.0.1 + resolution: "whatwg-url@npm:16.0.1" + dependencies: + "@exodus/bytes": "npm:^1.11.0" + tr46: "npm:^6.0.0" + webidl-conversions: "npm:^8.0.1" + checksum: 10c0/e75565566abf3a2cdbd9f06c965dbcccee6ec4e9f0d3728ad5e08ceb9944279848bcaa211d35a29cb6d2df1e467dd05cfb59fbddf8a0adcd7d0bce9ffb703fd2 + languageName: node + linkType: hard + "which@npm:^1.3.1": version: 1.3.1 resolution: "which@npm:1.3.1" @@ -7407,6 +7660,13 @@ __metadata: languageName: node linkType: hard +"xml-name-validator@npm:^5.0.0": + version: 5.0.0 + resolution: "xml-name-validator@npm:5.0.0" + checksum: 10c0/3fcf44e7b73fb18be917fdd4ccffff3639373c7cb83f8fc35df6001fecba7942f1dbead29d91ebb8315e2f2ff786b508f0c9dc0215b6353f9983c6b7d62cb1f5 + languageName: node + linkType: hard + "xmlchars@npm:^2.2.0": version: 2.2.0 resolution: "xmlchars@npm:2.2.0" From a905c33f83ed9794665da2d301f542201271878a Mon Sep 17 00:00:00 2001 From: Kyle Shepherd Date: Wed, 15 Jul 2026 10:34:15 -0500 Subject: [PATCH 2/5] Add explanatory comments with output examples to llms docs scripts --- tools/build-llms-docs.mjs | 86 +++++++++++++++++++++++++++++++++--- tools/llms-resolve-hooks.mjs | 8 ++-- 2 files changed, 85 insertions(+), 9 deletions(-) diff --git a/tools/build-llms-docs.mjs b/tools/build-llms-docs.mjs index cc783948..c7bb4843 100644 --- a/tools/build-llms-docs.mjs +++ b/tools/build-llms-docs.mjs @@ -8,6 +8,20 @@ * llms-docs/llms.txt — index of all pages * llms-docs/llms-full.txt — every page concatenated * llms-docs/llms/.md — one markdown file per docs page + * + * For example, Button.mdx becomes llms/components-button.md, which starts: + * + * # Button + * + * [Source Code](https://github.com/RoleModel/optics/blob/main/src/components/button.css) + * + * Button classes can be used on `button` or `a` html elements. ... + * + * ## Playground + * + * ```html + * + * ``` */ import fs from 'node:fs' import path from 'node:path' @@ -49,6 +63,18 @@ const warn = (file, message) => warnings.push(`${path.relative(ROOT, file)}: ${m // Design tokens, parsed from the `@tokens ` annotations in the CSS // --------------------------------------------------------------------------- +// Scans every CSS file in src/core/tokens for doc comments like: +// +// /** +// * Border Radius +// * @tokens Border Radius +// * @presenter BorderRadius +// */ +// --op-radius-small: 0.125rem; +// --op-radius-medium: 0.25rem; +// +// and returns { 'Border Radius': [{ name: '--op-radius-small', value: '0.125rem' }, ...], ... }. +// These are the same annotations storybook-design-token uses to build its doc blocks. const parseTokenCategories = () => { const categories = {} for (const file of fs.readdirSync(TOKENS_DIR)) { @@ -73,8 +99,15 @@ const parseTokenCategories = () => { const tokenCategories = parseTokenCategories() +// `|` would break out of a markdown table cell const escapeCell = (text) => text.replaceAll('|', '\\|') +// Renders one category as a markdown table, e.g. tokenTable('Border Radius'): +// +// | Token | Value | +// | --- | --- | +// | `--op-radius-small` | `0.125rem` | +// | `--op-radius-medium` | `0.25rem` | const tokenTable = (categoryName, file) => { const tokens = tokenCategories[categoryName] if (!tokens || tokens.length === 0) { @@ -89,6 +122,11 @@ const tokenTable = (categoryName, file) => { // Story rendering // --------------------------------------------------------------------------- +// Calls a story's render function (the same one Storybook calls) with its +// resolved args and returns the resulting markup, pretty-printed. For example +// the Button `Primary` story returns: +// +// const renderStory = async (storiesModule, storyName, file) => { const meta = storiesModule.default || {} const story = storiesModule[storyName] @@ -124,6 +162,13 @@ const renderStory = async (storiesModule, storyName, file) => { } } +// Markdown stand-in for Storybook's panel, built from the story +// meta's argTypes and default args. For the Button story it renders: +// +// | Arg | Default | Options | Description | +// | --- | --- | --- | --- | +// | `label` | `"Primary"` | | | +// | `variant` | `"default"` | `default`, `primary`, `destructive`, `warning` | | const controlsTable = (storiesModule, storyName) => { const meta = storiesModule.default || {} const story = storiesModule[storyName] || {} @@ -145,6 +190,8 @@ const controlsTable = (storiesModule, storyName) => { // MDX -> markdown // --------------------------------------------------------------------------- +// Mirrors Storybook's own URL slugs so page links stay stable, e.g. +// 'Components/Button Group' becomes 'components-button-group' const storybookSlug = (title) => title .toLowerCase() @@ -158,6 +205,7 @@ const walk = (dir) => return entry.name.endsWith('.mdx') ? [full] : [] }) +// String.replace with an async replacer (needed because rendering a story is async) const replaceAsync = async (text, regex, replacer) => { const parts = [] let lastIndex = 0 @@ -169,6 +217,8 @@ const replaceAsync = async (text, regex, replacer) => { return parts.join('') } +// Converts one MDX docs page to plain markdown by replacing each JSX/Storybook +// construct with a static markdown equivalent (details at each step below). const convertMdx = async (file) => { let text = fs.readFileSync(file, 'utf8') @@ -196,7 +246,11 @@ const convertMdx = async (file) => { text = text.replace(/\n?/g, '') text = text.replace(/\{\/\*[\s\S]*?\*\/\}\n?/g, '') - // Source-code breadcrumb helper -> plain link + // Source-code breadcrumb helper -> plain link. The MDX pages embed + // `createSourceCodeLink({ link: 'components/button.css' })` in a div; + // it becomes: + // + // [Source Code](https://github.com/RoleModel/optics/blob/main/src/components/button.css) text = text.replace( //g, (_, link) => `[Source Code](${SOURCE_URL}/${link})` @@ -204,7 +258,10 @@ const convertMdx = async (file) => { // Alert helper -> blockquote. The argument is a plain object literal; // evaluate it to cope with any key order, template literals, or HTML in - // the description. + // the description. `createAlert({ title: 'Note', description: 'Some info.' })` + // becomes: + // + // > **Note:** Some info. text = text.replace( //g, (whole, argsText) => { @@ -226,7 +283,12 @@ const convertMdx = async (file) => { } ) - // -> rendered HTML + // -> the story's rendered HTML in a fenced + // code block. `` becomes: + // + // ```html + // + // ``` text = await replaceAsync(text, //g, async (whole, ns, storyName) => { const storiesModule = storyModules[ns] if (!storiesModule) { @@ -237,14 +299,14 @@ const convertMdx = async (file) => { return html ? `\`\`\`html\n${html}\n\`\`\`` : '' }) - // -> args table + // -> markdown args table (see controlsTable above) text = text.replace(//g, (whole, ns, storyName) => { const storiesModule = storyModules[ns] if (!storiesModule) return '' return controlsTable(storiesModule, storyName) || '' }) - // -> token table + // -> markdown token table (see tokenTable above) text = text.replace(//g, (_, category) => tokenTable(category, file) ) @@ -256,7 +318,9 @@ const convertMdx = async (file) => { `_Full scale definitions: [scale_color_tokens.css](${SOURCE_URL}/core/tokens/scale_color_tokens.css)_` ) - // Storybook links (`(?path=/docs/--docs#anchor)`) -> markdown page links + // Storybook links -> relative markdown page links, so cross-references stay + // navigable inside the llms docs. `(?path=/docs/components-button--docs#usage)` + // becomes `(components-button.md#usage)`. text = text.replace(/\(\?path=\/docs\/([a-z0-9-]+?)--[a-z0-9-]+(#[^)]*)?\)/g, '($1.md$2)') // Inline JSX demos -> plain HTML @@ -297,6 +361,9 @@ for (const file of walk(STORIES_DIR).sort()) { if (page) pages.push(page) } +// Group pages by the first segment of their Storybook title (e.g. +// 'Components/Button' is in 'Components'), in the same order the docs +// site's sidebar uses. const SECTION_ORDER = ['Introduction', 'Overview', 'Tokens', 'Utilities', 'Components', 'Recipes'] const sectionOf = (page) => { const section = page.title.split('/')[0] @@ -317,6 +384,10 @@ for (const page of pages) { const version = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8')).version +// llms.txt index, following the llms.txt convention (https://llmstxt.org): +// an H1, a blockquote summary, then sections of page links. Each entry looks like: +// +// - [Components/Button](https://docs.optics.rolemodel.design/llms/components-button.md): Button classes can be used on `button` or `a` html elements. ... const indexLines = [ '# Optics Design System', '', @@ -339,6 +410,9 @@ for (const page of pages) { } fs.writeFileSync(path.join(OUT_DIR, 'llms.txt'), `${indexLines.join('\n')}\n`) +// llms-full.txt: the index followed by every page, each introduced by an +// HTML comment marker (``) +// and separated by a horizontal rule. const fullDoc = pages .map((page) => `\n\n${page.markdown}`) .join('\n---\n\n') diff --git a/tools/llms-resolve-hooks.mjs b/tools/llms-resolve-hooks.mjs index 0fba18b7..470305b6 100644 --- a/tools/llms-resolve-hooks.mjs +++ b/tools/llms-resolve-hooks.mjs @@ -1,6 +1,8 @@ -// Resolve hook so Node can import the story files directly, which use -// extensionless relative imports (e.g. `from '../Button/Button'`) that Vite -// normally resolves for Storybook. +// Node module-resolution hook (registered by build-llms-docs.mjs) so Node can +// import the story files directly. The stories use extensionless relative +// imports (e.g. `from '../Button/Button'`) that Vite normally resolves for +// Storybook but Node's ESM loader rejects; when such an import fails, retry +// it with `.js` appended (`'../Button/Button'` becomes `'../Button/Button.js'`). export async function resolve(specifier, context, nextResolve) { try { return await nextResolve(specifier, context) From b1b0f09dd6ed636ccd2e1c329052a29b7a489e5b Mon Sep 17 00:00:00 2001 From: Jeremy Walton Date: Wed, 15 Jul 2026 11:42:18 -0400 Subject: [PATCH 3/5] Tweak commands to match existing structure --- .github/workflows/linting.yml | 4 +++- .github/workflows/publish-storybook.yml | 4 ++-- .storybook/main.js | 2 +- README.md | 4 +++- package.json | 9 +++++---- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index d198d998..468f23a7 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -34,4 +34,6 @@ jobs: - name: Build Tokens run: yarn build:tokens - name: Build Storybook - run: yarn build-storybook + run: yarn build-docs:storybook + - name: Build LLMS Docs + run: yarn build-docs:llms diff --git a/.github/workflows/publish-storybook.yml b/.github/workflows/publish-storybook.yml index 6a4d3ce1..1e7e7dd0 100644 --- a/.github/workflows/publish-storybook.yml +++ b/.github/workflows/publish-storybook.yml @@ -24,12 +24,12 @@ jobs: yarn install --immutable - name: Build 🔧 run: | # build the Storybook files - yarn build-storybook + yarn build-docs - name: Deploy 🚀 uses: JamesIves/github-pages-deploy-action@3.6.2 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BRANCH: gh-pages # The branch the action should deploy to. - FOLDER: storybook-static # The folder that the build-storybook script generates files. + FOLDER: storybook-static # The folder that the build-docs:storybook script generates files. CLEAN: true # Automatically remove deleted files from the deploy branch TARGET_FOLDER: docs # The folder that we serve our Storybook files from diff --git a/.storybook/main.js b/.storybook/main.js index 029ea58d..77ecb3f0 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -20,7 +20,7 @@ const config = { to: '/public', }, // AI-friendly markdown docs (llms.txt + per-page markdown), generated by - // `yarn build:llms` which runs as part of the storybook scripts. + // `yarn build-docs:llms` which runs as part of the storybook scripts. { from: '../llms-docs', to: '/', diff --git a/README.md b/README.md index a9c3e175..9443563f 100644 --- a/README.md +++ b/README.md @@ -93,13 +93,15 @@ The visual graphic found on the Selective Imports page in the documentation is g ### AI-Friendly Documentation (llms.txt) +[llms txt](https://llmstxt.org/) + The docs site also serves the full documentation as plain markdown for AI tools: - [llms.txt](https://docs.optics.rolemodel.design/llms.txt) — index of every docs page - [llms-full.txt](https://docs.optics.rolemodel.design/llms-full.txt) — the entire documentation in one file - `https://docs.optics.rolemodel.design/llms/.md` — one markdown file per page -These are generated from the Storybook MDX docs by `yarn build:llms` (run automatically by the `storybook` and `build-storybook` scripts). Each `` embed is replaced with the story's actual rendered HTML, and each token doc block with a table parsed from the token CSS. +These are generated from the Storybook MDX docs by `yarn build-docs:llms`. Each `` embed is replaced with the story's actual rendered HTML, and each token doc block with a table parsed from the token CSS. ## License diff --git a/package.json b/package.json index 260f303f..03a398d5 100644 --- a/package.json +++ b/package.json @@ -11,16 +11,17 @@ "build:css-min": "postcss 'src/optics*.css' -d dist/css --ext .min.css --env=minify", "build:tokens": "node build_token_json --source=src/core/tokens --output=dist/tokens/tokens.json", "build:files": "mkdir -p dist/css/addons; cp LICENSE README.md package.json dist/; cp -rL src/addons src/core src/components dist/css", - "build:llms": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON tools/build-llms-docs.mjs", - "storybook": "yarn build:llms && storybook dev -p 6006 --docs", - "build-storybook": "yarn build:llms && storybook build --docs", + "build-docs": "yarn build-docs:llms && yarn build-docs:storybook", + "build-docs:llms": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON tools/build-llms-docs.mjs", + "build-docs:storybook": "storybook build --docs", + "storybook": "storybook dev -p 6006 --docs", "lint": "yarn lint:js && yarn lint:css", "lint-fix": "yarn lint:js --fix && yarn lint:css --fix", "lint:js": "eslint 'src/stories/**/*.js'", "lint:css": "stylelint 'src/**/*.css'", "prettier": "prettier -w .", "prettier-check": "prettier -c .", - "sanity-check": "yarn lint && yarn prettier && yarn build && yarn build-storybook && rm -rf ./dist && rm -rf ./storybook-static", + "sanity-check": "yarn lint && yarn prettier && yarn build && yarn build-docs && rm -rf ./dist && rm -rf ./storybook-static", "generate": "node ./tools/generate.js" }, "repository": { From 8106366188cd0e2cf01f1d7a76f83493fc0e2467 Mon Sep 17 00:00:00 2001 From: Jeremy Walton Date: Wed, 15 Jul 2026 11:46:52 -0400 Subject: [PATCH 4/5] Clean up after the llms generator during sanity check --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 03a398d5..7c9c7b36 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "lint:css": "stylelint 'src/**/*.css'", "prettier": "prettier -w .", "prettier-check": "prettier -c .", - "sanity-check": "yarn lint && yarn prettier && yarn build && yarn build-docs && rm -rf ./dist && rm -rf ./storybook-static", + "sanity-check": "yarn lint && yarn prettier && yarn build && yarn build-docs && rm -rf ./dist && rm -rf ./storybook-static && rm -rf ./llms-docs", "generate": "node ./tools/generate.js" }, "repository": { From 502d268e4c3a1983dcd5bcd90a3a8bdfc26f9436 Mon Sep 17 00:00:00 2001 From: Jeremy Walton Date: Wed, 15 Jul 2026 11:53:27 -0400 Subject: [PATCH 5/5] Fix the linting --- .github/workflows/linting.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 468f23a7..029a1868 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -33,7 +33,7 @@ jobs: run: yarn build:css - name: Build Tokens run: yarn build:tokens - - name: Build Storybook - run: yarn build-docs:storybook - name: Build LLMS Docs run: yarn build-docs:llms + - name: Build Storybook + run: yarn build-docs:storybook