
Diagram Creator
- 18 installs
- 16 repo stars
- Updated March 6, 2026
- ferdinandobons/diagram-creator-skill
Generate self-contained HTML architecture diagrams, flowcharts, schemas, and timelines from any file or topic, with pan-and-zoom canvas.
About
Turns any input file or plain description into a single self-contained HTML diagram with an infinite pan-and-zoom canvas, five themes, and eight topology layouts. A developer uses it to visualize architecture, schemas, flows, or timelines without external tooling.
- Accepts any file type - code, config, JSON/YAML, SQL, PDF, Office - and auto-extracts structure
- Output is a zero-dependency HTML file that opens in any browser
Diagram Creator by the numbers
- 18 all-time installs (skills.sh)
- Ranked #1,021 of 1,879 Documentation skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ferdinandobons/diagram-creator-skill --skill diagram-creatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 18 |
|---|---|
| repo stars | ★ 16 |
| Last updated | March 6, 2026 |
| Repository | ferdinandobons/diagram-creator-skill ↗ |
What it does
Generate self-contained HTML architecture diagrams, flowcharts, schemas, and timelines from any file or topic, with pan-and-zoom canvas.
Files
Diagram Creator
Generate stunning, self-contained HTML diagrams with professional typography, smooth animations, and an infinite canvas you can pan and zoom like Miro. Output is always a single .html file that opens directly in any browser — zero dependencies. Supports 5 themes and 8 topology layouts.
Input Handling
Accept any of these inputs and auto-process them:
| Input type | How to handle |
|---|---|
| Topic/description | "Docker networking", "OAuth 2.0 flow" — use directly |
| Markdown (.md, .mdx) | Extract structure from headings, lists, tables, relationships |
| JSON / YAML (.json, .yaml, .yml, .jsonl) | Parse structure — keys become nodes, nesting becomes layers |
| Code files (.py, .ts, .js, .go, .rs, .java, .rb, .php, .swift, .kt, .c, .cpp, .cs, .scala, etc.) | Extract classes, functions, imports, inheritance — diagram the architecture |
| Config / Infra (.toml, .ini, .env, .tf, .hcl, Dockerfile, docker-compose.yml, Makefile, nginx.conf) | Map services, variables, dependencies, infrastructure |
| API specs (.openapi.yaml, .swagger.json, .graphql, .proto) | Extract endpoints, types, service relationships |
| Database schemas (.sql, .prisma, .dbml) | Map tables, columns, foreign keys, relationships |
| CI/CD pipelines (.github/workflows/*.yml, .gitlab-ci.yml, Jenkinsfile) | Visualize pipeline stages and deployment flows |
| Data files (.csv, .tsv) | Identify columns as entities, rows as relationships |
| Microsoft Office (.xlsx, .xls, .docx, .doc, .pptx, .ppt) | Extract tables, headings, slide structure, org charts, SmartArt |
| Package manifests (package.json, requirements.txt, Cargo.toml, go.mod, pom.xml) | Map dependency trees |
| Existing diagrams (.drawio, .puml, .mermaid) | Read spec and re-render with this design system |
| PDF / Docs (.pdf, .txt, .rst, .adoc) | Read content, extract core structure |
| Notebooks (.ipynb) | Extract code cells, data flow, library dependencies |
If it's a file, this skill can diagram it. For formats not listed, read the content and extract whatever structure exists.
When receiving a file path, read the file, detect the extension, and intelligently extract what matters for the diagram. Don't ask the user to reformat — figure it out.
If the input is genuinely ambiguous (no clear structure to extract), ask clarifying questions before proceeding. See "Pre-generation Questions" below.
Pre-generation Questions
Always ask these questions before generating, unless the answer is completely obvious from the input. Ask one question at a time — wait for the user's answer before moving to the next. Present every question as a numbered list so the user can simply reply with a number.
Skip a question ONLY when the answer is unambiguous — e.g., "diagram of Docker bridge networking" makes topology (nested) and detail level (medium) obvious. When in doubt, ask.
Question flow (one at a time, in this order):
1. What to visualize? (skip only if input is already specific) Present 2-3 interpretations of the input as numbered options, plus an "Other" option. Tailor the options to what the user actually asked — don't use generic placeholders.
What do you want to visualize?
1) [interpretation A]
2) [interpretation B]
3) Other — describe below
→ Reply with the number:2. Layout? (skip if only one topology realistically fits — choose automatically in Step 0) Only list the 2-3 topologies that could realistically fit, not all 8. Adapt options based on the previous answer.
Layout?
1) Left-to-right — sequential flow
2) Timeline — vertical step sequence
→ Reply with the number:3. Detail level?
Detail level?
1) High-level — main entities only, understand in 5 seconds
2) Medium — entities with key info, labeled connections (default)
3) Detailed — full technical breakdown, all metadata
→ Reply with the number (default: 2):4. Theme?
Theme?
1) Dark (default)
2) Light
3) Corporate
4) Neon
5) Minimal
→ Reply with the number (default: 1):Once you have all the answers, proceed autonomously through all remaining steps without further questions.
Step 0 — Choose the Topology
Pick exactly one topology based on what fits the content best:
| Topology | When to use | Examples |
|---|---|---|
nested | Hierarchical containment, layers wrapping layers | Docker networking, K8s pods, OSI model, VPC subnets |
left-to-right | Sequential flow from A to Z | OAuth flow, CI/CD pipeline, user journey, request lifecycle |
hub-and-spoke | Central node connected to N surrounding nodes | API gateway, microservices, load balancer, team structure |
timeline | Ordered vertical sequence of events/steps | Deploy pipeline, project roadmap, historical events |
grid | Matrix/table comparison, feature grids | Feature comparison, skill map, competitive landscape |
tree | Hierarchical branching, org charts | Org chart, file tree, decision tree, taxonomy |
funnel | Progressive narrowing, conversion stages | Sales funnel, data pipeline, filtering process |
comparison | Side-by-side 2-3 options | Product vs product, technology choices, before/after |
Step 1 — Plan the Content
Before writing HTML, plan these elements in your thinking:
- Topic: one sentence describing what the diagram shows
- Detail level: choose one of three levels. Default to medium unless context suggests otherwise:
- High-level — Only the main entities of the system. Aggregate similar entities into single boxes (e.g., "Backend Services" instead of listing 10 microservices). No metadata in boxes — just name and role. Minimal connection labels. No callouts, badges, or notes. The viewer should understand the structure in 5 seconds.
- Medium (default) — The entities needed to understand how the system works, without sub-components. Each box gets 1-2 metadata relevant to the domain (infra: port and protocol; org chart: role and team; flow: input/output of each step). Label connections where it adds clarity. Callouts only for non-obvious key concepts. The viewer who knows the domain should understand the diagram without further explanation.
- Detailed — Everything: sub-components, configurations, technical notes. All relevant metadata per box (ports, protocols, versions, env vars, states). Badges, explanatory callouts, side notes. Goal: complete technical reference for people working on the system.
How to choose: (1) If the user specifies a level explicitly, use it. (2) If context clarifies intent — a presentation implies high-level, a technical doc for the team implies detailed — match it. (3) Otherwise default to medium. (4) If the chosen level produces too many elements for the topology, aggregate entities into groups rather than changing level or topology.
- Layers/Steps: each level with name, description, and accent color. For left-to-right topology, count the steps and apply the step count scaling table from
references/topology-layouts.mdto set step max-width and arrow width accordingly - Nodes: each box with emoji, name, and metadata density matching the detail level above
- Connections: what each arrow represents (label density matches detail level)
- Special Components: callout boxes, badges, info panels (reduce or skip for high-level)
- Legend Entries: one per accent color used
Step 1.5 — Choose the Theme
If the user specifies a theme, use it. Otherwise default to dark.
| Theme | Best for | Keywords |
|---|---|---|
dark (default) | Technical diagrams, dev docs | "dark", default |
light | Presentations, documentation sites | "light", "chiaro", "presentation" |
corporate | Business plans, pitch decks | "corporate", "business", "professional" |
neon | Creative, gaming, social media | "neon", "cyberpunk", "vibrant" |
minimal | Clean docs, technical specs | "minimal", "clean", "simple" |
See references/themes.md for the exact :root values and overrides per theme.
Step 2 — Build the HTML
Read the reference files for detailed CSS/layout specifications:
references/typography-and-colors.md— fonts, palette, backgroundreferences/themes.md— theme system with 5 color schemesreferences/topology-layouts.md— layout rules for each of the 8 topologiesreferences/components.md— node cards, badges, callouts, legend, animationsreferences/safety-rules.md— mandatory layout constraints for all topologiesreferences/canvas.md— infinite canvas with pan & zoom (Miro-like navigation)
Follow every rule in those references exactly. The design system is battle-tested — don't improvise fonts, colors, or layout patterns.
Always include the infinite canvas system from references/canvas.md. This lets users pan (click+drag) and zoom (scroll wheel) the diagram freely, like a whiteboard.
Step 3 — Output
- Single
.htmlfile, all CSS in<style>, all JS in<script> - No external dependencies except Google Fonts via
@import - No CSS frameworks, no JS libraries (vanilla JS for hub-and-spoke coordinates and canvas pan/zoom)
- Valid HTML5 with
<!DOCTYPE html>as first line - Must render correctly opened directly in a browser
- Responsive down to 360px viewport width
Save as diagram-[topic-slug].html in the current working directory. Open it in the browser with open (macOS) or xdg-open (Linux). Tell the user where it was saved.
Example: Kubernetes Pod Networking
Input: "Visualize Kubernetes pod networking architecture"
Topology: nested
Layers:
- Level 1: Internet / External Traffic — red (solid)
- Level 2: Kubernetes Cluster — orange (solid)
- Level 3: Worker Node — blue (solid)
- Level 4: Pod Network (CNI - Calico) — cyan (dashed)
Nodes (inside Level 4):
- nginx-ingress | IP: 10.244.0.5 | Port: 80:30080 | Ingress controller
- api-server | IP: 10.244.0.12 | Port: 8080 | Backend API
- postgres | IP: 10.244.0.20 | Port: 5432 | StatefulSet + PVC
- redis-cache | IP: 10.244.0.25 | internal only | Cache
- prometheus | IP: 10.244.0.30 | Port: 9090:30090 | Monitoring
Connections:
- Internet → Cluster: HTTPS on port 443
- Cluster → Node: kube-proxy iptables/IPVS rules
- Node → Pod Network: CNI assigns Pod IPs from CIDR 10.244.0.0/16
- Within Pod Network: direct communication via virtual switch
Legend: red=External, orange=Cluster boundary, blue=Worker node, cyan=Pod network, green=Exposed port
Example: OAuth 2.0 Authorization Code Flow
Input: "Create a diagram of the OAuth 2.0 authorization code flow"
Topology: left-to-right
Steps: 1. User — Clicks "Login with Google" — red 2. Client App — Redirects with client_id, scope — red 3. Auth Server — Consent screen — orange 4. Redirect — Auth code to redirect_uri — orange 5. Token Exchange — code + client_secret (server-to-server) — blue 6. Access Token — access_token + refresh_token — blue 7. API Call — Bearer token in header — cyan 8. Resource Server — Validates, returns data — green
Callout: "Step 5 happens server-to-server. The client_secret never reaches the browser."
Legend: red=Client-side, orange=Authorization, blue=Token exchange, cyan=API access, green=Protected resource
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kubernetes Pod Networking</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Syne:wght@400;700;800&display=swap');
:root {
--bg: #0a0e1a;
--panel: #0f1528;
--border: #1e2d4a;
--blue: #2d7aff;
--cyan: #00d4ff;
--green: #00e5a0;
--orange: #ff8c42;
--red: #ff4d6a;
--text: #c8d8f0;
--dim: #4a6080;
}
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
min-height: 100vh;
font-family: 'JetBrains Mono', monospace;
color: var(--text);
padding: 0;
justify-content: flex-start;
align-items: flex-start;
overflow-x: hidden;
}
body::before {
content: '';
position: fixed;
inset: 0;
background-image:
linear-gradient(rgba(45,122,255,0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(45,122,255,0.04) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none;
z-index: 0;
}
#canvas-container { position: fixed; inset: 0; overflow: hidden; cursor: grab; z-index: 1; }
#canvas-container.grabbing { cursor: grabbing; }
#canvas { position: absolute; transform-origin: 0 0; }
.wrapper { position: relative; z-index: 1; width: 100%; max-width: 900px; overflow-x: auto; min-width: 320px; padding: 40px 20px; }
h1 { font-family: 'Syne', sans-serif; font-size: 1.6rem; font-weight: 800; color: #fff; text-align: center; margin-bottom: 6px; }
h1 .highlight { color: var(--cyan); }
.subtitle { font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; font-weight: 400; text-transform: uppercase; letter-spacing: 0.1em; color: var(--dim); text-align: center; margin-bottom: 32px; }
.layer { border-radius: 16px; padding: 20px 24px; min-height: 80px; }
.layer-1 { background: rgba(255,77,106,0.06); border: 1px solid rgba(255,77,106,0.22); }
.layer-2 { background: rgba(255,140,66,0.06); border: 1px solid rgba(255,140,66,0.22); margin: 12px 0 0; }
.layer-3 { background: rgba(45,122,255,0.06); border: 1px solid rgba(45,122,255,0.22); margin: 12px 0 0; }
.layer-4 { background: rgba(0,212,255,0.06); border: 1px dashed rgba(0,212,255,0.22); margin: 12px 0 0; }
.layer-label { font-family: 'JetBrains Mono', monospace; font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.15em; color: var(--text); margin-bottom: 14px; }
.layer-label-red::before, .layer-label-orange::before, .layer-label-blue::before, .layer-label-cyan::before {
content: ''; display: inline-block; width: 6px; height: 6px; border-radius: 50%; margin-right: 8px; vertical-align: middle;
}
.layer-label-red::before { background: var(--red); box-shadow: 0 0 8px var(--red); }
.layer-label-orange::before { background: var(--orange); box-shadow: 0 0 8px var(--orange); }
.layer-label-blue::before { background: var(--blue); box-shadow: 0 0 8px var(--blue); }
.layer-label-cyan::before { background: var(--cyan); box-shadow: 0 0 8px var(--cyan); }
.ext-client { background: rgba(255,77,106,0.08); border: 1px solid rgba(255,77,106,0.25); border-radius: 10px; padding: 10px 18px; font-size: 0.75rem; color: var(--red); text-align: center; margin-bottom: 8px; }
.ext-client span { display: block; color: var(--dim); font-size: 0.62rem; margin-top: 2px; }
.connector { display: flex; flex-direction: column; align-items: center; margin: 0 auto; padding: 4px 0; }
.connector-line { width: 2px; height: 28px; background: linear-gradient(to bottom, var(--dim), var(--blue)); }
.connector-arrow { color: var(--blue); font-size: 10px; line-height: 1; }
.connector-label { font-size: 0.6rem; text-transform: uppercase; letter-spacing: 0.08em; background: var(--bg); border: 1px solid var(--border); border-radius: 4px; padding: 2px 8px; color: var(--dim); margin-top: 4px; }
.bus-line { height: 3px; background: linear-gradient(to right, transparent, var(--blue), var(--cyan), var(--blue), transparent); border-radius: 2px; margin: 12px 0 8px; animation: pulse-glow 2.5s ease-in-out infinite; }
.drops-row { display: grid; grid-template-columns: repeat(5, 1fr); gap: 16px; margin: 0; }
.drop { display: flex; justify-content: center; }
.drop-line { width: 1px; height: 16px; background: linear-gradient(to bottom, var(--cyan), transparent); }
.node-row { display: flex; flex-wrap: wrap; gap: 16px; justify-content: center; align-items: stretch; }
.node-card { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 14px 16px; flex: 1; min-width: 140px; max-width: 150px; transition: border-color 0.2s; z-index: 2; }
.node-card:hover { border-color: var(--cyan); }
.node-emoji { display: block; font-size: 1.4rem; margin-bottom: 8px; }
.node-name { font-family: 'Syne', sans-serif; font-size: 0.78rem; font-weight: 700; color: #fff; margin-bottom: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.node-detail { font-family: 'JetBrains Mono', monospace; font-size: 0.68rem; color: var(--cyan); margin-bottom: 4px; }
.node-note { font-size: 0.62rem; color: var(--dim); }
.badge-port { display: inline-block; background: rgba(0,229,160,0.12); color: var(--green); border: 1px solid rgba(0,229,160,0.3); border-radius: 4px; padding: 2px 6px; font-size: 0.62rem; margin-top: 4px; }
.badge-mechanism { display: inline-flex; align-items: center; gap: 6px; background: rgba(255,140,66,0.12); border: 1px solid rgba(255,140,66,0.35); color: var(--orange); border-radius: 8px; padding: 6px 14px; font-size: 0.72rem; font-weight: 600; }
.callout { background: rgba(0,212,255,0.05); border: 1px solid rgba(0,212,255,0.2); border-radius: 10px; padding: 12px 18px; font-size: 0.72rem; color: var(--text); text-align: center; margin-top: 12px; }
.callout strong { color: var(--cyan); }
.legend { display: flex; gap: 20px; flex-wrap: wrap; justify-content: center; margin-top: 28px; }
.legend-item { display: flex; align-items: center; gap: 6px; font-size: 0.65rem; color: var(--dim); letter-spacing: 0.05em; }
.legend-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
@keyframes fadeUp { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } }
@keyframes pulse-glow { 0%, 100% { box-shadow: 0 0 6px rgba(45,122,255,0.3); } 50% { box-shadow: 0 0 16px rgba(0,212,255,0.7); } }
.anim-1 { animation: fadeUp 0.5s ease both; animation-delay: 0.1s; }
.anim-2 { animation: fadeUp 0.5s ease both; animation-delay: 0.2s; }
.anim-3 { animation: fadeUp 0.5s ease both; animation-delay: 0.3s; }
.anim-4 { animation: fadeUp 0.5s ease both; animation-delay: 0.4s; }
.anim-5 { animation: fadeUp 0.5s ease both; animation-delay: 0.5s; }
.canvas-controls { position: fixed; bottom: 20px; right: 20px; display: flex; gap: 4px; z-index: 10; }
.canvas-controls button { width: 36px; height: 36px; border: 1px solid var(--border); background: var(--panel); color: var(--text); border-radius: 8px; font-family: 'JetBrains Mono', monospace; font-size: 0.85rem; font-weight: 700; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: border-color 0.2s, background 0.2s; }
.canvas-controls button:hover { border-color: var(--cyan); background: rgba(0,212,255,0.08); }
#zoom-reset { width: auto; padding: 0 12px; font-size: 0.6rem; text-transform: uppercase; letter-spacing: 0.08em; }
@media (max-width: 600px) {
.node-card { min-width: 120px; max-width: 100%; }
.drops-row { grid-template-columns: repeat(2, 1fr); }
}
</style>
</head>
<body>
<div id="canvas-container">
<div id="canvas" class="wrapper">
<h1 class="anim-1">Kubernetes <span class="highlight">Pod Networking</span></h1>
<div class="subtitle anim-1">Cluster → Node → Pod Network → Containers</div>
<div class="layer layer-1 anim-2">
<div class="layer-label layer-label-red">Internet · External Traffic</div>
<div class="ext-client">🌐 External Client<span>HTTPS on port 443</span></div>
<div class="connector">
<div class="connector-line"></div>
<div class="connector-arrow">▼</div>
<div class="connector-label">HTTPS :443</div>
</div>
<div class="layer layer-2 anim-3">
<div class="layer-label layer-label-orange">Kubernetes Cluster</div>
<div style="text-align:center;margin-bottom:8px;">
<div class="badge-mechanism">⚙️ kube-proxy iptables / IPVS rules</div>
</div>
<div class="connector">
<div class="connector-line"></div>
<div class="connector-arrow">▼</div>
<div class="connector-label">kube-proxy routing</div>
</div>
<div class="layer layer-3 anim-4">
<div class="layer-label layer-label-blue">Worker Node</div>
<div class="connector">
<div class="connector-line"></div>
<div class="connector-arrow">▼</div>
<div class="connector-label">CNI · CIDR 10.244.0.0/16</div>
</div>
<div class="layer layer-4 anim-5">
<div class="layer-label layer-label-cyan">Pod Network · CNI - Calico</div>
<div class="bus-line"></div>
<div class="drops-row">
<div class="drop"><div class="drop-line"></div></div>
<div class="drop"><div class="drop-line"></div></div>
<div class="drop"><div class="drop-line"></div></div>
<div class="drop"><div class="drop-line"></div></div>
<div class="drop"><div class="drop-line"></div></div>
</div>
<div class="node-row">
<div class="node-card">
<span class="node-emoji">🛡️</span>
<div class="node-name">nginx-ingress</div>
<div class="node-detail">10.244.0.5</div>
<div class="node-note">Ingress controller</div>
<div class="badge-port">:80 → 30080</div>
</div>
<div class="node-card">
<span class="node-emoji">⚙️</span>
<div class="node-name">api-server</div>
<div class="node-detail">10.244.0.12</div>
<div class="node-note">Backend API</div>
<div class="badge-port">:8080</div>
</div>
<div class="node-card">
<span class="node-emoji">🗄️</span>
<div class="node-name">postgres</div>
<div class="node-detail">10.244.0.20</div>
<div class="node-note">StatefulSet + PVC</div>
<div class="badge-port">:5432</div>
</div>
<div class="node-card">
<span class="node-emoji">⚡</span>
<div class="node-name">redis-cache</div>
<div class="node-detail">10.244.0.25</div>
<div class="node-note">internal only</div>
</div>
<div class="node-card">
<span class="node-emoji">📊</span>
<div class="node-name">prometheus</div>
<div class="node-detail">10.244.0.30</div>
<div class="node-note">Monitoring</div>
<div class="badge-port">:9090 → 30090</div>
</div>
</div>
<div class="callout">🔄 Pods communicate via <strong>virtual switch</strong> — direct pod-to-pod traffic within the CNI overlay</div>
</div>
</div>
</div>
</div>
<div class="legend">
<div class="legend-item"><div class="legend-dot" style="background:var(--red);box-shadow:0 0 6px var(--red)"></div>External</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--orange);box-shadow:0 0 6px var(--orange)"></div>Cluster boundary</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--blue);box-shadow:0 0 6px var(--blue)"></div>Worker node</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--cyan);box-shadow:0 0 6px var(--cyan)"></div>Pod network</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--green);box-shadow:0 0 6px var(--green)"></div>Exposed port</div>
</div>
</div>
</div>
<div class="canvas-controls">
<button id="zoom-in" title="Zoom in">+</button>
<button id="zoom-reset" title="Reset view">Reset</button>
<button id="zoom-out" title="Zoom out">−</button>
</div>
<script>
(function() {
var container = document.getElementById('canvas-container');
var canvas = document.getElementById('canvas');
var scale = 1, translateX = 0, translateY = 0, isPanning = false, startX, startY;
var MIN_SCALE = 0.2, MAX_SCALE = 3, ZOOM_STEP = 0.1;
function applyTransform() { canvas.style.transform = 'translate(' + translateX + 'px,' + translateY + 'px) scale(' + scale + ')'; }
function centerCanvas() {
var cw = container.clientWidth, ch = container.clientHeight;
var dw = canvas.scrollWidth * scale, dh = canvas.scrollHeight * scale;
translateX = Math.max(0, (cw - dw) / 2);
translateY = Math.max(20, (ch - dh) / 2);
applyTransform();
}
requestAnimationFrame(function() { requestAnimationFrame(centerCanvas); });
container.addEventListener('mousedown', function(e) { if (e.button !== 0) return; isPanning = true; startX = e.clientX - translateX; startY = e.clientY - translateY; container.classList.add('grabbing'); });
window.addEventListener('mousemove', function(e) { if (!isPanning) return; translateX = e.clientX - startX; translateY = e.clientY - startY; applyTransform(); });
window.addEventListener('mouseup', function() { isPanning = false; container.classList.remove('grabbing'); });
container.addEventListener('touchstart', function(e) { if (e.touches.length === 1) { isPanning = true; startX = e.touches[0].clientX - translateX; startY = e.touches[0].clientY - translateY; } }, { passive: true });
container.addEventListener('touchmove', function(e) { if (!isPanning || e.touches.length !== 1) return; translateX = e.touches[0].clientX - startX; translateY = e.touches[0].clientY - startY; applyTransform(); }, { passive: true });
container.addEventListener('touchend', function() { isPanning = false; });
container.addEventListener('wheel', function(e) {
e.preventDefault();
var rect = container.getBoundingClientRect(), mouseX = e.clientX - rect.left, mouseY = e.clientY - rect.top, prevScale = scale;
scale = e.deltaY < 0 ? Math.min(MAX_SCALE, scale + ZOOM_STEP) : Math.max(MIN_SCALE, scale - ZOOM_STEP);
var ratio = scale / prevScale;
translateX = mouseX - ratio * (mouseX - translateX); translateY = mouseY - ratio * (mouseY - translateY);
applyTransform();
}, { passive: false });
document.getElementById('zoom-in').addEventListener('click', function() { scale = Math.min(MAX_SCALE, scale + ZOOM_STEP); applyTransform(); });
document.getElementById('zoom-out').addEventListener('click', function() { scale = Math.max(MIN_SCALE, scale - ZOOM_STEP); applyTransform(); });
document.getElementById('zoom-reset').addEventListener('click', function() { scale = 1; centerCanvas(); });
window.addEventListener('keydown', function(e) {
if (e.key === '=' || e.key === '+') { scale = Math.min(MAX_SCALE, scale + ZOOM_STEP); applyTransform(); }
if (e.key === '-') { scale = Math.max(MIN_SCALE, scale - ZOOM_STEP); applyTransform(); }
if (e.key === '0') { scale = 1; centerCanvas(); }
});
})();
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OAuth 2.0 Authorization Code Flow</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Syne:wght@400;700;800&display=swap');
:root {
--bg: #0a0e1a;
--panel: #0f1528;
--border: #1e2d4a;
--blue: #2d7aff;
--cyan: #00d4ff;
--green: #00e5a0;
--orange: #ff8c42;
--red: #ff4d6a;
--text: #c8d8f0;
--dim: #4a6080;
}
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
min-height: 100vh;
font-family: 'JetBrains Mono', monospace;
color: var(--text);
padding: 0;
justify-content: flex-start;
align-items: flex-start;
overflow-x: hidden;
}
body::before {
content: '';
position: fixed;
inset: 0;
background-image:
linear-gradient(rgba(45,122,255,0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(45,122,255,0.04) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none;
z-index: 0;
}
#canvas-container { position: fixed; inset: 0; overflow: hidden; cursor: grab; z-index: 1; }
#canvas-container.grabbing { cursor: grabbing; }
#canvas { position: absolute; transform-origin: 0 0; }
.wrapper { position: relative; z-index: 1; width: 100%; max-width: 1100px; overflow-x: auto; min-width: 320px; padding: 40px 20px; }
h1 { font-family: 'Syne', sans-serif; font-size: 1.6rem; font-weight: 800; color: #fff; text-align: center; margin-bottom: 6px; }
h1 .highlight { color: var(--cyan); }
.subtitle { font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; font-weight: 400; text-transform: uppercase; letter-spacing: 0.1em; color: var(--dim); text-align: center; margin-bottom: 32px; }
.flow { display: flex; flex-direction: row; flex-wrap: wrap; align-items: center; justify-content: center; gap: 0; width: 100%; }
.step { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 16px 18px; min-width: 110px; max-width: 130px; flex: 1; text-align: center; transition: border-color 0.2s; }
.step:hover { border-color: var(--cyan); }
.step-number { width: 24px; height: 24px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 0.65rem; font-weight: 700; margin: 0 auto 8px; }
.step-emoji { font-size: 1.2rem; margin-bottom: 6px; }
.step-name { font-family: 'Syne', sans-serif; font-size: 0.78rem; font-weight: 700; color: #fff; margin-bottom: 4px; }
.step-desc { font-family: 'JetBrains Mono', monospace; font-size: 0.6rem; color: var(--dim); line-height: 1.4; }
.step-red .step-number { background: rgba(255,77,106,0.15); border: 1px solid var(--red); color: var(--red); }
.step-orange .step-number { background: rgba(255,140,66,0.15); border: 1px solid var(--orange); color: var(--orange); }
.step-blue .step-number { background: rgba(45,122,255,0.15); border: 1px solid var(--blue); color: var(--blue); }
.step-cyan .step-number { background: rgba(0,212,255,0.15); border: 1px solid var(--cyan); color: var(--cyan); }
.step-green .step-number { background: rgba(0,229,160,0.15); border: 1px solid var(--green); color: var(--green); }
.flow-arrow { display: flex; align-items: center; justify-content: center; width: 32px; flex-shrink: 0; }
.flow-arrow-line { width: 32px; height: 2px; background: linear-gradient(to right, var(--dim), var(--blue)); position: relative; }
.flow-arrow-line::after { content: '\25B6'; position: absolute; right: -8px; top: -7px; color: var(--blue); font-size: 0.65rem; }
.callout { background: rgba(0,212,255,0.05); border: 1px solid rgba(0,212,255,0.2); border-radius: 10px; padding: 12px 18px; font-size: 0.72rem; color: var(--text); text-align: center; margin-top: 24px; max-width: 600px; margin-left: auto; margin-right: auto; }
.callout strong { color: var(--cyan); }
.legend { display: flex; gap: 20px; flex-wrap: wrap; justify-content: center; margin-top: 28px; }
.legend-item { display: flex; align-items: center; gap: 6px; font-size: 0.65rem; color: var(--dim); letter-spacing: 0.05em; }
.legend-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
@keyframes fadeUp { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } }
.anim-1 { animation: fadeUp 0.5s ease both; animation-delay: 0.05s; }
.anim-2 { animation: fadeUp 0.5s ease both; animation-delay: 0.1s; }
.anim-3 { animation: fadeUp 0.5s ease both; animation-delay: 0.15s; }
.anim-4 { animation: fadeUp 0.5s ease both; animation-delay: 0.2s; }
.anim-5 { animation: fadeUp 0.5s ease both; animation-delay: 0.25s; }
.anim-6 { animation: fadeUp 0.5s ease both; animation-delay: 0.3s; }
.anim-7 { animation: fadeUp 0.5s ease both; animation-delay: 0.4s; }
.anim-8 { animation: fadeUp 0.5s ease both; animation-delay: 0.5s; }
.canvas-controls { position: fixed; bottom: 20px; right: 20px; display: flex; gap: 4px; z-index: 10; }
.canvas-controls button { width: 36px; height: 36px; border: 1px solid var(--border); background: var(--panel); color: var(--text); border-radius: 8px; font-family: 'JetBrains Mono', monospace; font-size: 0.85rem; font-weight: 700; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: border-color 0.2s, background 0.2s; }
.canvas-controls button:hover { border-color: var(--cyan); background: rgba(0,212,255,0.08); }
#zoom-reset { width: auto; padding: 0 12px; font-size: 0.6rem; text-transform: uppercase; letter-spacing: 0.08em; }
@media (max-width: 600px) {
.flow { flex-direction: column; }
.flow-arrow { transform: rotate(90deg); margin: 8px 0; }
.step { max-width: 100%; min-width: 0; width: 100%; }
}
</style>
</head>
<body>
<div id="canvas-container">
<div id="canvas" class="wrapper">
<h1 class="anim-1">OAuth 2.0 <span class="highlight">Authorization Code</span> Flow</h1>
<div class="subtitle anim-1">Client → Auth Server → Token Exchange → Resource</div>
<div class="flow">
<div class="step step-red anim-1"><div class="step-number">1</div><div class="step-emoji">👤</div><div class="step-name">User</div><div class="step-desc">Clicks "Login with Google"</div></div>
<div class="flow-arrow anim-2"><div class="flow-arrow-line"></div></div>
<div class="step step-red anim-2"><div class="step-number">2</div><div class="step-emoji">💻</div><div class="step-name">Client App</div><div class="step-desc">Redirects with client_id, scope</div></div>
<div class="flow-arrow anim-3"><div class="flow-arrow-line"></div></div>
<div class="step step-orange anim-3"><div class="step-number">3</div><div class="step-emoji">🔒</div><div class="step-name">Auth Server</div><div class="step-desc">Consent screen</div></div>
<div class="flow-arrow anim-4"><div class="flow-arrow-line"></div></div>
<div class="step step-orange anim-4"><div class="step-number">4</div><div class="step-emoji">↩️</div><div class="step-name">Redirect</div><div class="step-desc">Auth code to redirect_uri</div></div>
<div class="flow-arrow anim-5"><div class="flow-arrow-line"></div></div>
<div class="step step-blue anim-5"><div class="step-number">5</div><div class="step-emoji">🔄</div><div class="step-name">Token Exchange</div><div class="step-desc">code + client_secret (server-to-server)</div></div>
<div class="flow-arrow anim-6"><div class="flow-arrow-line"></div></div>
<div class="step step-blue anim-6"><div class="step-number">6</div><div class="step-emoji">🎫</div><div class="step-name">Access Token</div><div class="step-desc">access_token + refresh_token</div></div>
<div class="flow-arrow anim-7"><div class="flow-arrow-line"></div></div>
<div class="step step-cyan anim-7"><div class="step-number">7</div><div class="step-emoji">📡</div><div class="step-name">API Call</div><div class="step-desc">Bearer token in header</div></div>
<div class="flow-arrow anim-8"><div class="flow-arrow-line"></div></div>
<div class="step step-green anim-8"><div class="step-number">8</div><div class="step-emoji">🛡️</div><div class="step-name">Resource Server</div><div class="step-desc">Validates, returns data</div></div>
</div>
<div class="callout anim-8">🔒 Step 5 happens <strong>server-to-server</strong>. The client_secret never reaches the browser.</div>
<div class="legend">
<div class="legend-item"><div class="legend-dot" style="background:var(--red);box-shadow:0 0 6px var(--red)"></div>Client-side</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--orange);box-shadow:0 0 6px var(--orange)"></div>Authorization</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--blue);box-shadow:0 0 6px var(--blue)"></div>Token exchange</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--cyan);box-shadow:0 0 6px var(--cyan)"></div>API access</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--green);box-shadow:0 0 6px var(--green)"></div>Protected resource</div>
</div>
</div>
</div>
<div class="canvas-controls">
<button id="zoom-in" title="Zoom in">+</button>
<button id="zoom-reset" title="Reset view">Reset</button>
<button id="zoom-out" title="Zoom out">−</button>
</div>
<script>
(function() {
var container = document.getElementById('canvas-container');
var canvas = document.getElementById('canvas');
var scale = 1, translateX = 0, translateY = 0, isPanning = false, startX, startY;
var MIN_SCALE = 0.2, MAX_SCALE = 3, ZOOM_STEP = 0.1;
function applyTransform() { canvas.style.transform = 'translate(' + translateX + 'px,' + translateY + 'px) scale(' + scale + ')'; }
function centerCanvas() {
var cw = container.clientWidth, ch = container.clientHeight;
var dw = canvas.scrollWidth * scale, dh = canvas.scrollHeight * scale;
translateX = Math.max(0, (cw - dw) / 2);
translateY = Math.max(20, (ch - dh) / 2);
applyTransform();
}
requestAnimationFrame(function() { requestAnimationFrame(centerCanvas); });
container.addEventListener('mousedown', function(e) { if (e.button !== 0) return; isPanning = true; startX = e.clientX - translateX; startY = e.clientY - translateY; container.classList.add('grabbing'); });
window.addEventListener('mousemove', function(e) { if (!isPanning) return; translateX = e.clientX - startX; translateY = e.clientY - startY; applyTransform(); });
window.addEventListener('mouseup', function() { isPanning = false; container.classList.remove('grabbing'); });
container.addEventListener('touchstart', function(e) { if (e.touches.length === 1) { isPanning = true; startX = e.touches[0].clientX - translateX; startY = e.touches[0].clientY - translateY; } }, { passive: true });
container.addEventListener('touchmove', function(e) { if (!isPanning || e.touches.length !== 1) return; translateX = e.touches[0].clientX - startX; translateY = e.touches[0].clientY - startY; applyTransform(); }, { passive: true });
container.addEventListener('touchend', function() { isPanning = false; });
container.addEventListener('wheel', function(e) {
e.preventDefault();
var rect = container.getBoundingClientRect(), mouseX = e.clientX - rect.left, mouseY = e.clientY - rect.top, prevScale = scale;
scale = e.deltaY < 0 ? Math.min(MAX_SCALE, scale + ZOOM_STEP) : Math.max(MIN_SCALE, scale - ZOOM_STEP);
var ratio = scale / prevScale;
translateX = mouseX - ratio * (mouseX - translateX); translateY = mouseY - ratio * (mouseY - translateY);
applyTransform();
}, { passive: false });
document.getElementById('zoom-in').addEventListener('click', function() { scale = Math.min(MAX_SCALE, scale + ZOOM_STEP); applyTransform(); });
document.getElementById('zoom-out').addEventListener('click', function() { scale = Math.max(MIN_SCALE, scale - ZOOM_STEP); applyTransform(); });
document.getElementById('zoom-reset').addEventListener('click', function() { scale = 1; centerCanvas(); });
window.addEventListener('keydown', function(e) {
if (e.key === '=' || e.key === '+') { scale = Math.min(MAX_SCALE, scale + ZOOM_STEP); applyTransform(); }
if (e.key === '-') { scale = Math.max(MIN_SCALE, scale - ZOOM_STEP); applyTransform(); }
if (e.key === '0') { scale = 1; centerCanvas(); }
});
})();
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SaaS Microservices Architecture</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Syne:wght@400;700;800&display=swap');
:root {
--bg: #0a0e1a;
--panel: #0f1528;
--border: #1e2d4a;
--blue: #2d7aff;
--cyan: #00d4ff;
--green: #00e5a0;
--orange: #ff8c42;
--red: #ff4d6a;
--text: #c8d8f0;
--dim: #4a6080;
}
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg);
min-height: 100vh;
font-family: 'JetBrains Mono', monospace;
color: var(--text);
padding: 0;
justify-content: flex-start;
align-items: flex-start;
overflow-x: hidden;
}
body::before {
content: '';
position: fixed;
inset: 0;
background-image:
linear-gradient(rgba(45,122,255,0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(45,122,255,0.04) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none;
z-index: 0;
}
#canvas-container { position: fixed; inset: 0; overflow: hidden; cursor: grab; z-index: 1; }
#canvas-container.grabbing { cursor: grabbing; }
#canvas { position: absolute; transform-origin: 0 0; }
.wrapper { position: relative; z-index: 1; width: 100%; max-width: 900px; overflow-x: auto; min-width: 320px; padding: 40px 20px; }
h1 { font-family: 'Syne', sans-serif; font-size: 1.6rem; font-weight: 800; color: #fff; text-align: center; margin-bottom: 6px; }
h1 .highlight { color: var(--cyan); }
.subtitle { font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; font-weight: 400; text-transform: uppercase; letter-spacing: 0.1em; color: var(--dim); text-align: center; margin-bottom: 32px; }
.hub-container { position: relative; width: 100%; min-height: 520px; }
.hub-center { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); min-width: 160px; z-index: 2; background: var(--panel); border: 2px solid var(--blue); border-radius: 16px; padding: 20px 24px; text-align: center; box-shadow: 0 0 30px rgba(45,122,255,0.15); }
.hub-center:hover { border-color: var(--cyan); }
.hub-center .node-emoji { font-size: 1.8rem; margin-bottom: 8px; }
.hub-center .node-name { font-family: 'Syne', sans-serif; font-size: 1rem; font-weight: 800; color: #fff; margin-bottom: 4px; }
.hub-center .node-detail { font-family: 'JetBrains Mono', monospace; font-size: 0.7rem; color: var(--cyan); }
.spoke { position: absolute; z-index: 2; background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 14px 16px; min-width: 130px; max-width: 150px; text-align: center; transition: border-color 0.2s; }
.spoke:hover { border-color: var(--cyan); }
.spoke .node-emoji { font-size: 1.2rem; margin-bottom: 6px; }
.spoke .node-name { font-family: 'Syne', sans-serif; font-size: 0.78rem; font-weight: 700; color: #fff; margin-bottom: 4px; }
.spoke .node-detail { font-family: 'JetBrains Mono', monospace; font-size: 0.62rem; color: var(--dim); line-height: 1.4; }
.spoke-badge { display: inline-block; border-radius: 4px; padding: 2px 6px; font-size: 0.58rem; font-weight: 700; margin-top: 4px; letter-spacing: 0.05em; }
.badge-critical { background: rgba(255,77,106,0.12); color: var(--red); border: 1px solid rgba(255,77,106,0.3); }
.badge-money { background: rgba(0,229,160,0.12); color: var(--green); border: 1px solid rgba(0,229,160,0.3); }
.badge-async { background: rgba(255,140,66,0.12); color: var(--orange); border: 1px solid rgba(255,140,66,0.3); }
.badge-data { background: rgba(0,212,255,0.12); color: var(--cyan); border: 1px solid rgba(0,212,255,0.3); }
.badge-core { background: rgba(45,122,255,0.12); color: var(--blue); border: 1px solid rgba(45,122,255,0.3); }
.hub-svg { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 1; pointer-events: none; }
.info-panels { display: flex; flex-wrap: wrap; gap: 16px; justify-content: center; margin-top: 24px; }
.info-panel { background: rgba(0,212,255,0.05); border: 1px solid rgba(0,212,255,0.2); border-radius: 10px; padding: 14px 18px; flex: 1; min-width: 200px; max-width: 380px; }
.info-panel-title { font-family: 'JetBrains Mono', monospace; font-size: 0.62rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.15em; color: var(--cyan); margin-bottom: 8px; }
.info-panel-items { display: flex; flex-wrap: wrap; gap: 6px; }
.info-tag { background: var(--panel); border: 1px solid var(--border); border-radius: 4px; padding: 3px 8px; font-size: 0.6rem; color: var(--text); }
.callout { background: rgba(0,212,255,0.05); border: 1px solid rgba(0,212,255,0.2); border-radius: 10px; padding: 12px 18px; font-size: 0.72rem; color: var(--text); text-align: center; margin-top: 16px; }
.callout strong { color: var(--cyan); }
.legend { display: flex; gap: 20px; flex-wrap: wrap; justify-content: center; margin-top: 28px; }
.legend-item { display: flex; align-items: center; gap: 6px; font-size: 0.65rem; color: var(--dim); letter-spacing: 0.05em; }
.legend-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
@keyframes fadeUp { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } }
.anim-1 { animation: fadeUp 0.5s ease both; animation-delay: 0.1s; }
.anim-2 { animation: fadeUp 0.5s ease both; animation-delay: 0.2s; }
.anim-3 { animation: fadeUp 0.5s ease both; animation-delay: 0.3s; }
.anim-4 { animation: fadeUp 0.5s ease both; animation-delay: 0.4s; }
.anim-5 { animation: fadeUp 0.5s ease both; animation-delay: 0.5s; }
.canvas-controls { position: fixed; bottom: 20px; right: 20px; display: flex; gap: 4px; z-index: 10; }
.canvas-controls button { width: 36px; height: 36px; border: 1px solid var(--border); background: var(--panel); color: var(--text); border-radius: 8px; font-family: 'JetBrains Mono', monospace; font-size: 0.85rem; font-weight: 700; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: border-color 0.2s, background 0.2s; }
.canvas-controls button:hover { border-color: var(--cyan); background: rgba(0,212,255,0.08); }
#zoom-reset { width: auto; padding: 0 12px; font-size: 0.6rem; text-transform: uppercase; letter-spacing: 0.08em; }
</style>
</head>
<body>
<div id="canvas-container">
<div id="canvas" class="wrapper">
<h1 class="anim-1">SaaS <span class="highlight">Microservices</span> Architecture</h1>
<div class="subtitle anim-1">API Gateway · 8 Services · Event-Driven</div>
<div class="hub-container anim-2">
<svg class="hub-svg" id="hub-lines"></svg>
<div class="hub-center" id="hub-node">
<div class="node-emoji">🌐</div>
<div class="node-name">API Gateway</div>
<div class="node-detail">Kong / AWS · 3,200 req/s</div>
</div>
<div class="spoke" id="spoke-0"><div class="node-emoji">🔒</div><div class="node-name">Auth Service</div><div class="node-detail">JWT + OAuth2, sessions</div><div class="spoke-badge badge-critical">CRITICAL</div></div>
<div class="spoke" id="spoke-1"><div class="node-emoji">💳</div><div class="node-name">Billing</div><div class="node-detail">Stripe, invoicing, metering</div><div class="spoke-badge badge-money">$$$</div></div>
<div class="spoke" id="spoke-2"><div class="node-emoji">🔔</div><div class="node-name">Notifications</div><div class="node-detail">SES, FCM, Twilio</div><div class="spoke-badge badge-async">ASYNC</div></div>
<div class="spoke" id="spoke-3"><div class="node-emoji">📊</div><div class="node-name">Analytics</div><div class="node-detail">ClickHouse, event tracking</div><div class="spoke-badge badge-data">READ-HEAVY</div></div>
<div class="spoke" id="spoke-4"><div class="node-emoji">👤</div><div class="node-name">User Service</div><div class="node-detail">CRUD, profiles, RBAC</div><div class="spoke-badge badge-core">POSTGRESQL</div></div>
<div class="spoke" id="spoke-5"><div class="node-emoji">📦</div><div class="node-name">Storage</div><div class="node-detail">S3, file upload, CDN</div><div class="spoke-badge badge-data">BLOB</div></div>
<div class="spoke" id="spoke-6"><div class="node-emoji">🔍</div><div class="node-name">Search</div><div class="node-detail">Elasticsearch, faceted</div><div class="spoke-badge badge-core">INDEXED</div></div>
<div class="spoke" id="spoke-7"><div class="node-emoji">⚡</div><div class="node-name">WebSocket</div><div class="node-detail">Real-time, presence, collab</div><div class="spoke-badge badge-async">WS</div></div>
</div>
<div class="info-panels anim-4">
<div class="info-panel">
<div class="info-panel-title">Infrastructure</div>
<div class="info-panel-items">
<span class="info-tag">Kubernetes (EKS)</span>
<span class="info-tag">Terraform</span>
<span class="info-tag">GitHub Actions</span>
<span class="info-tag">RabbitMQ</span>
<span class="info-tag">Kafka</span>
</div>
</div>
<div class="info-panel">
<div class="info-panel-title">Observability</div>
<div class="info-panel-items">
<span class="info-tag">Datadog</span>
<span class="info-tag">ELK Stack</span>
<span class="info-tag">Sentry</span>
<span class="info-tag">OpenTelemetry</span>
</div>
</div>
</div>
<div class="callout anim-5">🔄 Services never call each other directly. Sync via <strong>gateway</strong>, async via <strong>message queue</strong>.</div>
<div class="legend anim-5">
<div class="legend-item"><div class="legend-dot" style="background:var(--blue);box-shadow:0 0 6px var(--blue)"></div>Gateway</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--cyan);box-shadow:0 0 6px var(--cyan)"></div>Core services</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--green);box-shadow:0 0 6px var(--green)"></div>Data services</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--orange);box-shadow:0 0 6px var(--orange)"></div>External integrations</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--red);box-shadow:0 0 6px var(--red)"></div>Critical path</div>
</div>
</div>
</div>
<div class="canvas-controls">
<button id="zoom-in" title="Zoom in">+</button>
<button id="zoom-reset" title="Reset view">Reset</button>
<button id="zoom-out" title="Zoom out">−</button>
</div>
<script>
// Position spokes with trigonometry
(function() {
var hubContainer = document.querySelector('.hub-container');
var spokeCount = 8;
var radius = 200;
function positionSpokes() {
var containerW = hubContainer.offsetWidth;
var containerH = hubContainer.offsetHeight;
var cx = containerW / 2;
var cy = containerH / 2;
var svgEl = document.getElementById('hub-lines');
svgEl.innerHTML = '';
for (var i = 0; i < spokeCount; i++) {
var angle = (2 * Math.PI / spokeCount) * i - Math.PI / 2;
var spoke = document.getElementById('spoke-' + i);
var x = cx + radius * Math.cos(angle);
var y = cy + radius * Math.sin(angle);
spoke.style.left = (x - spoke.offsetWidth / 2) + 'px';
spoke.style.top = (y - spoke.offsetHeight / 2) + 'px';
var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', cx); line.setAttribute('y1', cy);
line.setAttribute('x2', x); line.setAttribute('y2', y);
line.setAttribute('stroke', 'rgba(45,122,255,0.4)');
line.setAttribute('stroke-width', '1.5');
line.setAttribute('stroke-dasharray', '4 4');
var anim = document.createElementNS('http://www.w3.org/2000/svg', 'animate');
anim.setAttribute('attributeName', 'stroke-dashoffset');
anim.setAttribute('from', '0'); anim.setAttribute('to', '-8');
anim.setAttribute('dur', '0.6s'); anim.setAttribute('repeatCount', 'indefinite');
line.appendChild(anim);
svgEl.appendChild(line);
}
}
requestAnimationFrame(function() { requestAnimationFrame(positionSpokes); });
window.addEventListener('resize', positionSpokes);
})();
// Infinite Canvas
(function() {
var container = document.getElementById('canvas-container');
var canvas = document.getElementById('canvas');
var scale = 1, translateX = 0, translateY = 0, isPanning = false, startX, startY;
var MIN_SCALE = 0.2, MAX_SCALE = 3, ZOOM_STEP = 0.1;
function applyTransform() { canvas.style.transform = 'translate(' + translateX + 'px,' + translateY + 'px) scale(' + scale + ')'; }
function centerCanvas() {
var cw = container.clientWidth, ch = container.clientHeight;
var dw = canvas.scrollWidth * scale, dh = canvas.scrollHeight * scale;
translateX = Math.max(0, (cw - dw) / 2);
translateY = Math.max(20, (ch - dh) / 2);
applyTransform();
}
setTimeout(function() { centerCanvas(); }, 100);
container.addEventListener('mousedown', function(e) { if (e.button !== 0) return; isPanning = true; startX = e.clientX - translateX; startY = e.clientY - translateY; container.classList.add('grabbing'); });
window.addEventListener('mousemove', function(e) { if (!isPanning) return; translateX = e.clientX - startX; translateY = e.clientY - startY; applyTransform(); });
window.addEventListener('mouseup', function() { isPanning = false; container.classList.remove('grabbing'); });
container.addEventListener('touchstart', function(e) { if (e.touches.length === 1) { isPanning = true; startX = e.touches[0].clientX - translateX; startY = e.touches[0].clientY - translateY; } }, { passive: true });
container.addEventListener('touchmove', function(e) { if (!isPanning || e.touches.length !== 1) return; translateX = e.touches[0].clientX - startX; translateY = e.touches[0].clientY - startY; applyTransform(); }, { passive: true });
container.addEventListener('touchend', function() { isPanning = false; });
container.addEventListener('wheel', function(e) {
e.preventDefault();
var rect = container.getBoundingClientRect(), mouseX = e.clientX - rect.left, mouseY = e.clientY - rect.top, prevScale = scale;
scale = e.deltaY < 0 ? Math.min(MAX_SCALE, scale + ZOOM_STEP) : Math.max(MIN_SCALE, scale - ZOOM_STEP);
var ratio = scale / prevScale;
translateX = mouseX - ratio * (mouseX - translateX); translateY = mouseY - ratio * (mouseY - translateY);
applyTransform();
}, { passive: false });
document.getElementById('zoom-in').addEventListener('click', function() { scale = Math.min(MAX_SCALE, scale + ZOOM_STEP); applyTransform(); });
document.getElementById('zoom-out').addEventListener('click', function() { scale = Math.max(MIN_SCALE, scale - ZOOM_STEP); applyTransform(); });
document.getElementById('zoom-reset').addEventListener('click', function() { scale = 1; centerCanvas(); });
window.addEventListener('keydown', function(e) {
if (e.key === '=' || e.key === '+') { scale = Math.min(MAX_SCALE, scale + ZOOM_STEP); applyTransform(); }
if (e.key === '-') { scale = Math.max(MIN_SCALE, scale - ZOOM_STEP); applyTransform(); }
if (e.key === '0') { scale = 1; centerCanvas(); }
});
})();
</script>
</body>
</html>
Example: SaaS Microservices Architecture
Input: A JSON file describing a SaaS backend architecture
Topology: hub-and-spoke
Central node: API Gateway (Kong / AWS) — 3,200 req/s
Spoke nodes: 1. Auth Service — JWT + OAuth2, sessions — CRITICAL 2. Billing — Stripe, invoicing, metering — $$$ 3. Notifications — SES, FCM, Twilio — ASYNC 4. Analytics — ClickHouse, event tracking — READ-HEAVY 5. User Service — CRUD, profiles, RBAC — POSTGRESQL 6. Storage — S3, file upload, CDN — BLOB 7. Search — Elasticsearch, faceted — INDEXED 8. WebSocket — Real-time, presence, collab — WS
Info panels:
- Infrastructure: Kubernetes (EKS), Terraform, GitHub Actions, RabbitMQ, Kafka
- Observability: Datadog, ELK Stack, Sentry, OpenTelemetry
Callout: "Services never call each other directly. Sync via gateway, async via message queue."
Legend: blue=Gateway, cyan=Core services, green=Data services, orange=External integrations, red=Critical path
Infinite Canvas — Pan & Zoom
Every diagram gets an infinite canvas that lets the user navigate freely, like Miro or draw.io. This is implemented with vanilla JS — no libraries.
HTML Structure
Wrap the entire diagram inside a canvas container:
<body>
<!-- Grid background stays fixed via body::before -->
<div id="canvas-container">
<div id="canvas" class="wrapper">
<!-- ALL diagram content goes here -->
</div>
</div>
<div class="canvas-controls">
<button id="zoom-in" title="Zoom in">+</button>
<button id="zoom-reset" title="Reset view">Reset</button>
<button id="zoom-out" title="Zoom out">−</button>
</div>
</body>CSS
#canvas-container {
position: fixed;
inset: 0;
overflow: hidden;
cursor: grab;
z-index: 1;
}
#canvas-container.grabbing {
cursor: grabbing;
}
#canvas {
position: absolute;
transform-origin: 0 0;
/* Remove centering from body — the canvas handles positioning */
/* width and max-width are kept for content sizing */
}
/* Override body to not center — canvas does it */
body {
padding: 0;
justify-content: flex-start;
align-items: flex-start;
}
/* Controls */
.canvas-controls {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
gap: 4px;
z-index: 10;
}
.canvas-controls button {
width: 36px;
height: 36px;
border: 1px solid var(--border);
background: var(--panel);
color: var(--text);
border-radius: 8px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.85rem;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: border-color 0.2s, background 0.2s;
}
.canvas-controls button:hover {
border-color: var(--cyan);
background: rgba(0,212,255,0.08);
}
#zoom-reset {
width: auto;
padding: 0 12px;
font-size: 0.6rem;
text-transform: uppercase;
letter-spacing: 0.08em;
}JavaScript
Place this at the end of the <script> block, after any topology-specific JS (like hub-and-spoke coordinate calculations).
// Infinite Canvas — Pan & Zoom
(function() {
const container = document.getElementById('canvas-container');
const canvas = document.getElementById('canvas');
let scale = 1;
let translateX = 0;
let translateY = 0;
let isPanning = false;
let startX, startY;
const MIN_SCALE = 0.2;
const MAX_SCALE = 3;
const ZOOM_STEP = 0.1;
function applyTransform() {
canvas.style.transform = `translate(${translateX}px, ${translateY}px) scale(${scale})`;
}
function centerCanvas() {
const cw = container.clientWidth;
const ch = container.clientHeight;
const dw = canvas.scrollWidth * scale;
const dh = canvas.scrollHeight * scale;
translateX = (cw - dw) / 2;
translateY = Math.max(20, (ch - dh) / 2);
applyTransform();
}
// Fit entire diagram into the viewport, scaling down if needed
function fitToView() {
const cw = container.clientWidth;
const ch = container.clientHeight;
const dw = canvas.scrollWidth;
const dh = canvas.scrollHeight;
const scaleX = (cw - 40) / dw;
const scaleY = (ch - 40) / dh;
scale = Math.min(scaleX, scaleY, 1);
scale = Math.max(scale, MIN_SCALE);
centerCanvas();
}
// Initial fit after content renders
requestAnimationFrame(() => {
requestAnimationFrame(fitToView);
});
// Pan — mouse
container.addEventListener('mousedown', (e) => {
if (e.button !== 0) return; // left click only
isPanning = true;
startX = e.clientX - translateX;
startY = e.clientY - translateY;
container.classList.add('grabbing');
});
window.addEventListener('mousemove', (e) => {
if (!isPanning) return;
translateX = e.clientX - startX;
translateY = e.clientY - startY;
applyTransform();
});
window.addEventListener('mouseup', () => {
isPanning = false;
container.classList.remove('grabbing');
});
// Pan — touch
container.addEventListener('touchstart', (e) => {
if (e.touches.length === 1) {
isPanning = true;
startX = e.touches[0].clientX - translateX;
startY = e.touches[0].clientY - translateY;
}
}, { passive: true });
container.addEventListener('touchmove', (e) => {
if (!isPanning || e.touches.length !== 1) return;
translateX = e.touches[0].clientX - startX;
translateY = e.touches[0].clientY - startY;
applyTransform();
}, { passive: true });
container.addEventListener('touchend', () => { isPanning = false; });
// Zoom — scroll wheel (zoom toward cursor)
container.addEventListener('wheel', (e) => {
e.preventDefault();
const rect = container.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const mouseY = e.clientY - rect.top;
const prevScale = scale;
if (e.deltaY < 0) {
scale = Math.min(MAX_SCALE, scale + ZOOM_STEP);
} else {
scale = Math.max(MIN_SCALE, scale - ZOOM_STEP);
}
// Adjust translate so zoom centers on cursor position
const ratio = scale / prevScale;
translateX = mouseX - ratio * (mouseX - translateX);
translateY = mouseY - ratio * (mouseY - translateY);
applyTransform();
}, { passive: false });
// Button controls
document.getElementById('zoom-in').addEventListener('click', () => {
scale = Math.min(MAX_SCALE, scale + ZOOM_STEP);
applyTransform();
});
document.getElementById('zoom-out').addEventListener('click', () => {
scale = Math.max(MIN_SCALE, scale - ZOOM_STEP);
applyTransform();
});
document.getElementById('zoom-reset').addEventListener('click', () => {
fitToView();
});
// Keyboard shortcuts
window.addEventListener('keydown', (e) => {
if (e.key === '=' || e.key === '+') { scale = Math.min(MAX_SCALE, scale + ZOOM_STEP); applyTransform(); }
if (e.key === '-') { scale = Math.max(MIN_SCALE, scale - ZOOM_STEP); applyTransform(); }
if (e.key === '0') { fitToView(); }
});
})();Behavior
- Pan: click and drag anywhere on the canvas (cursor changes to grab/grabbing)
- Zoom: scroll wheel zooms toward cursor position
- Touch: single-finger drag to pan (mobile/tablet)
- Buttons: +/−/Reset controls in bottom-right corner
- Keyboard:
+zoom in,-zoom out,0fit to view - Initial state: diagram is auto-scaled to fit entirely within the viewport (scales down if content is wider/taller than viewport, stays at scale 1 if it fits)
Integration Notes
- The
.wrapperclass now lives on#canvasinstead of being a direct child of body bodyno longer usesjustify-content: center— the canvas JS handles centering- The grid background (
body::before) stays fixed and is NOT affected by pan/zoom — this creates a nice parallax effect - All existing topology JS (hub-and-spoke coordinate calculation) runs before the canvas JS
- The canvas system works identically across all themes
Components & Animations
Node Card (all topologies)
.node-card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 12px;
padding: 16px 20px;
transition: border-color 0.2s;
}
.node-card:hover { border-color: var(--cyan); }Internal structure (top to bottom): 1. Emoji icon: display: block, font-size: 1.4rem, margin-bottom: 8px (omit if node count >= 7) 2. .node-name: Syne 700, 0.85rem, white 3. .node-detail: JetBrains Mono, 0.7rem, var(--cyan) — for IPs, dates, roles, or any metadata 4. Port badge, status badge, or descriptive note
Port / Status Badge
.badge-port {
display: inline-block;
background: rgba(0,229,160,0.12);
color: var(--green);
border: 1px solid rgba(0,229,160,0.3);
border-radius: 4px;
padding: 2px 6px;
font-size: 0.62rem;
}For internal-only nodes: <div style="font-size:0.65rem; color:var(--dim)">internal only</div>
Layer Label
<div class="layer-label">Layer Name · metadata</div>.layer-label::before {
content: '';
display: inline-block;
width: 6px; height: 6px;
border-radius: 50%;
background: [accent-color];
box-shadow: 0 0 8px [accent-color];
margin-right: 8px;
vertical-align: middle;
}Callout / Info Box
.callout {
background: rgba(0,212,255,0.05);
border: 1px solid rgba(0,212,255,0.2);
border-radius: 10px;
padding: 12px 18px;
font-size: 0.72rem;
color: var(--text);
text-align: center;
margin-top: 12px;
}
.callout strong { color: var(--cyan); }Warning / Mechanism Badge
.badge-mechanism {
display: inline-flex;
align-items: center;
gap: 6px;
background: rgba(255,140,66,0.12);
border: 1px solid rgba(255,140,66,0.35);
color: var(--orange);
border-radius: 8px;
padding: 6px 14px;
font-size: 0.72rem;
font-weight: 600;
}External Client Card
.ext-client {
background: rgba(255,77,106,0.08);
border: 1px solid rgba(255,77,106,0.25);
border-radius: 10px;
padding: 10px 18px;
font-size: 0.75rem;
color: var(--red);
text-align: center;
}
.ext-client span {
display: block;
color: var(--dim);
font-size: 0.62rem;
margin-top: 2px;
}---
Animations
Entrance (all topologies)
@keyframes fadeUp {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}Apply: animation: fadeUp 0.5s ease both; animation-delay: calc(0.1s * [index]); Maximum delay: 0.5s. Do not animate more than 8 elements individually.
Bus line pulse (nested only)
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 6px rgba(45,122,255,0.3); }
50% { box-shadow: 0 0 16px rgba(0,212,255,0.7); }
}animation: pulse-glow 2.5s ease-in-out infinite;
SVG dash (hub-and-spoke only)
<animate attributeName="stroke-dashoffset" from="0" to="-8" dur="0.6s" repeatCount="indefinite"/>No other animations. No bounce, spin, scale, or color-shift effects.
---
Legend
Render at the bottom of .wrapper, outside all layer containers.
.legend {
display: flex;
gap: 20px;
flex-wrap: wrap;
justify-content: center;
margin-top: 28px;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.65rem;
color: var(--dim);
letter-spacing: 0.05em;
}
.legend-dot {
width: 8px; height: 8px;
border-radius: 50%;
flex-shrink: 0;
}One entry per accent color used. Dot box-shadow must match the accent color.
Example: <div class="legend-dot" style="background:var(--red);box-shadow:0 0 6px var(--red)"></div>
Mandatory Layout Safety Rules
These rules apply to EVERY topology. No exceptions.
1. Wrapper Overflow
.wrapper { overflow-x: auto; min-width: 320px; }Never let content escape the wrapper horizontally.
2. Flex Wrap Required
All flex rows must have flex-wrap: wrap unless the total content width is provably under 320px.
Exception: Left-to-right flow containers use flex-wrap: nowrap with width: max-content. The infinite canvas (pan & zoom) handles navigation — wrapping breaks the sequential flow.
3. No Absolute Positioning for Repeated Elements
Never use position: absolute to place cards, nodes, or labels — UNLESS the topology is hub-and-spoke (which requires it for coordinate calculation). For nested and left-to-right: use flex/grid only.
4. Drop Lines Use CSS Grid
(Nested topology only) grid-template-columns: repeat(N, 1fr) where N = exact node count. This is the only way to guarantee alignment with cards below.
5. Minimum Layer Height
All layers must have min-height: 80px. Prevents visual collapse.
6. Long Text Handling
If any node name or label exceeds 12 characters:
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;7. Node Count Scaling (nested, hub-and-spoke, grid, tree, funnel, comparison)
| Node count | max-width | font-size | Emoji |
|---|---|---|---|
| 2-4 nodes | 180px | default | visible |
| 5-6 nodes | 150px | 0.78rem | visible |
| 7+ nodes | 130px | 0.72rem | hidden |
Apply automatically based on declared node count.
7b. Step Count Scaling (left-to-right only)
Left-to-right flows scale step boxes and arrows based on step count. Canvas width is automatic (width: max-content). See topology-layouts.md for the full scaling table. Summary:
| Step count | Step max-width | Arrow width |
|---|---|---|
| 2-4 steps | 160px | 32px |
| 5-6 steps | 130px | 24px |
| 7-8 steps | 110px | 20px |
| 9+ steps | 100px | 16px |
Apply automatically. Use flex: 0 1 auto on step boxes (content-driven sizing). Set #canvas to width: max-content for left-to-right topology.
8. SVG Restriction
SVG lines are for hub-and-spoke topology ONLY. Never use SVG to draw lines in nested or left-to-right layouts. Never use CSS box-shadow or border tricks to simulate diagonal lines.
9. Z-Index Discipline
| Element | z-index |
|---|---|
| body::before (grid background) | 0 |
| .wrapper | 1 |
| SVG lines (hub-and-spoke) | 1 |
| Cards / nodes | 2 |
| Tooltips / badges | 3 |
10. Responsive Minimum
The diagram must not break at viewport width 360px. All content must reflow without horizontal scroll at that width.
Output Requirements
- Single
.htmlfile - All CSS in one
<style>block - All JS (if any) in one
<script>block - No external dependencies except Google Fonts (via
@importin CSS) - No CSS frameworks (Tailwind, Bootstrap, etc.)
- No JS libraries (jQuery, D3, etc.) — vanilla JS for hub-and-spoke coordinate calculation only
- Valid HTML5 —
<!DOCTYPE html>must be the first line - Must render correctly when opened directly in a browser without a local server
Theme System
The diagram supports 5 themes. The user can request a theme by name, or you pick the best default based on context (dark for technical, light for presentations, corporate for business docs).
If the user doesn't specify a theme, default to dark.
How Themes Work
All themes use the same CSS variable names. Only the :root values change. The rest of the CSS (layout, typography, components) stays identical. Apply the theme by replacing the :root block.
The background grid pattern opacity and card styles adapt per theme via the variables below.
---
Theme: Dark (default)
Best for: technical diagrams, developer docs, README screenshots.
:root {
--bg: #0a0e1a;
--panel: #0f1528;
--border: #1e2d4a;
--blue: #2d7aff;
--cyan: #00d4ff;
--green: #00e5a0;
--orange: #ff8c42;
--red: #ff4d6a;
--text: #c8d8f0;
--dim: #4a6080;
--grid-opacity: 0.04;
--title-color: #ffffff;
}Grid: rgba(45,122,255, var(--grid-opacity))
---
Theme: Light
Best for: presentations, documentation sites, print-friendly output.
:root {
--bg: #f8f9fc;
--panel: #ffffff;
--border: #e2e6f0;
--blue: #2563eb;
--cyan: #0891b2;
--green: #059669;
--orange: #d97706;
--red: #dc2626;
--text: #334155;
--dim: #94a3b8;
--grid-opacity: 0.06;
--title-color: #0f172a;
}Grid: rgba(37,99,235, var(--grid-opacity))
Additional overrides for light theme:
.node-card { box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
.node-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
.callout { background: rgba(8,145,178,0.05); border-color: rgba(8,145,178,0.2); }---
Theme: Corporate
Best for: business plans, pitch decks, stakeholder presentations.
:root {
--bg: #f1f5f9;
--panel: #ffffff;
--border: #cbd5e1;
--blue: #1e40af;
--cyan: #0369a1;
--green: #15803d;
--orange: #b45309;
--red: #b91c1c;
--text: #1e293b;
--dim: #64748b;
--grid-opacity: 0.03;
--title-color: #0f172a;
}Grid: rgba(30,64,175, var(--grid-opacity))
Additional overrides:
body::before { display: none; } /* no grid background — cleaner look */
.node-card { border-radius: 8px; box-shadow: 0 1px 2px rgba(0,0,0,0.06); }
h1 { font-size: 1.4rem; }---
Theme: Neon
Best for: creative projects, gaming, cyberpunk aesthetics, social media screenshots.
:root {
--bg: #0a0a0f;
--panel: #12121a;
--border: #2a2a3d;
--blue: #6366f1;
--cyan: #22d3ee;
--green: #34d399;
--orange: #fb923c;
--red: #f43f5e;
--text: #e2e8f0;
--dim: #6b7280;
--grid-opacity: 0.06;
--title-color: #ffffff;
}Grid: rgba(99,102,241, var(--grid-opacity))
Additional overrides:
.node-card:hover { box-shadow: 0 0 20px rgba(34,211,238,0.2); }
.bus-line { background: linear-gradient(to right, transparent, var(--blue), var(--cyan), #a78bfa, var(--blue), transparent); }
h1 .highlight { text-shadow: 0 0 20px rgba(34,211,238,0.5); }---
Theme: Minimal
Best for: clean documentation, technical specs, when the diagram should fade into the background.
:root {
--bg: #ffffff;
--panel: #fafafa;
--border: #e5e5e5;
--blue: #3b82f6;
--cyan: #06b6d4;
--green: #10b981;
--orange: #f59e0b;
--red: #ef4444;
--text: #404040;
--dim: #a3a3a3;
--grid-opacity: 0;
--title-color: #171717;
}Additional overrides:
body::before { display: none; } /* no grid */
.node-card { border-radius: 6px; padding: 12px 16px; }
.node-card:hover { border-color: var(--blue); } /* blue instead of cyan */
@keyframes fadeUp { from { opacity: 0; } to { opacity: 1; } } /* no translateY — subtler */---
Applying a Theme
In the SKILL.md workflow, the theme is selected in Step 1 (planning). The user may say:
- "light theme" / "tema chiaro"
- "corporate style" / "for a presentation"
- "neon" / "cyberpunk"
- "minimal" / "clean"
Map these to the theme name, then apply the corresponding :root block and any additional overrides. Everything else (layout, components, typography families) stays the same.
Topology Layout Rules
Each topology has specific HTML structure and CSS rules. Follow them exactly.
---
Nested
Hierarchical containment — layers wrapping layers.
Layer containers
border-radius: 16px;
padding: 20px 24px;
background: rgba([accent-rgb], 0.06);
border: 1px solid rgba([accent-rgb], 0.22);- Use
border-style: dashedfor virtual/logical layers - Use
border-style: solidfor physical layers
Layer indentation
- Level 1:
margin: 0 - Level 2:
margin: 0 20px(inside level 1's padding) - Level 3:
margin: 8px 0 0(nested inside level 2)
Node row (cards inside innermost layer)
display: flex;
flex-wrap: wrap; /* REQUIRED */
gap: 16px;
justify-content: center;
align-items: stretch; /* REQUIRED */Node cards
flex: 1,min-width: 140px,max-width: 180px- If nodes > 5: reduce max-width to 140px
- If node name > 10 chars:
font-size: 0.75rem,overflow: hidden,text-overflow: ellipsis,white-space: nowrap
Bus line + drop lines
NEVER use absolute positioning for drop lines. Use CSS Grid:
.drops-row {
display: grid;
grid-template-columns: repeat(N, 1fr); /* N = number of nodes */
gap: 16px;
margin: 0;
}
.drop {
display: flex;
justify-content: center;
}N must equal the exact number of nodes declared. This guarantees drop lines align with cards.
Connectors (between layers)
display: flex;
flex-direction: column;
align-items: center;
margin: 0 auto;- Arrow line:
width: 2px,height: 28px,background: linear-gradient(to bottom, var(--dim), var(--blue)) - Arrowhead:
::afterwithcontent: '▼',color: var(--blue),font-size: 10px - Label:
font-size: 0.6rem, uppercase,background: var(--bg),border: 1px solid var(--border),border-radius: 4px,padding: 2px 8px
Bus line
height: 3px;
background: linear-gradient(to right, transparent, var(--blue), var(--cyan), var(--blue), transparent);
border-radius: 2px;
animation: pulse-glow 2.5s ease-in-out infinite;---
Left-to-right
Sequential horizontal flow.
Outer container
display: flex;
flex-direction: row;
flex-wrap: nowrap; /* never wrap — infinite canvas handles navigation */
align-items: center;
justify-content: center;
gap: 0;
width: max-content; /* container grows to fit all steps */Canvas sizing for left-to-right
Set #canvas to width: max-content (instead of the default width: 100%; max-width: Xpx used by other topologies). This lets the canvas expand to the full flow width. The fitToView() function auto-scales to show everything on load.
Each step box
background: var(--panel);
border: 1px solid var(--border);
border-radius: 12px;
padding: 16px 20px;
flex: 0 1 auto; /* size from content, not equal distribution */
max-width: 160px;
text-align: center;
transition: border-color 0.2s;Hover: border-color: var(--cyan)
Step count scaling
Adjust step boxes and arrows based on the number of steps. Canvas width is handled automatically via width: max-content. Apply these rules automatically.
| Step count | Step max-width | Step padding | Arrow width |
|---|---|---|---|
| 2-4 steps | 160px | 16px 20px | 32px |
| 5-6 steps | 130px | 14px 16px | 24px |
| 7-8 steps | 110px | 12px 14px | 20px |
| 9+ steps | 100px | 10px 12px | 16px |
For 7+ steps, also reduce font-size to 0.72rem on .node-name and 0.6rem on .node-detail.
Text inside step boxes uses word-break: break-word — text wraps naturally within the box instead of overflowing.
Arrow between steps
The arrow container MUST use flex-direction: row so the line and arrowhead sit side by side horizontally:
.flow-arrow {
display: flex;
flex-direction: row; /* REQUIRED — keeps line and head on same axis */
align-items: center;
width: 32px; /* default — adjust per step count scaling table */
flex-shrink: 0;
}- Line:
width: calc(100% - 8px),height: 2px,background: linear-gradient(to right, var(--dim), var(--blue)) - Arrowhead: inline element with
content: '▶',color: var(--blue),font-size: 0.7rem - NEVER use
flex-direction: column— it stacks the arrowhead below the line
Responsive
@media (max-width: 600px) {
.flow-arrow { transform: rotate(90deg); margin: 8px 0; }
}Step numbering
Small circle above each step: 24x24px, border-radius: 50%, background: rgba([accent-rgb], 0.15), border: 1px solid [accent-color], font-size: 0.65rem, font-weight: 700
---
Hub-and-spoke
Central node connected to N surrounding nodes.
Layout
.hub-container {
position: relative;
width: 100%;
min-height: 420px; /* adjust based on spoke count */
}Central node
position: absolute,top: 50%,left: 50%,transform: translate(-50%, -50%)min-width: 160px,z-index: 2
Spoke nodes
Position with JavaScript trigonometry:
x = cx + radius * Math.cos(angle)
y = cy + radius * Math.sin(angle)
// where angle = (2 * Math.PI / N) * iradius = 160px(increase if nodes overlap — check: if two nodes < 130px apart, increase radius)
SVG connection lines
<svg style="position:absolute;inset:0;width:100%;height:100%;z-index:1;pointer-events:none">
<line x1="cx" y1="cy" x2="spoke-x" y2="spoke-y"
stroke="rgba(45,122,255,0.4)" stroke-width="1.5"
stroke-dasharray="4 4">
<animate attributeName="stroke-dashoffset" from="0" to="-8" dur="0.6s" repeatCount="indefinite"/>
</line>
</svg>Draw one <line> per spoke. Coordinates computed in <script> after DOM load.
---
Timeline
Ordered vertical sequence of steps.
Container
.timeline {
position: relative;
width: 100%;
padding-left: 48px; /* space for vertical line + step number */
}Vertical line
.timeline::before {
content: '';
position: absolute;
left: 20px; top: 0; bottom: 0;
width: 2px;
background: linear-gradient(to bottom, var(--blue), var(--cyan), transparent);
}Each step
position: relative;
margin-bottom: 24px;
padding: 16px 20px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 12px;Step number circle
position: absolute;
left: -38px; top: 16px;
width: 24px; height: 24px;
background: var(--panel);
border: 2px solid var(--blue);
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
font-size: 0.65rem; font-weight: 700; color: var(--blue);Status badges
.badge-success { background: rgba(0,229,160,0.12); color: var(--green); border: 1px solid rgba(0,229,160,0.3); }
.badge-active { background: rgba(45,122,255,0.12); color: var(--blue); border: 1px solid rgba(45,122,255,0.3); }
.badge-pending { background: rgba(74,96,128,0.12); color: var(--dim); border: 1px solid rgba(74,96,128,0.3); }All badges: border-radius: 4px, padding: 2px 8px, font-size: 0.62rem
---
Grid / Matrix
Comparison tables, feature matrices, skill maps, competitive landscapes.
Container
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 16px;
width: 100%;
}For explicit column counts (e.g., a 3-column comparison): use grid-template-columns: repeat(N, 1fr).
Column headers (optional)
.grid-header {
background: rgba([accent-rgb], 0.08);
border: 1px solid rgba([accent-rgb], 0.25);
border-radius: 10px;
padding: 12px 16px;
text-align: center;
font-family: 'Syne', sans-serif;
font-size: 0.85rem;
font-weight: 700;
color: white;
}Grid cells
Use the standard .node-card style. Each cell contains: 1. Emoji (optional) 2. Name/label (Syne 700) 3. Value or description (JetBrains Mono, 0.7rem) 4. Optional badge
Category row (spans full width)
.grid-category {
grid-column: 1 / -1;
font-family: 'JetBrains Mono', monospace;
font-size: 0.65rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.15em;
color: var(--dim);
padding: 8px 0 4px;
border-bottom: 1px solid var(--border);
margin-top: 8px;
}Highlighted cells
For cells that need emphasis (e.g., "best option", "recommended"):
.grid-highlight {
border-color: var(--green);
background: rgba(0,229,160,0.06);
}---
Tree / Org Chart
Organizational hierarchies, file trees, decision trees, taxonomy.
Layout approach
Use nested <ul> lists styled as a vertical tree with connecting lines.
Container
.tree {
position: relative;
width: 100%;
padding-left: 0;
}
.tree ul {
list-style: none;
padding-left: 28px;
position: relative;
}Tree connectors (CSS-only, no SVG)
.tree li {
position: relative;
padding: 6px 0 6px 20px;
}
/* vertical line */
.tree li::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 1px;
background: var(--border);
}
/* horizontal branch */
.tree li::after {
content: '';
position: absolute;
left: 0;
top: 18px;
width: 16px;
height: 1px;
background: var(--border);
}
/* hide vertical line after last child */
.tree li:last-child::before {
bottom: calc(100% - 18px);
}Tree node card
.tree-node {
display: inline-flex;
align-items: center;
gap: 8px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px 14px;
transition: border-color 0.2s;
}
.tree-node:hover { border-color: var(--cyan); }Internal structure: emoji + name (Syne 700) + optional role/detail (JetBrains Mono, dim)
Depth-based accent colors
- Level 0 (root):
border-left: 3px solid var(--red) - Level 1:
border-left: 3px solid var(--orange) - Level 2:
border-left: 3px solid var(--blue) - Level 3+:
border-left: 3px solid var(--cyan)
---
Funnel
Sales funnels, conversion pipelines, filtering processes, data reduction.
Layout
A vertical stack of progressively narrower boxes, centered.
Container
.funnel {
display: flex;
flex-direction: column;
align-items: center;
gap: 0;
width: 100%;
}Funnel stages
.funnel-stage {
position: relative;
text-align: center;
padding: 16px 24px;
border-radius: 8px;
margin-bottom: 4px;
transition: transform 0.2s;
min-height: 60px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.funnel-stage:hover { transform: scale(1.02); }Width progression (N stages)
Each stage gets progressively narrower. Calculate widths:
Stage 1 (top): width: 100%
Stage 2: width: 85%
Stage 3: width: 70%
Stage 4: width: 55%
Stage 5 (bottom): width: 40%Formula: width = 100% - ((index / (N-1)) * 60%) Minimum width: 35%.
Stage styling
Each stage gets a background with the accent color at low opacity:
background: rgba([accent-rgb], 0.08);
border: 1px solid rgba([accent-rgb], 0.25);Color progression top-to-bottom: red → orange → blue → cyan → green
Conversion arrows between stages
.funnel-arrow {
display: flex;
flex-direction: column;
align-items: center;
padding: 2px 0;
}Arrow line: width: 2px, height: 16px, gradient. Arrowhead: content: '▼'.
Conversion rate labels (optional)
Between stages, show percentage:
.conversion-label {
font-family: 'JetBrains Mono', monospace;
font-size: 0.6rem;
color: var(--dim);
background: var(--bg);
border: 1px solid var(--border);
border-radius: 4px;
padding: 1px 6px;
}---
Comparison / VS
Side-by-side comparison of 2-3 options. Product comparisons, technology choices, before/after.
Layout
.comparison {
display: grid;
grid-template-columns: repeat(N, 1fr); /* N = number of items being compared */
gap: 20px;
width: 100%;
}Max 3 columns. On mobile (max-width: 600px): stack to grid-template-columns: 1fr.
Column card
.compare-column {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 16px;
padding: 20px;
display: flex;
flex-direction: column;
gap: 12px;
}Column header
.compare-header {
text-align: center;
padding-bottom: 12px;
border-bottom: 1px solid var(--border);
}Contains: emoji (1.6rem) + name (Syne 800, 1rem) + subtitle (JetBrains Mono, dim)
Highlighted (recommended) column
.compare-column.recommended {
border-color: var(--cyan);
box-shadow: 0 0 20px rgba(0,212,255,0.1);
}
.compare-column.recommended::before {
content: 'RECOMMENDED';
display: block;
text-align: center;
font-family: 'JetBrains Mono', monospace;
font-size: 0.58rem;
font-weight: 700;
color: var(--cyan);
letter-spacing: 0.15em;
margin-bottom: 8px;
}Feature rows inside columns
.compare-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 0;
border-bottom: 1px solid rgba(30,45,74,0.4);
font-family: 'JetBrains Mono', monospace;
font-size: 0.68rem;
}
.compare-row:last-child { border-bottom: none; }
.compare-label { color: var(--dim); }
.compare-value { color: var(--text); font-weight: 600; }VS divider (between 2 columns)
.vs-badge {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--bg);
border: 2px solid var(--orange);
border-radius: 50%;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Syne', sans-serif;
font-weight: 800;
font-size: 0.7rem;
color: var(--orange);
z-index: 3;
}Only show VS badge when comparing exactly 2 items. For 3+ items, omit it.
Typography & Color System
Fonts
Import both fonts via @import at the top of the <style> block:
https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Syne:wght@400;700;800| Element | Font | Size | Weight | Extra |
|---|---|---|---|---|
| Title (h1) | Syne | 1.6rem | 800 | One keyword highlighted in var(--cyan) |
| Subtitle (.subtitle) | JetBrains Mono | 0.75rem | 400 | uppercase, letter-spacing: 0.1em, color: var(--dim) |
| Layer labels | JetBrains Mono | 0.65rem | 700 | uppercase, letter-spacing: 0.15em |
| Card/node names | Syne | 0.85rem | 700 | color: white |
| Secondary info | JetBrains Mono | 0.65-0.72rem | 400 | |
| Arrow labels | JetBrains Mono | 0.6rem | 400 | uppercase, letter-spacing: 0.08em |
Never use Inter, Roboto, Arial, system-ui, or any sans-serif fallback as primary font.
Color Palette
Define exactly these CSS custom properties on :root. Do not modify values.
:root {
--bg: #0a0e1a; /* page background */
--panel: #0f1528; /* card / inner panel background */
--border: #1e2d4a; /* default card border */
--blue: #2d7aff; /* primary accent */
--cyan: #00d4ff; /* secondary accent */
--green: #00e5a0; /* positive / exposed / success */
--orange: #ff8c42; /* host layer / warning / NAT */
--red: #ff4d6a; /* external / internet / danger */
--text: #c8d8f0; /* body text */
--dim: #4a6080; /* muted labels */
}Layer color assignment
- Outermost layer →
var(--red) - Second layer →
var(--orange) - Third layer →
var(--blue) - Fourth layer →
var(--cyan)(if needed) - Node hover border →
var(--cyan) - Exposed ports / positive states →
var(--green)
Background
body {
background: var(--bg);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
overflow-x: hidden;
}
body::before {
content: '';
position: fixed;
inset: 0;
background-image:
linear-gradient(rgba(45,122,255,0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(45,122,255,0.04) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none;
z-index: 0;
}
.wrapper {
position: relative;
z-index: 1;
width: 100%;
max-width: 900px;
}