Back to skills
SkillHub ClubWrite Technical DocsFull StackTech WriterDesigner

excalidraw-studio

Generate Excalidraw diagrams from natural language descriptions. Outputs .excalidraw JSON files openable in Excalidraw. Use when asked to "create a diagram", "make a flowchart", "visualize a process", "draw a system architecture", "create a mind map", "generate an Excalidraw file", "draw an ER diagram", "create a sequence diagram", or "make a class diagram". Supports flowcharts, relationship diagrams, mind maps, architecture, DFD, swimlane, class, sequence, and ER diagrams. Can use icon libraries (AWS, GCP, etc.) when set up. Do NOT use for code architecture analysis (use the architecture skills), Mermaid diagram rendering (use mermaid-studio), or non-visual documentation (use docs-writer).

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
1,741
Hot score
99
Updated
March 20, 2026
Overall rating
C4.0
Composite score
4.0
Best-practice grade
B75.6

Install command

npx @skill-hub/cli install tech-leads-club-agent-skills-excalidraw-studio

Repository

tech-leads-club/agent-skills

Skill path: packages/skills-catalog/skills/(tooling)/excalidraw-studio

Generate Excalidraw diagrams from natural language descriptions. Outputs .excalidraw JSON files openable in Excalidraw. Use when asked to "create a diagram", "make a flowchart", "visualize a process", "draw a system architecture", "create a mind map", "generate an Excalidraw file", "draw an ER diagram", "create a sequence diagram", or "make a class diagram". Supports flowcharts, relationship diagrams, mind maps, architecture, DFD, swimlane, class, sequence, and ER diagrams. Can use icon libraries (AWS, GCP, etc.) when set up. Do NOT use for code architecture analysis (use the architecture skills), Mermaid diagram rendering (use mermaid-studio), or non-visual documentation (use docs-writer).

Open repository

Best for

Primary workflow: Write Technical Docs.

Technical facets: Full Stack, Tech Writer, Designer.

Target audience: everyone.

License: CC-BY-4.0.

Original source

Catalog source: SkillHub Club.

Repository owner: tech-leads-club.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install excalidraw-studio into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/tech-leads-club/agent-skills before adding excalidraw-studio to shared team environments
  • Use excalidraw-studio for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: excalidraw-studio
description: Generate Excalidraw diagrams from natural language descriptions. Outputs .excalidraw JSON files openable in Excalidraw. Use when asked to "create a diagram", "make a flowchart", "visualize a process", "draw a system architecture", "create a mind map", "generate an Excalidraw file", "draw an ER diagram", "create a sequence diagram", or "make a class diagram". Supports flowcharts, relationship diagrams, mind maps, architecture, DFD, swimlane, class, sequence, and ER diagrams. Can use icon libraries (AWS, GCP, etc.) when set up. Do NOT use for code architecture analysis (use the architecture skills), Mermaid diagram rendering (use mermaid-studio), or non-visual documentation (use docs-writer).
license: CC-BY-4.0
metadata:
  author: Felipe Rodrigues - github.com/felipfr
  version: 1.0.1
---

# Excalidraw Studio

Generate Excalidraw-format diagrams from natural language descriptions. Outputs `.excalidraw` JSON files that can be opened directly in Excalidraw (web, VS Code extension, or Obsidian plugin).

## Workflow

```
UNDERSTAND → CHOOSE TYPE → EXTRACT → GENERATE → SAVE
```

### Step 1: Understand the Request

Analyze the user's description to determine:

1. **Diagram type** — Use the decision matrix below
2. **Key elements** — Entities, steps, concepts, actors
3. **Relationships** — Flow direction, connections, hierarchy
4. **Complexity** — Number of elements (target: under 20 for clarity)

### Step 2: Choose the Diagram Type and Visual Mode

**Diagram type:**

| User Intent                | Diagram Type         | Keywords                                      |
| -------------------------- | -------------------- | --------------------------------------------- |
| Process flow, steps        | **Flowchart**        | "workflow", "process", "steps"                |
| Connections, dependencies  | **Relationship**     | "relationship", "connections", "dependencies" |
| Concept hierarchy          | **Mind Map**         | "mind map", "concepts", "breakdown"           |
| System design              | **Architecture**     | "architecture", "system", "components"        |
| Data movement              | **Data Flow (DFD)**  | "data flow", "data processing"                |
| Cross-functional processes | **Swimlane**         | "business process", "swimlane", "actors"      |
| Object-oriented design     | **Class Diagram**    | "class", "inheritance", "OOP"                 |
| Interaction sequences      | **Sequence Diagram** | "sequence", "interaction", "messages"         |
| Database design            | **ER Diagram**       | "database", "entity", "data model"            |

**Visual mode** — decide upfront and apply consistently to all elements:

| Mode       | `roughness` | `fontFamily` | When to use                                          |
| ---------- | ----------- | ------------ | ---------------------------------------------------- |
| **Sketch** | `1`         | `5`          | Default — informal, approachable, Excalidraw-native  |
| **Clean**  | `0`         | `2`          | Executive presentations, formal specs                |
| **Mixed**  | zones: `0`, shapes: `1` | `5` | Architecture diagrams (structural zones + sketchy shapes) |

### Step 3: Extract Structured Information

Extract the key components based on diagram type. For each type, identify:

- **Nodes/entities** — What are the boxes/shapes?
- **Connections** — What connects to what, and with what label?
- **Hierarchy** — What contains what, what comes before what?
- **Decision points** — Where does the flow branch?

For detailed extraction guidelines per diagram type, read `references/element-types.md`.

### Step 4: Generate the Excalidraw JSON

**CRITICAL: Read `references/excalidraw-schema.md` before generating your first diagram.** It contains the correct element format, text container model, and binding system.

Key rules for generation:

1. **Text inside shapes** — Use `boundElements` on the shape and a separate text element with `containerId`. Never use a `label` shorthand:

   ```json
   [
     {
       "id": "step-1",
       "type": "rectangle",
       "x": 100, "y": 100, "width": 200, "height": 80,
       "boundElements": [{ "type": "text", "id": "text-step-1" }]
     },
     {
       "id": "text-step-1",
       "type": "text",
       "x": 130, "y": 128, "width": 140, "height": 24,
       "text": "My Step", "originalText": "My Step",
       "fontSize": 20, "fontFamily": 5,
       "textAlign": "center", "verticalAlign": "middle",
       "containerId": "step-1", "lineHeight": 1.25, "roundness": null
     }
   ]
   ```

2. **Arrow labels** — Also use `boundElements` + separate text element with `containerId`. Never use a `label` shorthand on arrows:

   ```json
   [
     {
       "id": "arrow-1",
       "type": "arrow",
       "x": 100, "y": 150,
       "points": [[0, 0], [200, 0]],
       "boundElements": [{ "type": "text", "id": "text-arrow-1" }]
     },
     {
       "id": "text-arrow-1",
       "type": "text",
       "x": 160, "y": 132, "width": 80, "height": 18,
       "text": "sends data", "originalText": "sends data",
       "fontSize": 14, "fontFamily": 5,
       "textAlign": "center", "verticalAlign": "middle",
       "containerId": "arrow-1", "lineHeight": 1.25, "roundness": null
     }
   ]
   ```

3. **Arrow bindings** — Use `startBinding`/`endBinding` (not `start`/`end`). Connected shapes must list the arrow in their `boundElements`:

   ```json
   {
     "id": "shape-1",
     "boundElements": [
       { "type": "text", "id": "text-shape-1" },
       { "type": "arrow", "id": "arrow-1" }
     ]
   }
   ```
   ```json
   {
     "id": "arrow-1",
     "type": "arrow",
     "startBinding": { "elementId": "shape-1", "focus": 0, "gap": 1 },
     "endBinding": { "elementId": "shape-2", "focus": 0, "gap": 1 }
   }
   ```

4. **Element order for z-index** — Always declare shapes first, arrows second, text elements last. This guarantees text renders on top and is never obscured by arrows or other shapes.

5. **Positioning** — Use grid-aligned coordinates (multiples of 20px when `gridSize: 20`). Leave 200-300px horizontal gap, 100-150px vertical gap between elements.

6. **Unique IDs** — Every element must have a unique `id`. Use descriptive IDs like `"step-1"`, `"decision-valid"`, `"arrow-1-to-2"`, `"text-step-1"`.

7. **Colors** — Use a consistent palette:

   | Role | Color | Hex |
   |------|-------|-----|
   | Primary entities | Light blue | `#a5d8ff` |
   | Process steps | Light green | `#b2f2bb` |
   | Important/Central | Yellow | `#ffd43b` |
   | Warnings/Errors | Light red | `#ffc9c9` |
   | Secondary | Cyan | `#96f2d7` |
   | Default stroke | Dark | `#1e1e1e` |

### Step 5: Save and Present

1. Save as `<descriptive-name>.excalidraw`
2. Provide a summary:

   ```
   Created: user-workflow.excalidraw
   Type: Flowchart
   Elements: 7 shapes, 6 arrows, 1 title
   Total: 14 elements

   To view:
   1. Visit https://excalidraw.com → Open → drag and drop the file
   2. Or use the Excalidraw VS Code extension
   3. Or open in Obsidian with the Excalidraw plugin
   ```

## Templates

Pre-built templates are available in `assets/` for quick starting points. Use these when the diagram type matches — they provide correct structure and styling:

| Template         | File                                                   |
| ---------------- | ------------------------------------------------------ |
| Flowchart        | `assets/flowchart-template.json`                    |
| Relationship     | `assets/relationship-template.json`                 |
| Mind Map         | `assets/mindmap-template.json`                      |
| Data Flow (DFD)  | `assets/data-flow-diagram-template.json`            |
| Swimlane         | `assets/business-flow-swimlane-template.json`       |
| Class Diagram    | `assets/class-diagram-template.json`                |
| Sequence Diagram | `assets/sequence-diagram-template.json`             |
| ER Diagram       | `assets/er-diagram-template.json`                   |

Read a template when creating that diagram type for the first time. Use its structure as a base, then modify elements to match the user's request.

## Icon Libraries

For professional architecture diagrams with service icons (AWS, GCP, Azure, etc.), icon libraries can be set up. Read `references/icon-libraries.md` when:

- User requests an AWS/cloud architecture diagram
- User mentions wanting specific service icons
- You need to check if icon libraries are available

## Best Practices

### Element Count

| Diagram Type          | Recommended | Maximum |
| --------------------- | ----------- | ------- |
| Flowchart steps       | 3-10        | 15      |
| Relationship entities | 3-8         | 12      |
| Mind map branches     | 4-6         | 8       |
| Sub-topics per branch | 2-4         | 6       |

If the user's request exceeds maximum, suggest breaking into multiple diagrams:

> "Your request includes 15 components. For clarity, I recommend: (1) High-level architecture diagram with 6 main components, (2) Detailed sub-diagrams for each subsystem. Want me to start with the high-level view?"

### Layout

- **Flow direction**: Left-to-right for processes, top-to-bottom for hierarchies
- **Spacing**: 200-300px horizontal, 100-150px vertical between elements
- **Grid alignment**: Position on multiples of 20px for clean alignment
- **Margins**: Minimum 50px from canvas edge
- **Text sizing**: 28-36px titles, 18-22px labels, 14-16px annotations
- **Font**: Use `fontFamily: 5` (Excalifont) for hand-drawn consistency. Fallback to `1` (Virgil) if 5 is not supported.
- **Background zones**: For architecture diagrams, add semi-transparent dashed zone rectangles (`opacity: 35`, `strokeStyle: "dashed"`, `roughness: 0`) as the first elements in the array to create visual grouping regions. See `references/excalidraw-schema.md` → Background Zones.
- **Element order**: zones first → shapes → arrows → text elements (ensures correct z-index and text always renders on top)

### Common Mistakes to Avoid

