From e00778797be91f23920624a9f345f99d54495c46 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Jun 2026 00:39:50 +0000 Subject: [PATCH] Add PDF/SVG export scripts, wire statefulness tooltip to aria-describedby - scripts/export-pdf.js prints index.html to exports/ai-agent-patterns.pdf via Playwright, with a new @media print block hiding the toolbar and preventing cards from splitting across page breaks. - scripts/export-svg.js extracts each pattern's flow diagram as a fully standalone exports/svg/pattern-{slug}.svg by baking computed styles and inlining the shared arrow marker/shadow filter defs. - Both wired into npm scripts and ci.yml's PR validation smoke test. - Statefulness meta value now has aria-describedby pointing at a visually hidden span with the same tooltip text, since the CSS ::after tooltip isn't exposed to screen readers. - Removed the leftover "Built with Claude" site footer text. - Updated CLAUDE.md/README.md to document all of the above. --- .github/workflows/ci.yml | 2 + CLAUDE.md | 30 +++++++++--- README.md | 20 +++++--- index.html | 19 +++++++- package.json | 4 +- scripts/export-pdf.js | 31 +++++++++++++ scripts/export-svg.js | 99 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 188 insertions(+), 17 deletions(-) create mode 100644 scripts/export-pdf.js create mode 100644 scripts/export-svg.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eeea9f5..c52c25f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,3 +15,5 @@ jobs: - run: npm run validate - run: npm run export:images - run: npm run export:pptx + - run: npm run export:pdf + - run: npm run export:svg diff --git a/CLAUDE.md b/CLAUDE.md index a0e8845..6621d3d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -103,6 +103,11 @@ tooltip on the "At a glance" panel's Statefulness row. Every distinct `statefulness` value used in `PATTERNS` must have an entry here — `scripts/validate-patterns.js` enforces this. +The tooltip text is also exposed to assistive tech: the Statefulness value +has `aria-describedby` pointing at a visually-hidden (`.sr-only`) span +holding the same string, since the `data-tip`/`::after` CSS tooltip content +isn't reliably read by screen readers on its own. + ### Deep dive button Each expanded card has a "Deep dive" button (`copyDeepDive` / @@ -141,11 +146,20 @@ Color each slide accent from the token system. Output: `exports/ai-agent-patterns.pptx` ### 4. PDF export -Use Playwright to print `index.html` to PDF. +Script target: `export-pdf.js`. Opens `index.html`, switches to `print` +media emulation (a `@media print` block in `index.html` hides the toolbar +and keeps cards from splitting across page breaks), and prints to A4 via +Playwright's `page.pdf()`. Output: `exports/ai-agent-patterns.pdf` ### 5. SVG per pattern card -Extract each card as a standalone SVG for embedding in docs/slides. +Script target: `export-svg.js`. Expands each card in turn and pulls its +`svg.diagram` flow diagram (the same SVG shown on the page), then bakes +every element's resolved `fill`/`stroke`/`font-*`/`filter` etc. into +inline `style` attributes via `getComputedStyle` and inlines the shared +`#arrow` marker and `#nodeShadow` filter `` — so each file has zero +dependency on `index.html`'s stylesheet or CSS custom properties and +renders correctly opened on its own or pasted into docs/slides. Output: `exports/svg/pattern-{slug}.svg` --- @@ -171,7 +185,8 @@ ai-agent-patterns/ │ ├── validate-patterns.js │ ├── export-images.js │ ├── export-pptx.js -│ └── export-pdf.js +│ ├── export-pdf.js +│ └── export-svg.js └── .github/ └── workflows/ ├── deploy.yml ← GitHub Pages deploy action @@ -225,10 +240,11 @@ the real `PATTERNS` data and renderer rather than a separate copy: no duplicate names, and that every distinct `statefulness` value has a matching entry in `STATEFULNESS_INFO` so the tooltip never renders empty). -2. `npm run export:images` / `npm run export:pptx` — runs the real export - scripts end-to-end as a smoke test. A thrown error here means the - renderer or PATTERNS data broke something the schema check can't see - (e.g. a layout exception), not just a malformed field. +2. `npm run export:images` / `npm run export:pptx` / `npm run export:pdf` / + `npm run export:svg` — runs all four real export scripts end-to-end as + a smoke test. A thrown error here means the renderer or PATTERNS data + broke something the schema check can't see (e.g. a layout exception), + not just a malformed field. This does not catch visual regressions (e.g. a node clipped at the edge of its SVG `viewBox`) — only crashes and schema violations. Visual diff --git a/README.md b/README.md index 65d1623..32b79c2 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ explanation). The "Deep dive" button copies a ready-to-paste prompt following up in a Claude conversation. The same `index.html` is the source of truth for every other export format -in this repo (LinkedIn images, PPTX slides, PDF) — all of them render -directly from the `PATTERNS` array defined in that file. +in this repo (LinkedIn images, PPTX slides, PDF, per-pattern SVGs) — all of +them render directly from the `PATTERNS` array defined in that file. ![Site preview](docs/preview.png)
AI Agent Design Patterns · Interactive Reference - Click any card to expand · Built with Claude + Click any card to expand
@@ -1014,7 +1029,7 @@

AI agent design patterns

