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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/skills/excalidraw-diagrams
Submodule excalidraw-diagrams added at cd421f
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,9 @@ yarn-debug.log*
yarn-error.log*

host_config.json

# Scripts tooling
scripts/node_modules/

# Claude Code local settings
.claude/settings.local.json
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule ".claude/skills/excalidraw-diagrams"]
path = .claude/skills/excalidraw-diagrams
url = https://github.com/robtaylor/excalidraw-diagrams
33 changes: 33 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# 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.

### wasmCloud Brand Color Palette

Use these colors for diagram elements (stroke / background):

| Color | Stroke | Background |
|--------|-----------|------------|
| Green | `#00bc8e` | `#e6f9f3` |
| Blue | `#005799` | `#e6f0f7` |
| Navy | `#002e5d` | `#e6ecf3` |
| Purple | `#6741d9` | `#f0ecff` |
| Gold | `#d4a017` | `#fff8e6` |
| Red | `#e03131` | `#ffe6e6` |
| Gray | `#788591` | `#f3f4f6` |
| Teal | `#099268` | `#e0f5f0` |

### Creating Diagrams with Claude Code

Use the `excalidraw-diagrams` skill (`/excalidraw-diagrams`) to generate diagrams programmatically with the brand palette above.

### Exporting to PNG

```bash
cd scripts && npm install && npx playwright install chromium
node export-excalidraw-png.js <input.excalidraw> [output.png]
```

See [docs/diagrams.md](docs/diagrams.md) for the full contributor workflow.
69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,72 @@ 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.

#### wasmCloud brand colors

All diagrams should use the wasmCloud brand palette for consistency.

| Color | Stroke | Background |
|--------|-----------|------------|
| Green | `#00bc8e` | `#e6f9f3` |
| Blue | `#005799` | `#e6f0f7` |
| Navy | `#002e5d` | `#e6ecf3` |
| Purple | `#6741d9` | `#f0ecff` |
| Gold | `#d4a017` | `#fff8e6` |
| Red | `#e03131` | `#ffe6e6` |
| Gray | `#788591` | `#f3f4f6` |
| Teal | `#099268` | `#e0f5f0` |

#### 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

Use the `excalidraw-diagrams` skill to generate diagrams programmatically:

```
/excalidraw-diagrams Create a flowchart showing the component deployment pipeline
```

The skill uses the `excalidraw_generator.py` library (in `.claude/skills/excalidraw-diagrams/`) which provides `Diagram`, `Flowchart`, and `ArchitectureDiagram` classes. Reference the brand colors from `CLAUDE.md` when specifying colors in your prompt.

#### 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 <input.excalidraw> [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**
181 changes: 181 additions & 0 deletions scripts/export-excalidraw-png.js
Original file line number Diff line number Diff line change
@@ -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 <input.excalidraw> [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 <input.excalidraw> [output.png]'
);
process.exit(1);
}

exportDiagram(args[0], args[1])
.then(() => process.exit(0))
.catch((err) => {
console.error(err);
process.exit(1);
});
7 changes: 7 additions & 0 deletions scripts/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"private": true,
"description": "Tooling scripts for wasmcloud.com docs",
"dependencies": {
"playwright": "^1.52.0"
}
}