docs: update AGENTS.md and README.md for rules system, remove beads
- Add rules/ directory documentation to both files - Update skill count from 25 to 15 modules - Remove beads references (issue tracking removed) - Update skills list with current active skills - Document flake.nix as proper Nix flake (not flake=false) - Add rules system integration section - Clean up sisyphus planning artifacts - Remove deprecated skills (memory, msteams, outlook)
This commit is contained in:
@@ -1,288 +0,0 @@
|
||||
# Arrow Routing Reference
|
||||
|
||||
Complete guide for creating elbow arrows with proper connections.
|
||||
|
||||
---
|
||||
|
||||
## Critical: Elbow Arrow Properties
|
||||
|
||||
Three required properties for 90-degree corners:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "arrow",
|
||||
"roughness": 0, // Clean lines
|
||||
"roundness": null, // Sharp corners (not curved)
|
||||
"elbowed": true // Enables elbow mode
|
||||
}
|
||||
```
|
||||
|
||||
**Without these, arrows will be curved, not 90-degree elbows.**
|
||||
|
||||
---
|
||||
|
||||
## Edge Calculation Formulas
|
||||
|
||||
| Shape Type | Edge | Formula |
|
||||
|------------|------|---------|
|
||||
| Rectangle | Top | `(x + width/2, y)` |
|
||||
| Rectangle | Bottom | `(x + width/2, y + height)` |
|
||||
| Rectangle | Left | `(x, y + height/2)` |
|
||||
| Rectangle | Right | `(x + width, y + height/2)` |
|
||||
| Ellipse | Top | `(x + width/2, y)` |
|
||||
| Ellipse | Bottom | `(x + width/2, y + height)` |
|
||||
|
||||
---
|
||||
|
||||
## Universal Arrow Routing Algorithm
|
||||
|
||||
```
|
||||
FUNCTION createArrow(source, target, sourceEdge, targetEdge):
|
||||
// Step 1: Get source edge point
|
||||
sourcePoint = getEdgePoint(source, sourceEdge)
|
||||
|
||||
// Step 2: Get target edge point
|
||||
targetPoint = getEdgePoint(target, targetEdge)
|
||||
|
||||
// Step 3: Calculate offsets
|
||||
dx = targetPoint.x - sourcePoint.x
|
||||
dy = targetPoint.y - sourcePoint.y
|
||||
|
||||
// Step 4: Determine routing pattern
|
||||
IF sourceEdge == "bottom" AND targetEdge == "top":
|
||||
IF abs(dx) < 10: // Nearly aligned
|
||||
points = [[0, 0], [0, dy]]
|
||||
ELSE: // Need L-shape
|
||||
points = [[0, 0], [dx, 0], [dx, dy]]
|
||||
|
||||
ELSE IF sourceEdge == "right" AND targetEdge == "left":
|
||||
IF abs(dy) < 10:
|
||||
points = [[0, 0], [dx, 0]]
|
||||
ELSE:
|
||||
points = [[0, 0], [0, dy], [dx, dy]]
|
||||
|
||||
ELSE IF sourceEdge == targetEdge: // U-turn
|
||||
clearance = 50
|
||||
IF sourceEdge == "right":
|
||||
points = [[0, 0], [clearance, 0], [clearance, dy], [dx, dy]]
|
||||
ELSE IF sourceEdge == "bottom":
|
||||
points = [[0, 0], [0, clearance], [dx, clearance], [dx, dy]]
|
||||
|
||||
// Step 5: Calculate bounding box
|
||||
width = max(abs(p[0]) for p in points)
|
||||
height = max(abs(p[1]) for p in points)
|
||||
|
||||
RETURN {x: sourcePoint.x, y: sourcePoint.y, points, width, height}
|
||||
|
||||
FUNCTION getEdgePoint(shape, edge):
|
||||
SWITCH edge:
|
||||
"top": RETURN (shape.x + shape.width/2, shape.y)
|
||||
"bottom": RETURN (shape.x + shape.width/2, shape.y + shape.height)
|
||||
"left": RETURN (shape.x, shape.y + shape.height/2)
|
||||
"right": RETURN (shape.x + shape.width, shape.y + shape.height/2)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Arrow Patterns Reference
|
||||
|
||||
| Pattern | Points | Use Case |
|
||||
|---------|--------|----------|
|
||||
| Down | `[[0,0], [0,h]]` | Vertical connection |
|
||||
| Right | `[[0,0], [w,0]]` | Horizontal connection |
|
||||
| L-left-down | `[[0,0], [-w,0], [-w,h]]` | Go left, then down |
|
||||
| L-right-down | `[[0,0], [w,0], [w,h]]` | Go right, then down |
|
||||
| L-down-left | `[[0,0], [0,h], [-w,h]]` | Go down, then left |
|
||||
| L-down-right | `[[0,0], [0,h], [w,h]]` | Go down, then right |
|
||||
| S-shape | `[[0,0], [0,h1], [w,h1], [w,h2]]` | Navigate around obstacles |
|
||||
| U-turn | `[[0,0], [w,0], [w,-h], [0,-h]]` | Callback/return arrows |
|
||||
|
||||
---
|
||||
|
||||
## Worked Examples
|
||||
|
||||
### Vertical Connection (Bottom to Top)
|
||||
|
||||
```
|
||||
Source: x=500, y=200, width=180, height=90
|
||||
Target: x=500, y=400, width=180, height=90
|
||||
|
||||
source_bottom = (500 + 180/2, 200 + 90) = (590, 290)
|
||||
target_top = (500 + 180/2, 400) = (590, 400)
|
||||
|
||||
Arrow x = 590, y = 290
|
||||
Distance = 400 - 290 = 110
|
||||
Points = [[0, 0], [0, 110]]
|
||||
```
|
||||
|
||||
### Fan-out (One to Many)
|
||||
|
||||
```
|
||||
Orchestrator: x=570, y=400, width=140, height=80
|
||||
Target: x=120, y=550, width=160, height=80
|
||||
|
||||
orchestrator_bottom = (570 + 140/2, 400 + 80) = (640, 480)
|
||||
target_top = (120 + 160/2, 550) = (200, 550)
|
||||
|
||||
Arrow x = 640, y = 480
|
||||
Horizontal offset = 200 - 640 = -440
|
||||
Vertical offset = 550 - 480 = 70
|
||||
|
||||
Points = [[0, 0], [-440, 0], [-440, 70]] // Left first, then down
|
||||
```
|
||||
|
||||
### U-turn (Callback)
|
||||
|
||||
```
|
||||
Source: x=570, y=400, width=140, height=80
|
||||
Target: x=550, y=270, width=180, height=90
|
||||
Connection: Right of source -> Right of target
|
||||
|
||||
source_right = (570 + 140, 400 + 80/2) = (710, 440)
|
||||
target_right = (550 + 180, 270 + 90/2) = (730, 315)
|
||||
|
||||
Arrow x = 710, y = 440
|
||||
Vertical distance = 315 - 440 = -125
|
||||
Final x offset = 730 - 710 = 20
|
||||
|
||||
Points = [[0, 0], [50, 0], [50, -125], [20, -125]]
|
||||
// Right 50px (clearance), up 125px, left 30px
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Staggering Multiple Arrows
|
||||
|
||||
When N arrows leave from same edge, spread evenly:
|
||||
|
||||
```
|
||||
FUNCTION getStaggeredPositions(shape, edge, numArrows):
|
||||
positions = []
|
||||
FOR i FROM 0 TO numArrows-1:
|
||||
percentage = 0.2 + (0.6 * i / (numArrows - 1))
|
||||
|
||||
IF edge == "bottom" OR edge == "top":
|
||||
x = shape.x + shape.width * percentage
|
||||
y = (edge == "bottom") ? shape.y + shape.height : shape.y
|
||||
ELSE:
|
||||
x = (edge == "right") ? shape.x + shape.width : shape.x
|
||||
y = shape.y + shape.height * percentage
|
||||
|
||||
positions.append({x, y})
|
||||
RETURN positions
|
||||
|
||||
// Examples:
|
||||
// 2 arrows: 20%, 80%
|
||||
// 3 arrows: 20%, 50%, 80%
|
||||
// 5 arrows: 20%, 35%, 50%, 65%, 80%
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Arrow Bindings
|
||||
|
||||
For better visual attachment, use `startBinding` and `endBinding`:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "arrow-workflow-convert",
|
||||
"type": "arrow",
|
||||
"x": 525,
|
||||
"y": 420,
|
||||
"width": 325,
|
||||
"height": 125,
|
||||
"points": [[0, 0], [-325, 0], [-325, 125]],
|
||||
"roughness": 0,
|
||||
"roundness": null,
|
||||
"elbowed": true,
|
||||
"startBinding": {
|
||||
"elementId": "cloud-workflows",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"fixedPoint": [0.5, 1]
|
||||
},
|
||||
"endBinding": {
|
||||
"elementId": "convert-pdf-service",
|
||||
"focus": 0,
|
||||
"gap": 1,
|
||||
"fixedPoint": [0.5, 0]
|
||||
},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow"
|
||||
}
|
||||
```
|
||||
|
||||
### fixedPoint Values
|
||||
|
||||
- Top center: `[0.5, 0]`
|
||||
- Bottom center: `[0.5, 1]`
|
||||
- Left center: `[0, 0.5]`
|
||||
- Right center: `[1, 0.5]`
|
||||
|
||||
### Update Shape boundElements
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "cloud-workflows",
|
||||
"boundElements": [
|
||||
{ "type": "text", "id": "cloud-workflows-text" },
|
||||
{ "type": "arrow", "id": "arrow-workflow-convert" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bidirectional Arrows
|
||||
|
||||
For two-way data flows:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "arrow",
|
||||
"startArrowhead": "arrow",
|
||||
"endArrowhead": "arrow"
|
||||
}
|
||||
```
|
||||
|
||||
Arrowhead options: `null`, `"arrow"`, `"bar"`, `"dot"`, `"triangle"`
|
||||
|
||||
---
|
||||
|
||||
## Arrow Labels
|
||||
|
||||
Position standalone text near arrow midpoint:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "arrow-api-db-label",
|
||||
"type": "text",
|
||||
"x": 305, // Arrow x + offset
|
||||
"y": 245, // Arrow midpoint
|
||||
"text": "SQL",
|
||||
"fontSize": 12,
|
||||
"containerId": null,
|
||||
"backgroundColor": "#ffffff"
|
||||
}
|
||||
```
|
||||
|
||||
**Positioning formula:**
|
||||
- Vertical: `label.y = arrow.y + (total_height / 2)`
|
||||
- Horizontal: `label.x = arrow.x + (total_width / 2)`
|
||||
- L-shaped: Position at corner or longest segment midpoint
|
||||
|
||||
---
|
||||
|
||||
## Width/Height Calculation
|
||||
|
||||
Arrow `width` and `height` = bounding box of path:
|
||||
|
||||
```
|
||||
points = [[0, 0], [-440, 0], [-440, 70]]
|
||||
width = abs(-440) = 440
|
||||
height = abs(70) = 70
|
||||
|
||||
points = [[0, 0], [50, 0], [50, -125], [20, -125]]
|
||||
width = max(abs(50), abs(20)) = 50
|
||||
height = abs(-125) = 125
|
||||
```
|
||||
67
skills/excalidraw/references/color-palette.md
Normal file
67
skills/excalidraw/references/color-palette.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Color Palette & Brand Style
|
||||
|
||||
**This is the single source of truth for all colors and brand-specific styles.** To customize diagrams for your own brand, edit this file — everything else in the skill is universal.
|
||||
|
||||
---
|
||||
|
||||
## Shape Colors (Semantic)
|
||||
|
||||
Colors encode meaning, not decoration. Each semantic purpose has a fill/stroke pair.
|
||||
|
||||
| Semantic Purpose | Fill | Stroke |
|
||||
|------------------|------|--------|
|
||||
| Primary/Neutral | `#3b82f6` | `#1e3a5f` |
|
||||
| Secondary | `#60a5fa` | `#1e3a5f` |
|
||||
| Tertiary | `#93c5fd` | `#1e3a5f` |
|
||||
| Start/Trigger | `#fed7aa` | `#c2410c` |
|
||||
| End/Success | `#a7f3d0` | `#047857` |
|
||||
| Warning/Reset | `#fee2e2` | `#dc2626` |
|
||||
| Decision | `#fef3c7` | `#b45309` |
|
||||
| AI/LLM | `#ddd6fe` | `#6d28d9` |
|
||||
| Inactive/Disabled | `#dbeafe` | `#1e40af` (use dashed stroke) |
|
||||
| Error | `#fecaca` | `#b91c1c` |
|
||||
|
||||
**Rule**: Always pair a darker stroke with a lighter fill for contrast.
|
||||
|
||||
---
|
||||
|
||||
## Text Colors (Hierarchy)
|
||||
|
||||
Use color on free-floating text to create visual hierarchy without containers.
|
||||
|
||||
| Level | Color | Use For |
|
||||
|-------|-------|---------|
|
||||
| Title | `#1e40af` | Section headings, major labels |
|
||||
| Subtitle | `#3b82f6` | Subheadings, secondary labels |
|
||||
| Body/Detail | `#64748b` | Descriptions, annotations, metadata |
|
||||
| On light fills | `#374151` | Text inside light-colored shapes |
|
||||
| On dark fills | `#ffffff` | Text inside dark-colored shapes |
|
||||
|
||||
---
|
||||
|
||||
## Evidence Artifact Colors
|
||||
|
||||
Used for code snippets, data examples, and other concrete evidence inside technical diagrams.
|
||||
|
||||
| Artifact | Background | Text Color |
|
||||
|----------|-----------|------------|
|
||||
| Code snippet | `#1e293b` | Syntax-colored (language-appropriate) |
|
||||
| JSON/data example | `#1e293b` | `#22c55e` (green) |
|
||||
|
||||
---
|
||||
|
||||
## Default Stroke & Line Colors
|
||||
|
||||
| Element | Color |
|
||||
|---------|-------|
|
||||
| Arrows | Use the stroke color of the source element's semantic purpose |
|
||||
| Structural lines (dividers, trees, timelines) | Primary stroke (`#1e3a5f`) or Slate (`#64748b`) |
|
||||
| Marker dots (fill + stroke) | Primary fill (`#3b82f6`) |
|
||||
|
||||
---
|
||||
|
||||
## Background
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Canvas background | `#ffffff` |
|
||||
@@ -1,91 +0,0 @@
|
||||
# Color Palettes Reference
|
||||
|
||||
Color schemes for different platforms and component types.
|
||||
|
||||
---
|
||||
|
||||
## Default Palette (Platform-Agnostic)
|
||||
|
||||
| Component Type | Background | Stroke | Example |
|
||||
|----------------|------------|--------|---------|
|
||||
| Frontend/UI | `#a5d8ff` | `#1971c2` | Next.js, React apps |
|
||||
| Backend/API | `#d0bfff` | `#7048e8` | API servers, processors |
|
||||
| Database | `#b2f2bb` | `#2f9e44` | PostgreSQL, MySQL, MongoDB |
|
||||
| Storage | `#ffec99` | `#f08c00` | Object storage, file systems |
|
||||
| AI/ML Services | `#e599f7` | `#9c36b5` | ML models, AI APIs |
|
||||
| External APIs | `#ffc9c9` | `#e03131` | Third-party services |
|
||||
| Orchestration | `#ffa8a8` | `#c92a2a` | Workflows, schedulers |
|
||||
| Validation | `#ffd8a8` | `#e8590c` | Validators, checkers |
|
||||
| Network/Security | `#dee2e6` | `#495057` | VPC, IAM, firewalls |
|
||||
| Classification | `#99e9f2` | `#0c8599` | Routers, classifiers |
|
||||
| Users/Actors | `#e7f5ff` | `#1971c2` | User ellipses |
|
||||
| Message Queue | `#fff3bf` | `#fab005` | Kafka, RabbitMQ, SQS |
|
||||
| Cache | `#ffe8cc` | `#fd7e14` | Redis, Memcached |
|
||||
| Monitoring | `#d3f9d8` | `#40c057` | Prometheus, Grafana |
|
||||
|
||||
---
|
||||
|
||||
## AWS Palette
|
||||
|
||||
| Service Category | Background | Stroke |
|
||||
|-----------------|------------|--------|
|
||||
| Compute (EC2, Lambda, ECS) | `#ff9900` | `#cc7a00` |
|
||||
| Storage (S3, EBS) | `#3f8624` | `#2d6119` |
|
||||
| Database (RDS, DynamoDB) | `#3b48cc` | `#2d3899` |
|
||||
| Networking (VPC, Route53) | `#8c4fff` | `#6b3dcc` |
|
||||
| Security (IAM, KMS) | `#dd344c` | `#b12a3d` |
|
||||
| Analytics (Kinesis, Athena) | `#8c4fff` | `#6b3dcc` |
|
||||
| ML (SageMaker, Bedrock) | `#01a88d` | `#017d69` |
|
||||
|
||||
---
|
||||
|
||||
## Azure Palette
|
||||
|
||||
| Service Category | Background | Stroke |
|
||||
|-----------------|------------|--------|
|
||||
| Compute | `#0078d4` | `#005a9e` |
|
||||
| Storage | `#50e6ff` | `#3cb5cc` |
|
||||
| Database | `#0078d4` | `#005a9e` |
|
||||
| Networking | `#773adc` | `#5a2ca8` |
|
||||
| Security | `#ff8c00` | `#cc7000` |
|
||||
| AI/ML | `#50e6ff` | `#3cb5cc` |
|
||||
|
||||
---
|
||||
|
||||
## GCP Palette
|
||||
|
||||
| Service Category | Background | Stroke |
|
||||
|-----------------|------------|--------|
|
||||
| Compute (GCE, Cloud Run) | `#4285f4` | `#3367d6` |
|
||||
| Storage (GCS) | `#34a853` | `#2d8e47` |
|
||||
| Database (Cloud SQL, Firestore) | `#ea4335` | `#c53929` |
|
||||
| Networking | `#fbbc04` | `#d99e04` |
|
||||
| AI/ML (Vertex AI) | `#9334e6` | `#7627b8` |
|
||||
|
||||
---
|
||||
|
||||
## Kubernetes Palette
|
||||
|
||||
| Component | Background | Stroke |
|
||||
|-----------|------------|--------|
|
||||
| Pod | `#326ce5` | `#2756b8` |
|
||||
| Service | `#326ce5` | `#2756b8` |
|
||||
| Deployment | `#326ce5` | `#2756b8` |
|
||||
| ConfigMap/Secret | `#7f8c8d` | `#626d6e` |
|
||||
| Ingress | `#00d4aa` | `#00a888` |
|
||||
| Node | `#303030` | `#1a1a1a` |
|
||||
| Namespace | `#f0f0f0` | `#c0c0c0` (dashed) |
|
||||
|
||||
---
|
||||
|
||||
## Diagram Type Suggestions
|
||||
|
||||
| Diagram Type | Recommended Layout | Key Elements |
|
||||
|--------------|-------------------|--------------|
|
||||
| Microservices | Vertical flow | Services, databases, queues, API gateway |
|
||||
| Data Pipeline | Horizontal flow | Sources, transformers, sinks, storage |
|
||||
| Event-Driven | Hub-and-spoke | Event bus center, producers/consumers |
|
||||
| Kubernetes | Layered groups | Namespace boxes, pods inside deployments |
|
||||
| CI/CD | Horizontal flow | Source -> Build -> Test -> Deploy -> Monitor |
|
||||
| Network | Hierarchical | Internet -> LB -> VPC -> Subnets -> Instances |
|
||||
| User Flow | Swimlanes | User actions, system responses, external calls |
|
||||
182
skills/excalidraw/references/element-templates.md
Normal file
182
skills/excalidraw/references/element-templates.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# Element Templates
|
||||
|
||||
Copy-paste JSON templates for each Excalidraw element type. The `strokeColor` and `backgroundColor` values are placeholders — always pull actual colors from `color-palette.md` based on the element's semantic purpose.
|
||||
|
||||
## Free-Floating Text (no container)
|
||||
```json
|
||||
{
|
||||
"type": "text",
|
||||
"id": "label1",
|
||||
"x": 100, "y": 100,
|
||||
"width": 200, "height": 25,
|
||||
"text": "Section Title",
|
||||
"originalText": "Section Title",
|
||||
"fontSize": 20,
|
||||
"fontFamily": 3,
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top",
|
||||
"strokeColor": "<title color from palette>",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"seed": 11111,
|
||||
"version": 1,
|
||||
"versionNonce": 22222,
|
||||
"isDeleted": false,
|
||||
"groupIds": [],
|
||||
"boundElements": null,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"containerId": null,
|
||||
"lineHeight": 1.25
|
||||
}
|
||||
```
|
||||
|
||||
## Line (structural, not arrow)
|
||||
```json
|
||||
{
|
||||
"type": "line",
|
||||
"id": "line1",
|
||||
"x": 100, "y": 100,
|
||||
"width": 0, "height": 200,
|
||||
"strokeColor": "<structural line color from palette>",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"seed": 44444,
|
||||
"version": 1,
|
||||
"versionNonce": 55555,
|
||||
"isDeleted": false,
|
||||
"groupIds": [],
|
||||
"boundElements": null,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [[0, 0], [0, 200]]
|
||||
}
|
||||
```
|
||||
|
||||
## Small Marker Dot
|
||||
```json
|
||||
{
|
||||
"type": "ellipse",
|
||||
"id": "dot1",
|
||||
"x": 94, "y": 94,
|
||||
"width": 12, "height": 12,
|
||||
"strokeColor": "<marker dot color from palette>",
|
||||
"backgroundColor": "<marker dot color from palette>",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"seed": 66666,
|
||||
"version": 1,
|
||||
"versionNonce": 77777,
|
||||
"isDeleted": false,
|
||||
"groupIds": [],
|
||||
"boundElements": null,
|
||||
"link": null,
|
||||
"locked": false
|
||||
}
|
||||
```
|
||||
|
||||
## Rectangle
|
||||
```json
|
||||
{
|
||||
"type": "rectangle",
|
||||
"id": "elem1",
|
||||
"x": 100, "y": 100, "width": 180, "height": 90,
|
||||
"strokeColor": "<stroke from palette based on semantic purpose>",
|
||||
"backgroundColor": "<fill from palette based on semantic purpose>",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"seed": 12345,
|
||||
"version": 1,
|
||||
"versionNonce": 67890,
|
||||
"isDeleted": false,
|
||||
"groupIds": [],
|
||||
"boundElements": [{"id": "text1", "type": "text"}],
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"roundness": {"type": 3}
|
||||
}
|
||||
```
|
||||
|
||||
## Text (centered in shape)
|
||||
```json
|
||||
{
|
||||
"type": "text",
|
||||
"id": "text1",
|
||||
"x": 130, "y": 132,
|
||||
"width": 120, "height": 25,
|
||||
"text": "Process",
|
||||
"originalText": "Process",
|
||||
"fontSize": 16,
|
||||
"fontFamily": 3,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"strokeColor": "<text color — match parent shape's stroke or use 'on light/dark fills' from palette>",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"seed": 11111,
|
||||
"version": 1,
|
||||
"versionNonce": 22222,
|
||||
"isDeleted": false,
|
||||
"groupIds": [],
|
||||
"boundElements": null,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"containerId": "elem1",
|
||||
"lineHeight": 1.25
|
||||
}
|
||||
```
|
||||
|
||||
## Arrow
|
||||
```json
|
||||
{
|
||||
"type": "arrow",
|
||||
"id": "arrow1",
|
||||
"x": 282, "y": 145, "width": 118, "height": 0,
|
||||
"strokeColor": "<arrow color — typically matches source element's stroke from palette>",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"angle": 0,
|
||||
"seed": 33333,
|
||||
"version": 1,
|
||||
"versionNonce": 44444,
|
||||
"isDeleted": false,
|
||||
"groupIds": [],
|
||||
"boundElements": null,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [[0, 0], [118, 0]],
|
||||
"startBinding": {"elementId": "elem1", "focus": 0, "gap": 2},
|
||||
"endBinding": {"elementId": "elem2", "focus": 0, "gap": 2},
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow"
|
||||
}
|
||||
```
|
||||
|
||||
For curves: use 3+ points in `points` array.
|
||||
@@ -1,381 +0,0 @@
|
||||
# Complete Examples Reference
|
||||
|
||||
Full JSON examples showing proper element structure.
|
||||
|
||||
---
|
||||
|
||||
## 3-Tier Architecture Example
|
||||
|
||||
This is a REFERENCE showing JSON structure. Replace IDs, labels, positions, and colors based on discovered components.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "claude-code-excalidraw-skill",
|
||||
"elements": [
|
||||
{
|
||||
"id": "user",
|
||||
"type": "ellipse",
|
||||
"x": 150,
|
||||
"y": 50,
|
||||
"width": 100,
|
||||
"height": 60,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1971c2",
|
||||
"backgroundColor": "#e7f5ff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"roundness": { "type": 2 },
|
||||
"seed": 1,
|
||||
"version": 1,
|
||||
"versionNonce": 1,
|
||||
"isDeleted": false,
|
||||
"boundElements": [{ "type": "text", "id": "user-text" }],
|
||||
"updated": 1,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "user-text",
|
||||
"type": "text",
|
||||
"x": 175,
|
||||
"y": 67,
|
||||
"width": 50,
|
||||
"height": 25,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"roundness": null,
|
||||
"seed": 2,
|
||||
"version": 1,
|
||||
"versionNonce": 2,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "User",
|
||||
"fontSize": 16,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"baseline": 14,
|
||||
"containerId": "user",
|
||||
"originalText": "User",
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "frontend",
|
||||
"type": "rectangle",
|
||||
"x": 100,
|
||||
"y": 180,
|
||||
"width": 200,
|
||||
"height": 80,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1971c2",
|
||||
"backgroundColor": "#a5d8ff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"roundness": { "type": 3 },
|
||||
"seed": 3,
|
||||
"version": 1,
|
||||
"versionNonce": 3,
|
||||
"isDeleted": false,
|
||||
"boundElements": [{ "type": "text", "id": "frontend-text" }],
|
||||
"updated": 1,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "frontend-text",
|
||||
"type": "text",
|
||||
"x": 105,
|
||||
"y": 195,
|
||||
"width": 190,
|
||||
"height": 50,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"roundness": null,
|
||||
"seed": 4,
|
||||
"version": 1,
|
||||
"versionNonce": 4,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Frontend\nNext.js",
|
||||
"fontSize": 16,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"baseline": 14,
|
||||
"containerId": "frontend",
|
||||
"originalText": "Frontend\nNext.js",
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "database",
|
||||
"type": "rectangle",
|
||||
"x": 100,
|
||||
"y": 330,
|
||||
"width": 200,
|
||||
"height": 80,
|
||||
"angle": 0,
|
||||
"strokeColor": "#2f9e44",
|
||||
"backgroundColor": "#b2f2bb",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"roundness": { "type": 3 },
|
||||
"seed": 5,
|
||||
"version": 1,
|
||||
"versionNonce": 5,
|
||||
"isDeleted": false,
|
||||
"boundElements": [{ "type": "text", "id": "database-text" }],
|
||||
"updated": 1,
|
||||
"link": null,
|
||||
"locked": false
|
||||
},
|
||||
{
|
||||
"id": "database-text",
|
||||
"type": "text",
|
||||
"x": 105,
|
||||
"y": 345,
|
||||
"width": 190,
|
||||
"height": 50,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"roundness": null,
|
||||
"seed": 6,
|
||||
"version": 1,
|
||||
"versionNonce": 6,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"text": "Database\nPostgreSQL",
|
||||
"fontSize": 16,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"baseline": 14,
|
||||
"containerId": "database",
|
||||
"originalText": "Database\nPostgreSQL",
|
||||
"lineHeight": 1.25
|
||||
},
|
||||
{
|
||||
"id": "arrow-user-frontend",
|
||||
"type": "arrow",
|
||||
"x": 200,
|
||||
"y": 115,
|
||||
"width": 0,
|
||||
"height": 60,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1971c2",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"roundness": null,
|
||||
"seed": 7,
|
||||
"version": 1,
|
||||
"versionNonce": 7,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [[0, 0], [0, 60]],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": null,
|
||||
"endBinding": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
},
|
||||
{
|
||||
"id": "arrow-frontend-database",
|
||||
"type": "arrow",
|
||||
"x": 200,
|
||||
"y": 265,
|
||||
"width": 0,
|
||||
"height": 60,
|
||||
"angle": 0,
|
||||
"strokeColor": "#2f9e44",
|
||||
"backgroundColor": "transparent",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 0,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"roundness": null,
|
||||
"seed": 8,
|
||||
"version": 1,
|
||||
"versionNonce": 8,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"points": [[0, 0], [0, 60]],
|
||||
"lastCommittedPoint": null,
|
||||
"startBinding": null,
|
||||
"endBinding": null,
|
||||
"startArrowhead": null,
|
||||
"endArrowhead": "arrow",
|
||||
"elbowed": true
|
||||
}
|
||||
],
|
||||
"appState": {
|
||||
"gridSize": 20,
|
||||
"viewBackgroundColor": "#ffffff"
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layout Patterns
|
||||
|
||||
### Vertical Flow (Most Common)
|
||||
|
||||
```
|
||||
Grid positioning:
|
||||
- Column width: 200-250px
|
||||
- Row height: 130-150px
|
||||
- Element size: 160-200px x 80-90px
|
||||
- Spacing: 40-50px between elements
|
||||
|
||||
Row positions (y):
|
||||
Row 0: 20 (title)
|
||||
Row 1: 100 (users/entry points)
|
||||
Row 2: 230 (frontend/gateway)
|
||||
Row 3: 380 (orchestration)
|
||||
Row 4: 530 (services)
|
||||
Row 5: 680 (data layer)
|
||||
Row 6: 830 (external services)
|
||||
|
||||
Column positions (x):
|
||||
Col 0: 100
|
||||
Col 1: 300
|
||||
Col 2: 500
|
||||
Col 3: 700
|
||||
Col 4: 900
|
||||
```
|
||||
|
||||
### Horizontal Flow (Pipelines)
|
||||
|
||||
```
|
||||
Stage positions (x):
|
||||
Stage 0: 100 (input/source)
|
||||
Stage 1: 350 (transform 1)
|
||||
Stage 2: 600 (transform 2)
|
||||
Stage 3: 850 (transform 3)
|
||||
Stage 4: 1100 (output/sink)
|
||||
|
||||
All stages at same y: 200
|
||||
Arrows: "right" -> "left" connections
|
||||
```
|
||||
|
||||
### Hub-and-Spoke
|
||||
|
||||
```
|
||||
Center hub: x=500, y=350
|
||||
8 positions at 45° increments:
|
||||
N: (500, 150)
|
||||
NE: (640, 210)
|
||||
E: (700, 350)
|
||||
SE: (640, 490)
|
||||
S: (500, 550)
|
||||
SW: (360, 490)
|
||||
W: (300, 350)
|
||||
NW: (360, 210)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complex Architecture Layout
|
||||
|
||||
```
|
||||
Row 0: Title/Header (y: 20)
|
||||
Row 1: Users/Clients (y: 80)
|
||||
Row 2: Frontend/Gateway (y: 200)
|
||||
Row 3: Orchestration (y: 350)
|
||||
Row 4: Processing Services (y: 550)
|
||||
Row 5: Data Layer (y: 680)
|
||||
Row 6: External Services (y: 830)
|
||||
|
||||
Columns (x):
|
||||
Col 0: 120
|
||||
Col 1: 320
|
||||
Col 2: 520
|
||||
Col 3: 720
|
||||
Col 4: 920
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Diagram Complexity Guidelines
|
||||
|
||||
| Complexity | Max Elements | Max Arrows | Approach |
|
||||
|------------|-------------|------------|----------|
|
||||
| Simple | 5-10 | 5-10 | Single file, no groups |
|
||||
| Medium | 10-25 | 15-30 | Use grouping rectangles |
|
||||
| Complex | 25-50 | 30-60 | Split into multiple diagrams |
|
||||
| Very Complex | 50+ | 60+ | Multiple focused diagrams |
|
||||
|
||||
**When to split:**
|
||||
- More than 50 elements
|
||||
- Create: `architecture-overview.excalidraw`, `architecture-data-layer.excalidraw`
|
||||
|
||||
**When to use groups:**
|
||||
- 3+ related services
|
||||
- Same deployment unit
|
||||
- Logical boundaries (VPC, Security Zone)
|
||||
@@ -1,210 +0,0 @@
|
||||
# Excalidraw JSON Format Reference
|
||||
|
||||
Complete reference for Excalidraw JSON structure and element types.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "claude-code-excalidraw-skill",
|
||||
"elements": [],
|
||||
"appState": {
|
||||
"gridSize": 20,
|
||||
"viewBackgroundColor": "#ffffff"
|
||||
},
|
||||
"files": {}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Element Types
|
||||
|
||||
| Type | Use For | Arrow Reliability |
|
||||
|------|---------|-------------------|
|
||||
| `rectangle` | Services, components, databases, containers, orchestrators, decision points | Excellent |
|
||||
| `ellipse` | Users, external systems, start/end points | Good |
|
||||
| `text` | Labels inside shapes, titles, annotations | N/A |
|
||||
| `arrow` | Data flow, connections, dependencies | N/A |
|
||||
| `line` | Grouping boundaries, separators | N/A |
|
||||
|
||||
### BANNED: Diamond Shapes
|
||||
|
||||
**NEVER use `type: "diamond"` in generated diagrams.**
|
||||
|
||||
Diamond arrow connections are fundamentally broken in raw Excalidraw JSON:
|
||||
- Excalidraw applies `roundness` to diamond vertices during rendering
|
||||
- Visual edges appear offset from mathematical edge points
|
||||
- No offset formula reliably compensates
|
||||
- Arrows appear disconnected/floating
|
||||
|
||||
**Use styled rectangles instead** for visual distinction:
|
||||
|
||||
| Semantic Meaning | Rectangle Style |
|
||||
|------------------|-----------------|
|
||||
| Orchestrator/Hub | Coral (`#ffa8a8`/`#c92a2a`) + strokeWidth: 3 |
|
||||
| Decision Point | Orange (`#ffd8a8`/`#e8590c`) + dashed stroke |
|
||||
| Central Router | Larger size + bold color |
|
||||
|
||||
---
|
||||
|
||||
## Required Element Properties
|
||||
|
||||
Every element MUST have these properties:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "unique-id-string",
|
||||
"type": "rectangle",
|
||||
"x": 100,
|
||||
"y": 100,
|
||||
"width": 200,
|
||||
"height": 80,
|
||||
"angle": 0,
|
||||
"strokeColor": "#1971c2",
|
||||
"backgroundColor": "#a5d8ff",
|
||||
"fillStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"strokeStyle": "solid",
|
||||
"roughness": 1,
|
||||
"opacity": 100,
|
||||
"groupIds": [],
|
||||
"frameId": null,
|
||||
"roundness": { "type": 3 },
|
||||
"seed": 1,
|
||||
"version": 1,
|
||||
"versionNonce": 1,
|
||||
"isDeleted": false,
|
||||
"boundElements": null,
|
||||
"updated": 1,
|
||||
"link": null,
|
||||
"locked": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Text Inside Shapes (Labels)
|
||||
|
||||
**Every labeled shape requires TWO elements:**
|
||||
|
||||
### Shape with boundElements
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "{component-id}",
|
||||
"type": "rectangle",
|
||||
"x": 500,
|
||||
"y": 200,
|
||||
"width": 200,
|
||||
"height": 90,
|
||||
"strokeColor": "#1971c2",
|
||||
"backgroundColor": "#a5d8ff",
|
||||
"boundElements": [{ "type": "text", "id": "{component-id}-text" }],
|
||||
// ... other required properties
|
||||
}
|
||||
```
|
||||
|
||||
### Text with containerId
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "{component-id}-text",
|
||||
"type": "text",
|
||||
"x": 505, // shape.x + 5
|
||||
"y": 220, // shape.y + (shape.height - text.height) / 2
|
||||
"width": 190, // shape.width - 10
|
||||
"height": 50,
|
||||
"text": "{Component Name}\n{Subtitle}",
|
||||
"fontSize": 16,
|
||||
"fontFamily": 1,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle",
|
||||
"containerId": "{component-id}",
|
||||
"originalText": "{Component Name}\n{Subtitle}",
|
||||
"lineHeight": 1.25,
|
||||
// ... other required properties
|
||||
}
|
||||
```
|
||||
|
||||
### DO NOT Use the `label` Property
|
||||
|
||||
The `label` property is for the JavaScript API, NOT raw JSON files:
|
||||
|
||||
```json
|
||||
// WRONG - will show empty boxes
|
||||
{ "type": "rectangle", "label": { "text": "My Label" } }
|
||||
|
||||
// CORRECT - requires TWO elements
|
||||
// 1. Shape with boundElements reference
|
||||
// 2. Separate text element with containerId
|
||||
```
|
||||
|
||||
### Text Positioning
|
||||
|
||||
- Text `x` = shape `x` + 5
|
||||
- Text `y` = shape `y` + (shape.height - text.height) / 2
|
||||
- Text `width` = shape `width` - 10
|
||||
- Use `\n` for multi-line labels
|
||||
- Always use `textAlign: "center"` and `verticalAlign: "middle"`
|
||||
|
||||
### ID Naming Convention
|
||||
|
||||
Always use pattern: `{shape-id}-text` for text element IDs.
|
||||
|
||||
---
|
||||
|
||||
## Dynamic ID Generation
|
||||
|
||||
IDs and labels are generated from codebase analysis:
|
||||
|
||||
| Discovered Component | Generated ID | Generated Label |
|
||||
|---------------------|--------------|-----------------|
|
||||
| Express API server | `express-api` | `"API Server\nExpress.js"` |
|
||||
| PostgreSQL database | `postgres-db` | `"PostgreSQL\nDatabase"` |
|
||||
| Redis cache | `redis-cache` | `"Redis\nCache Layer"` |
|
||||
| S3 bucket for uploads | `s3-uploads` | `"S3 Bucket\nuploads/"` |
|
||||
| Lambda function | `lambda-processor` | `"Lambda\nProcessor"` |
|
||||
| React frontend | `react-frontend` | `"React App\nFrontend"` |
|
||||
|
||||
---
|
||||
|
||||
## Grouping with Dashed Rectangles
|
||||
|
||||
For logical groupings (namespaces, VPCs, pipelines):
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "group-ai-pipeline",
|
||||
"type": "rectangle",
|
||||
"x": 100,
|
||||
"y": 500,
|
||||
"width": 1000,
|
||||
"height": 280,
|
||||
"strokeColor": "#9c36b5",
|
||||
"backgroundColor": "transparent",
|
||||
"strokeStyle": "dashed",
|
||||
"roughness": 0,
|
||||
"roundness": null,
|
||||
"boundElements": null
|
||||
}
|
||||
```
|
||||
|
||||
Group labels are standalone text (no containerId) at top-left:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "group-ai-pipeline-label",
|
||||
"type": "text",
|
||||
"x": 120,
|
||||
"y": 510,
|
||||
"text": "AI Processing Pipeline (Cloud Run)",
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "top",
|
||||
"containerId": null
|
||||
}
|
||||
```
|
||||
71
skills/excalidraw/references/json-schema.md
Normal file
71
skills/excalidraw/references/json-schema.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Excalidraw JSON Schema
|
||||
|
||||
## Element Types
|
||||
|
||||
| Type | Use For |
|
||||
|------|---------|
|
||||
| `rectangle` | Processes, actions, components |
|
||||
| `ellipse` | Entry/exit points, external systems |
|
||||
| `diamond` | Decisions, conditionals |
|
||||
| `arrow` | Connections between shapes |
|
||||
| `text` | Labels inside shapes |
|
||||
| `line` | Non-arrow connections |
|
||||
| `frame` | Grouping containers |
|
||||
|
||||
## Common Properties
|
||||
|
||||
All elements share these:
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `id` | string | Unique identifier |
|
||||
| `type` | string | Element type |
|
||||
| `x`, `y` | number | Position in pixels |
|
||||
| `width`, `height` | number | Size in pixels |
|
||||
| `strokeColor` | string | Border color (hex) |
|
||||
| `backgroundColor` | string | Fill color (hex or "transparent") |
|
||||
| `fillStyle` | string | "solid", "hachure", "cross-hatch" |
|
||||
| `strokeWidth` | number | 1, 2, or 4 |
|
||||
| `strokeStyle` | string | "solid", "dashed", "dotted" |
|
||||
| `roughness` | number | 0 (smooth), 1 (default), 2 (rough) |
|
||||
| `opacity` | number | 0-100 |
|
||||
| `seed` | number | Random seed for roughness |
|
||||
|
||||
## Text-Specific Properties
|
||||
|
||||
| Property | Description |
|
||||
|----------|-------------|
|
||||
| `text` | The display text |
|
||||
| `originalText` | Same as text |
|
||||
| `fontSize` | Size in pixels (16-20 recommended) |
|
||||
| `fontFamily` | 3 for monospace (use this) |
|
||||
| `textAlign` | "left", "center", "right" |
|
||||
| `verticalAlign` | "top", "middle", "bottom" |
|
||||
| `containerId` | ID of parent shape |
|
||||
|
||||
## Arrow-Specific Properties
|
||||
|
||||
| Property | Description |
|
||||
|----------|-------------|
|
||||
| `points` | Array of [x, y] coordinates |
|
||||
| `startBinding` | Connection to start shape |
|
||||
| `endBinding` | Connection to end shape |
|
||||
| `startArrowhead` | null, "arrow", "bar", "dot", "triangle" |
|
||||
| `endArrowhead` | null, "arrow", "bar", "dot", "triangle" |
|
||||
|
||||
## Binding Format
|
||||
|
||||
```json
|
||||
{
|
||||
"elementId": "shapeId",
|
||||
"focus": 0,
|
||||
"gap": 2
|
||||
}
|
||||
```
|
||||
|
||||
## Rectangle Roundness
|
||||
|
||||
Add for rounded corners:
|
||||
```json
|
||||
"roundness": { "type": 3 }
|
||||
```
|
||||
205
skills/excalidraw/references/render_excalidraw.py
Normal file
205
skills/excalidraw/references/render_excalidraw.py
Normal file
@@ -0,0 +1,205 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render Excalidraw JSON to PNG using Playwright + headless Chromium.
|
||||
|
||||
Usage:
|
||||
python3 render_excalidraw.py <path-to-file.excalidraw> [--output path.png] [--scale 2] [--width 1920]
|
||||
|
||||
Dependencies (playwright, chromium) are provided by the Nix flake / direnv environment.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def validate_excalidraw(data: dict) -> list[str]:
|
||||
"""Validate Excalidraw JSON structure. Returns list of errors (empty = valid)."""
|
||||
errors: list[str] = []
|
||||
|
||||
if data.get("type") != "excalidraw":
|
||||
errors.append(f"Expected type 'excalidraw', got '{data.get('type')}'")
|
||||
|
||||
if "elements" not in data:
|
||||
errors.append("Missing 'elements' array")
|
||||
elif not isinstance(data["elements"], list):
|
||||
errors.append("'elements' must be an array")
|
||||
elif len(data["elements"]) == 0:
|
||||
errors.append("'elements' array is empty — nothing to render")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def compute_bounding_box(elements: list[dict]) -> tuple[float, float, float, float]:
|
||||
"""Compute bounding box (min_x, min_y, max_x, max_y) across all elements."""
|
||||
min_x = float("inf")
|
||||
min_y = float("inf")
|
||||
max_x = float("-inf")
|
||||
max_y = float("-inf")
|
||||
|
||||
for el in elements:
|
||||
if el.get("isDeleted"):
|
||||
continue
|
||||
x = el.get("x", 0)
|
||||
y = el.get("y", 0)
|
||||
w = el.get("width", 0)
|
||||
h = el.get("height", 0)
|
||||
|
||||
# For arrows/lines, points array defines the shape relative to x,y
|
||||
if el.get("type") in ("arrow", "line") and "points" in el:
|
||||
for px, py in el["points"]:
|
||||
min_x = min(min_x, x + px)
|
||||
min_y = min(min_y, y + py)
|
||||
max_x = max(max_x, x + px)
|
||||
max_y = max(max_y, y + py)
|
||||
else:
|
||||
min_x = min(min_x, x)
|
||||
min_y = min(min_y, y)
|
||||
max_x = max(max_x, x + abs(w))
|
||||
max_y = max(max_y, y + abs(h))
|
||||
|
||||
if min_x == float("inf"):
|
||||
return (0, 0, 800, 600)
|
||||
|
||||
return (min_x, min_y, max_x, max_y)
|
||||
|
||||
|
||||
def render(
|
||||
excalidraw_path: Path,
|
||||
output_path: Path | None = None,
|
||||
scale: int = 2,
|
||||
max_width: int = 1920,
|
||||
) -> Path:
|
||||
"""Render an .excalidraw file to PNG. Returns the output PNG path."""
|
||||
# Import playwright here so validation errors show before import errors
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright
|
||||
except ImportError:
|
||||
print("ERROR: playwright not installed.", file=sys.stderr)
|
||||
print("Ensure the Nix dev shell is active (direnv allow).", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Read and validate
|
||||
raw = excalidraw_path.read_text(encoding="utf-8")
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"ERROR: Invalid JSON in {excalidraw_path}: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
errors = validate_excalidraw(data)
|
||||
if errors:
|
||||
print(f"ERROR: Invalid Excalidraw file:", file=sys.stderr)
|
||||
for err in errors:
|
||||
print(f" - {err}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Compute viewport size from element bounding box
|
||||
elements = [e for e in data["elements"] if not e.get("isDeleted")]
|
||||
min_x, min_y, max_x, max_y = compute_bounding_box(elements)
|
||||
padding = 80
|
||||
diagram_w = max_x - min_x + padding * 2
|
||||
diagram_h = max_y - min_y + padding * 2
|
||||
|
||||
# Cap viewport width, let height be natural
|
||||
vp_width = min(int(diagram_w), max_width)
|
||||
vp_height = max(int(diagram_h), 600)
|
||||
|
||||
# Output path
|
||||
if output_path is None:
|
||||
output_path = excalidraw_path.with_suffix(".png")
|
||||
|
||||
# Template path (same directory as this script)
|
||||
template_path = Path(__file__).parent / "render_template.html"
|
||||
if not template_path.exists():
|
||||
print(f"ERROR: Template not found at {template_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
template_url = template_path.as_uri()
|
||||
|
||||
with sync_playwright() as p:
|
||||
try:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
except Exception as e:
|
||||
if "Executable doesn't exist" in str(e) or "browserType.launch" in str(e):
|
||||
print("ERROR: Chromium not installed for Playwright.", file=sys.stderr)
|
||||
print("Ensure the Nix dev shell is active (direnv allow).", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
raise
|
||||
|
||||
page = browser.new_page(
|
||||
viewport={"width": vp_width, "height": vp_height},
|
||||
device_scale_factor=scale,
|
||||
)
|
||||
|
||||
# Load the template
|
||||
page.goto(template_url)
|
||||
|
||||
# Wait for the ES module to load (imports from esm.sh)
|
||||
page.wait_for_function("window.__moduleReady === true", timeout=30000)
|
||||
|
||||
# Inject the diagram data and render
|
||||
json_str = json.dumps(data)
|
||||
result = page.evaluate(f"window.renderDiagram({json_str})")
|
||||
|
||||
if not result or not result.get("success"):
|
||||
error_msg = (
|
||||
result.get("error", "Unknown render error")
|
||||
if result
|
||||
else "renderDiagram returned null"
|
||||
)
|
||||
print(f"ERROR: Render failed: {error_msg}", file=sys.stderr)
|
||||
browser.close()
|
||||
sys.exit(1)
|
||||
|
||||
# Wait for render completion signal
|
||||
page.wait_for_function("window.__renderComplete === true", timeout=15000)
|
||||
|
||||
# Screenshot the SVG element
|
||||
svg_el = page.query_selector("#root svg")
|
||||
if svg_el is None:
|
||||
print("ERROR: No SVG element found after render.", file=sys.stderr)
|
||||
browser.close()
|
||||
sys.exit(1)
|
||||
|
||||
svg_el.screenshot(path=str(output_path))
|
||||
browser.close()
|
||||
|
||||
return output_path
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Entry point for rendering Excalidraw JSON files to PNG."""
|
||||
parser = argparse.ArgumentParser(description="Render Excalidraw JSON to PNG")
|
||||
parser.add_argument("input", type=Path, help="Path to .excalidraw JSON file")
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
"-o",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Output PNG path (default: same name with .png)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--scale", "-s", type=int, default=2, help="Device scale factor (default: 2)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--width",
|
||||
"-w",
|
||||
type=int,
|
||||
default=1920,
|
||||
help="Max viewport width (default: 1920)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.input.exists():
|
||||
print(f"ERROR: File not found: {args.input}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
png_path = render(args.input, args.output, args.scale, args.width)
|
||||
print(str(png_path))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
57
skills/excalidraw/references/render_template.html
Normal file
57
skills/excalidraw/references/render_template.html
Normal file
@@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { background: #ffffff; overflow: hidden; }
|
||||
#root { display: inline-block; }
|
||||
#root svg { display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
<script type="module">
|
||||
import { exportToSvg } from "https://esm.sh/@excalidraw/excalidraw?bundle";
|
||||
|
||||
window.renderDiagram = async function(jsonData) {
|
||||
try {
|
||||
const data = typeof jsonData === "string" ? JSON.parse(jsonData) : jsonData;
|
||||
const elements = data.elements || [];
|
||||
const appState = data.appState || {};
|
||||
const files = data.files || {};
|
||||
|
||||
// Force white background in appState
|
||||
appState.viewBackgroundColor = appState.viewBackgroundColor || "#ffffff";
|
||||
appState.exportWithDarkMode = false;
|
||||
|
||||
const svg = await exportToSvg({
|
||||
elements: elements,
|
||||
appState: {
|
||||
...appState,
|
||||
exportBackground: true,
|
||||
},
|
||||
files: files,
|
||||
});
|
||||
|
||||
// Clear any previous render
|
||||
const root = document.getElementById("root");
|
||||
root.innerHTML = "";
|
||||
root.appendChild(svg);
|
||||
|
||||
window.__renderComplete = true;
|
||||
window.__renderError = null;
|
||||
return { success: true, width: svg.getAttribute("width"), height: svg.getAttribute("height") };
|
||||
} catch (err) {
|
||||
window.__renderComplete = true;
|
||||
window.__renderError = err.message;
|
||||
return { success: false, error: err.message };
|
||||
}
|
||||
};
|
||||
|
||||
// Signal that the module is loaded and ready
|
||||
window.__moduleReady = true;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,182 +0,0 @@
|
||||
# Validation Reference
|
||||
|
||||
Checklists, validation algorithms, and common bug fixes.
|
||||
|
||||
---
|
||||
|
||||
## Pre-Flight Validation Algorithm
|
||||
|
||||
Run BEFORE writing the file:
|
||||
|
||||
```
|
||||
FUNCTION validateDiagram(elements):
|
||||
errors = []
|
||||
|
||||
// 1. Validate shape-text bindings
|
||||
FOR each shape IN elements WHERE shape.boundElements != null:
|
||||
FOR each binding IN shape.boundElements:
|
||||
textElement = findById(elements, binding.id)
|
||||
IF textElement == null:
|
||||
errors.append("Shape {shape.id} references missing text {binding.id}")
|
||||
ELSE IF textElement.containerId != shape.id:
|
||||
errors.append("Text containerId doesn't match shape")
|
||||
|
||||
// 2. Validate arrow connections
|
||||
FOR each arrow IN elements WHERE arrow.type == "arrow":
|
||||
sourceShape = findShapeNear(elements, arrow.x, arrow.y)
|
||||
IF sourceShape == null:
|
||||
errors.append("Arrow {arrow.id} doesn't start from shape edge")
|
||||
|
||||
finalPoint = arrow.points[arrow.points.length - 1]
|
||||
endX = arrow.x + finalPoint[0]
|
||||
endY = arrow.y + finalPoint[1]
|
||||
targetShape = findShapeNear(elements, endX, endY)
|
||||
IF targetShape == null:
|
||||
errors.append("Arrow {arrow.id} doesn't end at shape edge")
|
||||
|
||||
IF arrow.points.length > 2:
|
||||
IF arrow.elbowed != true:
|
||||
errors.append("Arrow {arrow.id} missing elbowed:true")
|
||||
IF arrow.roundness != null:
|
||||
errors.append("Arrow {arrow.id} should have roundness:null")
|
||||
|
||||
// 3. Validate unique IDs
|
||||
ids = [el.id for el in elements]
|
||||
duplicates = findDuplicates(ids)
|
||||
IF duplicates.length > 0:
|
||||
errors.append("Duplicate IDs: {duplicates}")
|
||||
|
||||
// 4. Validate bounding boxes
|
||||
FOR each arrow IN elements WHERE arrow.type == "arrow":
|
||||
maxX = max(abs(p[0]) for p in arrow.points)
|
||||
maxY = max(abs(p[1]) for p in arrow.points)
|
||||
IF arrow.width < maxX OR arrow.height < maxY:
|
||||
errors.append("Arrow {arrow.id} bounding box too small")
|
||||
|
||||
RETURN errors
|
||||
|
||||
FUNCTION findShapeNear(elements, x, y, tolerance=15):
|
||||
FOR each shape IN elements WHERE shape.type IN ["rectangle", "ellipse"]:
|
||||
edges = [
|
||||
(shape.x + shape.width/2, shape.y), // top
|
||||
(shape.x + shape.width/2, shape.y + shape.height), // bottom
|
||||
(shape.x, shape.y + shape.height/2), // left
|
||||
(shape.x + shape.width, shape.y + shape.height/2) // right
|
||||
]
|
||||
FOR each edge IN edges:
|
||||
IF abs(edge.x - x) < tolerance AND abs(edge.y - y) < tolerance:
|
||||
RETURN shape
|
||||
RETURN null
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklists
|
||||
|
||||
### Before Generating
|
||||
|
||||
- [ ] Identified all components from codebase
|
||||
- [ ] Mapped all connections/data flows
|
||||
- [ ] Chose layout pattern (vertical, horizontal, hub-and-spoke)
|
||||
- [ ] Selected color palette (default, AWS, Azure, K8s)
|
||||
- [ ] Planned grid positions
|
||||
- [ ] Created ID naming scheme
|
||||
|
||||
### During Generation
|
||||
|
||||
- [ ] Every labeled shape has BOTH shape AND text elements
|
||||
- [ ] Shape has `boundElements: [{ "type": "text", "id": "{id}-text" }]`
|
||||
- [ ] Text has `containerId: "{shape-id}"`
|
||||
- [ ] Multi-point arrows have `elbowed: true`, `roundness: null`, `roughness: 0`
|
||||
- [ ] Arrows have `startBinding` and `endBinding`
|
||||
- [ ] No diamond shapes used
|
||||
- [ ] Applied staggering formula for multiple arrows
|
||||
|
||||
### Arrow Validation (Every Arrow)
|
||||
|
||||
- [ ] Arrow `x,y` calculated from shape edge
|
||||
- [ ] Final point offset = `targetEdge - sourceEdge`
|
||||
- [ ] Arrow `width` = `max(abs(point[0]))`
|
||||
- [ ] Arrow `height` = `max(abs(point[1]))`
|
||||
- [ ] U-turn arrows have 40-60px clearance
|
||||
|
||||
### After Generation
|
||||
|
||||
- [ ] All `boundElements` IDs reference valid text elements
|
||||
- [ ] All `containerId` values reference valid shapes
|
||||
- [ ] All arrows start within 15px of shape edge
|
||||
- [ ] All arrows end within 15px of shape edge
|
||||
- [ ] No duplicate IDs
|
||||
- [ ] Arrow bounding boxes match points
|
||||
- [ ] File is valid JSON
|
||||
|
||||
---
|
||||
|
||||
## Common Bugs and Fixes
|
||||
|
||||
### Bug: Arrow appears disconnected/floating
|
||||
|
||||
**Cause**: Arrow `x,y` not calculated from shape edge.
|
||||
|
||||
**Fix**:
|
||||
```
|
||||
Rectangle bottom: arrow_x = shape.x + shape.width/2
|
||||
arrow_y = shape.y + shape.height
|
||||
```
|
||||
|
||||
### Bug: Arrow endpoint doesn't reach target
|
||||
|
||||
**Cause**: Final point offset calculated incorrectly.
|
||||
|
||||
**Fix**:
|
||||
```
|
||||
target_edge = (target.x + target.width/2, target.y)
|
||||
offset_x = target_edge.x - arrow.x
|
||||
offset_y = target_edge.y - arrow.y
|
||||
Final point = [offset_x, offset_y]
|
||||
```
|
||||
|
||||
### Bug: Multiple arrows from same source overlap
|
||||
|
||||
**Cause**: All arrows start from identical `x,y`.
|
||||
|
||||
**Fix**: Stagger start positions:
|
||||
```
|
||||
For 5 arrows from bottom edge:
|
||||
arrow1.x = shape.x + shape.width * 0.2
|
||||
arrow2.x = shape.x + shape.width * 0.35
|
||||
arrow3.x = shape.x + shape.width * 0.5
|
||||
arrow4.x = shape.x + shape.width * 0.65
|
||||
arrow5.x = shape.x + shape.width * 0.8
|
||||
```
|
||||
|
||||
### Bug: Callback arrow doesn't loop correctly
|
||||
|
||||
**Cause**: U-turn path lacks clearance.
|
||||
|
||||
**Fix**: Use 4-point path:
|
||||
```
|
||||
Points = [[0, 0], [clearance, 0], [clearance, -vert], [final_x, -vert]]
|
||||
clearance = 40-60px
|
||||
```
|
||||
|
||||
### Bug: Labels don't appear inside shapes
|
||||
|
||||
**Cause**: Using `label` property instead of separate text element.
|
||||
|
||||
**Fix**: Create TWO elements:
|
||||
1. Shape with `boundElements` referencing text
|
||||
2. Text with `containerId` referencing shape
|
||||
|
||||
### Bug: Arrows are curved, not 90-degree
|
||||
|
||||
**Cause**: Missing elbow properties.
|
||||
|
||||
**Fix**: Add all three:
|
||||
```json
|
||||
{
|
||||
"roughness": 0,
|
||||
"roundness": null,
|
||||
"elbowed": true
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user