diff --git a/.gitignore b/.gitignore index 813d5773..ab67af37 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,10 @@ yarn-debug.log* yarn-error.log* host_config.json -plan.md \ No newline at end of file +plan.md + +# Scripts tooling +scripts/node_modules/ + +# Agent skills (managed via skills-lock.json + `npx skills experimental_install`) +.agents/skills/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..d246f760 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,35 @@ +# CLAUDE.md + +## Diagrams + +This project uses [Excalidraw](https://excalidraw.com/) for diagrams. Source files (`.excalidraw`) are stored alongside docs in `versioned_docs/version-next/images/` and exported as transparent PNGs at 3x scale. + +### Brand guidelines skill + +Colors, typography, and the official wasmCloud Excalidraw library are defined in the `brand-guidelines` skill (managed via `skills-lock.json`; see [README.md](README.md#diagram-workflow) for setup). The skill is open source and donated by Cosmonic — for this repository, follow the **wasmCloud** sections. + +The wasmCloud palette in brief: + +| Color | Hex | Use | +|-------------|-----------|-------------------------------------| +| Green Aqua | `#00C389` | Primary brand color, main elements | +| Space Blue | `#002E5D` | Dark backgrounds, outlines | +| Gunmetal | `#253746` | Text, secondary backgrounds | +| Light Gray | `#768692` | Secondary text | +| Gainsboro | `#D9E1E2` | Light backgrounds, borders | +| Yellow | `#FFB600` | Highlights, CTAs | + +The `brand-guidelines` skill is the canonical source; defer to it when the palette here conflicts with the skill content. + +### Creating diagrams + +Load the [wasmCloud and Wasm Excalidraw library](https://excalidraw.com/?addLibrary=https%3A%2F%2Fraw.githubusercontent.com%2Fexcalidraw%2Fexcalidraw-libraries%2Fricochet-wasmcloud-and-wasm-1770917744834%2Flibraries%2Fricochet%2Fwasmcloud-and-wasm.excalidrawlib%3Fraw%3Dtrue) and apply the wasmCloud palette. The `brand-guidelines` skill has more detailed guidance for diagrams created with the Excalidraw MCP server. + +### Exporting to PNG + +```bash +cd scripts && npm install && npx playwright install chromium +node export-excalidraw-png.js [output.png] +``` + +See [README.md](README.md#diagram-workflow) for the full contributor workflow. diff --git a/README.md b/README.md index f91700da..e885a7ee 100644 --- a/README.md +++ b/README.md @@ -28,3 +28,90 @@ To serve the generated static content: ```bash npm run serve ``` + +### Diagram workflow + +The wasmCloud docs use [Excalidraw](https://excalidraw.com/) for diagrams. Source `.excalidraw` files live in `versioned_docs/version-next/images/` alongside the exported PNGs. + +#### Brand colors and the wasmCloud Excalidraw library + +Colors, typography, and the official wasmCloud Excalidraw library are defined in the [`brand-guidelines` skill](https://github.com/cosmonic-labs/skills/tree/main/brand-guidelines). The skill is open source and donated by Cosmonic — for this repository, use the **wasmCloud** sections. + +The wasmCloud palette in brief: + +| Color | Hex | Use | +|-------------|-----------|-------------------------------------| +| Green Aqua | `#00C389` | Primary brand color, main elements | +| Space Blue | `#002E5D` | Dark backgrounds, outlines | +| Gunmetal | `#253746` | Text, secondary backgrounds | +| Light Gray | `#768692` | Secondary text | +| Gainsboro | `#D9E1E2` | Light backgrounds, borders | +| Yellow | `#FFB600` | Highlights, CTAs | + +The `brand-guidelines` skill is the canonical source; defer to it when the palette here conflicts with the skill content. + +To load the [wasmCloud and Wasm Excalidraw library](https://excalidraw.com/?addLibrary=https%3A%2F%2Fraw.githubusercontent.com%2Fexcalidraw%2Fexcalidraw-libraries%2Fricochet-wasmcloud-and-wasm-1770917744834%2Flibraries%2Fricochet%2Fwasmcloud-and-wasm.excalidrawlib%3Fraw%3Dtrue) in the Excalidraw web UI, click the link and confirm the library install. + +#### Installing the brand-guidelines skill locally + +The skill ships as a project-level Claude Code skill, pinned in `skills-lock.json`. To install or restore it on a fresh checkout: + +```bash +npx skills experimental_install +``` + +If you prefer to add the skill manually (or to a different agent): + +```bash +npx skills add cosmonic-labs/skills --skill brand-guidelines --agent claude-code +``` + +The installed skill content is gitignored; only `skills-lock.json` is committed. + +#### Editing existing diagrams + +Two options: + +1. **VS Code Excalidraw extension** — install the [Excalidraw extension](https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor) and open `.excalidraw` files directly in VS Code. +2. **excalidraw.com** — open [excalidraw.com](https://excalidraw.com/) and drag-and-drop the `.excalidraw` file onto the canvas. Save by downloading the file back (hamburger menu > Save to disk). + +#### Creating new diagrams with Claude Code + +With the `brand-guidelines` skill installed, ask Claude Code to create a diagram and the skill will surface the wasmCloud palette, typography, and Excalidraw library to use. For example: + +``` +Create an Excalidraw flowchart showing the component deployment pipeline, using the brand-guidelines skill. +``` + +#### Exporting to PNG + +Diagrams must be exported as **transparent PNGs at 3x scale** before committing. + +##### Automated export (Playwright) + +One-time setup: + +```bash +cd scripts +npm install +npx playwright install chromium +``` + +Export a diagram: + +```bash +node export-excalidraw-png.js [output.png] +``` + +If no output path is given, the PNG is written next to the `.excalidraw` file with the same name. + +##### Manual export (no Playwright) + +If you don't want to install Playwright: + +1. Open the `.excalidraw` file at [excalidraw.com](https://excalidraw.com/) +2. Select all elements (Ctrl+A / Cmd+A) +3. Open the export dialog (hamburger menu > Export image) +4. Toggle **Background** off +5. Set **Scale** to **3x** +6. Click **Copy to clipboard** or **Save as PNG** diff --git a/scripts/export-excalidraw-png.js b/scripts/export-excalidraw-png.js new file mode 100644 index 00000000..4233559e --- /dev/null +++ b/scripts/export-excalidraw-png.js @@ -0,0 +1,181 @@ +#!/usr/bin/env node +/** + * Export Excalidraw diagrams to high-quality transparent PNGs using Playwright. + * + * - Disables background for transparency + * - Sets export scale to 3x for high quality + * - Uses clipboard copy for native rendering with Excalidraw fonts + * + * Setup: + * cd scripts && npm install && npx playwright install chromium + * + * Usage: + * node export-excalidraw-png.js [output.png] + */ + +const { chromium } = require('playwright'); +const fs = require('fs'); +const path = require('path'); + +async function exportDiagram(inputPath, outputPath) { + const absoluteInput = path.resolve(inputPath); + if (!fs.existsSync(absoluteInput)) { + console.error(`Input file not found: ${absoluteInput}`); + process.exit(1); + } + + const diagramData = fs.readFileSync(absoluteInput, 'utf-8'); + const absoluteOutput = path.resolve( + outputPath || inputPath.replace(/\.excalidraw$/, '.png') + ); + + console.log(`Exporting: ${path.basename(absoluteInput)}`); + + const browser = await chromium.launch({ headless: true }); + const context = await browser.newContext({ + viewport: { width: 1920, height: 1200 }, + permissions: ['clipboard-read', 'clipboard-write'], + }); + const page = await context.newPage(); + page.on('dialog', (dialog) => dialog.accept()); + + try { + await page.goto('https://excalidraw.com/', { waitUntil: 'networkidle' }); + await page.waitForSelector('.excalidraw', { timeout: 30000 }); + await page.waitForTimeout(2000); + + // Load diagram via file drop + await page.evaluate(async (content) => { + const blob = new Blob([content], { type: 'application/json' }); + const file = new File([blob], 'diagram.excalidraw', { + type: 'application/json', + }); + const dt = new DataTransfer(); + dt.items.add(file); + document.querySelector('.excalidraw')?.dispatchEvent( + new DragEvent('drop', { + bubbles: true, + cancelable: true, + dataTransfer: dt, + }) + ); + }, diagramData); + + await page.waitForTimeout(2000); + + // Select all elements + await page.keyboard.press('Control+a'); + await page.waitForTimeout(500); + + // Open export dialog via menu + const menuButton = + (await page.$('button[aria-label="Main menu"]')) || + (await page.$('.App-menu button')); + if (menuButton) await menuButton.click(); + await page.waitForTimeout(500); + + const exportOption = + (await page.$('text="Export image..."')) || + (await page.$('text="Export image"')) || + (await page.$('[data-testid="dropdown-menu-export"]')); + if (exportOption) { + await exportOption.click(); + } else { + await page.keyboard.press('Meta+Shift+e'); + } + await page.waitForTimeout(1500); + + // Toggle OFF background for transparency + const bgToggle = await page.$('label:has-text("Background")'); + if (bgToggle) { + await bgToggle.click(); + await page.waitForTimeout(300); + console.log(' Background: off'); + } + + // Set scale to 3x by clicking the third radio input in the Scale group + await page.evaluate(() => { + const labels = document.querySelectorAll('label'); + for (const label of labels) { + if (label.textContent.trim() === 'Scale') { + const parent = label.closest('div') || label.parentElement; + if (parent) { + const inputs = parent.querySelectorAll( + 'input[type="radio"], input' + ); + if (inputs.length >= 3) inputs[2].click(); + } + break; + } + } + }); + await page.waitForTimeout(1000); + console.log(' Scale: 3x'); + + // Copy to clipboard (respects current scale and background settings) + const copyBtn = + (await page.$('button[aria-label="Copy PNG to clipboard"]')) || + (await page.$('button:has-text("Copy to clipboard")')); + if (!copyBtn) throw new Error('Could not find "Copy to clipboard" button'); + + await copyBtn.click(); + await page.waitForTimeout(3000); + + // Read PNG from clipboard + const pngData = await page.evaluate(async () => { + try { + const items = await navigator.clipboard.read(); + for (const item of items) { + if (item.types.includes('image/png')) { + const blob = await item.getType('image/png'); + const arrayBuffer = await blob.arrayBuffer(); + const bytes = new Uint8Array(arrayBuffer); + let binary = ''; + for (let i = 0; i < bytes.length; i++) { + binary += String.fromCharCode(bytes[i]); + } + return btoa(binary); + } + } + return null; + } catch (e) { + return null; + } + }); + + if (pngData && pngData.length > 100) { + const buffer = Buffer.from(pngData, 'base64'); + if (buffer[0] === 0x89 && buffer[1] === 0x50) { + fs.writeFileSync(absoluteOutput, buffer); + console.log( + ` -> ${path.basename(absoluteOutput)} (${(buffer.length / 1024).toFixed(0)} KB)` + ); + } else { + throw new Error('Clipboard data is not a valid PNG'); + } + } else { + throw new Error('No PNG data in clipboard'); + } + } catch (error) { + console.error(` Error: ${error.message}`); + await browser.close(); + process.exit(1); + } + + await browser.close(); +} + +const args = process.argv.slice(2); +if (args.length < 1) { + console.log( + 'Usage: node export-excalidraw-png.js [output.png]' + ); + process.exit(1); +} + +exportDiagram(args[0], args[1]) + .then(() => process.exit(0)) + .catch((err) => { + console.error(err); + process.exit(1); + }); diff --git a/scripts/package.json b/scripts/package.json new file mode 100644 index 00000000..f29fd11a --- /dev/null +++ b/scripts/package.json @@ -0,0 +1,7 @@ +{ + "private": true, + "description": "Tooling scripts for wasmcloud.com docs", + "dependencies": { + "playwright": "^1.52.0" + } +} diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 00000000..27e345d2 --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,11 @@ +{ + "version": 1, + "skills": { + "brand-guidelines": { + "source": "cosmonic-labs/skills", + "sourceType": "github", + "skillPath": "brand-guidelines/SKILL.md", + "computedHash": "0dc065af28b0081de7be1a5d1f299676126339664b3071d1e17270326cae91a7" + } + } +}