Skip to content
Merged
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
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,10 @@ yarn-debug.log*
yarn-error.log*

host_config.json
plan.md
plan.md

# Scripts tooling
scripts/node_modules/

# Agent skills (managed via skills-lock.json + `npx skills experimental_install`)
.agents/skills/
35 changes: 35 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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 <input.excalidraw> [output.png]
```

See [README.md](README.md#diagram-workflow) for the full contributor workflow.
87 changes: 87 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <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"
}
}
11 changes: 11 additions & 0 deletions skills-lock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": 1,
"skills": {
"brand-guidelines": {
"source": "cosmonic-labs/skills",
"sourceType": "github",
"skillPath": "brand-guidelines/SKILL.md",
"computedHash": "0dc065af28b0081de7be1a5d1f299676126339664b3071d1e17270326cae91a7"
}
}
}
Loading