- ❌ Using `label: { text: "..." }` shorthand on shapes or arrows — not supported by the Excalidraw parser
- ❌ Putting `text` directly on shape elements without `containerId`
- ❌ Using `start`/`end` for arrow bindings — use `startBinding`/`endBinding` with `elementId`/`focus`/`gap`
- ❌ Forgetting to add arrows to their connected shapes' `boundElements` arrays
- ❌ Omitting `originalText`, `lineHeight`, `autoResize`, or `backgroundColor: "transparent"` from text elements inside containers
- ❌ Omitting required base properties (`angle`, `strokeStyle`, `opacity`, `groupIds`, `frameId`, `index`, `isDeleted`, `seed`, `version`, `versionNonce`, `updated`, `link`, `locked`) — elements will not render
- ❌ Missing `"files": {}` at the top level of the JSON
- ❌ Using `roundness: { "type": 3 }` on ellipses — ellipses must use `roundness: null`
- ❌ Missing `lastCommittedPoint`, `startArrowhead`, `endArrowhead` on arrows
- ❌ Declaring text elements before arrows — text renders underneath and gets obscured
- ❌ Floating arrows without bindings (won't move with shapes)
- ❌ Overlapping elements (increase spacing)
- ❌ Inconsistent color usage (define palette upfront)
- ❌ Too many elements on one diagram (break into sub-diagrams)

## Validation Checklist

Before delivering the diagram, verify:

- [ ] All elements have unique IDs
- [ ] Every element has ALL required base properties: `angle`, `strokeStyle`, `opacity`, `groupIds`, `frameId`, `index`, `isDeleted`, `link`, `locked`, `seed`, `version`, `versionNonce`, `updated`
- [ ] `index` values are assigned in order (`"a0"`, `"a1"`, …) with text elements getting higher values than shapes/arrows
- [ ] Top-level JSON includes `"files": {}`
- [ ] Shapes with text use `boundElements` + separate text element with `containerId`
- [ ] Text elements inside containers have `containerId`, `originalText`, `lineHeight: 1.25`, `autoResize: true`, `roundness: null`, `backgroundColor: "transparent"`
- [ ] Arrows use `startBinding`/`endBinding` (with `elementId`, `focus`, `gap`) when connecting shapes, plus `lastCommittedPoint: null`, `startArrowhead: null`, `endArrowhead: "arrow"`
- [ ] Connected shapes list the arrow in their `boundElements` arrays
- [ ] Element order: shapes → arrows → text elements (text always on top)
- [ ] Ellipses use `roundness: null` (not `{ "type": 3 }`)
- [ ] Coordinates prevent overlapping (check spacing)
- [ ] Text is readable (font size 16+)
- [ ] Colors follow consistent scheme
- [ ] File is valid JSON
- [ ] Element count is reasonable (<20 for clarity)

## Troubleshooting

| Issue                         | Solution                                                                                      |
| ----------------------------- | --------------------------------------------------------------------------------------------- |
| Text not showing in shapes    | Use `boundElements` + separate text element with `containerId`, `originalText`, `lineHeight`  |
| Text hidden behind arrows     | Move text elements to end of `elements` array (after all arrows)                             |
| Arrows don't move with shapes | Use `startBinding`/`endBinding` with `elementId`, `focus: 0`, `gap: 1`                       |
| Shape not moving with arrows  | Add the arrow to the shape's `boundElements` array                                            |
| Elements overlap              | Increase spacing between coordinates                                                          |
| Text doesn't fit              | Increase shape width or reduce font size                                                      |
| Too many elements             | Break into multiple diagrams                                                                  |
| Colors look inconsistent      | Define color palette upfront, apply consistently                                              |

## Limitations

- Complex curves are simplified to straight/basic curved lines
- Hand-drawn roughness is set to default (1)
- No embedded images in auto-generation (use icon libraries for service icons)
- Maximum recommended: 20 elements per diagram for clarity
- No automatic collision detection — use spacing guidelines


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### references/element-types.md

```markdown
# Excalidraw Element Types Guide

Read this file when you need detailed guidance on which elements to use for specific diagram types, and how to construct them correctly.

For the JSON properties and format, see `excalidraw-schema.md`.

## Element Type Overview

| Type        | Shape | Primary Use                            | Text binding        | Arrow binding   |
| ----------- | ----- | -------------------------------------- | ------------------- | --------------- |
| `rectangle` | □     | Boxes, containers, process steps       | via `boundElements` | Arrows can bind |
| `ellipse`   | ○     | Start/end, states, emphasis            | via `boundElements` | Arrows can bind |
| `diamond`   | ◇     | Decision points, conditions            | via `boundElements` | Arrows can bind |
| `arrow`     | →     | Directional flow, relationships        | via `boundElements` | Binds to shapes |
| `line`      | —     | Non-directional connections, dividers  | ❌                   | Binds to shapes |
| `text`      | A     | Standalone labels, titles, annotations | —                   | Not bindable    |

## Shapes — Rectangle, Ellipse, Diamond

### When to use each

| Shape         | Best for                                         | Visual meaning                            |
| ------------- | ------------------------------------------------ | ----------------------------------------- |
| **Rectangle** | Process steps, entities, components, data stores | "This is a thing" or "This is an action"  |
| **Ellipse**   | Start/end terminals, states, emphasis            | "This is a boundary" or "This is a state" |
| **Diamond**   | Decision points, conditional branches            | "This is a question"                      |

### Text in shapes

**NEVER use `label: { text: "..." }` shorthand** — it is not supported in the `.excalidraw` file format. Always create a separate `text` element and link it via `containerId` and `boundElements`.

```json
[
  {
    "id": "step-1",
    "type": "rectangle",
    "x": 100,
    "y": 100,
    "width": 200,
    "height": 80,
    "backgroundColor": "#a5d8ff",
    "fillStyle": "solid",
    "strokeColor": "#1971c2",
    "strokeWidth": 2,
    "roundness": { "type": 3 },
    "boundElements": [
      { "type": "text", "id": "text-step-1" }
    ]
  },
  {
    "id": "text-step-1",
    "type": "text",
    "x": 130,
    "y": 128,
    "width": 140,
    "height": 24,
    "text": "Process Input",
    "originalText": "Process Input",
    "fontSize": 20,
    "fontFamily": 5,
    "textAlign": "center",
    "verticalAlign": "middle",
    "containerId": "step-1",
    "lineHeight": 1.25,
    "strokeColor": "#1e1e1e",
    "backgroundColor": "transparent",
    "autoResize": true,
    "roundness": null
  }
]
```

**Multi-line text:** Use `\n` in the `text` and `originalText` fields:

```json
{
  "text": "User\nAuthentication\nService",
  "originalText": "User\nAuthentication\nService"
}
```

**Text element positioning inside a container at (x, y, w, h):**

- `text.x = container.x + 20`
- `text.y = container.y + (container.height / 2) - (fontSize / 2)`
- `text.width = container.width - 40`
- `text.height = fontSize * 1.25`

### Size guidelines

| Content length | Rectangle | Ellipse | Diamond |
| -------------- | --------- | ------- | ------- |
| 1 word         | 140×70    | 120×120 | 140×140 |
| 2-4 words      | 200×80    | 160×120 | 180×180 |
| Short sentence | 280×100   | 200×140 | 220×220 |

### Styling for elegance

**Use stroke + fill combinations** — matching stroke to the fill's deeper shade:

| Role    | Fill      | Stroke    | Effect                        |
| ------- | --------- | --------- | ----------------------------- |
| Primary | `#a5d8ff` | `#1971c2` | Blue card with defined border |
| Success | `#b2f2bb` | `#2f9e44` | Green step with emphasis      |
| Warning | `#ffec99` | `#e67700` | Amber decision with warmth    |
| Danger  | `#ffc9c9` | `#e03131` | Red error with urgency        |
| Neutral | `#e9ecef` | `#868e96` | Subtle, de-emphasized         |
| Accent  | `#d0bfff` | `#7048e8` | Purple highlight              |

**fillStyle variations** for visual variety within the same diagram:

- `"solid"` — Clean, modern look (default for most shapes)
- `"hachure"` — Sketchy fill, good for secondary/background elements
- `"cross-hatch"` — Dense fill, good for emphasis or "completed" states

## Arrows

### Basic directional arrow

```json
{
  "id": "flow-1",
  "type": "arrow",
  "x": 300,
  "y": 140,
  "width": 200,
  "height": 0,
  "points": [
    [0, 0],
    [200, 0]
  ],
  "strokeWidth": 2,
  "roundness": { "type": 2 },
  "lastCommittedPoint": null,
  "startArrowhead": null,
  "endArrowhead": "arrow"
}
```

### Arrow with label

Arrow labels also require `boundElements` on the arrow + a separate text element with `containerId`. **Never use `label: { text: "..." }` on arrows.**

```json
[
  {
    "id": "flow-1",
    "type": "arrow",
    "x": 300,
    "y": 140,
    "width": 200,
    "height": 0,
    "points": [[0, 0], [200, 0]],
    "strokeWidth": 2,
    "roundness": { "type": 2 },
    "lastCommittedPoint": null,
    "startArrowhead": null,
    "endArrowhead": "arrow",
    "boundElements": [
      { "type": "text", "id": "text-flow-1" }
    ]
  },
  {
    "id": "text-flow-1",
    "type": "text",
    "x": 360,
    "y": 122,
    "width": 80,
    "height": 18,
    "text": "HTTP/JSON",
    "originalText": "HTTP/JSON",
    "fontSize": 14,
    "fontFamily": 5,
    "textAlign": "center",
    "verticalAlign": "middle",
    "containerId": "flow-1",
    "lineHeight": 1.25,
    "strokeColor": "#1e1e1e",
    "backgroundColor": "transparent",
    "autoResize": true,
    "roundness": null
  }
]
```

### Bound arrow (connects to shapes)

Use `startBinding`/`endBinding` — **never `start`/`end`**. Connected shapes must list the arrow in their `boundElements`.

```json
[
  {
    "id": "source-box",
    "type": "rectangle",
    "boundElements": [
      { "type": "text", "id": "text-source" },
      { "type": "arrow", "id": "flow-1" }
    ]
  },
  {
    "id": "target-box",
    "type": "rectangle",
    "boundElements": [
      { "type": "text", "id": "text-target" },
      { "type": "arrow", "id": "flow-1" }
    ]
  },
  {
    "id": "flow-1",
    "type": "arrow",
    "points": [[0, 0], [200, 0]],
    "startBinding": { "elementId": "source-box", "focus": 0, "gap": 1 },
    "endBinding": { "elementId": "target-box", "focus": 0, "gap": 1 },
    "lastCommittedPoint": null,
    "startArrowhead": null,
    "endArrowhead": "arrow"
  }
]
```

### Arrow styles for semantic meaning

| Style                  | strokeStyle | strokeWidth | Meaning                              |
| ---------------------- | ----------- | ----------- | ------------------------------------ |
| **Primary flow**       | `"solid"`   | 2           | Main path, normal flow               |
| **Important flow**     | `"solid"`   | 3           | Critical path, emphasis              |
| **Optional/alternate** | `"dashed"`  | 2           | Optional path, fallback              |
| **Indirect/async**     | `"dotted"`  | 2           | Event-driven, async, weak dependency |

### Arrow directions

| Direction          | Points                           | Use case                |
| ------------------ | -------------------------------- | ----------------------- |
| → Right            | `[[0, 0], [200, 0]]`             | Process flow            |
| ↓ Down             | `[[0, 0], [0, 150]]`             | Hierarchy, sequence     |
| ↘ Diagonal         | `[[0, 0], [200, 150]]`           | Cross-connections       |
| → then ↓ (L-shape) | `[[0, 0], [200, 0], [200, 150]]` | Routing around elements |

## Lines

Non-directional connections with no arrowhead:

```json
{
  "type": "line",
  "x": 100,
  "y": 300,
  "points": [
    [0, 0],
    [400, 0]
  ],
  "strokeStyle": "dashed",
  "strokeWidth": 1,
  "strokeColor": "#868e96"
}
```

**Use cases:** Section dividers, boundaries, non-directional relationships (UML association).

## Standalone Text

For titles, headers, annotations not inside a shape. Set `containerId: null`:

```json
{
  "id": "title-1",
  "type": "text",
  "x": 100,
  "y": 40,
  "width": 300,
  "height": 35,
  "text": "System Architecture Overview",
  "originalText": "System Architecture Overview",
  "fontSize": 28,
  "fontFamily": 5,
  "textAlign": "center",
  "strokeColor": "#1e1e1e",
  "containerId": null,
  "lineHeight": 1.25,
  "roundness": null
}
```

**Width/height calculation:**

- Width ≈ `text.length × fontSize × 0.6`
- Height ≈ `fontSize × 1.2 × numberOfLines`

## Background Zones (Visual Grouping)

Background zones are large semi-transparent rectangles placed **behind** other elements to visually group them into regions. They are a key technique for professional architecture diagrams.

**Key properties for a zone:**

- `opacity: 35` — semi-transparent so elements behind/in front remain visible
- `strokeStyle: "dashed"` — clearly marks it as a boundary, not a shape
- `roughness: 0` — clean edges for background zones
- `fillStyle: "solid"` — needed for the opacity to show color

Zones must be declared **first** in the `elements` array so they render behind everything else.

```json
[
  {
    "id": "zone-backend",
    "type": "rectangle",
    "x": 300,
    "y": 140,
    "width": 480,
    "height": 320,
    "strokeColor": "#1971c2",
    "backgroundColor": "#dbe4ff",
    "fillStyle": "solid",
    "strokeWidth": 1,
    "strokeStyle": "dashed",
    "roughness": 0,
    "opacity": 35,
    "roundness": { "type": 3 },
    "boundElements": []
  }
]
```

Add a standalone text label near the top-left corner of the zone:

```json
{
  "id": "label-zone-backend",
  "type": "text",
  "x": 320,
  "y": 148,
  "width": 120,
  "height": 20,
  "text": "Backend Services",
  "originalText": "Backend Services",
  "fontSize": 14,
  "fontFamily": 5,
  "strokeColor": "#1971c2",
  "containerId": null,
  "lineHeight": 1.25,
  "roundness": null
}
```

**Element order with zones:** zones → shapes → arrows → text elements

**Zone color recommendations:**

| Zone purpose    | backgroundColor | strokeColor |
| --------------- | --------------- | ----------- |
| Services/Logic  | `#dbe4ff`       | `#4c6ef5`   |
| Data layer      | `#d3f9d8`       | `#2f9e44`   |
| External/Users  | `#fff9db`       | `#f08c00`   |
| Messaging/Events| `#f3d9fa`       | `#ae3ec9`   |
| Infrastructure  | `#e3fafc`       | `#0c8599`   |

## Visual Modes

Choose the diagram's visual mode upfront and apply it consistently to all elements.

### Sketch Mode (default — recommended for most diagrams)

Hand-drawn aesthetic, matches Excalidraw's signature look:

- `roughness: 1` on all shapes and arrows
- `fontFamily: 5` (Excalifont) for all text
- `strokeWidth: 2` for shapes, `strokeWidth: 2` for arrows

Best for: informal diagrams, brainstorming, process docs, most use cases.

### Clean Mode (for formal/technical diagrams)

Precise, polished, presentation-ready:

- `roughness: 0` on all shapes and arrows
- `fontFamily: 2` (Helvetica) for body text, `fontFamily: 5` for titles
- `strokeWidth: 1.5-2` for shapes

Best for: executive presentations, client-facing docs, technical specifications.

**Mixed mode:** Use `roughness: 0` for background zones and `roughness: 1` for shapes — zones feel like structure, shapes feel like content.

## Diagram Type Recipes

### Flowchart

```
[Start ellipse] → [Step rect] → [Decision diamond] → Yes → [Step rect] → [End ellipse]
                                                     ↓ No
                                                [Step rect]
```

- Start/End: `ellipse` with light green/red (`#b2f2bb`/`#ffc9c9`), `roundness: null`
- Steps: `rectangle` with light blue (`#a5d8ff`)
- Decisions: `diamond` with amber (`#ffec99`)
- Flow: solid arrows, labeled at decision branches ("Yes"/"No")

### Architecture Diagram

Use **background zones** to show layers (see Background Zones section):

```
[zone: Infrastructure]
  [zone: Backend Services]
    [API Gateway rect] → [Order Service rect] → [Orders DB ellipse]
                              ↓
                        [Event Bus rect] → [Worker rect]
  [zone: Data/Consumers]
```

- Components: `rectangle` with varied colors by layer
- Connections: solid arrows with protocol labels ("REST", "gRPC", "SQL")
- Zone boundaries: semi-transparent dashed rectangles with `opacity: 35`

### ER Diagram

- Entities: `rectangle` with entity name (bold, larger font)
- Attributes: listed in multi-line text inside the entity box
- Relationships: `diamond` with relationship name
- Cardinality: standalone text labels near arrows ("1", "N", "0..1")

### Sequence Diagram

- Actors: `rectangle` at top with actor name
- Lifelines: `line` (vertical, dashed, `strokeStyle: "dashed"`)
- Messages: `arrow` (horizontal, solid = sync, dashed = async)
- Return: `arrow` (dashed, reverse direction)
- Activation: thin `rectangle` on lifeline, no fill

### Mind Map

- Center: large `ellipse` with main topic (bright color, `roughness: 1`)
- Branches: `rectangle` connected via diagonal arrows from center
- Sub-topics: smaller `rectangle` connected from branches
- Use different colors per branch for visual grouping (one color family per branch)

### Class Diagram

- Classes: `rectangle` with multi-line text:

  ```
  ClassName
  ─────────
  -field: Type
  +field: Type
  ─────────
  +method(): Return
  -method(arg): Return
  ```

- Inheritance: solid arrow with label "extends"
- Implementation: dashed arrow with label "implements"
- Association: solid line (no arrowhead)

### Swimlane

- Lanes: tall `rectangle` with `fillStyle: "hachure"`, `opacity: 40`, `roughness: 0`
- Lane headers: standalone `text` at top of each lane
- Activities: `rectangle` inside lanes
- Handoffs: arrows crossing lane boundaries

### Data Flow Diagram (DFD)

- External entities: `rectangle` (bold stroke `strokeWidth: 3`)
- Processes: `ellipse` with process number and name
- Data stores: `rectangle` with open side (use two horizontal lines via `line` elements)
- Data flows: labeled arrows (always show data name on arrow)
- Direction: left-to-right or top-left to bottom-right

## Design Principles for Elegant Diagrams

1. **Visual hierarchy** — Use size and color intensity to signal importance. Primary elements get saturated fills; secondary elements use neutral or hachure fills.

2. **Consistent stroke weight** — Use `strokeWidth: 2` for all shapes and arrows. Only increase to 3-4 to emphasize critical paths.

3. **Color harmony** — Use at most 3-4 fill colors per diagram. Pick from the same palette row (see excalidraw-schema.md Design Tokens). Avoid mixing warm and cool haphazardly.

4. **Whitespace is structure** — More spacing between unrelated groups, less between related elements. This creates visual grouping without borders.

5. **Aligned, not scattered** — Align elements on a grid. Centers should be aligned vertically or horizontally whenever possible.

6. **Label everything that isn't obvious** — Every arrow should either have a label or its meaning should be clear from context. Every shape needs text.

7. **Flow direction convention** — Left-to-right for process flows. Top-to-bottom for hierarchies and sequences. Pick one and be consistent.

8. **Matching stroke to fill** — Use the deeper shade from the palette as stroke color for the corresponding fill. This creates depth and definition.

9. **Background zones over borders** — For grouping related elements, prefer semi-transparent background zones over explicit border rectangles. Zones feel spatial; borders feel like containers.

10. **Choose visual mode upfront** — Decide Sketch or Clean mode before generating elements. Never mix `roughness: 0` and `roughness: 1` on same-level shapes (zones and shapes can differ intentionally).

## Summary

| When you need...               | Use this element                                                              |
| ------------------------------ | ----------------------------------------------------------------------------- |
| Process box, entity, component | `rectangle` with `boundElements` + separate text element                      |
| Decision point                 | `diamond` with `boundElements` + separate text element                        |
| Start/End terminal             | `ellipse` with `boundElements` + separate text element (`roundness: null`)    |
| Flow direction                 | `arrow` with `startBinding`/`endBinding`, label via `boundElements` if needed |
| Title/Header                   | `text` (large font, `containerId: null`)                                      |
| Annotation                     | `text` (small font, `containerId: null`, positioned near target)              |
| Non-directional connection     | `line`                                                                        |
| Section divider                | `line` (horizontal, dashed)                                                   |
| Visual grouping region         | `rectangle` (large, `opacity: 35`, `strokeStyle: "dashed"`, `roughness: 0`)  |

```

### references/excalidraw-schema.md

```markdown
# Excalidraw JSON Schema Reference

Read this file before generating your first diagram. It contains the correct element format, text container model, and binding system.

## Top-Level Structure

```json
{
  "type": "excalidraw",
  "version": 2,
  "source": "https://excalidraw.com",
  "elements": [],
  "appState": {
    "viewBackgroundColor": "#ffffff",
    "gridSize": 20
  },
  "files": {}
}
```

## Element Properties

All elements share these base properties:

| Property          | Type        | Default         | Description                                                                             |
| ----------------- | ----------- | --------------- | --------------------------------------------------------------------------------------- |
| `id`              | string      | required        | Unique identifier (e.g., `"step-1"`, `"arrow-a-b"`)                                     |
| `type`            | string      | required        | `"rectangle"`, `"ellipse"`, `"diamond"`, `"arrow"`, `"line"`, `"text"`                  |
| `x`, `y`          | number      | required        | Position in pixels from top-left. Use multiples of 20 for grid alignment.               |
| `width`, `height` | number      | required        | Dimensions in pixels                                                                    |
| `strokeColor`     | string      | `"#1e1e1e"`     | Hex color for outline                                                                   |
| `backgroundColor` | string      | `"transparent"` | Hex color for fill                                                                      |
| `fillStyle`       | string      | `"solid"`       | `"solid"`, `"hachure"`, `"cross-hatch"`                                                 |
| `strokeWidth`     | number      | `2`             | Outline thickness (1-4)                                                                 |
| `strokeStyle`     | string      | `"solid"`       | `"solid"`, `"dashed"`, `"dotted"`                                                       |
| `roughness`       | number      | `1`             | Hand-drawn effect (0 = clean, 1 = sketch, 2 = rough)                                    |
| `opacity`         | number      | `100`           | Transparency (0-100)                                                                    |
| `roundness`       | object/null | varies          | `{ "type": 3 }` for rounded corners, `{ "type": 2 }` for curved arrows, `null` for text |
| `groupIds`        | string[]    | `[]`            | Group membership for compound elements                                                  |
| `locked`          | boolean     | `false`         | Lock element from editing                                                               |

**Required properties on ALL elements (Excalidraw will reject or misrender elements missing these):**

```json
{
  "angle": 0,
  "strokeStyle": "solid",
  "opacity": 100,
  "groupIds": [],
  "frameId": null,
  "index": "a0",
  "isDeleted": false,
  "link": null,
  "locked": false,
  "seed": 1234567890,
  "version": 1,
  "versionNonce": 987654321,
  "updated": 1706659200000
}
```

- `index` is a fractional z-index string. Use `"a0"`, `"a1"`, `"a2"`, etc. in element order. Text elements must have higher index values than shapes and arrows so they render on top.
- Generate unique `seed` and `versionNonce` per element (any distinct integers work).
- Omitting any of these properties will cause elements to not render correctly.

**Additional required properties for arrows:**

```json
{
  "lastCommittedPoint": null,
  "startArrowhead": null,
  "endArrowhead": "arrow",
  "backgroundColor": "transparent",
  "fillStyle": "solid"
}
```

**Additional required properties for text elements inside containers:**

```json
{
  "backgroundColor": "transparent",
  "fillStyle": "solid",
  "strokeWidth": 1,
  "autoResize": true
}
```

## Text Inside Shapes (CRITICAL)

**DO NOT use a `label` shorthand or put `text` directly on shape elements.** The `label` shorthand is not parsed by Excalidraw's file format.

**Correct approach:** Add `boundElements` to the shape, then create a **separate text element** that references the shape via `containerId`. Always declare shapes first, arrows second, and text elements last — this ensures text renders on top and is never obscured by arrows.

```json
[
  {
    "id": "step-1",
    "type": "rectangle",
    "x": 100,
    "y": 100,
    "width": 200,
    "height": 80,
    "backgroundColor": "#a5d8ff",
    "fillStyle": "solid",
    "strokeColor": "#1971c2",
    "strokeWidth": 2,
    "roundness": { "type": 3 },
    "boundElements": [
      { "type": "text", "id": "text-step-1" }
    ]
  },
  {
    "id": "text-step-1",
    "type": "text",
    "x": 130,
    "y": 128,
    "width": 140,
    "height": 24,
    "text": "Process Input",
    "originalText": "Process Input",
    "fontSize": 20,
    "fontFamily": 5,
    "textAlign": "center",
    "verticalAlign": "middle",
    "containerId": "step-1",
    "lineHeight": 1.25,
    "strokeColor": "#1e1e1e",
    "roundness": null
  }
]
```

This works for `rectangle`, `ellipse`, and `diamond` elements.

**Required properties for text elements inside containers:**

| Property        | Value      | Description                                                          |
| --------------- | ---------- | -------------------------------------------------------------------- |
| `containerId`   | required   | ID of the parent shape                                               |
| `originalText`  | required   | Exact copy of `text` — used by the Excalidraw editor                 |
| `lineHeight`    | `1.25`     | Always set this for contained text                                   |
| `text`          | required   | The text content. Use `\n` for line breaks.                          |
| `fontSize`      | `20`       | 14-36 depending on purpose                                           |
| `fontFamily`    | `5`        | 5 = Excalifont (hand-drawn), 1 = Virgil, 2 = Helvetica, 3 = Cascadia |
| `textAlign`     | `"center"` | `"left"`, `"center"`, `"right"`                                      |
| `verticalAlign` | `"middle"` | `"top"`, `"middle"`, `"bottom"`                                      |
| `strokeColor`   | `"#1e1e1e"` | Text color                                                          |
| `roundness`     | `null`     | Always null for text elements                                        |

**Text element positioning inside a container at (x, y, w, h):**

- `text.x = container.x + 20`
- `text.y = container.y + (container.height / 2) - (fontSize / 2)`
- `text.width = container.width - 40`
- `text.height = fontSize * lineHeight`

## Arrows and Bindings (CRITICAL)

### Basic Arrow

```json
{
  "id": "arrow-1",
  "type": "arrow",
  "x": 300,
  "y": 140,
  "width": 100,
  "height": 0,
  "points": [
    [0, 0],
    [100, 0]
  ],
  "roundness": { "type": 2 },
  "strokeWidth": 2
}
```

### Arrow with Label

Arrow labels also require `boundElements` on the arrow and a separate text element with `containerId`. **Do NOT use a `label` shorthand** — it is not supported.

```json
[
  {
    "id": "arrow-1",
    "type": "arrow",
    "x": 300,
    "y": 140,
    "width": 200,
    "height": 0,
    "points": [
      [0, 0],
      [200, 0]
    ],
    "roundness": { "type": 2 },
    "strokeWidth": 2,
    "boundElements": [
      { "type": "text", "id": "text-arrow-1" }
    ]
  },
  {
    "id": "text-arrow-1",
    "type": "text",
    "x": 360,
    "y": 122,
    "width": 80,
    "height": 18,
    "text": "sends data",
    "originalText": "sends data",
    "fontSize": 14,
    "fontFamily": 5,
    "textAlign": "center",
    "verticalAlign": "middle",
    "containerId": "arrow-1",
    "lineHeight": 1.25,
    "strokeColor": "#1e1e1e",
    "roundness": null
  }
]
```

### Bound Arrows (connect to shapes)

For arrows that move when shapes are repositioned, use `startBinding` and `endBinding`. **Do NOT use `start`/`end`** — they are not valid Excalidraw properties.

Every connected shape must also list the arrow in its `boundElements` array.

**Element ordering matters for z-index:** declare shapes first, arrows second, text elements last — so text always renders on top.

```json
[
  {
    "id": "box-a",
    "type": "rectangle",
    "x": 100,
    "y": 100,
    "width": 160,
    "height": 80,
    "boundElements": [
      { "type": "text", "id": "text-box-a" },
      { "type": "arrow", "id": "arrow-a-b" }
    ]
  },
  {
    "id": "box-b",
    "type": "rectangle",
    "x": 460,
    "y": 100,
    "width": 160,
    "height": 80,
    "boundElements": [
      { "type": "text", "id": "text-box-b" },
      { "type": "arrow", "id": "arrow-a-b" }
    ]
  },
  {
    "id": "arrow-a-b",
    "type": "arrow",
    "x": 260,
    "y": 140,
    "width": 200,
    "height": 0,
    "points": [
      [0, 0],
      [200, 0]
    ],
    "startBinding": { "elementId": "box-a", "focus": 0, "gap": 1 },
    "endBinding": { "elementId": "box-b", "focus": 0, "gap": 1 },
    "boundElements": [
      { "type": "text", "id": "text-arrow-a-b" }
    ]
  },
  {
    "id": "text-box-a",
    "type": "text",
    "x": 120,
    "y": 128,
    "width": 120,
    "height": 24,
    "text": "Service A",
    "originalText": "Service A",
    "fontSize": 20,
    "fontFamily": 5,
    "textAlign": "center",
    "verticalAlign": "middle",
    "containerId": "box-a",
    "lineHeight": 1.25,
    "strokeColor": "#1e1e1e",
    "roundness": null
  },
  {
    "id": "text-box-b",
    "type": "text",
    "x": 480,
    "y": 128,
    "width": 120,
    "height": 24,
    "text": "Service B",
    "originalText": "Service B",
    "fontSize": 20,
    "fontFamily": 5,
    "textAlign": "center",
    "verticalAlign": "middle",
    "containerId": "box-b",
    "lineHeight": 1.25,
    "strokeColor": "#1e1e1e",
    "roundness": null
  },
  {
    "id": "text-arrow-a-b",
    "type": "text",
    "x": 320,
    "y": 122,
    "width": 80,
    "height": 18,
    "text": "REST API",
    "originalText": "REST API",
    "fontSize": 14,
    "fontFamily": 5,
    "textAlign": "center",
    "verticalAlign": "middle",
    "containerId": "arrow-a-b",
    "lineHeight": 1.25,
    "strokeColor": "#1e1e1e",
    "roundness": null
  }
]
```

**Binding properties:**

| Property    | Value   | Description                                             |
| ----------- | ------- | ------------------------------------------------------- |
| `elementId` | string  | ID of the connected shape                               |
| `focus`     | `0`     | Connection point: 0 = center, -1/+1 = top-bottom edges  |
| `gap`       | `1`     | Gap in pixels between arrow tip and shape boundary      |

### Arrow Directions

| Direction      | Points                           |
| -------------- | -------------------------------- |
| Horizontal (→) | `[[0, 0], [200, 0]]`             |
| Vertical (↓)   | `[[0, 0], [0, 150]]`             |
| Diagonal (↘)   | `[[0, 0], [200, 150]]`           |
| L-shaped (→↓)  | `[[0, 0], [200, 0], [200, 150]]` |

## Design Tokens — Elegant Palette

Use these curated colors for professional, modern diagrams. Avoid raw primary colors.

### Light Theme (default)

| Role                  | Fill        | Stroke       | Hex Fill  | Hex Stroke |
| --------------------- | ----------- | ------------ | --------- | ---------- |
| **Primary**           | Soft blue   | Deeper blue  | `#a5d8ff` | `#1971c2`  |
| **Success/Process**   | Mint green  | Forest green | `#b2f2bb` | `#2f9e44`  |
| **Warning/Decision**  | Warm amber  | Deep amber   | `#ffec99` | `#e67700`  |
| **Danger/Error**      | Soft rose   | Deep rose    | `#ffc9c9` | `#e03131`  |
| **Neutral/Secondary** | Light gray  | Medium gray  | `#e9ecef` | `#868e96`  |
| **Accent**            | Soft violet | Deep violet  | `#d0bfff` | `#7048e8`  |
| **Info/Highlight**    | Soft cyan   | Teal         | `#96f2d7` | `#0c8599`  |
| **Canvas**            | White       | —            | `#ffffff` | —          |
| **Default stroke**    | —           | Near-black   | —         | `#1e1e1e`  |

### Open Colors — Full Palette

Excalidraw's native color picker is built around [Open Colors](https://yeun.github.io/open-colors/). Use shade-2 for fills and shade-8 for matching strokes to create depth.

| Family  | Fill (shade-2) | Stroke (shade-8) | Fill hex  | Stroke hex |
| ------- | -------------- | ---------------- | --------- | ---------- |
| Gray    | `gray-2`       | `gray-8`         | `#e9ecef` | `#343a40`  |
| Red     | `red-2`        | `red-8`          | `#ffc9c9` | `#c92a2a`  |
| Pink    | `pink-2`       | `pink-8`         | `#fcc2d7` | `#a61e4d`  |
| Grape   | `grape-2`      | `grape-8`        | `#e5dbff` | `#6741d9`  |
| Violet  | `violet-2`     | `violet-8`       | `#d0bfff` | `#5f3dc4`  |
| Indigo  | `indigo-2`     | `indigo-8`       | `#bac8ff` | `#3b5bdb`  |
| Blue    | `blue-2`       | `blue-8`         | `#a5d8ff` | `#1864ab`  |
| Cyan    | `cyan-2`       | `cyan-8`         | `#99e9f2` | `#0b7285`  |
| Teal    | `teal-2`       | `teal-8`         | `#96f2d7` | `#087f5b`  |
| Green   | `green-2`      | `green-8`        | `#b2f2bb` | `#2b8a3e`  |
| Lime    | `lime-2`       | `lime-8`         | `#d8f5a2` | `#5c940d`  |
| Yellow  | `yellow-2`     | `yellow-8`       | `#ffec99` | `#e67700`  |
| Orange  | `orange-2`     | `orange-8`       | `#ffd8a8` | `#d9480f`  |

### Curated Professional Palettes

Use one palette per diagram for visual coherence:

**Blue-Tech** (APIs, microservices, cloud):

- API/Gateway: fill `#a5d8ff`, stroke `#1971c2`
- Services: fill `#b2f2bb`, stroke `#2f9e44`
- Data: fill `#96f2d7`, stroke `#0c8599`
- Events: fill `#d0bfff`, stroke `#7048e8`

**Warm-Neutral** (business processes, workflows):

- Primary: fill `#ffd8a8`, stroke `#d9480f`
- Secondary: fill `#ffec99`, stroke `#e67700`
- Supporting: fill `#e9ecef`, stroke `#868e96`
- Action: fill `#b2f2bb`, stroke `#2f9e44`

**Monochrome** (clean/minimal):

- Main: fill `#e9ecef`, stroke `#343a40`
- Emphasis: fill `#ced4da`, stroke `#212529`
- Accent: fill `#a5d8ff`, stroke `#1971c2` (single color accent)

### Dark Theme

When user requests dark mode, set `"viewBackgroundColor": "#1e1e1e"` and use these:

| Role               | Fill       | Stroke      | Hex Fill  | Hex Stroke |
| ------------------ | ---------- | ----------- | --------- | ---------- |
| **Primary**        | Deep blue  | Light blue  | `#1864ab` | `#74c0fc`  |
| **Success**        | Deep green | Light green | `#2b8a3e` | `#8ce99a`  |
| **Warning**        | Deep amber | Light amber | `#e67700` | `#ffd43b`  |
| **Danger**         | Deep red   | Light red   | `#c92a2a` | `#ff8787`  |
| **Neutral**        | Dark gray  | Light gray  | `#343a40` | `#adb5bd`  |
| **Default stroke** | —          | White       | —         | `#ffffff`  |

### Typography Scale

| Purpose         | Font Size | Font Family     |
| --------------- | --------- | --------------- |
| Diagram title   | 28-32     | `fontFamily: 5` |
| Section header  | 22-24     | `fontFamily: 5` |
| Element label   | 18-20     | `fontFamily: 5` |
| Arrow label     | 14-16     | `fontFamily: 5` |
| Annotation/note | 12-14     | `fontFamily: 5` |

### Spacing System

All spacing based on `gridSize: 20`:

| Context                         | Value     | Grid multiples |
| ------------------------------- | --------- | -------------- |
| Between elements (horizontal)   | 200-300px | 10-15 units    |
| Between elements (vertical)     | 100-150px | 5-7.5 units    |
| Element padding (inside shapes) | 20-40px   | 1-2 units      |
| Arrow-to-shape clearance        | 20px      | 1 unit         |
| Canvas margin                   | 60px      | 3 units        |
| Between groups of elements      | 400px     | 20 units       |

## Font Families

| ID  | Name       | Style                | When to use                                        |
| --- | ---------- | -------------------- | -------------------------------------------------- |
| 5   | Excalifont | Hand-drawn (newest)  | Default — matches Excalidraw's signature aesthetic |
| 1   | Virgil     | Hand-drawn (classic) | Fallback if fontFamily 5 is not supported          |
| 2   | Helvetica  | Clean sans-serif     | Technical/formal diagrams when requested           |
| 3   | Cascadia   | Monospace            | Code labels, technical identifiers                 |

**Default to fontFamily 5 for all text** unless the user explicitly requests a formal/clean style.

## Visual Modes

Choose a visual mode upfront and apply it consistently. Never mix modes on same-level elements.

### Sketch Mode (default)

Excalidraw's signature hand-drawn aesthetic:

```json
{
  "roughness": 1,
  "fontFamily": 5
}
```

Apply `roughness: 1` to all shapes and arrows. Use `fontFamily: 5` (Excalifont) for all text.

### Clean Mode

Precise, polished, presentation-ready:

```json
{
  "roughness": 0,
  "fontFamily": 2
}
```

Apply `roughness: 0` to all shapes and arrows. Use `fontFamily: 2` (Helvetica) for body, `fontFamily: 5` for titles.

### Mixed Mode (recommended for architecture)

Background zones clean (`roughness: 0`) + foreground shapes sketchy (`roughness: 1`):

- Zones: `roughness: 0` — structural, precise
- Shapes: `roughness: 1` — content, approachable

## Background Zones

Background zones are semi-transparent dashed rectangles placed **before all other elements** (lowest `index` values) to create visual grouping regions.

**Required zone properties:**

```json
{
  "id": "zone-backend",
  "type": "rectangle",
  "x": 300,
  "y": 140,
  "width": 480,
  "height": 320,
  "angle": 0,
  "strokeColor": "#4c6ef5",
  "backgroundColor": "#dbe4ff",
  "fillStyle": "solid",
  "strokeWidth": 1,
  "strokeStyle": "dashed",
  "roughness": 0,
  "opacity": 35,
  "roundness": { "type": 3 },
  "groupIds": [],
  "frameId": null,
  "index": "a0",
  "isDeleted": false,
  "link": null,
  "locked": false,
  "seed": 111111111,
  "version": 1,
  "versionNonce": 222222222,
  "updated": 1706659200000,
  "boundElements": []
}
```

**Critical zone properties:**

- `opacity: 35` — semi-transparent (20-40 range works; 35 is sweet spot)
- `strokeStyle: "dashed"` — marks zone as boundary, not a regular shape
- `roughness: 0` — clean zone edges even in sketch-mode diagrams
- `fillStyle: "solid"` — required for opacity tinting to work

**Zone label** — add a standalone text near top-left of zone, after other text in the array:

```json
{
  "id": "label-zone-backend",
  "type": "text",
  "x": 320,
  "y": 148,
  "width": 140,
  "height": 18,
  "text": "Backend Services",
  "originalText": "Backend Services",
  "fontSize": 14,
  "fontFamily": 5,
  "strokeColor": "#4c6ef5",
  "textAlign": "left",
  "containerId": null,
  "lineHeight": 1.25,
  "roundness": null
}
```

**Element ordering when zones are present:**

1. Background zones (indices `a0`, `a1`, ...)
2. Content shapes (indices continue after zones)
3. Arrows
4. Text elements (highest indices)

## Frames

Frames are named containers that group elements visually with a title bar. They appear in Excalidraw's left panel for navigation.

```json
{
  "id": "frame-1",
  "type": "frame",
  "x": 80,
  "y": 80,
  "width": 680,
  "height": 480,
  "angle": 0,
  "strokeColor": "#bbb",
  "backgroundColor": "transparent",
  "fillStyle": "solid",
  "strokeWidth": 2,
  "strokeStyle": "solid",
  "roughness": 0,
  "opacity": 100,
  "groupIds": [],
  "frameId": null,
  "index": "a0",
  "isDeleted": false,
  "link": null,
  "locked": false,
  "seed": 100000001,
  "version": 1,
  "versionNonce": 100000002,
  "updated": 1706659200000,
  "name": "Order Processing Flow",
  "boundElements": []
}
```

Elements inside a frame reference it via `"frameId": "frame-1"`. The frame `name` appears as the frame's title.

Use frames for: multi-page diagrams, distinct diagram sections, export boundaries.

## Coordinate System

- Origin `(0, 0)` is top-left corner
- X increases to the right
- Y increases downward
- All units are in pixels
- Align to grid: position on multiples of 20 (when `gridSize: 20`)

## Element Sizing Guide

| Shape     | Content                  | Width     | Height    |
| --------- | ------------------------ | --------- | --------- |
| Rectangle | Single word              | 140-160px | 60-80px   |
| Rectangle | Short phrase (2-4 words) | 180-220px | 80-100px  |
| Rectangle | Sentence                 | 250-320px | 100-120px |
| Ellipse   | Short text (circle)      | 120×120px | —         |
| Ellipse   | Longer text              | 160×120px | —         |
| Diamond   | Short question           | 140×140px | —         |
| Diamond   | Longer question          | 180×180px | —         |

**Width formula for text:** `text.length × fontSize × 0.6`
**Height formula:** `fontSize × 1.2 × numberOfLines`

## Grouping Elements

Use `groupIds` to create compound elements that move together:

```json
[
  {
    "id": "server-box",
    "type": "rectangle",
    "x": 100,
    "y": 100,
    "width": 180,
    "height": 80,
    "groupIds": ["server-group"],
    "boundElements": [
      { "type": "text", "id": "text-server-box" }
    ]
  },
  {
    "id": "server-icon",
    "type": "text",
    "x": 105,
    "y": 185,
    "width": 30,
    "height": 30,
    "text": "🖥️",
    "fontSize": 20,
    "fontFamily": 5,
    "groupIds": ["server-group"],
    "containerId": null,
    "roundness": null
  },
  {
    "id": "text-server-box",
    "type": "text",
    "x": 120,
    "y": 128,
    "width": 140,
    "height": 24,
    "text": "Web Server",
    "originalText": "Web Server",
    "fontSize": 20,
    "fontFamily": 5,
    "textAlign": "center",
    "verticalAlign": "middle",
    "containerId": "server-box",
    "lineHeight": 1.25,
    "strokeColor": "#1e1e1e",
    "groupIds": ["server-group"],
    "roundness": null
  }
]
```

## `customData` for Metadata

Store extra information on elements that persists with the file but doesn't render:

```json
{
  "id": "step-1",
  "type": "rectangle",
  "customData": {
    "diagramType": "flowchart",
    "stepNumber": 1,
    "generatedBy": "excalidraw-studio"
  }
}
```

## Complete Minimal Example

A flowchart with two connected shapes. Note the element order: shapes → arrows → text elements (ensures text is always rendered on top, never obscured by arrows).

```json
{
  "type": "excalidraw",
  "version": 2,
  "source": "https://excalidraw.com",
  "elements": [
    {
      "id": "title",
      "type": "text",
      "x": 100,
      "y": 40,
      "width": 300,
      "height": 35,
      "text": "User Registration Flow",
      "originalText": "User Registration Flow",
      "fontSize": 28,
      "fontFamily": 5,
      "textAlign": "center",
      "strokeColor": "#1e1e1e",
      "opacity": 100,
      "roundness": null,
      "containerId": null,
      "lineHeight": 1.25
    },
    {
      "id": "step-1",
      "type": "rectangle",
      "x": 100,
      "y": 120,
      "width": 200,
      "height": 80,
      "strokeColor": "#1971c2",
      "backgroundColor": "#a5d8ff",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "roughness": 1,
      "roundness": { "type": 3 },
      "boundElements": [
        { "type": "text", "id": "text-step-1" },
        { "type": "arrow", "id": "arrow-1-2" }
      ]
    },
    {
      "id": "step-2",
      "type": "rectangle",
      "x": 400,
      "y": 120,
      "width": 200,
      "height": 80,
      "strokeColor": "#2f9e44",
      "backgroundColor": "#b2f2bb",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "roughness": 1,
      "roundness": { "type": 3 },
      "boundElements": [
        { "type": "text", "id": "text-step-2" },
        { "type": "arrow", "id": "arrow-1-2" }
      ]
    },
    {
      "id": "arrow-1-2",
      "type": "arrow",
      "x": 300,
      "y": 160,
      "width": 100,
      "height": 0,
      "points": [
        [0, 0],
        [100, 0]
      ],
      "strokeColor": "#1e1e1e",
      "strokeWidth": 2,
      "roundness": { "type": 2 },
      "startBinding": { "elementId": "step-1", "focus": 0, "gap": 1 },
      "endBinding": { "elementId": "step-2", "focus": 0, "gap": 1 }
    },
    {
      "id": "text-step-1",
      "type": "text",
      "x": 130,
      "y": 148,
      "width": 140,
      "height": 24,
      "text": "Enter Email",
      "originalText": "Enter Email",
      "fontSize": 20,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "step-1",
      "lineHeight": 1.25,
      "strokeColor": "#1e1e1e",
      "roundness": null
    },
    {
      "id": "text-step-2",
      "type": "text",
      "x": 430,
      "y": 148,
      "width": 140,
      "height": 24,
      "text": "Verify Email",
      "originalText": "Verify Email",
      "fontSize": 20,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "step-2",
      "lineHeight": 1.25,
      "strokeColor": "#1e1e1e",
      "roundness": null
    }
  ],
  "appState": {
    "viewBackgroundColor": "#ffffff",
    "gridSize": 20
  },
  "files": {}
}
```

```

### assets/flowchart-template.json

```json
{
  "type": "excalidraw",
  "version": 2,
  "source": "https://excalidraw.com",
  "elements": [
    {
      "id": "start",
      "type": "ellipse",
      "x": 280, "y": 80, "width": 200, "height": 60,
      "angle": 0,
      "strokeColor": "#2f9e44",
      "backgroundColor": "#b2f2bb",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a0",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 1001,
      "version": 1,
      "versionNonce": 1002,
      "updated": 1706659200000,
      "roundness": null,
      "boundElements": [
        { "type": "text", "id": "text-start" },
        { "type": "arrow", "id": "arrow-start-step1" }
      ]
    },
    {
      "id": "step1",
      "type": "rectangle",
      "x": 260, "y": 200, "width": 240, "height": 80,
      "angle": 0,
      "strokeColor": "#1971c2",
      "backgroundColor": "#a5d8ff",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a1",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 1003,
      "version": 1,
      "versionNonce": 1004,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [
        { "type": "text", "id": "text-step1" },
        { "type": "arrow", "id": "arrow-start-step1" },
        { "type": "arrow", "id": "arrow-step1-decision" }
      ]
    },
    {
      "id": "decision",
      "type": "diamond",
      "x": 260, "y": 340, "width": 240, "height": 100,
      "angle": 0,
      "strokeColor": "#e67700",
      "backgroundColor": "#ffec99",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a2",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 1005,
      "version": 1,
      "versionNonce": 1006,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [
        { "type": "text", "id": "text-decision" },
        { "type": "arrow", "id": "arrow-step1-decision" },
        { "type": "arrow", "id": "arrow-yes" },
        { "type": "arrow", "id": "arrow-no" }
      ]
    },
    {
      "id": "step2",
      "type": "rectangle",
      "x": 600, "y": 350, "width": 200, "height": 80,
      "angle": 0,
      "strokeColor": "#1971c2",
      "backgroundColor": "#a5d8ff",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a3",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 1007,
      "version": 1,
      "versionNonce": 1008,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [
        { "type": "text", "id": "text-step2" },
        { "type": "arrow", "id": "arrow-yes" }
      ]
    },
    {
      "id": "step3",
      "type": "rectangle",
      "x": 260, "y": 500, "width": 240, "height": 80,
      "angle": 0,
      "strokeColor": "#1971c2",
      "backgroundColor": "#a5d8ff",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a4",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 1009,
      "version": 1,
      "versionNonce": 1010,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [
        { "type": "text", "id": "text-step3" },
        { "type": "arrow", "id": "arrow-no" },
        { "type": "arrow", "id": "arrow-step3-end" }
      ]
    },
    {
      "id": "end",
      "type": "ellipse",
      "x": 280, "y": 640, "width": 200, "height": 60,
      "angle": 0,
      "strokeColor": "#e03131",
      "backgroundColor": "#ffc9c9",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a5",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 1011,
      "version": 1,
      "versionNonce": 1012,
      "updated": 1706659200000,
      "roundness": null,
      "boundElements": [
        { "type": "text", "id": "text-end" },
        { "type": "arrow", "id": "arrow-step3-end" }
      ]
    },
    {
      "id": "arrow-start-step1",
      "type": "arrow",
      "x": 380, "y": 140, "width": 0, "height": 60,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a6",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 1013,
      "version": 1,
      "versionNonce": 1014,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [],
      "points": [[0, 0], [0, 60]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": { "elementId": "start", "focus": 0, "gap": 1 },
      "endBinding": { "elementId": "step1", "focus": 0, "gap": 1 }
    },
    {
      "id": "arrow-step1-decision",
      "type": "arrow",
      "x": 380, "y": 280, "width": 0, "height": 60,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a7",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 1015,
      "version": 1,
      "versionNonce": 1016,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [],
      "points": [[0, 0], [0, 60]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": { "elementId": "step1", "focus": 0, "gap": 1 },
      "endBinding": { "elementId": "decision", "focus": 0, "gap": 1 }
    },
    {
      "id": "arrow-yes",
      "type": "arrow",
      "x": 500, "y": 390, "width": 100, "height": 0,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a8",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 1017,
      "version": 1,
      "versionNonce": 1018,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [],
      "points": [[0, 0], [100, 0]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": { "elementId": "decision", "focus": 0, "gap": 1 },
      "endBinding": { "elementId": "step2", "focus": 0, "gap": 1 }
    },
    {
      "id": "arrow-no",
      "type": "arrow",
      "x": 380, "y": 440, "width": 0, "height": 60,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a9",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 1019,
      "version": 1,
      "versionNonce": 1020,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [],
      "points": [[0, 0], [0, 60]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": { "elementId": "decision", "focus": 0, "gap": 1 },
      "endBinding": { "elementId": "step3", "focus": 0, "gap": 1 }
    },
    {
      "id": "arrow-step3-end",
      "type": "arrow",
      "x": 380, "y": 580, "width": 0, "height": 60,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b0",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 1021,
      "version": 1,
      "versionNonce": 1022,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [],
      "points": [[0, 0], [0, 60]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": { "elementId": "step3", "focus": 0, "gap": 1 },
      "endBinding": { "elementId": "end", "focus": 0, "gap": 1 }
    },
    {
      "id": "title",
      "type": "text",
      "x": 220, "y": 30,
      "width": 320, "height": 35,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b1",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 1023,
      "version": 1,
      "versionNonce": 1024,
      "updated": 1706659200000,
      "text": "Flowchart Title",
      "originalText": "Flowchart Title",
      "fontSize": 28,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "top",
      "containerId": null,
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "label-yes",
      "type": "text",
      "x": 512, "y": 370,
      "width": 40, "height": 18,
      "angle": 0,
      "strokeColor": "#2f9e44",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b2",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 1025,
      "version": 1,
      "versionNonce": 1026,
      "updated": 1706659200000,
      "text": "Yes",
      "originalText": "Yes",
      "fontSize": 14,
      "fontFamily": 5,
      "textAlign": "left",
      "verticalAlign": "top",
      "containerId": null,
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "label-no",
      "type": "text",
      "x": 390, "y": 450,
      "width": 30, "height": 18,
      "angle": 0,
      "strokeColor": "#e03131",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b3",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 1027,
      "version": 1,
      "versionNonce": 1028,
      "updated": 1706659200000,
      "text": "No",
      "originalText": "No",
      "fontSize": 14,
      "fontFamily": 5,
      "textAlign": "left",
      "verticalAlign": "top",
      "containerId": null,
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-start",
      "type": "text",
      "x": 300, "y": 100,
      "width": 160, "height": 25,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b4",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 1029,
      "version": 1,
      "versionNonce": 1030,
      "updated": 1706659200000,
      "text": "Start",
      "originalText": "Start",
      "fontSize": 20,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "start",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-step1",
      "type": "text",
      "x": 280, "y": 230,
      "width": 200, "height": 25,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b5",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 1031,
      "version": 1,
      "versionNonce": 1032,
      "updated": 1706659200000,
      "text": "Step 1",
      "originalText": "Step 1",
      "fontSize": 20,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "step1",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-decision",
      "type": "text",
      "x": 280, "y": 378,
      "width": 200, "height": 25,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b6",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 1033,
      "version": 1,
      "versionNonce": 1034,
      "updated": 1706659200000,
      "text": "Condition?",
      "originalText": "Condition?",
      "fontSize": 20,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "decision",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-step2",
      "type": "text",
      "x": 620, "y": 380,
      "width": 160, "height": 25,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b7",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 1035,
      "version": 1,
      "versionNonce": 1036,
      "updated": 1706659200000,
      "text": "Step 2 (Yes)",
      "originalText": "Step 2 (Yes)",
      "fontSize": 20,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "step2",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-step3",
      "type": "text",
      "x": 280, "y": 530,
      "width": 200, "height": 25,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b8",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 1037,
      "version": 1,
      "versionNonce": 1038,
      "updated": 1706659200000,
      "text": "Step 3 (No)",
      "originalText": "Step 3 (No)",
      "fontSize": 20,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "step3",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-end",
      "type": "text",
      "x": 300, "y": 660,
      "width": 160, "height": 25,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b9",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 1039,
      "version": 1,
      "versionNonce": 1040,
      "updated": 1706659200000,
      "text": "End",
      "originalText": "End",
      "fontSize": 20,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "end",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    }
  ],
  "appState": {
    "viewBackgroundColor": "#ffffff",
    "gridSize": 20
  },
  "files": {}
}

```

### assets/relationship-template.json

```json
{
  "type": "excalidraw",
  "version": 2,
  "source": "https://excalidraw.com",
  "elements": [
    {
      "id": "entity-a",
      "type": "rectangle",
      "x": 60, "y": 220, "width": 200, "height": 80,
      "angle": 0,
      "strokeColor": "#1971c2",
      "backgroundColor": "#a5d8ff",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a0",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 7001,
      "version": 1,
      "versionNonce": 7002,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [
        { "type": "text", "id": "text-entity-a" },
        { "type": "arrow", "id": "arrow-a-b" }
      ]
    },
    {
      "id": "entity-b",
      "type": "rectangle",
      "x": 380, "y": 100, "width": 200, "height": 80,
      "angle": 0,
      "strokeColor": "#2f9e44",
      "backgroundColor": "#b2f2bb",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a1",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 7003,
      "version": 1,
      "versionNonce": 7004,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [
        { "type": "text", "id": "text-entity-b" },
        { "type": "arrow", "id": "arrow-a-b" },
        { "type": "arrow", "id": "arrow-b-c" }
      ]
    },
    {
      "id": "entity-c",
      "type": "rectangle",
      "x": 380, "y": 340, "width": 200, "height": 80,
      "angle": 0,
      "strokeColor": "#7048e8",
      "backgroundColor": "#d0bfff",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a2",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 7005,
      "version": 1,
      "versionNonce": 7006,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [
        { "type": "text", "id": "text-entity-c" },
        { "type": "arrow", "id": "arrow-b-c" }
      ]
    },
    {
      "id": "arrow-a-b",
      "type": "arrow",
      "x": 260, "y": 260, "width": 120, "height": -120,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a3",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 7007,
      "version": 1,
      "versionNonce": 7008,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [{ "type": "text", "id": "text-rel-a-b" }],
      "points": [[0, 0], [120, -120]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": { "elementId": "entity-a", "focus": 0, "gap": 1 },
      "endBinding": { "elementId": "entity-b", "focus": 0, "gap": 1 }
    },
    {
      "id": "arrow-b-c",
      "type": "arrow",
      "x": 480, "y": 180, "width": 0, "height": 160,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a4",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 7009,
      "version": 1,
      "versionNonce": 7010,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [{ "type": "text", "id": "text-rel-b-c" }],
      "points": [[0, 0], [0, 160]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": { "elementId": "entity-b", "focus": 0, "gap": 1 },
      "endBinding": { "elementId": "entity-c", "focus": 0, "gap": 1 }
    },
    {
      "id": "title",
      "type": "text",
      "x": 180, "y": 40,
      "width": 440, "height": 35,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a5",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 7011,
      "version": 1,
      "versionNonce": 7012,
      "updated": 1706659200000,
      "text": "Relationship Diagram",
      "originalText": "Relationship Diagram",
      "fontSize": 28,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "top",
      "containerId": null,
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-entity-a",
      "type": "text",
      "x": 80, "y": 250,
      "width": 160, "height": 25,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a6",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 7013,
      "version": 1,
      "versionNonce": 7014,
      "updated": 1706659200000,
      "text": "Entity A",
      "originalText": "Entity A",
      "fontSize": 18,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "entity-a",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-entity-b",
      "type": "text",
      "x": 400, "y": 130,
      "width": 160, "height": 25,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a7",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 7015,
      "version": 1,
      "versionNonce": 7016,
      "updated": 1706659200000,
      "text": "Entity B",
      "originalText": "Entity B",
      "fontSize": 18,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "entity-b",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-entity-c",
      "type": "text",
      "x": 400, "y": 370,
      "width": 160, "height": 25,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a8",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 7017,
      "version": 1,
      "versionNonce": 7018,
      "updated": 1706659200000,
      "text": "Entity C",
      "originalText": "Entity C",
      "fontSize": 18,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "entity-c",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-rel-a-b",
      "type": "text",
      "x": 278, "y": 182,
      "width": 100, "height": 18,
      "angle": 0,
      "strokeColor": "#868e96",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b0",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 7019,
      "version": 1,
      "versionNonce": 7020,
      "updated": 1706659200000,
      "text": "uses",
      "originalText": "uses",
      "fontSize": 14,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "arrow-a-b",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-rel-b-c",
      "type": "text",
      "x": 448, "y": 243,
      "width": 80, "height": 18,
      "angle": 0,
      "strokeColor": "#868e96",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b1",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 7021,
      "version": 1,
      "versionNonce": 7022,
      "updated": 1706659200000,
      "text": "contains",
      "originalText": "contains",
      "fontSize": 14,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "arrow-b-c",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    }
  ],
  "appState": {
    "viewBackgroundColor": "#ffffff",
    "gridSize": 20
  },
  "files": {}
}

```

### assets/mindmap-template.json

```json
{
  "type": "excalidraw",
  "version": 2,
  "source": "https://excalidraw.com",
  "elements": [
    {
      "id": "center",
      "type": "ellipse",
      "x": 380, "y": 280, "width": 200, "height": 80,
      "angle": 0,
      "strokeColor": "#e67700",
      "backgroundColor": "#ffec99",
      "fillStyle": "solid",
      "strokeWidth": 3,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a0",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2001,
      "version": 1,
      "versionNonce": 2002,
      "updated": 1706659200000,
      "roundness": null,
      "boundElements": [
        { "type": "text", "id": "text-center" },
        { "type": "arrow", "id": "arrow-center-b1" },
        { "type": "arrow", "id": "arrow-center-b2" },
        { "type": "arrow", "id": "arrow-center-b3" },
        { "type": "arrow", "id": "arrow-center-b4" }
      ]
    },
    {
      "id": "branch1",
      "type": "rectangle",
      "x": 80, "y": 180, "width": 200, "height": 60,
      "angle": 0,
      "strokeColor": "#1971c2",
      "backgroundColor": "#a5d8ff",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a1",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2003,
      "version": 1,
      "versionNonce": 2004,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [
        { "type": "text", "id": "text-branch1" },
        { "type": "arrow", "id": "arrow-center-b1" },
        { "type": "arrow", "id": "arrow-b1-s1" },
        { "type": "arrow", "id": "arrow-b1-s2" }
      ]
    },
    {
      "id": "branch2",
      "type": "rectangle",
      "x": 80, "y": 400, "width": 200, "height": 60,
      "angle": 0,
      "strokeColor": "#2f9e44",
      "backgroundColor": "#b2f2bb",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a2",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2005,
      "version": 1,
      "versionNonce": 2006,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [
        { "type": "text", "id": "text-branch2" },
        { "type": "arrow", "id": "arrow-center-b2" },
        { "type": "arrow", "id": "arrow-b2-s1" },
        { "type": "arrow", "id": "arrow-b2-s2" }
      ]
    },
    {
      "id": "branch3",
      "type": "rectangle",
      "x": 680, "y": 180, "width": 200, "height": 60,
      "angle": 0,
      "strokeColor": "#7048e8",
      "backgroundColor": "#d0bfff",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a3",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2007,
      "version": 1,
      "versionNonce": 2008,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [
        { "type": "text", "id": "text-branch3" },
        { "type": "arrow", "id": "arrow-center-b3" },
        { "type": "arrow", "id": "arrow-b3-s1" },
        { "type": "arrow", "id": "arrow-b3-s2" }
      ]
    },
    {
      "id": "branch4",
      "type": "rectangle",
      "x": 680, "y": 400, "width": 200, "height": 60,
      "angle": 0,
      "strokeColor": "#0c8599",
      "backgroundColor": "#96f2d7",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a4",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2009,
      "version": 1,
      "versionNonce": 2010,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [
        { "type": "text", "id": "text-branch4" },
        { "type": "arrow", "id": "arrow-center-b4" },
        { "type": "arrow", "id": "arrow-b4-s1" },
        { "type": "arrow", "id": "arrow-b4-s2" }
      ]
    },
    {
      "id": "sub-b1-1",
      "type": "rectangle",
      "x": -160, "y": 140, "width": 180, "height": 50,
      "angle": 0,
      "strokeColor": "#1971c2",
      "backgroundColor": "#dbe4ff",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a5",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2011,
      "version": 1,
      "versionNonce": 2012,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [
        { "type": "text", "id": "text-sub-b1-1" },
        { "type": "arrow", "id": "arrow-b1-s1" }
      ]
    },
    {
      "id": "sub-b1-2",
      "type": "rectangle",
      "x": -160, "y": 220, "width": 180, "height": 50,
      "angle": 0,
      "strokeColor": "#1971c2",
      "backgroundColor": "#dbe4ff",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a6",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2013,
      "version": 1,
      "versionNonce": 2014,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [
        { "type": "text", "id": "text-sub-b1-2" },
        { "type": "arrow", "id": "arrow-b1-s2" }
      ]
    },
    {
      "id": "sub-b2-1",
      "type": "rectangle",
      "x": -160, "y": 360, "width": 180, "height": 50,
      "angle": 0,
      "strokeColor": "#2f9e44",
      "backgroundColor": "#d3f9d8",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a7",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2015,
      "version": 1,
      "versionNonce": 2016,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [
        { "type": "text", "id": "text-sub-b2-1" },
        { "type": "arrow", "id": "arrow-b2-s1" }
      ]
    },
    {
      "id": "sub-b2-2",
      "type": "rectangle",
      "x": -160, "y": 440, "width": 180, "height": 50,
      "angle": 0,
      "strokeColor": "#2f9e44",
      "backgroundColor": "#d3f9d8",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a8",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2017,
      "version": 1,
      "versionNonce": 2018,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [
        { "type": "text", "id": "text-sub-b2-2" },
        { "type": "arrow", "id": "arrow-b2-s2" }
      ]
    },
    {
      "id": "sub-b3-1",
      "type": "rectangle",
      "x": 940, "y": 140, "width": 180, "height": 50,
      "angle": 0,
      "strokeColor": "#7048e8",
      "backgroundColor": "#e5dbff",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a9",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2019,
      "version": 1,
      "versionNonce": 2020,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [
        { "type": "text", "id": "text-sub-b3-1" },
        { "type": "arrow", "id": "arrow-b3-s1" }
      ]
    },
    {
      "id": "sub-b3-2",
      "type": "rectangle",
      "x": 940, "y": 220, "width": 180, "height": 50,
      "angle": 0,
      "strokeColor": "#7048e8",
      "backgroundColor": "#e5dbff",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b0",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2021,
      "version": 1,
      "versionNonce": 2022,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [
        { "type": "text", "id": "text-sub-b3-2" },
        { "type": "arrow", "id": "arrow-b3-s2" }
      ]
    },
    {
      "id": "sub-b4-1",
      "type": "rectangle",
      "x": 940, "y": 360, "width": 180, "height": 50,
      "angle": 0,
      "strokeColor": "#0c8599",
      "backgroundColor": "#c3fae8",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b1",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2023,
      "version": 1,
      "versionNonce": 2024,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [
        { "type": "text", "id": "text-sub-b4-1" },
        { "type": "arrow", "id": "arrow-b4-s1" }
      ]
    },
    {
      "id": "sub-b4-2",
      "type": "rectangle",
      "x": 940, "y": 440, "width": 180, "height": 50,
      "angle": 0,
      "strokeColor": "#0c8599",
      "backgroundColor": "#c3fae8",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b2",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2025,
      "version": 1,
      "versionNonce": 2026,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [
        { "type": "text", "id": "text-sub-b4-2" },
        { "type": "arrow", "id": "arrow-b4-s2" }
      ]
    },
    {
      "id": "arrow-center-b1",
      "type": "arrow",
      "x": 380, "y": 310, "width": 100, "height": -100,
      "angle": 0,
      "strokeColor": "#1971c2",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b3",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2027,
      "version": 1,
      "versionNonce": 2028,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [],
      "points": [[0, 0], [-100, -100]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": { "elementId": "center", "focus": 0, "gap": 1 },
      "endBinding": { "elementId": "branch1", "focus": 0, "gap": 1 }
    },
    {
      "id": "arrow-center-b2",
      "type": "arrow",
      "x": 380, "y": 340, "width": 100, "height": 90,
      "angle": 0,
      "strokeColor": "#2f9e44",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b4",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2029,
      "version": 1,
      "versionNonce": 2030,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [],
      "points": [[0, 0], [-100, 90]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": { "elementId": "center", "focus": 0, "gap": 1 },
      "endBinding": { "elementId": "branch2", "focus": 0, "gap": 1 }
    },
    {
      "id": "arrow-center-b3",
      "type": "arrow",
      "x": 580, "y": 310, "width": 100, "height": -100,
      "angle": 0,
      "strokeColor": "#7048e8",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b5",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2031,
      "version": 1,
      "versionNonce": 2032,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [],
      "points": [[0, 0], [100, -100]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": { "elementId": "center", "focus": 0, "gap": 1 },
      "endBinding": { "elementId": "branch3", "focus": 0, "gap": 1 }
    },
    {
      "id": "arrow-center-b4",
      "type": "arrow",
      "x": 580, "y": 340, "width": 100, "height": 90,
      "angle": 0,
      "strokeColor": "#0c8599",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b6",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2033,
      "version": 1,
      "versionNonce": 2034,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [],
      "points": [[0, 0], [100, 90]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": { "elementId": "center", "focus": 0, "gap": 1 },
      "endBinding": { "elementId": "branch4", "focus": 0, "gap": 1 }
    },
    {
      "id": "arrow-b1-s1",
      "type": "arrow",
      "x": 80, "y": 200, "width": 60, "height": -35,
      "angle": 0,
      "strokeColor": "#1971c2",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b7",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2035,
      "version": 1,
      "versionNonce": 2036,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [],
      "points": [[0, 0], [-60, -35]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": { "elementId": "branch1", "focus": 0, "gap": 1 },
      "endBinding": { "elementId": "sub-b1-1", "focus": 0, "gap": 1 }
    },
    {
      "id": "arrow-b1-s2",
      "type": "arrow",
      "x": 80, "y": 220, "width": 60, "height": 25,
      "angle": 0,
      "strokeColor": "#1971c2",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b8",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2037,
      "version": 1,
      "versionNonce": 2038,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [],
      "points": [[0, 0], [-60, 25]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": { "elementId": "branch1", "focus": 0, "gap": 1 },
      "endBinding": { "elementId": "sub-b1-2", "focus": 0, "gap": 1 }
    },
    {
      "id": "arrow-b2-s1",
      "type": "arrow",
      "x": 80, "y": 415, "width": 60, "height": -30,
      "angle": 0,
      "strokeColor": "#2f9e44",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b9",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2039,
      "version": 1,
      "versionNonce": 2040,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [],
      "points": [[0, 0], [-60, -30]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": { "elementId": "branch2", "focus": 0, "gap": 1 },
      "endBinding": { "elementId": "sub-b2-1", "focus": 0, "gap": 1 }
    },
    {
      "id": "arrow-b2-s2",
      "type": "arrow",
      "x": 80, "y": 440, "width": 60, "height": 45,
      "angle": 0,
      "strokeColor": "#2f9e44",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "c0",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2041,
      "version": 1,
      "versionNonce": 2042,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [],
      "points": [[0, 0], [-60, 45]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": { "elementId": "branch2", "focus": 0, "gap": 1 },
      "endBinding": { "elementId": "sub-b2-2", "focus": 0, "gap": 1 }
    },
    {
      "id": "arrow-b3-s1",
      "type": "arrow",
      "x": 880, "y": 200, "width": 60, "height": -35,
      "angle": 0,
      "strokeColor": "#7048e8",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "c1",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2043,
      "version": 1,
      "versionNonce": 2044,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [],
      "points": [[0, 0], [60, -35]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": { "elementId": "branch3", "focus": 0, "gap": 1 },
      "endBinding": { "elementId": "sub-b3-1", "focus": 0, "gap": 1 }
    },
    {
      "id": "arrow-b3-s2",
      "type": "arrow",
      "x": 880, "y": 220, "width": 60, "height": 25,
      "angle": 0,
      "strokeColor": "#7048e8",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "c2",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2045,
      "version": 1,
      "versionNonce": 2046,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [],
      "points": [[0, 0], [60, 25]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": { "elementId": "branch3", "focus": 0, "gap": 1 },
      "endBinding": { "elementId": "sub-b3-2", "focus": 0, "gap": 1 }
    },
    {
      "id": "arrow-b4-s1",
      "type": "arrow",
      "x": 880, "y": 415, "width": 60, "height": -30,
      "angle": 0,
      "strokeColor": "#0c8599",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "c3",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2047,
      "version": 1,
      "versionNonce": 2048,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [],
      "points": [[0, 0], [60, -30]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": { "elementId": "branch4", "focus": 0, "gap": 1 },
      "endBinding": { "elementId": "sub-b4-1", "focus": 0, "gap": 1 }
    },
    {
      "id": "arrow-b4-s2",
      "type": "arrow",
      "x": 880, "y": 440, "width": 60, "height": 45,
      "angle": 0,
      "strokeColor": "#0c8599",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "c4",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2049,
      "version": 1,
      "versionNonce": 2050,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [],
      "points": [[0, 0], [60, 45]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": { "elementId": "branch4", "focus": 0, "gap": 1 },
      "endBinding": { "elementId": "sub-b4-2", "focus": 0, "gap": 1 }
    },
    {
      "id": "title",
      "type": "text",
      "x": 300, "y": 220,
      "width": 360, "height": 35,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "c5",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2051,
      "version": 1,
      "versionNonce": 2052,
      "updated": 1706659200000,
      "text": "Mind Map Title",
      "originalText": "Mind Map Title",
      "fontSize": 28,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "top",
      "containerId": null,
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-center",
      "type": "text",
      "x": 400, "y": 307,
      "width": 160, "height": 25,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "c6",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2053,
      "version": 1,
      "versionNonce": 2054,
      "updated": 1706659200000,
      "text": "Central Topic",
      "originalText": "Central Topic",
      "fontSize": 20,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "center",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-branch1",
      "type": "text",
      "x": 100, "y": 197,
      "width": 160, "height": 25,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "c7",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2055,
      "version": 1,
      "versionNonce": 2056,
      "updated": 1706659200000,
      "text": "Branch 1",
      "originalText": "Branch 1",
      "fontSize": 18,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "branch1",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-branch2",
      "type": "text",
      "x": 100, "y": 417,
      "width": 160, "height": 25,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "c8",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2057,
      "version": 1,
      "versionNonce": 2058,
      "updated": 1706659200000,
      "text": "Branch 2",
      "originalText": "Branch 2",
      "fontSize": 18,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "branch2",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-branch3",
      "type": "text",
      "x": 700, "y": 197,
      "width": 160, "height": 25,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "c9",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2059,
      "version": 1,
      "versionNonce": 2060,
      "updated": 1706659200000,
      "text": "Branch 3",
      "originalText": "Branch 3",
      "fontSize": 18,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "branch3",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-branch4",
      "type": "text",
      "x": 700, "y": 417,
      "width": 160, "height": 25,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "d0",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2061,
      "version": 1,
      "versionNonce": 2062,
      "updated": 1706659200000,
      "text": "Branch 4",
      "originalText": "Branch 4",
      "fontSize": 18,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "branch4",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-sub-b1-1",
      "type": "text",
      "x": -140, "y": 152,
      "width": 140, "height": 20,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "d1",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2063,
      "version": 1,
      "versionNonce": 2064,
      "updated": 1706659200000,
      "text": "Sub-topic 1.1",
      "originalText": "Sub-topic 1.1",
      "fontSize": 14,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "sub-b1-1",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-sub-b1-2",
      "type": "text",
      "x": -140, "y": 232,
      "width": 140, "height": 20,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "d2",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2065,
      "version": 1,
      "versionNonce": 2066,
      "updated": 1706659200000,
      "text": "Sub-topic 1.2",
      "originalText": "Sub-topic 1.2",
      "fontSize": 14,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "sub-b1-2",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-sub-b2-1",
      "type": "text",
      "x": -140, "y": 372,
      "width": 140, "height": 20,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "d3",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2067,
      "version": 1,
      "versionNonce": 2068,
      "updated": 1706659200000,
      "text": "Sub-topic 2.1",
      "originalText": "Sub-topic 2.1",
      "fontSize": 14,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "sub-b2-1",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-sub-b2-2",
      "type": "text",
      "x": -140, "y": 452,
      "width": 140, "height": 20,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "d4",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2069,
      "version": 1,
      "versionNonce": 2070,
      "updated": 1706659200000,
      "text": "Sub-topic 2.2",
      "originalText": "Sub-topic 2.2",
      "fontSize": 14,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "sub-b2-2",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-sub-b3-1",
      "type": "text",
      "x": 960, "y": 152,
      "width": 140, "height": 20,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "d5",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2071,
      "version": 1,
      "versionNonce": 2072,
      "updated": 1706659200000,
      "text": "Sub-topic 3.1",
      "originalText": "Sub-topic 3.1",
      "fontSize": 14,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "sub-b3-1",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-sub-b3-2",
      "type": "text",
      "x": 960, "y": 232,
      "width": 140, "height": 20,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "d6",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2073,
      "version": 1,
      "versionNonce": 2074,
      "updated": 1706659200000,
      "text": "Sub-topic 3.2",
      "originalText": "Sub-topic 3.2",
      "fontSize": 14,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "sub-b3-2",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-sub-b4-1",
      "type": "text",
      "x": 960, "y": 372,
      "width": 140, "height": 20,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "d7",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2075,
      "version": 1,
      "versionNonce": 2076,
      "updated": 1706659200000,
      "text": "Sub-topic 4.1",
      "originalText": "Sub-topic 4.1",
      "fontSize": 14,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "sub-b4-1",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-sub-b4-2",
      "type": "text",
      "x": 960, "y": 452,
      "width": 140, "height": 20,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "d8",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 2077,
      "version": 1,
      "versionNonce": 2078,
      "updated": 1706659200000,
      "text": "Sub-topic 4.2",
      "originalText": "Sub-topic 4.2",
      "fontSize": 14,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "sub-b4-2",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    }
  ],
  "appState": {
    "viewBackgroundColor": "#ffffff",
    "gridSize": 20
  },
  "files": {}
}

```

### assets/data-flow-diagram-template.json

```json
{
  "type": "excalidraw",
  "version": 2,
  "source": "https://excalidraw.com",
  "elements": [
    {
      "id": "external-user",
      "type": "rectangle",
      "x": 60, "y": 240, "width": 160, "height": 80,
      "angle": 0,
      "strokeColor": "#e67700",
      "backgroundColor": "#ffec99",
      "fillStyle": "solid",
      "strokeWidth": 3,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a0",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 6001,
      "version": 1,
      "versionNonce": 6002,
      "updated": 1706659200000,
      "roundness": null,
      "boundElements": [
        { "type": "text", "id": "text-external-user" },
        { "type": "arrow", "id": "arrow-user-process" }
      ]
    },
    {
      "id": "process",
      "type": "ellipse",
      "x": 320, "y": 220, "width": 200, "height": 120,
      "angle": 0,
      "strokeColor": "#1971c2",
      "backgroundColor": "#a5d8ff",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a1",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 6003,
      "version": 1,
      "versionNonce": 6004,
      "updated": 1706659200000,
      "roundness": null,
      "boundElements": [
        { "type": "text", "id": "text-process" },
        { "type": "arrow", "id": "arrow-user-process" },
        { "type": "arrow", "id": "arrow-process-db" }
      ]
    },
    {
      "id": "datastore",
      "type": "rectangle",
      "x": 620, "y": 240, "width": 200, "height": 80,
      "angle": 0,
      "strokeColor": "#0c8599",
      "backgroundColor": "#96f2d7",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a2",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 6005,
      "version": 1,
      "versionNonce": 6006,
      "updated": 1706659200000,
      "roundness": null,
      "boundElements": [
        { "type": "text", "id": "text-datastore" },
        { "type": "arrow", "id": "arrow-process-db" }
      ]
    },
    {
      "id": "datastore-top-line",
      "type": "line",
      "x": 620, "y": 240, "width": 200, "height": 0,
      "angle": 0,
      "strokeColor": "#0c8599",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a3",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 6007,
      "version": 1,
      "versionNonce": 6008,
      "updated": 1706659200000,
      "roundness": null,
      "boundElements": [],
      "points": [[0, 0], [200, 0]]
    },
    {
      "id": "arrow-user-process",
      "type": "arrow",
      "x": 220, "y": 280, "width": 100, "height": 0,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a4",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 6009,
      "version": 1,
      "versionNonce": 6010,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [{ "type": "text", "id": "text-arrow-user-process" }],
      "points": [[0, 0], [100, 0]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": { "elementId": "external-user", "focus": 0, "gap": 1 },
      "endBinding": { "elementId": "process", "focus": 0, "gap": 1 }
    },
    {
      "id": "arrow-process-db",
      "type": "arrow",
      "x": 520, "y": 280, "width": 100, "height": 0,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a5",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 6011,
      "version": 1,
      "versionNonce": 6012,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [{ "type": "text", "id": "text-arrow-process-db" }],
      "points": [[0, 0], [100, 0]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": { "elementId": "process", "focus": 0, "gap": 1 },
      "endBinding": { "elementId": "datastore", "focus": 0, "gap": 1 }
    },
    {
      "id": "title",
      "type": "text",
      "x": 200, "y": 160,
      "width": 480, "height": 35,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a6",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 6013,
      "version": 1,
      "versionNonce": 6014,
      "updated": 1706659200000,
      "text": "Data Flow Diagram",
      "originalText": "Data Flow Diagram",
      "fontSize": 28,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "top",
      "containerId": null,
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-external-user",
      "type": "text",
      "x": 80, "y": 270,
      "width": 120, "height": 25,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a7",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 6015,
      "version": 1,
      "versionNonce": 6016,
      "updated": 1706659200000,
      "text": "User",
      "originalText": "User",
      "fontSize": 18,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "external-user",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-process",
      "type": "text",
      "x": 340, "y": 263,
      "width": 160, "height": 34,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a8",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 6017,
      "version": 1,
      "versionNonce": 6018,
      "updated": 1706659200000,
      "text": "1.0\nProcess Data",
      "originalText": "1.0\nProcess Data",
      "fontSize": 16,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "process",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-datastore",
      "type": "text",
      "x": 640, "y": 270,
      "width": 160, "height": 25,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a9",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 6019,
      "version": 1,
      "versionNonce": 6020,
      "updated": 1706659200000,
      "text": "D1: Database",
      "originalText": "D1: Database",
      "fontSize": 16,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "datastore",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-arrow-user-process",
      "type": "text",
      "x": 242, "y": 262,
      "width": 100, "height": 18,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b0",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 6021,
      "version": 1,
      "versionNonce": 6022,
      "updated": 1706659200000,
      "text": "input data",
      "originalText": "input data",
      "fontSize": 14,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "arrow-user-process",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-arrow-process-db",
      "type": "text",
      "x": 542, "y": 262,
      "width": 100, "height": 18,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b1",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 6023,
      "version": 1,
      "versionNonce": 6024,
      "updated": 1706659200000,
      "text": "processed data",
      "originalText": "processed data",
      "fontSize": 14,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "arrow-process-db",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    }
  ],
  "appState": {
    "viewBackgroundColor": "#ffffff",
    "gridSize": 20
  },
  "files": {}
}

```

### assets/business-flow-swimlane-template.json

```json
{
  "type": "excalidraw",
  "version": 2,
  "source": "https://excalidraw.com",
  "elements": [
    {
      "id": "zone-lane1",
      "type": "rectangle",
      "x": 60, "y": 100, "width": 800, "height": 160,
      "angle": 0,
      "strokeColor": "#1971c2",
      "backgroundColor": "#dbe4ff",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "dashed",
      "roughness": 0,
      "opacity": 35,
      "groupIds": [],
      "frameId": null,
      "index": "a0",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 5001,
      "version": 1,
      "versionNonce": 5002,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": []
    },
    {
      "id": "zone-lane2",
      "type": "rectangle",
      "x": 60, "y": 280, "width": 800, "height": 160,
      "angle": 0,
      "strokeColor": "#2f9e44",
      "backgroundColor": "#d3f9d8",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "dashed",
      "roughness": 0,
      "opacity": 35,
      "groupIds": [],
      "frameId": null,
      "index": "a1",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 5003,
      "version": 1,
      "versionNonce": 5004,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": []
    },
    {
      "id": "step1",
      "type": "rectangle",
      "x": 120, "y": 140, "width": 180, "height": 80,
      "angle": 0,
      "strokeColor": "#1971c2",
      "backgroundColor": "#a5d8ff",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a2",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 5005,
      "version": 1,
      "versionNonce": 5006,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [
        { "type": "text", "id": "text-step1" },
        { "type": "arrow", "id": "arrow1" }
      ]
    },
    {
      "id": "step2",
      "type": "rectangle",
      "x": 380, "y": 320, "width": 180, "height": 80,
      "angle": 0,
      "strokeColor": "#2f9e44",
      "backgroundColor": "#b2f2bb",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a3",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 5007,
      "version": 1,
      "versionNonce": 5008,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [
        { "type": "text", "id": "text-step2" },
        { "type": "arrow", "id": "arrow1" },
        { "type": "arrow", "id": "arrow2" }
      ]
    },
    {
      "id": "step3",
      "type": "rectangle",
      "x": 640, "y": 140, "width": 180, "height": 80,
      "angle": 0,
      "strokeColor": "#1971c2",
      "backgroundColor": "#a5d8ff",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a4",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 5009,
      "version": 1,
      "versionNonce": 5010,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [
        { "type": "text", "id": "text-step3" },
        { "type": "arrow", "id": "arrow2" }
      ]
    },
    {
      "id": "arrow1",
      "type": "arrow",
      "x": 300, "y": 180, "width": 80, "height": 180,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a5",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 5011,
      "version": 1,
      "versionNonce": 5012,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [],
      "points": [[0, 0], [80, 0], [80, 180]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": { "elementId": "step1", "focus": 0, "gap": 1 },
      "endBinding": { "elementId": "step2", "focus": 0, "gap": 1 }
    },
    {
      "id": "arrow2",
      "type": "arrow",
      "x": 560, "y": 360, "width": 80, "height": -180,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a6",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 5013,
      "version": 1,
      "versionNonce": 5014,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [],
      "points": [[0, 0], [80, 0], [80, -180]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": { "elementId": "step2", "focus": 0, "gap": 1 },
      "endBinding": { "elementId": "step3", "focus": 0, "gap": 1 }
    },
    {
      "id": "title",
      "type": "text",
      "x": 200, "y": 50,
      "width": 480, "height": 35,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a7",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 5015,
      "version": 1,
      "versionNonce": 5016,
      "updated": 1706659200000,
      "text": "Business Process Flow",
      "originalText": "Business Process Flow",
      "fontSize": 28,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "top",
      "containerId": null,
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "label-lane1",
      "type": "text",
      "x": 72, "y": 110,
      "width": 120, "height": 20,
      "angle": 0,
      "strokeColor": "#1971c2",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a8",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 5017,
      "version": 1,
      "versionNonce": 5018,
      "updated": 1706659200000,
      "text": "Customer",
      "originalText": "Customer",
      "fontSize": 14,
      "fontFamily": 5,
      "textAlign": "left",
      "verticalAlign": "top",
      "containerId": null,
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "label-lane2",
      "type": "text",
      "x": 72, "y": 290,
      "width": 120, "height": 20,
      "angle": 0,
      "strokeColor": "#2f9e44",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a9",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 5019,
      "version": 1,
      "versionNonce": 5020,
      "updated": 1706659200000,
      "text": "Sales Team",
      "originalText": "Sales Team",
      "fontSize": 14,
      "fontFamily": 5,
      "textAlign": "left",
      "verticalAlign": "top",
      "containerId": null,
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-step1",
      "type": "text",
      "x": 140, "y": 170,
      "width": 140, "height": 25,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b0",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 5021,
      "version": 1,
      "versionNonce": 5022,
      "updated": 1706659200000,
      "text": "Submit Request",
      "originalText": "Submit Request",
      "fontSize": 16,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "step1",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-step2",
      "type": "text",
      "x": 400, "y": 350,
      "width": 140, "height": 25,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b1",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 5023,
      "version": 1,
      "versionNonce": 5024,
      "updated": 1706659200000,
      "text": "Review Request",
      "originalText": "Review Request",
      "fontSize": 16,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "step2",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-step3",
      "type": "text",
      "x": 660, "y": 170,
      "width": 140, "height": 25,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b2",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 5025,
      "version": 1,
      "versionNonce": 5026,
      "updated": 1706659200000,
      "text": "Approve",
      "originalText": "Approve",
      "fontSize": 16,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "step3",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    }
  ],
  "appState": {
    "viewBackgroundColor": "#ffffff",
    "gridSize": 20
  },
  "files": {}
}

```

### assets/class-diagram-template.json

```json
{
  "type": "excalidraw",
  "version": 2,
  "source": "https://excalidraw.com",
  "elements": [
    {
      "id": "class-base",
      "type": "rectangle",
      "x": 100, "y": 120, "width": 280, "height": 200,
      "angle": 0,
      "strokeColor": "#1971c2",
      "backgroundColor": "#a5d8ff",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a0",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 4001,
      "version": 1,
      "versionNonce": 4002,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [
        { "type": "arrow", "id": "arrow-inherit" }
      ]
    },
    {
      "id": "class-child",
      "type": "rectangle",
      "x": 500, "y": 120, "width": 280, "height": 200,
      "angle": 0,
      "strokeColor": "#7048e8",
      "backgroundColor": "#d0bfff",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a1",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 4003,
      "version": 1,
      "versionNonce": 4004,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [
        { "type": "arrow", "id": "arrow-inherit" }
      ]
    },
    {
      "id": "sep-base-1",
      "type": "line",
      "x": 100, "y": 168, "width": 280, "height": 0,
      "angle": 0,
      "strokeColor": "#1971c2",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a2",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 4005,
      "version": 1,
      "versionNonce": 4006,
      "updated": 1706659200000,
      "roundness": null,
      "boundElements": [],
      "points": [[0, 0], [280, 0]]
    },
    {
      "id": "sep-base-2",
      "type": "line",
      "x": 100, "y": 258, "width": 280, "height": 0,
      "angle": 0,
      "strokeColor": "#1971c2",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a3",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 4007,
      "version": 1,
      "versionNonce": 4008,
      "updated": 1706659200000,
      "roundness": null,
      "boundElements": [],
      "points": [[0, 0], [280, 0]]
    },
    {
      "id": "sep-child-1",
      "type": "line",
      "x": 500, "y": 168, "width": 280, "height": 0,
      "angle": 0,
      "strokeColor": "#7048e8",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a4",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 4009,
      "version": 1,
      "versionNonce": 4010,
      "updated": 1706659200000,
      "roundness": null,
      "boundElements": [],
      "points": [[0, 0], [280, 0]]
    },
    {
      "id": "sep-child-2",
      "type": "line",
      "x": 500, "y": 258, "width": 280, "height": 0,
      "angle": 0,
      "strokeColor": "#7048e8",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a5",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 4011,
      "version": 1,
      "versionNonce": 4012,
      "updated": 1706659200000,
      "roundness": null,
      "boundElements": [],
      "points": [[0, 0], [280, 0]]
    },
    {
      "id": "arrow-inherit",
      "type": "arrow",
      "x": 380, "y": 220, "width": 120, "height": 0,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a6",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 4013,
      "version": 1,
      "versionNonce": 4014,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [{ "type": "text", "id": "text-inherit-label" }],
      "points": [[0, 0], [120, 0]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "triangle",
      "startBinding": { "elementId": "class-base", "focus": 0, "gap": 1 },
      "endBinding": { "elementId": "class-child", "focus": 0, "gap": 1 }
    },
    {
      "id": "title",
      "type": "text",
      "x": 200, "y": 60,
      "width": 480, "height": 35,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a7",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 4015,
      "version": 1,
      "versionNonce": 4016,
      "updated": 1706659200000,
      "text": "Class Diagram",
      "originalText": "Class Diagram",
      "fontSize": 28,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "top",
      "containerId": null,
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-base-name",
      "type": "text",
      "x": 120, "y": 126,
      "width": 240, "height": 25,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a8",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 4017,
      "version": 1,
      "versionNonce": 4018,
      "updated": 1706659200000,
      "text": "BaseClass",
      "originalText": "BaseClass",
      "fontSize": 18,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "top",
      "containerId": null,
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-base-fields",
      "type": "text",
      "x": 112, "y": 176,
      "width": 260, "height": 72,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a9",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 4019,
      "version": 1,
      "versionNonce": 4020,
      "updated": 1706659200000,
      "text": "+ id: string\n- name: string\n# createdAt: Date",
      "originalText": "+ id: string\n- name: string\n# createdAt: Date",
      "fontSize": 14,
      "fontFamily": 5,
      "textAlign": "left",
      "verticalAlign": "top",
      "containerId": null,
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-base-methods",
      "type": "text",
      "x": 112, "y": 266,
      "width": 260, "height": 45,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b0",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 4021,
      "version": 1,
      "versionNonce": 4022,
      "updated": 1706659200000,
      "text": "+ save(): void\n+ delete(): boolean",
      "originalText": "+ save(): void\n+ delete(): boolean",
      "fontSize": 14,
      "fontFamily": 5,
      "textAlign": "left",
      "verticalAlign": "top",
      "containerId": null,
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-child-name",
      "type": "text",
      "x": 520, "y": 126,
      "width": 240, "height": 25,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b1",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 4023,
      "version": 1,
      "versionNonce": 4024,
      "updated": 1706659200000,
      "text": "ChildClass",
      "originalText": "ChildClass",
      "fontSize": 18,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "top",
      "containerId": null,
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-child-fields",
      "type": "text",
      "x": 512, "y": 176,
      "width": 260, "height": 72,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b2",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 4025,
      "version": 1,
      "versionNonce": 4026,
      "updated": 1706659200000,
      "text": "- role: string\n+ permissions: string[]",
      "originalText": "- role: string\n+ permissions: string[]",
      "fontSize": 14,
      "fontFamily": 5,
      "textAlign": "left",
      "verticalAlign": "top",
      "containerId": null,
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-child-methods",
      "type": "text",
      "x": 512, "y": 266,
      "width": 260, "height": 45,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b3",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 4027,
      "version": 1,
      "versionNonce": 4028,
      "updated": 1706659200000,
      "text": "+ hasPermission(p): boolean\n+ elevate(): void",
      "originalText": "+ hasPermission(p): boolean\n+ elevate(): void",
      "fontSize": 14,
      "fontFamily": 5,
      "textAlign": "left",
      "verticalAlign": "top",
      "containerId": null,
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-inherit-label",
      "type": "text",
      "x": 400, "y": 202,
      "width": 80, "height": 18,
      "angle": 0,
      "strokeColor": "#868e96",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b4",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 4029,
      "version": 1,
      "versionNonce": 4030,
      "updated": 1706659200000,
      "text": "extends",
      "originalText": "extends",
      "fontSize": 14,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "arrow-inherit",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    }
  ],
  "appState": {
    "viewBackgroundColor": "#ffffff",
    "gridSize": 20
  },
  "files": {}
}

```

### assets/sequence-diagram-template.json

```json
{
  "type": "excalidraw",
  "version": 2,
  "source": "https://excalidraw.com",
  "elements": [
    {
      "id": "actor-client",
      "type": "rectangle",
      "x": 80, "y": 80, "width": 160, "height": 60,
      "angle": 0,
      "strokeColor": "#1971c2",
      "backgroundColor": "#a5d8ff",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a0",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 3001,
      "version": 1,
      "versionNonce": 3002,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [{ "type": "text", "id": "text-actor-client" }]
    },
    {
      "id": "actor-server",
      "type": "rectangle",
      "x": 380, "y": 80, "width": 160, "height": 60,
      "angle": 0,
      "strokeColor": "#2f9e44",
      "backgroundColor": "#b2f2bb",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a1",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 3003,
      "version": 1,
      "versionNonce": 3004,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [{ "type": "text", "id": "text-actor-server" }]
    },
    {
      "id": "actor-db",
      "type": "rectangle",
      "x": 680, "y": 80, "width": 160, "height": 60,
      "angle": 0,
      "strokeColor": "#7048e8",
      "backgroundColor": "#d0bfff",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a2",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 3005,
      "version": 1,
      "versionNonce": 3006,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [{ "type": "text", "id": "text-actor-db" }]
    },
    {
      "id": "lifeline-client",
      "type": "line",
      "x": 160, "y": 140, "width": 0, "height": 440,
      "angle": 0,
      "strokeColor": "#868e96",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "dashed",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a3",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 3007,
      "version": 1,
      "versionNonce": 3008,
      "updated": 1706659200000,
      "roundness": null,
      "boundElements": [],
      "points": [[0, 0], [0, 440]]
    },
    {
      "id": "lifeline-server",
      "type": "line",
      "x": 460, "y": 140, "width": 0, "height": 440,
      "angle": 0,
      "strokeColor": "#868e96",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "dashed",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a4",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 3009,
      "version": 1,
      "versionNonce": 3010,
      "updated": 1706659200000,
      "roundness": null,
      "boundElements": [],
      "points": [[0, 0], [0, 440]]
    },
    {
      "id": "lifeline-db",
      "type": "line",
      "x": 760, "y": 140, "width": 0, "height": 440,
      "angle": 0,
      "strokeColor": "#868e96",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "dashed",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a5",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 3011,
      "version": 1,
      "versionNonce": 3012,
      "updated": 1706659200000,
      "roundness": null,
      "boundElements": [],
      "points": [[0, 0], [0, 440]]
    },
    {
      "id": "activation-server",
      "type": "rectangle",
      "x": 452, "y": 200, "width": 16, "height": 200,
      "angle": 0,
      "strokeColor": "#2f9e44",
      "backgroundColor": "#b2f2bb",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 0,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a6",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 3013,
      "version": 1,
      "versionNonce": 3014,
      "updated": 1706659200000,
      "roundness": null,
      "boundElements": []
    },
    {
      "id": "msg1",
      "type": "arrow",
      "x": 160, "y": 200, "width": 300, "height": 0,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a7",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 3015,
      "version": 1,
      "versionNonce": 3016,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [{ "type": "text", "id": "text-msg1" }],
      "points": [[0, 0], [300, 0]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": null,
      "endBinding": null
    },
    {
      "id": "msg2",
      "type": "arrow",
      "x": 460, "y": 280, "width": 300, "height": 0,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a8",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 3017,
      "version": 1,
      "versionNonce": 3018,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [{ "type": "text", "id": "text-msg2" }],
      "points": [[0, 0], [300, 0]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": null,
      "endBinding": null
    },
    {
      "id": "msg3",
      "type": "arrow",
      "x": 760, "y": 340, "width": 300, "height": 0,
      "angle": 0,
      "strokeColor": "#868e96",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "dashed",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a9",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 3019,
      "version": 1,
      "versionNonce": 3020,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [{ "type": "text", "id": "text-msg3" }],
      "points": [[0, 0], [-300, 0]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": null,
      "endBinding": null
    },
    {
      "id": "msg4",
      "type": "arrow",
      "x": 460, "y": 400, "width": 300, "height": 0,
      "angle": 0,
      "strokeColor": "#868e96",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "dashed",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b0",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 3021,
      "version": 1,
      "versionNonce": 3022,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [{ "type": "text", "id": "text-msg4" }],
      "points": [[0, 0], [-300, 0]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": null,
      "endBinding": null
    },
    {
      "id": "title",
      "type": "text",
      "x": 200, "y": 30,
      "width": 520, "height": 35,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b1",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 3023,
      "version": 1,
      "versionNonce": 3024,
      "updated": 1706659200000,
      "text": "Sequence Diagram",
      "originalText": "Sequence Diagram",
      "fontSize": 28,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "top",
      "containerId": null,
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-actor-client",
      "type": "text",
      "x": 100, "y": 100,
      "width": 120, "height": 25,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b2",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 3025,
      "version": 1,
      "versionNonce": 3026,
      "updated": 1706659200000,
      "text": "Client",
      "originalText": "Client",
      "fontSize": 18,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "actor-client",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-actor-server",
      "type": "text",
      "x": 400, "y": 100,
      "width": 120, "height": 25,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b3",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 3027,
      "version": 1,
      "versionNonce": 3028,
      "updated": 1706659200000,
      "text": "Server",
      "originalText": "Server",
      "fontSize": 18,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "actor-server",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-actor-db",
      "type": "text",
      "x": 700, "y": 100,
      "width": 120, "height": 25,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b4",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 3029,
      "version": 1,
      "versionNonce": 3030,
      "updated": 1706659200000,
      "text": "Database",
      "originalText": "Database",
      "fontSize": 18,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "actor-db",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-msg1",
      "type": "text",
      "x": 220, "y": 182,
      "width": 180, "height": 18,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b5",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 3031,
      "version": 1,
      "versionNonce": 3032,
      "updated": 1706659200000,
      "text": "1: request()",
      "originalText": "1: request()",
      "fontSize": 14,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "msg1",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-msg2",
      "type": "text",
      "x": 520, "y": 262,
      "width": 180, "height": 18,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b6",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 3033,
      "version": 1,
      "versionNonce": 3034,
      "updated": 1706659200000,
      "text": "2: query()",
      "originalText": "2: query()",
      "fontSize": 14,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "msg2",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-msg3",
      "type": "text",
      "x": 520, "y": 322,
      "width": 180, "height": 18,
      "angle": 0,
      "strokeColor": "#868e96",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b7",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 3035,
      "version": 1,
      "versionNonce": 3036,
      "updated": 1706659200000,
      "text": "3: result rows",
      "originalText": "3: result rows",
      "fontSize": 14,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "msg3",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-msg4",
      "type": "text",
      "x": 220, "y": 382,
      "width": 180, "height": 18,
      "angle": 0,
      "strokeColor": "#868e96",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b8",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 3037,
      "version": 1,
      "versionNonce": 3038,
      "updated": 1706659200000,
      "text": "4: response",
      "originalText": "4: response",
      "fontSize": 14,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "msg4",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    }
  ],
  "appState": {
    "viewBackgroundColor": "#ffffff",
    "gridSize": 20
  },
  "files": {}
}

```

### assets/er-diagram-template.json

```json
{
  "type": "excalidraw",
  "version": 2,
  "source": "https://excalidraw.com",
  "elements": [
    {
      "id": "entity-user",
      "type": "rectangle",
      "x": 60, "y": 120, "width": 220, "height": 180,
      "angle": 0,
      "strokeColor": "#1971c2",
      "backgroundColor": "#a5d8ff",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a0",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 8001,
      "version": 1,
      "versionNonce": 8002,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [
        { "type": "arrow", "id": "arrow-user-order" }
      ]
    },
    {
      "id": "entity-order",
      "type": "rectangle",
      "x": 400, "y": 120, "width": 220, "height": 180,
      "angle": 0,
      "strokeColor": "#e67700",
      "backgroundColor": "#ffec99",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a1",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 8003,
      "version": 1,
      "versionNonce": 8004,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [
        { "type": "arrow", "id": "arrow-user-order" },
        { "type": "arrow", "id": "arrow-order-product" }
      ]
    },
    {
      "id": "entity-product",
      "type": "rectangle",
      "x": 740, "y": 120, "width": 220, "height": 180,
      "angle": 0,
      "strokeColor": "#2f9e44",
      "backgroundColor": "#b2f2bb",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a2",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 8005,
      "version": 1,
      "versionNonce": 8006,
      "updated": 1706659200000,
      "roundness": { "type": 3 },
      "boundElements": [
        { "type": "arrow", "id": "arrow-order-product" }
      ]
    },
    {
      "id": "sep-user",
      "type": "line",
      "x": 60, "y": 168, "width": 220, "height": 0,
      "angle": 0,
      "strokeColor": "#1971c2",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a3",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 8007,
      "version": 1,
      "versionNonce": 8008,
      "updated": 1706659200000,
      "roundness": null,
      "boundElements": [],
      "points": [[0, 0], [220, 0]]
    },
    {
      "id": "sep-order",
      "type": "line",
      "x": 400, "y": 168, "width": 220, "height": 0,
      "angle": 0,
      "strokeColor": "#e67700",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a4",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 8009,
      "version": 1,
      "versionNonce": 8010,
      "updated": 1706659200000,
      "roundness": null,
      "boundElements": [],
      "points": [[0, 0], [220, 0]]
    },
    {
      "id": "sep-product",
      "type": "line",
      "x": 740, "y": 168, "width": 220, "height": 0,
      "angle": 0,
      "strokeColor": "#2f9e44",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a5",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 8011,
      "version": 1,
      "versionNonce": 8012,
      "updated": 1706659200000,
      "roundness": null,
      "boundElements": [],
      "points": [[0, 0], [220, 0]]
    },
    {
      "id": "arrow-user-order",
      "type": "arrow",
      "x": 280, "y": 210, "width": 120, "height": 0,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a6",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 8013,
      "version": 1,
      "versionNonce": 8014,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [{ "type": "text", "id": "text-rel-user-order" }],
      "points": [[0, 0], [120, 0]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": { "elementId": "entity-user", "focus": 0, "gap": 1 },
      "endBinding": { "elementId": "entity-order", "focus": 0, "gap": 1 }
    },
    {
      "id": "arrow-order-product",
      "type": "arrow",
      "x": 620, "y": 210, "width": 120, "height": 0,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 2,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a7",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 8015,
      "version": 1,
      "versionNonce": 8016,
      "updated": 1706659200000,
      "roundness": { "type": 2 },
      "boundElements": [{ "type": "text", "id": "text-rel-order-product" }],
      "points": [[0, 0], [120, 0]],
      "lastCommittedPoint": null,
      "startArrowhead": null,
      "endArrowhead": "arrow",
      "startBinding": { "elementId": "entity-order", "focus": 0, "gap": 1 },
      "endBinding": { "elementId": "entity-product", "focus": 0, "gap": 1 }
    },
    {
      "id": "title",
      "type": "text",
      "x": 300, "y": 60,
      "width": 440, "height": 35,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a8",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 8017,
      "version": 1,
      "versionNonce": 8018,
      "updated": 1706659200000,
      "text": "ER Diagram",
      "originalText": "ER Diagram",
      "fontSize": 28,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "top",
      "containerId": null,
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-user-name",
      "type": "text",
      "x": 80, "y": 128,
      "width": 180, "height": 25,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "a9",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 8019,
      "version": 1,
      "versionNonce": 8020,
      "updated": 1706659200000,
      "text": "User",
      "originalText": "User",
      "fontSize": 18,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "top",
      "containerId": null,
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-user-attrs",
      "type": "text",
      "x": 72, "y": 176,
      "width": 200, "height": 80,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b0",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 8021,
      "version": 1,
      "versionNonce": 8022,
      "updated": 1706659200000,
      "text": "PK user_id: int\n   name: varchar\n   email: varchar\n   created_at: date",
      "originalText": "PK user_id: int\n   name: varchar\n   email: varchar\n   created_at: date",
      "fontSize": 13,
      "fontFamily": 5,
      "textAlign": "left",
      "verticalAlign": "top",
      "containerId": null,
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-order-name",
      "type": "text",
      "x": 420, "y": 128,
      "width": 180, "height": 25,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b1",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 8023,
      "version": 1,
      "versionNonce": 8024,
      "updated": 1706659200000,
      "text": "Order",
      "originalText": "Order",
      "fontSize": 18,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "top",
      "containerId": null,
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-order-attrs",
      "type": "text",
      "x": 412, "y": 176,
      "width": 200, "height": 80,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b2",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 8025,
      "version": 1,
      "versionNonce": 8026,
      "updated": 1706659200000,
      "text": "PK order_id: int\nFK user_id: int\n   total: decimal\n   order_date: date",
      "originalText": "PK order_id: int\nFK user_id: int\n   total: decimal\n   order_date: date",
      "fontSize": 13,
      "fontFamily": 5,
      "textAlign": "left",
      "verticalAlign": "top",
      "containerId": null,
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-product-name",
      "type": "text",
      "x": 760, "y": 128,
      "width": 180, "height": 25,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b3",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 8027,
      "version": 1,
      "versionNonce": 8028,
      "updated": 1706659200000,
      "text": "Product",
      "originalText": "Product",
      "fontSize": 18,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "top",
      "containerId": null,
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-product-attrs",
      "type": "text",
      "x": 752, "y": 176,
      "width": 200, "height": 64,
      "angle": 0,
      "strokeColor": "#1e1e1e",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b4",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 8029,
      "version": 1,
      "versionNonce": 8030,
      "updated": 1706659200000,
      "text": "PK product_id: int\n   name: varchar\n   price: decimal",
      "originalText": "PK product_id: int\n   name: varchar\n   price: decimal",
      "fontSize": 13,
      "fontFamily": 5,
      "textAlign": "left",
      "verticalAlign": "top",
      "containerId": null,
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-rel-user-order",
      "type": "text",
      "x": 298, "y": 192,
      "width": 100, "height": 18,
      "angle": 0,
      "strokeColor": "#868e96",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b5",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 8031,
      "version": 1,
      "versionNonce": 8032,
      "updated": 1706659200000,
      "text": "places (1:N)",
      "originalText": "places (1:N)",
      "fontSize": 13,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "arrow-user-order",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    },
    {
      "id": "text-rel-order-product",
      "type": "text",
      "x": 638, "y": 192,
      "width": 100, "height": 18,
      "angle": 0,
      "strokeColor": "#868e96",
      "backgroundColor": "transparent",
      "fillStyle": "solid",
      "strokeWidth": 1,
      "strokeStyle": "solid",
      "roughness": 1,
      "opacity": 100,
      "groupIds": [],
      "frameId": null,
      "index": "b6",
      "isDeleted": false,
      "link": null,
      "locked": false,
      "seed": 8033,
      "version": 1,
      "versionNonce": 8034,
      "updated": 1706659200000,
      "text": "contains (N:M)",
      "originalText": "contains (N:M)",
      "fontSize": 13,
      "fontFamily": 5,
      "textAlign": "center",
      "verticalAlign": "middle",
      "containerId": "arrow-order-product",
      "lineHeight": 1.25,
      "autoResize": true,
      "roundness": null
    }
  ],
  "appState": {
    "viewBackgroundColor": "#ffffff",
    "gridSize": 20
  },
  "files": {}
}

```

### references/icon-libraries.md

```markdown
# Icon Libraries

Read this file when the user requests diagrams with service icons (AWS, GCP, Azure, Kubernetes, etc.) or asks for professional architecture diagrams.

## How It Works

Excalidraw supports icon libraries (`.excalidrawlib` files) that provide professional, standardized icons. This skill can use pre-split icon libraries to create polished architecture diagrams.

## Checking for Available Libraries

```
Look for: libraries/<library-name>/reference.md
```

If `reference.md` exists, the library is ready. It contains a lookup table of all available icon names.

If no libraries are set up, offer two options:

1. Create the diagram using basic shapes (rectangles, ellipses) with labels — still functional, just less visually polished
2. Guide the user through library setup (see below)

## Using Icons (Python Scripts — Recommended)

The repository includes Python scripts that handle icon integration without consuming AI context tokens:

### Adding icons to a diagram

```bash
python scripts/add-icon-to-diagram.py \
  <diagram-path> <icon-name> <x> <y> \
  [--label "Text"] [--library-path PATH]
```

The `--label` flag adds a text label below the icon. Edit via `.excalidraw.edit` is enabled by default; pass `--no-use-edit-suffix` to disable.

**Examples:**

```bash
# Add EC2 icon at position (400, 300) with label
python scripts/add-icon-to-diagram.py diagram.excalidraw EC2 400 300 --label "Web Server"

# Add icon from a different library
python scripts/add-icon-to-diagram.py diagram.excalidraw Compute-Engine 500 200 \
  --library-path libraries/gcp-icons --label "API Server"
```

### Adding connecting arrows

```bash
python scripts/add-arrow.py \
  <diagram-path> <from-x> <from-y> <to-x> <to-y> \
  [--label "Text"] [--style solid|dashed|dotted] [--color HEX]
```

**Examples:**

```bash
# Simple arrow
python scripts/add-arrow.py diagram.excalidraw 300 250 500 300

# Arrow with label and custom style
python scripts/add-arrow.py diagram.excalidraw 400 350 600 400 \
  --label "HTTPS" --style dashed --color "#7950f2"
```

### Complete workflow

```bash
# 1. Create base diagram with title and structure
#    (Create .excalidraw file with basic elements: title text, region rectangles)

# 2. Check icon availability
#    Read: libraries/aws-architecture-icons/reference.md

# 3. Add icons with labels
python scripts/add-icon-to-diagram.py my-diagram.excalidraw "Internet-gateway" 200 150 --label "Internet Gateway"
python scripts/add-icon-to-diagram.py my-diagram.excalidraw VPC 250 250
python scripts/add-icon-to-diagram.py my-diagram.excalidraw ELB 350 300 --label "Load Balancer"
python scripts/add-icon-to-diagram.py my-diagram.excalidraw EC2 450 350 --label "Web Server"
python scripts/add-icon-to-diagram.py my-diagram.excalidraw RDS 550 400 --label "Database"

# 4. Add connecting arrows
python scripts/add-arrow.py my-diagram.excalidraw 250 200 300 250 --label "traffic"
python scripts/add-arrow.py my-diagram.excalidraw 300 300 400 300
python scripts/add-arrow.py my-diagram.excalidraw 500 380 600 400 --label "SQL" --style dashed
```

**Why use scripts:**

- ✅ No token consumption — icon JSON data (200-1000 lines each) never enters AI context
- ✅ Accurate coordinates — calculations handled deterministically
- ✅ Automatic ID management — no risk of collision
- ✅ Fast and reusable — works with any Excalidraw library

## Setting Up a Library

Guide the user through these steps:

### Step 1: Create library directory

```bash
mkdir -p skills/excalidraw-studio/libraries/<library-name>
```

### Step 2: Download library

- Visit: <https://libraries.excalidraw.com/>
- Search for the desired icon set (e.g., "AWS Architecture Icons")
- Click download to get the `.excalidrawlib` file
- Place it in the directory from Step 1

### Step 3: Run splitter script

```bash
python skills/excalidraw-studio/scripts/split-excalidraw-library.py \
  skills/excalidraw-studio/libraries/<library-name>/
```

### Step 4: Verify

After running the script, this structure should exist:

```
libraries/<library-name>/
  <library-name>.excalidrawlib  (original)
  reference.md                  (generated — icon lookup table)
  icons/                        (generated — individual icon files)
    API-Gateway.json
    EC2.json
    Lambda.json
    S3.json
    ...
```

## Manual Icon Integration (Fallback)

Only use this if Python scripts are unavailable. This approach is token-expensive and error-prone.

1. Read `libraries/<library-name>/reference.md` to find icon names
2. Read individual icon JSON files from `icons/` (200-1000 lines each)
3. Extract elements array, calculate bounding box, apply coordinate offset
4. Generate new unique IDs, update groupIds references
5. Copy transformed elements into the diagram

**Challenges:**

- ⚠️ High token consumption (200-1000 lines per icon)
- ⚠️ Complex coordinate transformation
- ⚠️ Risk of ID collision
- ⚠️ Time-consuming for many icons

## Supported Libraries

This workflow works with any valid `.excalidrawlib` file from <https://libraries.excalidraw.com/>. Common categories:

- Cloud service icons (AWS, GCP, Azure)
- Kubernetes / infrastructure icons
- UI / Material icons
- Network diagram icons

Availability and naming vary; verify on the site before recommending to users.

```



---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### scripts/README.md

```markdown
# Excalidraw Library Tools

This directory contains scripts for working with Excalidraw libraries.

## split-excalidraw-library.py

Splits an Excalidraw library file (`*.excalidrawlib`) into individual icon JSON files for efficient token usage by AI assistants.

### Prerequisites

- Python 3.6 or higher
- No additional dependencies required (uses only standard library)

### Usage

```bash
python split-excalidraw-library.py <path-to-library-directory>
```

### Step-by-Step Workflow

1. **Create library directory**:

   ```bash
   mkdir -p skills/excalidraw-studio/libraries/aws-architecture-icons
   ```

2. **Download and place library file**:
   - Visit: <https://libraries.excalidraw.com/>
   - Search for "AWS Architecture Icons" and download the `.excalidrawlib` file
   - Rename it to match the directory name: `aws-architecture-icons.excalidrawlib`
   - Place it in the directory created in step 1

3. **Run the script**:

   ```bash
   python skills/excalidraw-studio/scripts/split-excalidraw-library.py skills/excalidraw-studio/libraries/aws-architecture-icons/
   ```

### Output Structure

The script creates the following structure in the library directory:

```
skills/excalidraw-studio/libraries/aws-architecture-icons/
  aws-architecture-icons.excalidrawlib  # Original file (kept)
  reference.md                          # Generated: Quick reference table
  icons/                                # Generated: Individual icon files
    API-Gateway.json
    CloudFront.json
    EC2.json
    S3.json
    ...
```

### What the Script Does

1. **Reads** the `.excalidrawlib` file
2. **Extracts** each icon from the `libraryItems` array
3. **Sanitizes** icon names to create valid filenames (spaces → hyphens, removes special characters)
4. **Saves** each icon as a separate JSON file in the `icons/` directory
5. **Generates** a `reference.md` file with a table mapping icon names to filenames

### Benefits

- **Token Efficiency**: AI can first read the lightweight `reference.md` to find relevant icons, then load only the specific icon files needed
- **Organization**: Icons are organized in a clear directory structure
- **Extensibility**: Users can add multiple library sets side-by-side

### Recommended Workflow

1. Download desired Excalidraw libraries from <https://libraries.excalidraw.com/>
2. Run this script on each library file
3. Move the generated folders to `../libraries/`
4. The AI assistant will use `reference.md` files to locate and use icons efficiently

### Library Sources (Examples — verify availability)

- Examples found on <https://libraries.excalidraw.com/> may include cloud/service icon sets.
- Availability changes over time; verify the exact library names on the site before use.
- This script works with any valid `.excalidrawlib` file you provide.

### Troubleshooting

**Error: File not found**

- Check that the file path is correct
- Make sure the file has a `.excalidrawlib` extension

**Error: Invalid library file format**

- Ensure the file is a valid Excalidraw library file
- Check that it contains a `libraryItems` array

### License Considerations

When using third-party icon libraries:

- **AWS Architecture Icons**: Subject to AWS Content License
- **GCP Icons**: Subject to Google's terms
- **Other libraries**: Check each library's license

This script is for personal/organizational use. Redistribution of split icon files should comply with the original library's license terms.

## add-icon-to-diagram.py

Adds a specific icon from a split Excalidraw library into an existing `.excalidraw` diagram. The script handles coordinate translation and ID collision avoidance, and can optionally add a label under the icon.

### Prerequisites

- Python 3.6 or higher
- A diagram file (`.excalidraw`)
- A split icon library directory (created by `split-excalidraw-library.py`)

### Usage

```bash
python add-icon-to-diagram.py <diagram-path> <icon-name> <x> <y> [OPTIONS]
```

**Options**

- `--library-path PATH` : Path to the icon library directory (default: `aws-architecture-icons`)
- `--label TEXT` : Add a text label below the icon
-- `--use-edit-suffix` : Edit via `.excalidraw.edit` to avoid editor overwrite issues (enabled by default; pass `--no-use-edit-suffix` to disable)

### Examples

```bash
# Add EC2 icon at position (400, 300)
python add-icon-to-diagram.py diagram.excalidraw EC2 400 300

# Add VPC icon with label
python add-icon-to-diagram.py diagram.excalidraw VPC 200 150 --label "VPC"

# Safe edit mode is enabled by default (avoids editor overwrite issues)
# Use `--no-use-edit-suffix` to disable
python add-icon-to-diagram.py diagram.excalidraw EC2 500 300

# Add icon from another library
python add-icon-to-diagram.py diagram.excalidraw Compute-Engine 500 200 \
   --library-path libraries/gcp-icons --label "API Server"
```

### What the Script Does

1. **Loads** the icon JSON from the library’s `icons/` directory
2. **Calculates** the icon’s bounding box
3. **Offsets** all coordinates to the target position
4. **Generates** unique IDs for all elements and groups
5. **Appends** the transformed elements to the diagram
6. **(Optional)** Adds a label beneath the icon

---

## add-arrow.py

Adds a straight arrow between two points in an existing `.excalidraw` diagram. Supports optional labels and line styles.

### Prerequisites

- Python 3.6 or higher
- A diagram file (`.excalidraw`)

### Usage

```bash
python add-arrow.py <diagram-path> <from-x> <from-y> <to-x> <to-y> [OPTIONS]
```

**Options**

- `--style {solid|dashed|dotted}` : Line style (default: `solid`)
- `--color HEX` : Arrow color (default: `#1e1e1e`)
- `--label TEXT` : Add a text label on the arrow
-- `--use-edit-suffix` : Edit via `.excalidraw.edit` to avoid editor overwrite issues (enabled by default; pass `--no-use-edit-suffix` to disable)

### Examples

```bash
# Simple arrow
python add-arrow.py diagram.excalidraw 300 200 500 300

# Arrow with label
python add-arrow.py diagram.excalidraw 300 200 500 300 --label "HTTPS"

# Dashed arrow with custom color
python add-arrow.py diagram.excalidraw 400 350 600 400 --style dashed --color "#7950f2"

# Safe edit mode is enabled by default (avoids editor overwrite issues)
# Use `--no-use-edit-suffix` to disable
python add-arrow.py diagram.excalidraw 300 200 500 300
```

### What the Script Does

1. **Creates** an arrow element from the given coordinates
2. **(Optional)** Adds a label near the arrow midpoint
3. **Appends** elements to the diagram
4. **Saves** the updated file

```

### scripts/add-arrow.py

```python
#!/usr/bin/env python3
"""
Add arrows (connections) between elements in Excalidraw diagrams.

Usage:
    python add-arrow.py <diagram_path> <from_x> <from_y> <to_x> <to_y> [OPTIONS]

Options:
    --style {solid|dashed|dotted}    Arrow line style (default: solid)
    --color HEX                      Arrow color (default: #1e1e1e)
    --label TEXT                     Add text label on the arrow
    --use-edit-suffix                Edit via .excalidraw.edit to avoid editor overwrite issues (enabled by default; use --no-use-edit-suffix to disable)

Examples:
    python add-arrow.py diagram.excalidraw 300 200 500 300
    python add-arrow.py diagram.excalidraw 300 200 500 300 --label "HTTP"
    python add-arrow.py diagram.excalidraw 300 200 500 300 --style dashed --color "#7950f2"
    python add-arrow.py diagram.excalidraw 300 200 500 300 --use-edit-suffix
"""

import json
import sys
import uuid
from pathlib import Path
from typing import Dict, Any


def generate_unique_id() -> str:
    """Generate a unique ID for Excalidraw elements."""
    return str(uuid.uuid4()).replace('-', '')[:16]


def prepare_edit_path(diagram_path: Path, use_edit_suffix: bool) -> tuple[Path, Path | None]:
    """
    Prepare a safe edit path to avoid editor overwrite issues.

    Returns:
        (work_path, final_path)
        - work_path: file path to read/write during edit
        - final_path: file path to rename back to (or None if not used)
    """
    if not use_edit_suffix:
        return diagram_path, None

    if diagram_path.suffix != ".excalidraw":
        return diagram_path, None

    edit_path = diagram_path.with_suffix(diagram_path.suffix + ".edit")

    if diagram_path.exists():
        if edit_path.exists():
            raise FileExistsError(f"Edit file already exists: {edit_path}")
        diagram_path.rename(edit_path)

    return edit_path, diagram_path


def finalize_edit_path(work_path: Path, final_path: Path | None) -> None:
    """Finalize edit by renaming .edit back to .excalidraw if needed."""
    if final_path is None:
        return

    if final_path.exists():
        final_path.unlink()

    work_path.rename(final_path)


def create_arrow(
    from_x: float,
    from_y: float,
    to_x: float,
    to_y: float,
    style: str = "solid",
    color: str = "#1e1e1e",
    label: str = None
) -> list:
    """
    Create an arrow element.
    
    Args:
        from_x: Starting X coordinate
        from_y: Starting Y coordinate
        to_x: Ending X coordinate
        to_y: Ending Y coordinate
        style: Line style (solid, dashed, dotted)
        color: Arrow color
        label: Optional text label on the arrow
    
    Returns:
        List of elements (arrow and optional label)
    """
    elements = []
    
    # Arrow element
    arrow = {
        "id": generate_unique_id(),
        "type": "arrow",
        "x": from_x,
        "y": from_y,
        "width": to_x - from_x,
        "height": to_y - from_y,
        "angle": 0,
        "strokeColor": color,
        "backgroundColor": "transparent",
        "fillStyle": "solid",
        "strokeWidth": 2,
        "strokeStyle": style,
        "roughness": 1,
        "opacity": 100,
        "groupIds": [],
        "frameId": None,
        "index": "a0",
        "roundness": {
            "type": 2
        },
        "seed": 1000000000 + hash(f"{from_x}{from_y}{to_x}{to_y}") % 1000000000,
        "version": 1,
        "versionNonce": 2000000000 + hash(f"{from_x}{from_y}{to_x}{to_y}") % 1000000000,
        "isDeleted": False,
        "boundElements": [],
        "updated": 1738195200000,
        "link": None,
        "locked": False,
        "points": [
            [0, 0],
            [to_x - from_x, to_y - from_y]
        ],
        "startBinding": None,
        "endBinding": None,
        "startArrowhead": None,
        "endArrowhead": "arrow",
        "lastCommittedPoint": None
    }
    elements.append(arrow)
    
    # Optional label
    if label:
        mid_x = (from_x + to_x) / 2 - (len(label) * 5)
        mid_y = (from_y + to_y) / 2 - 10
        
        label_element = {
            "id": generate_unique_id(),
            "type": "text",
            "x": mid_x,
            "y": mid_y,
            "width": len(label) * 10,
            "height": 20,
            "angle": 0,
            "strokeColor": color,
            "backgroundColor": "transparent",
            "fillStyle": "solid",
            "strokeWidth": 2,
            "strokeStyle": "solid",
            "roughness": 1,
            "opacity": 100,
            "groupIds": [],
            "frameId": None,
            "index": "a0",
            "roundness": None,
            "seed": 1000000000 + hash(label) % 1000000000,
            "version": 1,
            "versionNonce": 2000000000 + hash(label) % 1000000000,
            "isDeleted": False,
            "boundElements": [],
            "updated": 1738195200000,
            "link": None,
            "locked": False,
            "text": label,
            "fontSize": 14,
            "fontFamily": 5,
            "textAlign": "center",
            "verticalAlign": "top",
            "containerId": None,
            "originalText": label,
            "autoResize": True,
            "lineHeight": 1.25
        }
        elements.append(label_element)
    
    return elements


def add_arrow_to_diagram(
    diagram_path: Path,
    from_x: float,
    from_y: float,
    to_x: float,
    to_y: float,
    style: str = "solid",
    color: str = "#1e1e1e",
    label: str = None
) -> None:
    """
    Add an arrow to an Excalidraw diagram.
    
    Args:
        diagram_path: Path to the Excalidraw diagram file
        from_x: Starting X coordinate
        from_y: Starting Y coordinate
        to_x: Ending X coordinate
        to_y: Ending Y coordinate
        style: Line style (solid, dashed, dotted)
        color: Arrow color
        label: Optional text label
    """
    print(f"Creating arrow from ({from_x}, {from_y}) to ({to_x}, {to_y})")
    arrow_elements = create_arrow(from_x, from_y, to_x, to_y, style, color, label)
    
    if label:
        print(f"  With label: '{label}'")
    
    # Load diagram
    print(f"Loading diagram: {diagram_path}")
    with open(diagram_path, 'r', encoding='utf-8') as f:
        diagram = json.load(f)
    
    # Add arrow elements
    if 'elements' not in diagram:
        diagram['elements'] = []
    
    original_count = len(diagram['elements'])
    diagram['elements'].extend(arrow_elements)
    print(f"  Added {len(arrow_elements)} elements (total: {original_count} -> {len(diagram['elements'])})")
    
    # Save diagram
    print(f"Saving diagram")
    with open(diagram_path, 'w', encoding='utf-8') as f:
        json.dump(diagram, f, indent=2, ensure_ascii=False)
    
    print(f"✓ Successfully added arrow to diagram")


def main():
    """Main entry point."""
    if len(sys.argv) < 6:
        print("Usage: python add-arrow.py <diagram_path> <from_x> <from_y> <to_x> <to_y> [OPTIONS]")
        print("\nOptions:")
        print("  --style {solid|dashed|dotted}    Line style (default: solid)")
        print("  --color HEX                      Color (default: #1e1e1e)")
        print("  --label TEXT                     Text label on arrow")
        print("  --use-edit-suffix                Edit via .excalidraw.edit to avoid editor overwrite issues (enabled by default; use --no-use-edit-suffix to disable)")
        print("\nExamples:")
        print("  python add-arrow.py diagram.excalidraw 300 200 500 300")
        print("  python add-arrow.py diagram.excalidraw 300 200 500 300 --label 'HTTP'")
        sys.exit(1)
    
    diagram_path = Path(sys.argv[1])
    from_x = float(sys.argv[2])
    from_y = float(sys.argv[3])
    to_x = float(sys.argv[4])
    to_y = float(sys.argv[5])
    
    # Parse optional arguments
    style = "solid"
    color = "#1e1e1e"
    label = None
    # Default: use edit suffix to avoid editor overwrite issues
    use_edit_suffix = True
    
    i = 6
    while i < len(sys.argv):
        if sys.argv[i] == '--style':
            if i + 1 < len(sys.argv):
                style = sys.argv[i + 1]
                if style not in ['solid', 'dashed', 'dotted']:
                    print(f"Error: Invalid style '{style}'. Must be: solid, dashed, or dotted")
                    sys.exit(1)
                i += 2
            else:
                print("Error: --style requires an argument")
                sys.exit(1)
        elif sys.argv[i] == '--color':
            if i + 1 < len(sys.argv):
                color = sys.argv[i + 1]
                i += 2
            else:
                print("Error: --color requires an argument")
                sys.exit(1)
        elif sys.argv[i] == '--label':
            if i + 1 < len(sys.argv):
                label = sys.argv[i + 1]
                i += 2
            else:
                print("Error: --label requires a text argument")
                sys.exit(1)
        elif sys.argv[i] == '--use-edit-suffix':
            use_edit_suffix = True
            i += 1
        elif sys.argv[i] == '--no-use-edit-suffix':
            use_edit_suffix = False
            i += 1
        else:
            print(f"Error: Unknown option: {sys.argv[i]}")
            sys.exit(1)
    
    # Validate inputs
    if not diagram_path.exists():
        print(f"Error: Diagram file not found: {diagram_path}")
        sys.exit(1)
    
    try:
        work_path, final_path = prepare_edit_path(diagram_path, use_edit_suffix)
        add_arrow_to_diagram(work_path, from_x, from_y, to_x, to_y, style, color, label)
        finalize_edit_path(work_path, final_path)
    except Exception as e:
        print(f"Error: {e}")
        sys.exit(1)


if __name__ == '__main__':
    main()

```

### scripts/add-icon-to-diagram.py

```python
#!/usr/bin/env python3
"""
Add icons from Excalidraw libraries to diagrams.

This script reads an icon JSON file from an Excalidraw library, transforms its coordinates
to a target position, generates unique IDs, and adds it to an existing Excalidraw diagram.
Works with any Excalidraw library (AWS, GCP, Azure, Kubernetes, etc.).

Usage:
    python add-icon-to-diagram.py <diagram_path> <icon_name> <x> <y> [OPTIONS]

Options:
    --library-path PATH    Path to the icon library directory (default: aws-architecture-icons)
    --label TEXT           Add a text label below the icon
    --use-edit-suffix      Edit via .excalidraw.edit to avoid editor overwrite issues (enabled by default; use --no-use-edit-suffix to disable)

Examples:
    python add-icon-to-diagram.py diagram.excalidraw EC2 500 300
    python add-icon-to-diagram.py diagram.excalidraw EC2 500 300 --label "Web Server"
    python add-icon-to-diagram.py diagram.excalidraw VPC 200 150 --library-path libraries/gcp-icons
    python add-icon-to-diagram.py diagram.excalidraw EC2 500 300 --use-edit-suffix
"""

import json
import sys
import uuid
from pathlib import Path
from typing import Dict, List, Any, Tuple


def generate_unique_id() -> str:
    """Generate a unique ID for Excalidraw elements."""
    return str(uuid.uuid4()).replace('-', '')[:16]


def calculate_bounding_box(elements: List[Dict[str, Any]]) -> Tuple[float, float, float, float]:
    """Calculate the bounding box (min_x, min_y, max_x, max_y) of icon elements."""
    if not elements:
        return (0, 0, 0, 0)
    
    min_x = float('inf')
    min_y = float('inf')
    max_x = float('-inf')
    max_y = float('-inf')
    
    for element in elements:
        if 'x' in element and 'y' in element:
            x = element['x']
            y = element['y']
            width = element.get('width', 0)
            height = element.get('height', 0)
            
            min_x = min(min_x, x)
            min_y = min(min_y, y)
            max_x = max(max_x, x + width)
            max_y = max(max_y, y + height)
    
    return (min_x, min_y, max_x, max_y)


def transform_icon_elements(
    elements: List[Dict[str, Any]], 
    target_x: float, 
    target_y: float
) -> List[Dict[str, Any]]:
    """
    Transform icon elements to target coordinates with unique IDs.
    
    Args:
        elements: Icon elements from JSON file
        target_x: Target X coordinate (top-left position)
        target_y: Target Y coordinate (top-left position)
    
    Returns:
        Transformed elements with new coordinates and IDs
    """
    if not elements:
        return []
    
    # Calculate bounding box
    min_x, min_y, max_x, max_y = calculate_bounding_box(elements)
    
    # Calculate offset
    offset_x = target_x - min_x
    offset_y = target_y - min_y
    
    # Create ID mapping: old_id -> new_id
    id_mapping = {}
    for element in elements:
        if 'id' in element:
            old_id = element['id']
            id_mapping[old_id] = generate_unique_id()
    
    # Create group ID mapping
    group_id_mapping = {}
    for element in elements:
        if 'groupIds' in element:
            for old_group_id in element['groupIds']:
                if old_group_id not in group_id_mapping:
                    group_id_mapping[old_group_id] = generate_unique_id()
    
    # Transform elements
    transformed = []
    for element in elements:
        new_element = element.copy()
        
        # Update coordinates
        if 'x' in new_element:
            new_element['x'] = new_element['x'] + offset_x
        if 'y' in new_element:
            new_element['y'] = new_element['y'] + offset_y
        
        # Update ID
        if 'id' in new_element:
            new_element['id'] = id_mapping[new_element['id']]
        
        # Update group IDs
        if 'groupIds' in new_element:
            new_element['groupIds'] = [
                group_id_mapping[gid] for gid in new_element['groupIds']
            ]
        
        # Update binding references if they exist
        if 'startBinding' in new_element and new_element['startBinding']:
            if 'elementId' in new_element['startBinding']:
                old_id = new_element['startBinding']['elementId']
                if old_id in id_mapping:
                    new_element['startBinding']['elementId'] = id_mapping[old_id]
        
        if 'endBinding' in new_element and new_element['endBinding']:
            if 'elementId' in new_element['endBinding']:
                old_id = new_element['endBinding']['elementId']
                if old_id in id_mapping:
                    new_element['endBinding']['elementId'] = id_mapping[old_id]
        
        # Update containerId if it exists
        if 'containerId' in new_element and new_element['containerId']:
            old_id = new_element['containerId']
            if old_id in id_mapping:
                new_element['containerId'] = id_mapping[old_id]
        
        # Update boundElements if they exist
        if 'boundElements' in new_element and new_element['boundElements']:
            new_bound_elements = []
            for bound_elem in new_element['boundElements']:
                if isinstance(bound_elem, dict) and 'id' in bound_elem:
                    old_id = bound_elem['id']
                    if old_id in id_mapping:
                        bound_elem['id'] = id_mapping[old_id]
                new_bound_elements.append(bound_elem)
            new_element['boundElements'] = new_bound_elements
        
        transformed.append(new_element)
    
    return transformed


def load_icon(icon_name: str, library_path: Path) -> List[Dict[str, Any]]:
    """
    Load icon elements from library.
    
    Args:
        icon_name: Name of the icon (e.g., "EC2", "VPC")
        library_path: Path to the icon library directory
    
    Returns:
        List of icon elements
    """
    icon_file = library_path / "icons" / f"{icon_name}.json"
    
    if not icon_file.exists():
        raise FileNotFoundError(f"Icon file not found: {icon_file}")
    
    with open(icon_file, 'r', encoding='utf-8') as f:
        icon_data = json.load(f)
    
    return icon_data.get('elements', [])


def prepare_edit_path(diagram_path: Path, use_edit_suffix: bool) -> tuple[Path, Path | None]:
    """
    Prepare a safe edit path to avoid editor overwrite issues.

    Returns:
        (work_path, final_path)
        - work_path: file path to read/write during edit
        - final_path: file path to rename back to (or None if not used)
    """
    if not use_edit_suffix:
        return diagram_path, None

    if diagram_path.suffix != ".excalidraw":
        return diagram_path, None

    edit_path = diagram_path.with_suffix(diagram_path.suffix + ".edit")

    if diagram_path.exists():
        if edit_path.exists():
            raise FileExistsError(f"Edit file already exists: {edit_path}")
        diagram_path.rename(edit_path)

    return edit_path, diagram_path


def finalize_edit_path(work_path: Path, final_path: Path | None) -> None:
    """Finalize edit by renaming .edit back to .excalidraw if needed."""
    if final_path is None:
        return

    if final_path.exists():
        final_path.unlink()

    work_path.rename(final_path)


def create_text_label(text: str, x: float, y: float) -> Dict[str, Any]:
    """
    Create a text label element.
    
    Args:
        text: Label text
        x: X coordinate
        y: Y coordinate
    
    Returns:
        Text element dictionary
    """
    return {
        "id": generate_unique_id(),
        "type": "text",
        "x": x,
        "y": y,
        "width": len(text) * 10,  # Approximate width
        "height": 20,
        "angle": 0,
        "strokeColor": "#1e1e1e",
        "backgroundColor": "transparent",
        "fillStyle": "solid",
        "strokeWidth": 2,
        "strokeStyle": "solid",
        "roughness": 1,
        "opacity": 100,
        "groupIds": [],
        "frameId": None,
        "index": "a0",
        "roundness": None,
        "seed": 1000000000 + hash(text) % 1000000000,
        "version": 1,
        "versionNonce": 2000000000 + hash(text) % 1000000000,
        "isDeleted": False,
        "boundElements": [],
        "updated": 1738195200000,
        "link": None,
        "locked": False,
        "text": text,
        "fontSize": 16,
        "fontFamily": 5,  # Excalifont
        "textAlign": "center",
        "verticalAlign": "top",
        "containerId": None,
        "originalText": text,
        "autoResize": True,
        "lineHeight": 1.25
    }


def add_icon_to_diagram(
    diagram_path: Path,
    icon_name: str,
    x: float,
    y: float,
    library_path: Path,
    label: str = None
) -> None:
    """
    Add an icon to an Excalidraw diagram.
    
    Args:
        diagram_path: Path to the Excalidraw diagram file
        icon_name: Name of the icon to add
        x: Target X coordinate
        y: Target Y coordinate
        library_path: Path to the icon library directory
        label: Optional text label to add below the icon
    """
    # Load icon elements
    print(f"Loading icon: {icon_name}")
    icon_elements = load_icon(icon_name, library_path)
    print(f"  Loaded {len(icon_elements)} elements")
    
    # Transform icon elements
    print(f"Transforming to position ({x}, {y})")
    transformed_elements = transform_icon_elements(icon_elements, x, y)
    
    # Calculate icon bounding box for label positioning
    if label and transformed_elements:
        min_x, min_y, max_x, max_y = calculate_bounding_box(transformed_elements)
        icon_width = max_x - min_x
        icon_height = max_y - min_y
        
        # Position label below icon, centered
        label_x = min_x + (icon_width / 2) - (len(label) * 5)
        label_y = max_y + 10
        
        label_element = create_text_label(label, label_x, label_y)
        transformed_elements.append(label_element)
        print(f"  Added label: '{label}'")
    
    # Load diagram
    print(f"Loading diagram: {diagram_path}")
    with open(diagram_path, 'r', encoding='utf-8') as f:
        diagram = json.load(f)
    
    # Add transformed elements
    if 'elements' not in diagram:
        diagram['elements'] = []
    
    original_count = len(diagram['elements'])
    diagram['elements'].extend(transformed_elements)
    print(f"  Added {len(transformed_elements)} elements (total: {original_count} -> {len(diagram['elements'])})")
    
    # Save diagram
    print(f"Saving diagram")
    with open(diagram_path, 'w', encoding='utf-8') as f:
        json.dump(diagram, f, indent=2, ensure_ascii=False)
    
    print(f"✓ Successfully added '{icon_name}' icon to diagram")


def main():
    """Main entry point."""
    if len(sys.argv) < 5:
        print("Usage: python add-icon-to-diagram.py <diagram_path> <icon_name> <x> <y> [OPTIONS]")
        print("\nOptions:")
        print("  --library-path PATH    Path to icon library directory")
        print("  --label TEXT           Add text label below icon")
        print("  --use-edit-suffix      Edit via .excalidraw.edit to avoid editor overwrite issues (enabled by default; use --no-use-edit-suffix to disable)")
        print("\nExamples:")
        print("  python add-icon-to-diagram.py diagram.excalidraw EC2 500 300")
        print("  python add-icon-to-diagram.py diagram.excalidraw EC2 500 300 --label 'Web Server'")
        sys.exit(1)
    
    diagram_path = Path(sys.argv[1])
    icon_name = sys.argv[2]
    x = float(sys.argv[3])
    y = float(sys.argv[4])
    
    # Default library path
    script_dir = Path(__file__).parent
    default_library_path = script_dir.parent / "libraries" / "aws-architecture-icons"
    
    # Parse optional arguments
    library_path = default_library_path
    label = None
    # Default: use edit suffix to avoid editor overwrite issues
    use_edit_suffix = True
    
    i = 5
    while i < len(sys.argv):
        if sys.argv[i] == '--library-path':
            if i + 1 < len(sys.argv):
                library_path = Path(sys.argv[i + 1])
                i += 2
            else:
                print("Error: --library-path requires a path argument")
                sys.exit(1)
        elif sys.argv[i] == '--label':
            if i + 1 < len(sys.argv):
                label = sys.argv[i + 1]
                i += 2
            else:
                print("Error: --label requires a text argument")
                sys.exit(1)
        elif sys.argv[i] == '--use-edit-suffix':
            use_edit_suffix = True
            i += 1
        elif sys.argv[i] == '--no-use-edit-suffix':
            use_edit_suffix = False
            i += 1
        else:
            print(f"Error: Unknown option: {sys.argv[i]}")
            sys.exit(1)
    
    # Validate inputs
    if not diagram_path.exists():
        print(f"Error: Diagram file not found: {diagram_path}")
        sys.exit(1)
    
    if not library_path.exists():
        print(f"Error: Library path not found: {library_path}")
        sys.exit(1)
    
    try:
        work_path, final_path = prepare_edit_path(diagram_path, use_edit_suffix)
        add_icon_to_diagram(work_path, icon_name, x, y, library_path, label)
        finalize_edit_path(work_path, final_path)
    except Exception as e:
        print(f"Error: {e}")
        sys.exit(1)


if __name__ == '__main__':
    main()


```

### scripts/split-excalidraw-library.py

```python
#!/usr/bin/env python3
"""
Excalidraw Library Splitter

This script splits an Excalidraw library file (*.excalidrawlib) into individual
icon JSON files and generates a reference.md file for easy lookup.

The script expects the following structure:
  skills/excalidraw-studio/libraries/{icon-set-name}/
    {icon-set-name}.excalidrawlib  (place this file first)

Usage:
    python split-excalidraw-library.py <path-to-library-directory>

Example:
    python split-excalidraw-library.py skills/excalidraw-studio/libraries/aws-architecture-icons/
"""

import json
import os
import re
import sys
from pathlib import Path


def sanitize_filename(name: str) -> str:
    """
    Sanitize icon name to create a valid filename.

    Args:
        name: Original icon name

    Returns:
        Sanitized filename safe for all platforms
    """
    # Replace spaces with hyphens
    filename = name.replace(' ', '-')

    # Remove or replace special characters
    filename = re.sub(r'[^\w\-.]', '', filename)

    # Remove multiple consecutive hyphens
    filename = re.sub(r'-+', '-', filename)

    # Remove leading/trailing hyphens
    filename = filename.strip('-')

    return filename


def find_library_file(directory: Path) -> Path:
    """
    Find the .excalidrawlib file in the given directory.

    Args:
        directory: Directory to search

    Returns:
        Path to the library file

    Raises:
        SystemExit: If no library file or multiple library files found
    """
    library_files = list(directory.glob('*.excalidrawlib'))

    if len(library_files) == 0:
        print(f"Error: No .excalidrawlib file found in {directory}")
        print(f"Please place a .excalidrawlib file in {directory} first.")
        sys.exit(1)

    if len(library_files) > 1:
        print(f"Error: Multiple .excalidrawlib files found in {directory}")
        print(f"Please keep only one library file in {directory}.")
        sys.exit(1)

    return library_files[0]


def split_library(library_dir: str) -> None:
    """
    Split an Excalidraw library file into individual icon files.

    Args:
        library_dir: Path to the directory containing the .excalidrawlib file
    """
    library_dir = Path(library_dir)

    if not library_dir.exists():
        print(f"Error: Directory not found: {library_dir}")
        sys.exit(1)

    if not library_dir.is_dir():
        print(f"Error: Path is not a directory: {library_dir}")
        sys.exit(1)

    # Find the library file
    library_path = find_library_file(library_dir)
    print(f"Found library: {library_path.name}")

    # Load library file
    print(f"Loading library data...")
    with open(library_path, 'r', encoding='utf-8') as f:
        library_data = json.load(f)

    # Validate library structure
    if 'libraryItems' not in library_data:
        print("Error: Invalid library file format (missing 'libraryItems')")
        sys.exit(1)

    # Create icons directory
    icons_dir = library_dir / 'icons'
    icons_dir.mkdir(exist_ok=True)
    print(f"Output directory: {library_dir}")

    # Process each library item (icon)
    library_items = library_data['libraryItems']
    icon_list = []

    print(f"Processing {len(library_items)} icons...")

    for item in library_items:
        # Get icon name
        icon_name = item.get('name', 'Unnamed')

        # Create sanitized filename
        filename = sanitize_filename(icon_name) + '.json'

        # Save icon data
        icon_path = icons_dir / filename
        with open(icon_path, 'w', encoding='utf-8') as f:
            json.dump(item, f, ensure_ascii=False, indent=2)

        # Add to reference list
        icon_list.append({
            'name': icon_name,
            'filename': filename
        })

        print(f"  ✓ {icon_name} → {filename}")

    # Sort icon list by name
    icon_list.sort(key=lambda x: x['name'])

    # Generate reference.md
    library_name = library_path.stem
    reference_path = library_dir / 'reference.md'
    with open(reference_path, 'w', encoding='utf-8') as f:
        f.write(f"# {library_name} Reference\n\n")
        f.write(f"This directory contains {len(icon_list)} icons extracted from `{library_path.name}`.\n\n")
        f.write("## Available Icons\n\n")
        f.write("| Icon Name | Filename |\n")
        f.write("|-----------|----------|\n")

        for icon in icon_list:
            f.write(f"| {icon['name']} | `icons/{icon['filename']}` |\n")

        f.write("\n## Usage\n\n")
        f.write("Each icon JSON file contains the complete `elements` array needed to render that icon in Excalidraw.\n")
        f.write("You can copy the elements from these files into your Excalidraw diagrams.\n")

    print(f"\n✅ Successfully split library into {len(icon_list)} icons")
    print(f"📄 Reference file created: {reference_path}")
    print(f"📁 Icons directory: {icons_dir}")


def main():
    """Main entry point."""
    if hasattr(sys.stdout, "reconfigure"):
        # Ensure consistent UTF-8 output on Windows consoles.
        sys.stdout.reconfigure(encoding="utf-8")
    if len(sys.argv) != 2:
        print("Usage: python split-excalidraw-library.py <path-to-library-directory>")
        print("\nExample:")
        print("  python split-excalidraw-library.py skills/excalidraw-studio/libraries/aws-architecture-icons/")
        print("\nNote: The directory should contain a .excalidrawlib file.")
        sys.exit(1)

    library_dir = sys.argv[1]
    split_library(library_dir)


if __name__ == '__main__':
    main()

```