Complexity${p.complexity}
Agent count${p.agents}
Loop type${p.loops}
-
Statefulness${p.statefulness}
+
Statefulness${p.statefulness}${STATEFULNESS_INFO[p.statefulness] || ''}
`; diff --git a/package.json b/package.json index 22c0afe..0659005 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "scripts": { "validate": "node scripts/validate-patterns.js", "export:images": "node scripts/export-images.js", - "export:pptx": "node scripts/export-pptx.js" + "export:pptx": "node scripts/export-pptx.js", + "export:pdf": "node scripts/export-pdf.js", + "export:svg": "node scripts/export-svg.js" }, "devDependencies": { "playwright": "^1.47.0", diff --git a/scripts/export-pdf.js b/scripts/export-pdf.js new file mode 100644 index 0000000..eea0a75 --- /dev/null +++ b/scripts/export-pdf.js @@ -0,0 +1,31 @@ +const path = require('path'); +const fs = require('fs'); +const { chromium } = require('playwright'); + +const ROOT = path.resolve(__dirname, '..'); +const INDEX_HTML = path.join(ROOT, 'index.html'); +const OUTPUT_FILE = path.join(ROOT, 'exports', 'ai-agent-patterns.pdf'); + +async function main() { + fs.mkdirSync(path.dirname(OUTPUT_FILE), { recursive: true }); + + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 1130, height: 900 } }); + await page.goto(`file://${INDEX_HTML}`); + await page.emulateMedia({ media: 'print' }); + + await page.pdf({ + path: OUTPUT_FILE, + format: 'A4', + printBackground: true, + margin: { top: '20px', bottom: '20px', left: '20px', right: '20px' }, + }); + console.log(`Saved ${OUTPUT_FILE}`); + + await browser.close(); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/export-svg.js b/scripts/export-svg.js new file mode 100644 index 0000000..9f32d12 --- /dev/null +++ b/scripts/export-svg.js @@ -0,0 +1,99 @@ +const path = require('path'); +const fs = require('fs'); +const { chromium } = require('playwright'); + +const ROOT = path.resolve(__dirname, '..'); +const INDEX_HTML = path.join(ROOT, 'index.html'); +const OUTPUT_DIR = path.join(ROOT, 'exports', 'svg'); + +// CSS properties that style the flow diagram nodes/edges/labels (see the +// .node-*, .edge*, .node-label, .edge-label, .group-box rules in index.html). +// Baked into inline `style` attributes so each exported SVG is standalone — +// no external stylesheet or CSS custom properties required to render it. +const BAKED_PROPS = [ + 'fill', 'stroke', 'stroke-width', 'stroke-dasharray', 'stroke-linecap', + 'filter', 'font-family', 'font-size', 'font-weight', 'letter-spacing', + 'text-anchor', 'dominant-baseline', +]; + +function slugify(name) { + return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); +} + +async function main() { + fs.mkdirSync(OUTPUT_DIR, { recursive: true }); + + const browser = await chromium.launch(); + const page = await browser.newPage(); + await page.goto(`file://${INDEX_HTML}`); + + const names = await page.evaluate(() => PATTERNS.map((p) => p.name)); + + for (let i = 0; i < names.length; i++) { + await page.evaluate((idx) => toggleCard(idx), i); + await page.waitForTimeout(150); + + const markup = await page.evaluate((bakedProps) => { + const svg = document.querySelector('.card.expanded .diagram-wrap svg.diagram'); + if (!svg) return null; + + const bakeOne = (el) => { + const cs = getComputedStyle(el); + const decls = bakedProps + .map((prop) => { + let v = cs.getPropertyValue(prop); + if (!v) return ''; + if (prop === 'filter' && v.includes('#nodeShadow')) v = 'url(#nodeShadow)'; + return `${prop}:${v}`; + }) + .filter(Boolean) + .join(';'); + if (decls) el.setAttribute('style', decls); + if (el.classList.contains('edge-label')) { + el.textContent = el.textContent.toUpperCase(); + } + }; + + // Resolve the shared marker/filter defs (var(--border-mid) etc. only + // resolve while still attached to the live document) before cloning. + const sprite = document.querySelector('body > svg[aria-hidden="true"]'); + const markerPath = sprite.querySelector('marker#arrow path'); + const filterEl = sprite.querySelector('filter#nodeShadow'); + bakeOne(markerPath); + + svg.querySelectorAll('rect, path, text').forEach(bakeOne); + + const clone = svg.cloneNode(true); + clone.removeAttribute('class'); + clone.querySelectorAll('[class]').forEach((el) => el.removeAttribute('class')); + + const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs'); + defs.appendChild(sprite.querySelector('marker#arrow').cloneNode(true)); + defs.appendChild(filterEl.cloneNode(true)); + clone.insertBefore(defs, clone.firstChild); + + const [, , w, h] = clone.getAttribute('viewBox').split(' '); + clone.setAttribute('width', w); + clone.setAttribute('height', h); + + return new XMLSerializer().serializeToString(clone); + }, BAKED_PROPS); + + if (markup) { + const file = path.join(OUTPUT_DIR, `pattern-${slugify(names[i])}.svg`); + fs.writeFileSync(file, `\n${markup}\n`); + console.log(`Saved ${file}`); + } else { + console.warn(`Could not expand card ${i} (${names[i]}), skipping`); + } + + await page.evaluate((idx) => toggleCard(idx), i); + } + + await browser.close(); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +});