# Architecture Diagram React

> Use when the user wants to embed an architecture diagram directly inside a React/Vite site as inline SVG (no html2canvas, no external scripts, no separate HTML file). Produces an `arch-diagram` fenced markdown block that the site's React renderer expands — including `<lucide-icon>` placeholders for semantic icons. Companion to the `architecture-diagram` skill, which produces standalone HTML.

- Skill: `sergio-bershadsky/architecture-diagram-react` (Agent Skill)
- Install (CLI): `npx skillmds@latest add sergio-bershadsky/architecture-diagram-react`
- Raw SKILL.md: https://api.skillmd.com/api/skills/sergio-bershadsky/architecture-diagram-react/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: sergio-bershadsky (https://skillmd.com/u/sergio-bershadsky)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/sergio-bershadsky/architecture-diagram-react

---


# Architecture Diagram — React/Inline-SVG Embedding

This is the **embedded** counterpart to the `architecture-diagram` skill. The parent skill ships a self-contained `.html` file with its own toolbar, html2canvas/jsPDF CDN scripts, summary cards, and footer. This skill produces a leaner artifact suited for direct embedding inside a React-rendered markdown page: a fenced markdown block whose body is inline `<svg>` augmented with `<lucide-icon>` placeholders.

The runtime renderer lives in the consumer repo (see "Consumer contract" below). The React component provides the chrome (frame, FIG label, copy/save toolbar, slate-950 grid background, JetBrains Mono surface) — so the SVG you emit must contain **only the diagram itself**, not the chrome.

## What triggers this skill

- "Embed this diagram inside the blog post"
- "Generate the SVG for an `arch-diagram` block"
- "Make a React-embedded version of the architecture diagram"
- Any request where the user wants the diagram **inline in a page**, not as a downloadable HTML file
- Follow-on to the `architecture-diagram` skill when the user says "now put this on the site"

If the user wants a shareable standalone artifact (Slack, PDF, slides), use the parent `architecture-diagram` skill instead.

## Workflow

1. **Read** the parent skill's [`design-system.md`](../architecture-diagram/resources/design-system.md) and [`label-placement.md`](../architecture-diagram/resources/label-placement.md). Every rule there still applies: semantic colors, JetBrains Mono sizes, halos, label-placement priority, 40 px minimum gaps, 100–200 px between bands. **None of those rules are duplicated here.**
2. **Produce the SVG** following those rules — `viewBox`, component rects, arrows, halos, leader lines.
3. **Convert to embedded form** — strip everything that belongs to the standalone HTML chrome (see "What to strip" below).
4. **Add the metadata comment** — `<!-- fig: ... | title: ... | label: ... -->` as the first line inside the fence.
5. **Promote icons to `<lucide-icon>` placeholders** wherever a component box would benefit from a recognisable glyph (see "Lucide icon promotion" below).
6. **Run the audit** if available — `audit-labels.py` works on raw SVG too; pipe the fenced-block body to it.
7. **Output** as a single fenced markdown block tagged `arch-diagram`.

## Output contract — the fenced block

````markdown
```arch-diagram
<!-- fig: 2.1 | title: REQUEST FLOW | label: SYSTEM -->
<svg viewBox="0 0 1000 400" xmlns="http://www.w3.org/2000/svg">
  ...everything from <defs> through every <rect>, <path>, <text>, <lucide-icon>...
</svg>
```
````

- **Fence language: `arch-diagram`** — the React renderer keys off this exact tag.
- **Metadata comment is the first line** inside the fence. Fields are pipe-separated `key: value`. Recognised keys:
  - `fig` — appears as `FIG {fig}` in the chrome (e.g., `2.1`, `A`, `3`)
  - `title` — uppercase title shown after `FIG x //`
  - `label` — small right-aligned tag (e.g., `SYSTEM`, `CONTROL_PLANE`, `AUTH`)
  - All three are optional; defaults are `A` / `ARCHITECTURE` / `ARCH_V2`.
- **Body must start with `<svg>`** (after the metadata comment). The renderer's auto-detection looks for `<svg` after the comment. If the body doesn't begin with `<svg`, the renderer treats it as ordinary code.

## What to strip from a standalone-HTML diagram

When converting output of the parent skill to embedded form, **remove**:

