SuperAPI AI Directory

architecture-diagram-react

Frontend & websites

View repository

Updated 10-16 14:37

About this skill

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 and 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

```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:

<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:

<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):

<!-- 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:

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

```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.