| Strip | Why |
|---|---|
| `<!DOCTYPE html>`, `<html>`, `<head>`, `<body>` | Renderer is inside a React tree |
| Google Fonts `<link>` | Surface already loads JetBrains Mono via `font-family` |
| html2canvas + jspdf `<script>` tags | Renderer provides Copy/Save via React |
| `.toolbar`, `.toolbar-toggle`, `.toolbar-actions` markup + CSS | Chrome is the React component's job |
| `.container`, `.header`, `.header-row`, `.pulse-dot`, `.subtitle` | Chrome — frame + FIG label come from React |
| `.summary-cards`, `.card`, footer divs | Embedded diagrams have no info-cards / footer |
| `copyAsImage()`, `downloadPNG()`, `downloadPDF()` scripts | Replaced by React Copy/Save |
| `@media print { ... }` | Not applicable inside an SPA page |
| Page-level `body { padding: 2rem; ... }` styles | Renderer controls outer padding |

**Keep:**

- The whole `<svg>` block: `<defs>`, `<pattern>`, `<marker>`, all `<rect>`/`<path>`/`<line>`/`<polyline>`/`<text>` elements, arrow leader lines, halo styles inlined as SVG attributes.
- **Halos must move from CSS into inline SVG attrs** since the renderer doesn't ship the global `svg text { paint-order: stroke fill; ... }` selector. Each `<text>` that needs a halo must carry its own `paint-order="stroke fill"`, `stroke="rgba(2, 6, 23, 0.75)"`, `stroke-width="1"`. The renderer adds nothing to text — what you emit is what renders.
- Background grid pattern is optional — the renderer surface already paints a slate-950 + 40 px grid. If the SVG also draws one, it just doubles up; usually safe to omit.

## Chip-rect backgrounds for floating labels — *exactly one*

Per the parent skill's rule #3, every floating `<text>` (one that does not live inside a coloured component box — titles, edge labels, cluster names, annotations) needs a backing chip rect drawn **immediately before** the text:

```svg
<rect x="..." y="..." width="..." height="..." rx="3"
      fill="rgba(15,23,42,0.92)" stroke="rgba(148,163,184,0.75)" stroke-width="1"/>
<text x="..." y="..." font-size="..." ...
      paint-order="stroke fill" stroke="rgba(2,6,23,0.65)" stroke-width="3">
  LABEL
</text>
```

**Exactly one chip per label.** Do *not* nest chips. Do *not* draw a slightly-larger outer chip around the canonical one — the result is a double-bordered pill that reads as a bug.

Two patterns produce the double-border bug:
1. The author writes a chip-rect AND a downstream post-pass (e.g. `add_chip_rects.py`) re-runs and adds another because its idempotency check missed yours. Mitigation: the post-pass now scans the 240 characters before each `<text>` for `fill="rgba(15,23,42,0.92)"` and skips if found. Authors must use the canonical `fill` value verbatim — no alpha variants, no off-by-one rgba.
2. The author wraps the label with a *decorative* outer rect for emphasis. Don't. Use one canonical chip. If you want a title to feel heavier, use larger `font-size` and `font-weight="700"`, not a second border.

**The chip is for floating labels only.** Text already living inside a coloured component box (e.g. a service name inside a cyan-stroked rect) inherits the box as its background — no chip needed and adding one looks doubly busy.

## Lucide icon promotion

Replace bespoke icon `<path>` blocks with `<lucide-icon>` tags. The React renderer expands these at runtime via `lucide-react`, so authoring stays compact and icons match the rest of the site.

**Syntax:**

```html
<lucide-icon name="database" x="56" y="56" size="20" color="#a78bfa" stroke-width="2"/>
```

- `name` — required; one of the supported names below. Unknown names are silently stripped at render time, so prefer the parent skill's bespoke SVG over an unsupported guess.
- `x`, `y` — top-left of the icon's 24×24 box in SVG user units (default `0`).
- `size` — pixel size of the bounding box (default `24`; expansion scales by `size/24`).
- `color` — stroke color (default `currentColor`); use the **stroke** colour of the surrounding component box from the semantic palette.
- `stroke-width` — default `2`. Use `1.5` when the icon sits inside a small (≤60 px) component box; default for ≥80 px.

**Supported names** (32 total, from `client/src/components/markdown/archDiagramIcons.ts` in the consumer repo):

`activity`, `bell`, `box`, `boxes`, `cloud`, `code`, `cog`, `container`, `cpu`, `database`, `file-code`, `git-branch`, `globe`, `hard-drive`, `key`, `key-round`, `layers`, `lock`, `mail`, `message-square`, `monitor`, `network`, `search`, `server`, `shield`, `smartphone`, `terminal`, `user`, `users`, `webhook`, `workflow`, `zap`.

**Recommended mapping** (extend the parent skill's semantic colors with semantic glyphs):

| Component type | Stroke color | Recommended icon |
|---|---|---|
| Frontend / web client | `#22d3ee` cyan | `monitor`, `smartphone`, `globe` |
| Backend / service | `#34d399` emerald | `server`, `cog`, `workflow`, `box`, `container` |
| Database / store | `#a78bfa` violet | `database`, `hard-drive`, `layers` |
| Cloud edge / LB / CDN | `#fbbf24` amber | `cloud`, `network`, `globe` |
| Security / IDP / KMS | `#fb7185` rose | `shield`, `lock`, `key`, `key-round` |
| Message bus / events | `#fb923c` orange | `zap`, `webhook`, `activity` |
| External / generic | `#94a3b8` slate | `box`, `boxes`, `mail`, `bell` |
| Compute / worker | `#34d399` emerald | `cpu`, `terminal`, `code`, `file-code` |
| Search / index | `#a78bfa` violet | `search` |
| Identity / user | `#94a3b8` slate | `user`, `users` |
| VCS / pipeline | `#34d399` emerald | `git-branch`, `workflow` |

If no listed icon fits, **don't invent one** — author the path inline as the parent skill does. Adding a name to the consumer requires editing `archDiagramIcons.ts` (the icon must be imported from `lucide-react`).

### Reserve icon space before placing text — NON-NEGOTIABLE

When an icon sits in a box's left padding, the label inside that box **must not centre on the full box width** — the text glyphs will slide under the icon and intersect it. This bug is visually obvious and unprofessional; it has been reported on shipped diagrams more than once.

**Mandatory formulas — compute every time, do not eyeball coordinates:**

```
icon_left   = box_x + 16            # 16 px left padding
icon_right  = icon_left + size      # size is the <lucide-icon> `size` attr
box_right   = box_x + box_w
```

**Pattern A — right-shifted centre (use for short labels):**
```
text_anchor = "middle"
text_x      = (icon_right + 8 + box_right) / 2     # midpoint of space RIGHT of icon
```

**Pattern B — left-aligned (use for long or multi-line labels):**
```
text_anchor = "start"
text_x      = icon_right + 8        # 8 px gap between icon and first glyph
```

**The wrong pattern that produces visible icon/text intersection — DO NOT EMIT:**
```
text_x      = box_cx                # centres across whole box, overlays the icon
text_anchor = "middle"
```

Minimum reserved space on the icon side: `size + 16 px`. For a 20 px icon that's a 36 px guard. Mirror for right-side icons. Vertical guard for top-positioned icons is `size + 8 px`.

**Placement example — correct math applied (180×80 box, 20 px icon):**

```html
<!-- Box: x=40, w=180 → box_right=220.  Icon: x=56..76.
     text_x = (76 + 8 + 220) / 2 = 152  (not 130 = box_cx) -->
<rect x="40" y="40" width="180" height="80" rx="6"
      fill="rgba(6,78,59,0.4)" stroke="#34d399" stroke-width="1.5"/>
<lucide-icon name="server" x="56" y="56" size="20" color="#34d399"/>
<text x="152" y="76" fill="white" font-size="12" font-weight="600"
      text-anchor="middle">API GATEWAY</text>
<text x="152" y="92" fill="#94a3b8" font-size="9" text-anchor="middle">
  AUTHN / RATE-LIMIT
</text>
```

For small (≤80 px wide) boxes the right-of-icon space is too narrow for any label — drop the icon or stack vertically: icon at `(box_cx − size/2, box_y + 10)`, label at `(box_cx, box_y + size + 22)` with `text-anchor="middle"`.

## Consumer contract

The renderer in the consumer repo (this site) lives at:

- `client/src/components/markdown/ArchDiagram.tsx` — frame, toolbar, sanitization, render.
- `client/src/components/markdown/archDiagramIcons.ts` — `<lucide-icon>` expansion + supported-name allowlist.
- `client/src/components/markdown/CodeBlock.tsx` — trigger: `language-arch-diagram` className **or** auto-detected leading `<svg>` (after optional metadata comment).

The renderer:

1. Parses the `<!-- ... -->` metadata comment off the front.
2. Runs `expandLucideIcons()` over the body, replacing `<lucide-icon ... />` with positioned/scaled `<g>` groups containing real Lucide paths.
3. Sanitizes through DOMPurify's SVG profile.
4. Paints the body inside an accent-bordered frame with slate-950 grid background, JetBrains Mono font-family, and a `ZOOM` / `COPY_SVG` / `SAVE` toolbar.

### Zoom (fullscreen) mode

Every rendered diagram has a `ZOOM` button in its toolbar. Clicking it opens a fullscreen modal overlay:

- The SVG is re-rendered at viewport width (capped at 1600 px) with `width: 100%; height: auto`, so authors can rely on the *intrinsic* `viewBox` aspect ratio — no special "zoom mode" markup needed in the SVG itself.
- The modal toolbar carries the same `COPY_SVG` and `SAVE` controls plus an explicit `CLOSE` button. ESC and clicking the backdrop also dismiss.
- Body scroll is locked while the modal is open and restored on close.
- The same DOMPurify-sanitized body and the same slate-950 grid surface are reused — visual continuity between inline and zoomed.

**Authoring implications:**

- **Design for the inline view first.** Zoom is a reading aid, not the default. If the diagram is unreadable at the inline width (~720 px), simplify it — don't push detail into the zoom layer and call it done.
- **Use the `viewBox` aspect ratio you actually want.** The zoom layer scales the SVG to fill width while preserving aspect; ultra-tall or ultra-wide viewBoxes will read poorly at zoom too.
- **Halos still required.** Zoom does not relax the halo rule. Each `<text>` that overlaps anything must carry its own `paint-order="stroke fill"`, `stroke="rgba(2,6,23,0.65)"`, `stroke-width="3"`.

**What this means for what you emit:**

- You cannot use `<script>` — sanitizer strips it.
- You cannot use `<foreignObject>` — both unsupported in html2canvas (parent skill rule) and stripped by the SVG profile.
- You cannot rely on external CSS — every style must be an SVG attribute or inline `style="..."`.
- You **can** use `<defs>`, `<pattern>`, `<marker>`, `<linearGradient>`, `<radialGradient>`, `<clipPath>`, `<mask>` — all in the SVG profile allowlist.

## Audit

Run the parent skill's audit script on the fenced-block body. Extract everything from `<svg` to `</svg>` and feed it in:

```bash
sed -n '/^<svg/,/<\/svg>/p' diagram-body.txt > /tmp/diagram.svg
python3 ${CLAUDE_PLUGIN_ROOT}/skills/architecture-diagram/resources/audit-labels.py /tmp/diagram.svg
```

If the audit flags labels for being far from the arrow midpoint without a leader line, fix them per the parent skill's algorithm.

## What you must not do

- ❌ Don't emit a full `<html>` document — only the fenced block.
- ❌ Don't include `<script>`, `<link>`, `<style>` outside `<defs>` — sanitizer strips them anyway.
- ❌ Don't omit halos on overlapping text — the React surface doesn't add them globally; each `<text>` must carry its own halo attrs where needed.
- ❌ Don't invent unsupported `<lucide-icon>` names — they silently disappear.
- ❌ Don't double-paint the grid — the React surface already provides slate-950 + 40 px grid; only repeat if the diagram needs a different grid density inside.
- ❌ Don't reach for `<foreignObject>` for rich labels — text + halo is the supported path.

## Minimal example

````markdown
```arch-diagram
<!-- fig: 1 | title: HELLO WORLD | label: DEMO -->
<svg viewBox="0 0 600 200" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <marker id="ah" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
      <polygon points="0 0, 10 3.5, 0 7" fill="#64748b"/>
    </marker>
  </defs>

  <!-- Box 1: x=40..220, icon 56..76 → text_x = (76+8+220)/2 = 152 -->
  <rect x="40" y="60" width="180" height="80" rx="6"
        fill="rgba(8,51,68,0.4)" stroke="#22d3ee" stroke-width="1.5"/>
  <lucide-icon name="monitor" x="56" y="76" size="20" color="#22d3ee"/>
  <text x="152" y="96" fill="white" font-size="12" font-weight="600"
        text-anchor="middle"
        paint-order="stroke fill" stroke="rgba(2, 6, 23, 0.75)" stroke-width="1">CLIENT</text>

  <line x1="220" y1="100" x2="380" y2="100"
        stroke="#64748b" stroke-width="1.5" marker-end="url(#ah)"/>
  <text x="300" y="92" fill="#cbd5e1" font-size="9" text-anchor="middle"
        paint-order="stroke fill" stroke="rgba(2, 6, 23, 0.75)" stroke-width="1">HTTPS</text>

  <!-- Box 2: x=380..560, icon 396..416 → text_x = (416+8+560)/2 = 492 -->
  <rect x="380" y="60" width="180" height="80" rx="6"
        fill="rgba(6,78,59,0.4)" stroke="#34d399" stroke-width="1.5"/>
  <lucide-icon name="server" x="396" y="76" size="20" color="#34d399"/>
  <text x="492" y="96" fill="white" font-size="12" font-weight="600"
        text-anchor="middle"
        paint-order="stroke fill" stroke="rgba(2, 6, 23, 0.75)" stroke-width="1">API</text>
</svg>
```
````

## Attribution

This skill is the React/inline-SVG embedding companion to the `architecture-diagram` skill. All design-system rules — colors, fonts, halos, label placement, spacing — come from the parent skill; this skill only documents the format conversion and consumer contract.

