본문으로 건너뛰기
cccskills
무료GitHub에 공개

arbitrum-brand-svg-diagrams

Create or improve lightweight, brand-consistent SVG concept diagrams for the Arbitrum docs, using the official Google Drive infographic asset kit as the palette/style source. Use when asked to "make a diagram more efficient", replace a bloated haw-*.svg (draw.io PNG-in-SVG exports), replace an ASCII / box-drawing diagram in an MDX page with a real image, author a new concept diagram for content/docs/how-arbitrum-works or similar, or build a diagram from the Arbitrum brand assets. Hand-authored lean vector SVG — not Mermaid, not a full Excalidraw/draw.io pipeline.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

OffchainLabs/arbitrum-docs스타 252(GitHub 252 + cccskills 0)
연동 도구Google Drive

실행 점수 미검증

설치 방법 보기

포함된 파일(8)

  • SKILL.md43.3 KB
  • assets/background.min.svg44.1 KB
  • assets/background.svg78.8 KB
  • assets/bg-frag.svg44.0 KB
  • tools/check_contrast.py4.5 KB
  • tools/excalidraw_bridge.py8.9 KB
  • tools/round_arrows.py1.4 KB
  • tools/test_security.py5.8 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

Arbitrum brand SVG diagrams

Author small, crisp, on-brand SVG concept diagrams for the docs and replace bloated raster-in-SVG exports. A hand-authored vector diagram is a few KB and scales perfectly; the draw.io exports it replaces are often >1MB.

When to use

  • "This diagram is too heavy / make it more efficient" — a public/img/*.svg that is really a draw.io export with a base64 PNG inside (often 1–2 MB).
  • Replacing an ASCII / box-drawing diagram in an MDX page with a real image. These often carry a {/* TODO: replace with an SVG diagram asset */} marker and a <div className="ascii-diagram"> wrapper — remove both.
  • Creating a new conceptual diagram (layers, boxes, flows, brackets) for a docs page, especially content/docs/how-arbitrum-works/**.
  • You have the Arbitrum brand asset kit as a source and want palette/style consistency.

When NOT to use

  • Complex or interactive diagrams (animated flows, clickable nodes, the transaction-lifecycle visualizers). Those are React widgets under components/widgets/ (see EdgeChallengeFlow and CentralizedAuction). This skill is for static concept art only.
  • Photographic / screenshot content — keep as PNG/WebP.
  • Never propose Mermaid or any text-DSL diagram tool (standing preference).

Asset sources (the palette comes from here)

Google Drive "Arbitrum Infographic Library" folder id 11WkZ7mn3wLDXB_-QYfjBA5-6hwCYkG2o. Subfolders: Basicshapes/, AdditionalIcons/, Numbers/, OCL_logos/, foundation_logos/, plus background.svg.

Official palette (extracted from background.svg):

HexRole
#213147Arbitrum dark navy — process steps / base surfaces
#12aaffArbitrum signature cyan — entities, services, value destinations
#1b4addArbitrum blue — mid-gradient (background only)
#152c4edark navy — top of the gradient (background only)
#9dccedlight-blue tint — connectors, arrowheads, lane headers
#0b1b2enear-black — text on cyan
#e6007amagenta — contracts / inboxes / pools / convergence points
#f97316orange — legacy "this is code" fill; use a code block instead (see below)

Map color to meaning: pick one role per hue and hold it for the whole diagram (e.g. cyan = where value lands, magenta = system contract or pool, navy = process step). Add a small color key when you use three or more.

Text color is measured, not chosen

An earlier version of this skill said "white text on cyan/magenta boxes" for baked-background diagrams. White on cyan is 2.55:1 and fails WCAG at every size. Use this table instead — every figure below is computed, not estimated:

Box fillTitle textSublabel textNever use
cyan #12aaff#0b1b2e — 6.8:1#0b2438 — 6.2:1white → 2.55:1 FAIL
magenta #e6007a#ffffff — 4.5:1#ffffff — 4.5:1#ffe3f1 → 3.8:1 FAIL
navy #213147#ffffff — 13.2:1#c9dcef — 9.4:1—
Nova orange #ff7700#0b1b2e — 6.5:1#0b2438 — 6.0:1white → 2.66:1 FAIL
legacy orange #f97316#0b1b2e — 6.2:1—white → 2.80:1 FAIL

Both orange rows fail on white, so the legacy "orange box + white label = this is code" convention is itself inaccessible — a second reason to use the palenight code block below. #ff7700 is the real Arbitrum Nova brand orange (it comes out of the Nova logomark); reach for it only when the box genuinely is Nova.

Magenta is the one fill with no headroom: white measures 4.52:1 against a 4.5 floor. It carries a sublabel only at white — never tint it, and never assume a smaller sublabel is safe there.

Text sitting directly on the gradient has no fixed backdrop, so its contrast depends on where on the canvas it sits — the gradient runs dark navy at the top to bright cyan at the bottom. Measure it against the render (Verification step 5).

The safe fill inverts near the bottom. Light text works down to roughly three-quarters of the canvas, then fails as the gradient reaches full cyan, and dark text takes over. Measured on two real diagrams:

Depth down the canvas#9dcced#eaf5ff#0b1b2eUse
upper half5.0–8.2:18.2:1+low#9dcced
~75–80%3.14.83.3#eaf5ff
~90%+ (near-full cyan)1.82.75.8#0b1b2e

So there is no single "lower half" answer: at 79% down, light wins and dark fails; at 94% down, light fails at 2.7:1 and dark wins. A short canvas reaches full cyan sooner than a tall one, so depth is a fraction of canvas height, not a pixel count.

#cfe4f5 looks fine in a render but measures 4.3:1 over the lower gradient and fails. Don't trust your eyes on gradient text — and don't trust this table either. It tells you which candidates to try; check_contrast.py decides.

Remember WCAG's "large text" exemption (3:1) needs 24px regular or 18.66px bold — an 18px regular sublabel does not qualify, so hold it to 4.5:1.

Fetching Drive assets: the brand background ships inside this skill — no Drive round-trip needed:

.claude/skills/arbitrum-brand-svg-diagrams/assets/
  background.svg       79 KB   Inkscape source
  background.min.svg   44 KB   minified, standalone
  bg-frag.svg          44 KB   ready-to-inline <g> fragment   <- use this one

These are not at the repo root: a path like assets/bg-frag.svg will not resolve. For other assets: mcp__claude_ai_Google_Drive__search_files with parentId = '<folderId>' (the query field is parentId, not parents); download_file_content returns base64 (decode before reading; large files land in a tool-results file, decode with base64.b64decode(...)).

The one gotcha that dictates the whole design

ImageZoom (Fumadocs' own component, fumadocs-ui/components/image-zoom, registered globally in components/mdx.tsx, with no components/mdx/ImageZoom/ wrapper and no ImageWithCaption alias; a markdown ![alt](/img/x.svg) also renders through it, via the img: mapping in that same registry) renders the image as a plain <img src=…>. Consequences:

  1. The dark-mode class lives on <html> and cannot reach inside an <img>-embedded SVG. @media (prefers-color-scheme) tracks the OS, not the site toggle. So you cannot theme the SVG to the page.
  2. Therefore: build the diagram from opaque, self-contained shapes with their own contrast. No text floating on transparency. Navy fill + white text and cyan fill + dark text both read on any page background.
  3. The SVG's own <title>/<desc> never reach a screen reader through <img>. Include them anyway — they help anyone who opens the file directly — but the alt attribute on the MDX tag carries the whole a11y burden. Write the full description there, and keep the two in sync.
  4. No external resources load at all: no webfonts, no <image href=…>, no external stylesheet. Everything must be inline.

Background for <ImageZoom> legibility

Pages that use <ImageZoom> (click-to-zoom) composite the image on a light/variable overlay. A transparent SVG whose labels are white then becomes unreadable when zoomed. Diagrams that were originally designed on the Arbitrum brand background (navy→blue→cyan gradient + circuit motif + logo) — most how-arbitrum-works flow diagrams — must bake that background in as an opaque, full-canvas base layer. Then the whole frame is opaque and reads on any overlay; the zoom backdrop no longer matters.

  • The background ships with the skill (see the assets/ listing above). bg-frag.svg is already minified and wrapped in <g transform="scale(3.779534)">…</g> to fill a 1600×900 canvas.
  • Inline it, never reference it. An <img>-embedded SVG cannot load external resources, so <image href="bg.svg"> renders blank. Emit the contents of bg-frag.svg as the first layer of a 1600×900 diagram — it is a single 44 KB line, so generate the file with a script (see Workflow) rather than trying to hand-write it.
  • The background's viewBox is exactly 16:9, so a 1600×900 canvas fits it undistorted (uniform ×3.779534). Use that canvas for all such diagrams for a consistent set. Keep content clear of the bottom-left logo (x<330 AND y>810).
  • Gradient stops (if you need a matching solid): cyan #12aaff → blue #1b4add → dark navy #152c4e.
  • Each diagram carries its own ~45 KB copy of the background — still a 99%+ cut from the 6 MB raster it replaces.

Small diagrams: brand gradient, not the baked background

A two- or three-box explainer sitting inline next to a paragraph does not earn 45 KB of circuit motif, and a 1600×900 canvas holding four boxes looks absurd. But it still cannot be transparent — see the stroke trap below. Use the brand gradient stops as a plain linearGradient over a full-bleed rect:

<linearGradient id="brandbg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#152c4e"/><stop offset="0.55" stop-color="#1b4add"/><stop offset="1" stop-color="#12aaff"/>
</linearGradient>
<rect x="0" y="0" width="800" height="300" fill="url(#brandbg)"/>

That lands at ~2 KB, stays opaque under <ImageZoom>, and reads as the same family as the 1600×900 diagrams because the stops are identical. Size the canvas to the content and pick a className near it (img-600px for an 800-wide canvas keeps type crisp at 0.75× rather than 0.5625×).

The stroke trap — why transparent still fails. Gotcha 2 above says "no text floating on transparency", but strokes have the same problem and are easier to miss. A #9dcced connector is near-invisible on a white page; a #213147 one disappears on the dark theme. Since the SVG cannot see the page theme, any diagram with a connector, bracket, or divider needs its own opaque backdrop. Transparent canvas is safe only for a diagram built purely from opaque filled shapes that touch each other — haw-geth-sandwich qualifies; almost nothing with an arrow does.

Keep the backdrop rect square-cornered. A rounded full-bleed panel leaves four transparent corners and fails the byte-identical opacity test in Verification 3b. Let the page CSS round the image instead.

Brand logos (Arbitrum One, Nova)

When a box names a specific chain, its logomark carries identity far better than a colour swatch plus a key row — add the logo and you can delete that key entry. Use the _Logomark_ variants (mark only); the horizontal and vertical lockups bundle the wordmark and duplicate your text label.

LogoDrive file idviewBoxBrand fill
Arbitrum One10B54LXfYk7hw5qwsWt3H2EThNrCXJcWk373.9 × 422multi
Arbitrum Nova12cg33GwnrLB7JXGw6Y8njTsBGwQiea02372.84 × 420.63#ff7700

Both are genuine vector, ~2 KB each; the pair costs about +3.3 KB inlined. _AllWhite_ variants exist if you ever need a knockout.

Three things will bite, all of them silent:

  1. Fills live in a <style> block, keyed by class (.st0….st3 on the Arbitrum mark, .cls-1 on Nova). Resolve every class to an explicit fill= attribute before inlining. Two logos in one document otherwise collide on those generic names and one repaints the other. Expand shorthand hex while you are there (#f70 → #ff7700).
  2. Put each mark on a white chip, not straight on the box. The Arbitrum logomark's own body is navy #213147 with cyan #12aaff strokes and a #9dcced ring — drop it on a cyan box and half the mark vanishes. A white rounded rect behind it renders both marks as the brand intends. Size the glyph to roughly 78% of the chip.
  3. A logo box still obeys the contrast table. A Nova-orange box takes dark labels; white on it is 2.66:1. Giving a box a brand colour exempts it from nothing.

Scale by height so the two marks match optically, then centre on the width that scale produces — they are not the same aspect ratio:

scale = GLYPH / view_h
off_x = chip_x + (CHIP - view_w * scale) / 2

Budget the room first: a logo chip above the name needs roughly +30px of box height over a text-only box.

Workflow

Steps 1, 3, 4 and 6 are common to both jobs; steps 2 and 5 differ.

  1. Read the concept, not just the old image. Open the MDX section and pull the exact terms/relationships the diagram must convey (e.g. the "Geth sandwich": Geth = bread top+bottom, ArbOS = filling; STF spans bottom Geth + part of ArbOS). The diagram must match the prose, and reuse its exact nouns.
  2. Diagnose the old file (replacing only): head -c 1500 file.svg — if you see data:image/png;base64 and an mxfile blob, it's a draw.io raster export; replacing it with real vector is the whole win.
  3. Pick the canvas. Baked brand background + <ImageZoom> → 1600×900. Transparent concept art → size to content.
  4. Generate the SVG with a throwaway Python script (see below). Include <title>/<desc>.
  5. Wire it up — see the two paths below.
  6. Verify (below) before claiming done.

Generate with a script, don't hand-write the file

The inlined background is one 44 KB line, so Write-ing the SVG by hand is not practical. Put a small assembler in the scratchpad that reads bg-frag.svg, emits your geometry, and writes public/img/NAME.svg. Only the SVG is committed; the script is disposable, but keep it for the session so you can iterate on coordinates and re-run instead of hand-patching a 44 KB file.

Two things the script must respect: emit the background as the first layer, and set font-family once on a wrapping <g> (SVG inherits it) so you are not repeating the stack on every <text>.

Re-run order is always: generate → round_arrows.py → verify. Regenerating overwrites the rounded corners, so the rounding step is never optional.

Path A — replacing an existing diagram

Keep the exact filename/path so the MDX reference needs no edit. The old file is often too big to Read; move it to Trash (trash …) then write fresh. No pnpm build needed: the link graph is untouched.

Path B — new diagram (also: retiring an ASCII diagram)

  1. Name it for its section, matching neighbours in the same folder (arb-chain-* under launch-arbitrum-chain, haw-* under how-arbitrum-works).
  2. Add the MDX tag yourself. <ImageZoom> is registered globally in components/mdx.tsx, so no import needed. A plain markdown ![alt](/img/NAME.svg) also renders through it (same registry's img: mapping), so either form works.
  3. Don't reach for an img-*px className. Those came from Docusaurus' src/css/custom.css and are defined nowhere here. app/global.css caps every prose image at 600px wide; a diagram that needs the full column carries data-wide on the image (see "Image sizing" in CONTRIBUTE.md). Most 1600×900 diagrams are wide images.
  4. Write the alt text in full (see gotcha 3) and keep it in the diagram's <desc> too.
  5. Check the page still compiles and lints:
npx prettier --write content/docs/path/to/page.mdx
pnpm lint
node --input-type=module -e "
import {compile} from '@mdx-js/mdx'; import {readFileSync} from 'node:fs';
await compile(readFileSync('content/docs/path/to/page.mdx','utf8').replace(/^---[\s\S]*?\n---\n/,''));
console.log('MDX compiles ok');"

The MDX compile is much cheaper than pnpm build and catches the failure mode a new JSX tag actually introduces. A full build is still unnecessary unless you changed a doc link.

  1. Check what the removal orphaned. Retiring an ASCII diagram can strip the last consumer of a helper CSS class. Grep for it and report it — don't delete shared CSS unasked. (The known Docusaurus case, .ascii-diagram in src/css/partials/_misc-classes.scss, no longer applies: this tree has no src/, and the class is defined nowhere.)

Diagram labels are prose: the style guide applies

STYLE-GUIDE.md at the repo root governs text inside the diagram, not just the page. .claude/hooks/require-pattern-guide.sh fires on a write to a content/docs/*.mdx or content/partials/*.mdx file and denies the first one per session, returning the guide's text so it lands in context.

Read the guide before you draft labels and alt text, not after. The hook fires on the MDX write, not on the SVG — so you can generate, round, and verify a whole diagram, then get blocked on the one-line tag at the end and have to rewrite label text you already baked into the file (and into its <desc>). Reading it up front costs one tool call; discovering it late costs a regenerate.

The rules that bite hardest on labels:

  • Active voice, name the actor. "ArbOS credits it in the same transaction", not "credited immediately". Lane headers are the usual offender.
  • One term, one meaning. If the page says child chain, the diagram says child chain — never L2 in the art and child chain in the prose.
  • Sentence case for headers and labels.

Write the alt text to the same standard; it is page prose.

SVG authoring rules learned the hard way

  • Rounded-rect paths bake the radius into BOTH the arc commands and the line endpoints. Changing a corner radius means recomputing every coordinate on that corner, not just the A r,r number. (A plain <rect rx=…> is fine when you don't need per-corner control.)

  • Text must fit inside its opaque pill. If the text is wider than the pill, the overflow falls onto transparent background and disappears on light themes. Size the font to fit, or widen the pill.

  • Pre-check every label's width before you render. Estimate at 0.52 em/char for the sans stack and 0.60 em/char for the mono stack, and compare against the box's inner width (width − 2×16px padding). A 44-char sublabel at 18px is 44 × 0.52 × 18 ≈ 412px, so it needs a box ≥ 444px wide. This costs nothing and removes most render-and-fix loops. Re-run the check whenever you bump a font size — that is exactly when a label that used to fit starts overflowing.

  • Size text for the DISPLAYED width, not the canvas. A 1600-wide diagram shown with className="img-900px" is scaled to 0.5625×, so 16px type lands at 9px on the page. Floors for a 1600×900 canvas: ≥21px box titles, ≥18px sublabels, ≥17px supplementary chips. Confirm with the display-width render (Verification step 4) — not the 1:1 render, which flatters everything.

  • Multiple spaces collapse in SVG <text>. For separators like A · B, add xml:space="preserve" to the <text>.

  • Use a system font stack (Inter, ui-sans-serif, system-ui, …) — webfonts won't load inside an <img>-embedded SVG.

  • Quote multi-word font names with SINGLE quotes. The stack goes into a double-quoted attribute, so "Segoe UI" and "SF Mono" close it early and the file is no longer well-formed XML. Write 'Segoe UI', 'SF Mono'. This bites on the very first render, before any layout work is visible:

    xml.parsers.expat.ExpatError: not well-formed (invalid token): line 10, column 65
    

    Verification step 1 catches it — which is the reason it runs first.

  • Metaphor helps recall: e.g. bun-shaped corners (larger radius on the outer edges of top/bottom layers) made the "sandwich" read at a glance.

Syntax-highlight code blocks (not plain orange)

When a diagram shows function signatures, render them as a palenight code block (the docs' own dark Prism darkTheme) rather than white text on an orange box — the highlighting then matches the real code blocks on the page, and it fixes an accessibility bug: white on #f97316 is 2.80:1 and fails WCAG, so the legacy orange treatment was never readable to begin with. Box fill #292d3e; token colors pulled from the installed renderer (node -e 'console.log(require("prism-react-renderer").themes.palenight)'):

TokenHex
function / method name#82aaff
punctuation ()#c792ea
keyword (event, function)#c792ea italic
class / event name#ffcb6b
plain text#bfc7d5

Render each token as a <tspan fill="…">. Add xml:space="preserve" to the <text> or a trailing space in a tspan (e.g. event before the name) collapses and fuses the words. For anything beyond trivial signatures, tokenize with Pygments' Solidity/Rust lexer instead of a regex (see Tooling below).

Round elbow-arrow corners

Directional turns look better curved than at sharp 90°. stroke-linejoin="round" is invisible at a 2px stroke, so round geometrically: tools/round_arrows.py rewrites every marker-end arrow path with ≥3 points, inserting a quadratic Bézier (~12px radius) at each corner. It skips 2-point straight arrows, icon curves (A/C), and paths without a marker.

python3 .claude/skills/arbitrum-brand-svg-diagrams/tools/round_arrows.py public/img/NAME.svg

Only single-path elbows round — T-junction fans (a trunk that branches) are not "turns" and stay square, which is correct.

The tool imposes hard authoring constraints

It is a regex post-processor, so how you write the arrow decides whether it works. Get these wrong and it silently skips the path — or, in the first case, corrupts the geometry. Author every arrow to this shape:

<path d="M310 440 L350 440 L350 523 L422 523" fill="none" stroke="#9dcced" stroke-width="2.5" marker-end="url(#arrow)"/>
RequirementWhyFailure mode if violated
Full L polylines — no H/VIt pairs every number in d as (x,y)geometry silently corrupted (see below)
Marker id is exactly arrowgreps the literal marker-end="url(#arrow)"path skipped, corner stays square
marker-end is an inline attributesame literal grepmoving it into a <style> class → skipped
fill="none" present inlineit injects stroke-linejoin beside itrounding applied without the linejoin
d is the first attributeregex is <path d="…"path skipped
Self-closing />regex requires itpath skipped

The H/V row is the dangerous one, and its behaviour depends on how many numbers the path happens to contain — measured:

M50 350 H200 V100 H500        5 numbers -> 2 pts -> skipped, unchanged
M50 350 H200 V100 H500 V50    6 numbers -> 3 pts -> REWRITTEN as
                              M50 350 L193.8 110.3 Q200 100 211.8 98 L500 50

An even count ≥6 produces a diagonal mess out of an orthogonal elbow, and nothing warns you. An odd count is merely skipped. So H/V is never safe — and the failure is silent either way. Always write full L pairs.

Everything else in the table degrades harmlessly to "stayed square".

Always check the printed count against the number of elbows you authored — that is your only signal the tool matched anything:

public/img/NAME.svg: rounded 2 elbow arrow(s)

rounded 0 is the correct result for a diagram whose connectors are all straight 2-point drops — a pure layer stack has no turns to round. Compare the count to the elbows you authored, not to zero. Still run the tool: it is the cheapest check that your arrows match the authoring contract above, and it must run after every regenerate, since regenerating overwrites its output.

rounded 0 also means "already rounded". The tool is idempotent: once a corner is a Q curve the path no longer matches its straight-elbow pattern, so a second run on an unchanged file reports 0. Don't read that as a regression — count the baked-in corners instead:

grep -o ' Q[0-9]' public/img/NAME.svg | wc -l   # quadratic corners present

Because arrow styling has to be inline for this tool, don't try to factor arrows into a <style> block. Text and panels can use classes; arrows cannot.

Arrowheads: three failures no tool catches

check_contrast.py inspects <text> only, and round_arrows.py only looks at path geometry. Nothing validates a marker. All three of these render without warning and are only visible in the display-width image.

1. Markers scale with stroke-width. markerUnits defaults to strokeWidth, so the head's on-canvas size is markerWidth × stroke-width. A markerWidth="7" head is 17.5px on a 2.5px stroke but 31.5px on a 4.5px one. Two of those on a short segment collide and fuse into a solid diamond that reads as a blob, not an arrow:

Mermaid <==> rendered as a 65px double-headed link
  2 heads x (7 x 4.5) = 63px of arrowhead on a 65px line  ->  diamond blob

Give any thick link its own fixed-size marker instead of resizing the geometry:

<marker id="arrowbig" viewBox="0 0 10 10" refX="9" refY="5"
        markerWidth="16" markerHeight="16" markerUnits="userSpaceOnUse"
        orient="auto-start-reverse">
  <path d="M0 0 L10 5 L0 10 z" fill="#9dcced"/>
</marker>

markerUnits="userSpaceOnUse" pins the head to 16px whatever the stroke does. refX/refY stay in viewBox units either way.

2. Marker color inverts down the gradient exactly like text does. A #9dcced arrowhead is fine in the upper two-thirds and near-invisible on the near-full cyan at the bottom. A legend or key row sitting in the last tenth needs its own ink-coloured marker (fill="#0b1b2e"), so a diagram often carries two color variants of the same head. The contrast checker will not tell you — it never looks at markers.

3. Label chips swallow arrowheads. A chip centred on a horizontal connector must clear half the head plus its own half-height. With the standard 7×2.5 head (17.5px tall) and a 25px chip, the label baseline needs ≥18px of clearance from the arrow:

label_baseline = arrow_y - 20     # 2px of margin over the 18px minimum

Too little and the head disappears under the chip — the arrow still reads as a line, so it looks like you drew a connector with no direction.

Verify markers by eye at the display width. They are the one part of the diagram with no automated gate, so budget a zoom crop (below) for any diagram with a bi-directional link, a thick link, or chipped horizontal connectors.

Zoom-crop to inspect detail

rsvg-convert cannot crop, and reading the full render downsamples exactly the detail you are checking. Rewrite the viewBox on a throwaway copy instead:

sed 's|viewBox="0 0 1360 1340" width="1360" height="1340"|viewBox="760 600 560 400" width="1120" height="800"|' \
  public/img/NAME.svg > /tmp/crop.svg
rsvg-convert -b '#ffffff' /tmp/crop.svg -o /tmp/crop.png

The width/height set the zoom factor (2× above). This is how you confirm a marker-start actually rendered, that a rounded elbow curved, and that text sits inside its chip.

Converting a Mermaid source

Mermaid is not an output format here, but it is a common input — someone pastes a flowchart/sequenceDiagram and wants it on-brand. Four things to settle before you place a single box:

  • Ask which way it should flow when the request is ambiguous. "Keep the horizontal layout" means one thing for a flowchart LR and something else for a sequenceDiagram, whose participants are already horizontal while time runs down. Guessing wrong throws away the whole diagram, so it is worth one question with two ASCII mockups rather than a full build.
  • Drop the invisible edges. ~~~ is a layout hint that exists only to steer Mermaid's auto-layout. Hand placement makes them meaningless — one source this session had 12 of them against 14 real --> edges.
  • Mermaid's per-subgraph style fill: has no brand equivalent. Don't try to map five arbitrary subgraph tints onto the palette. Let lanes carry the grouping and let fill carry role (see the key below); you usually delete a key row's worth of color in the process.
  • Node text is prose. Expand <br/> into title + sublabels, convert & to "and", apply sentence case, and run the terminology table over it — L2 in a pasted source becomes child chain in the art.

A four-role key covers nearly every architecture diagram and keeps a set of diagrams consistent with each other:

FillRoleText
#9dccedpeople (actors)#0b1b2e
#12aaffuser-facing app#0b1b2e / #0b2438
#213147service or handler#ffffff / #c9dcef
#e6007adata store#ffffff only

Reuse it verbatim across related diagrams — a reader who learns the key once should not have to relearn it on the next figure.

Sequence diagrams

Participants as columns, time down. What differs from a flowchart:

  • Lifelines are dashed verticals (#9dcced, ~1.5px, stroke-dasharray="6 7", opacity ~0.5) running from under each participant box to a common floor.
  • Labels go above the arrow, not centred on it — a centred chip on a long message hides the arrow it describes. Baseline at arrow_y - 20.
  • Every message is a straight 2-point path, so round_arrows.py reports rounded 0. That is the correct result, not a miss.
  • Two head styles. Solid request → filled triangle. Dashed response (Mermaid -->>) → open V, fill="none" + stroke-linejoin="round". The distinction is load-bearing; don't collapse it.
  • Row pitch ~62px at 16px labels keeps a chip clear of the arrowhead above.
  • Uniform column spacing, and let long labels overflow their span. Sizing columns to the longest label makes the canvas absurd; the chip masks the lifelines it crosses, which is what makes overflow safe.

Segment complex diagrams into labeled lanes

If a diagram crams several parallel mechanisms into one flat row, readers can't tell them apart. Group each into its own bounded, labeled lane — a translucent panel (fill="#ffffff" at ~6% opacity + a faint 1px stroke) with a header borrowed from the prose (e.g. 1 · Native token bridging). Mirror the doc's own section structure. Route the convergence/consumption flow into a visually separate zone below a divider.

Two worked examples: haw-l1-to-l2.svg (on the haw-svg-diagrams branch — see Reference examples) and arb-chain-fee-lifecycle.svg, which uses two lanes to separate fees paid in the same transaction from fees paid after a batch posting report — mirroring the split the page's own fee table makes.

Import / export to Excalidraw

tools/excalidraw_bridge.py round-trips a diagram between a brand SVG and an Excalidraw scene (.excalidraw), so anyone can open a diagram on excalidraw.com, tweak layout/labels, and bring it back — or sketch in Excalidraw and get a docs-ready SVG.

T=.claude/skills/arbitrum-brand-svg-diagrams/tools/excalidraw_bridge.py
python3 "$T" export public/img/NAME.svg /tmp/NAME.excalidraw   # SVG  -> Excalidraw (edit on excalidraw.com)
python3 "$T" import  /tmp/NAME.excalidraw public/img/NAME.svg  # Excalidraw -> brand SVG (docs-ready)

Scenes use roughness: 0, so Excalidraw renders the clean (non hand-drawn) style that matches the docs, and brand fill colors are preserved on the way in and out.

Round-trip scope — the bridge covers this skill's core vocabulary: <rect> (→ Excalidraw rectangle), <text>, and straight <line> arrows. To keep a diagram fully round-trippable:

  • Author boxes as plain <rect rx=…>, not <path> rounded rects.
  • Prefer straight <line> arrows; elbow <path> connectors and icon glyphs are dropped on export (re-add them in Excalidraw, or keep arrows straight).
  • Text position/size is approximate after a round-trip — treat the imported SVG as an editable starting point, then re-run Verification.

Verification (do all five)

S=public/img/NAME.svg
# 1. well-formed XML
python3 -c "import xml.dom.minidom as m; m.parse('$S'); print('XML ok')"
# 2. size win
stat -f%z "$S"     # a few KB transparent; ~50 KB with the baked background
# 3. render on WHITE (simulates the <ImageZoom> overlay) and on page-dark
rsvg-convert -b '#ffffff' -z 1 "$S" -o /tmp/light.png
rsvg-convert -b '#1b1b1d' -z 1 "$S" -o /tmp/dark.png
# 3b. OBJECTIVE opacity test — for a baked-background diagram these must be
#     byte-identical. Any difference means a transparent region that will wash
#     out under zoom. Beats eyeballing the two images.
shasum -a 256 /tmp/light.png /tmp/dark.png | awk '{print $1}' | sort -u | wc -l
#     -> 1 = fully opaque (correct).  2 = transparency present, go find it.
# 4. render at the REAL DISPLAY WIDTH from the className (img-900px -> 900)
rsvg-convert -b '#ffffff' -w 900 "$S" -o /tmp/display.png

Read /tmp/display.png and /tmp/light.png both. The 1:1 render flatters small type; the display-width render is where you find text that is technically present but unreadable in the page. Clipped text, low contrast, and label/icon overlap only show up visually.

5. Measure contrast — every label, automatically

Don't reason about where the gradient is light or dark, and don't spot-check by hand. tools/check_contrast.py renders the diagram twice — once as authored, once with all <text> stripped — then samples the text-free render underneath each label to learn its true background. That works identically for text on an opaque box and text on the gradient, where the backdrop varies with position.

python3 .claude/skills/arbitrum-brand-svg-diagrams/tools/check_contrast.py public/img/NAME.svg

It prints a ratio per label, applies the correct threshold (3:1 only for genuinely large text: ≥24px, or ≥18.66px bold — otherwise 4.5:1), and exits non-zero on any failure, so it can gate a commit.

Run it, don't reason about it. On its first run against a finished diagram it caught four failures across two rounds — including two that hand-sampling had already missed, because manual checks cover the labels you think to check and skip small chips and legend headings. A later diagram opened at 8 failures of 14 connector labels, so treat a clean first run as luck, not skill.

It checks <text> and nothing else. Arrowheads, strokes and panel borders have no automated gate at all — see "Arrowheads: three failures no tool catches".

Two recurring offenders worth knowing up front:

  • #9dcced is safe for lane headers over the upper gradient (5.0–8.2:1), drops to 3.1:1 around 75–80% down, and collapses to 1.8:1 near the bottom. Switch to #eaf5ff below mid-canvas, then to dark #0b1b2e in the last tenth — the safe fill inverts, so never carry one choice down the whole canvas.
  • White on magenta is 4.52:1 — it passes, but that is the ceiling for that fill. There is no headroom, so don't tint magenta sublabels at all.
  • Dark labels on any orange. White is 2.66:1 on Nova #ff7700 and 2.80:1 on #f97316; both fail.

The gradient has a dead band — put connector labels on chips

The inversion above has a gap in the middle where no plain text fill passes at all. Around 75–80% down, the gradient sits near #1778ed, and every candidate measured against it fails:

FillRatio on #1778ed4.5 needed
#eaf5ff3.84fail
#ffffff4.25fail
#0b1b2e4.09fail
chip #152c4e bg12.65pass

So "switch to the other fill" has no answer in that band. Don't hunt for a better hex — there isn't one. Give the label its own opaque backdrop:

<rect x="…" y="…" width="…" height="25" rx="6" fill="#152c4e" opacity="0.92"/>
<text … font-size="16" fill="#eaf5ff" text-anchor="middle">Proxies</text>

Chip every connector label, not just the failing ones. Mixed treatment reads as arbitrary, and a chip is position-independent — you can move a label later without re-deriving its color. It also masks whatever the label crosses (lifelines, lane borders, other connectors), which is the same trick Mermaid uses with its white label backgrounds.

Size the chip from the width estimate (0.52 em/char sans, 0.60 mono) plus ~12px padding. Slight text overflow past a chip is safe here: the few pixels that land on bare gradient are still #eaf5ff, which is exactly the fill you would have used anyway.

A diagram with no edge labels needs none of this — bare connectors on an opaque backdrop have no contrast requirement, and the file lands ~35% smaller.

No pnpm build is needed for either path — the link graph is untouched by an image swap. For a new diagram, run the MDX compile check from Path B instead; that is where a new JSX tag can actually break the site.

Caption styling

<ImageZoom> (fumadocs-ui/components/image-zoom) has no caption prop and renders no <figure>/<figcaption>. Caption an image with a <figure> block in the MDX, with the markdown image between blank lines so it parses as markdown rather than JSX text (see "Image captions" in INTERNALS.md):

<figure>

![Transaction lifecycle](/img/haw-transaction-lifecycle.svg)

<figcaption>Transaction lifecycle</figcaption>
</figure>

Never bake the caption into the diagram. Theme tokens are --color-fd-*; never use the legacy Docusaurus --ifm-* ones.

Reference examples

Every exemplar below is committed under public/img/ on this branch, so open the working-tree file directly. Check the size first: a hand-authored diagram is tens of KB, while a draw.io export that still embeds a raster is measured in MB.

stat -f%z public/img/haw-l1-to-l2.svg      # ~56 KB -> hand-authored
DiagramHand-authoredShows
haw-l1-to-l256 KBbaked background, labeled lanes, code blocks, rounded elbows
haw-aliasing50 KBbaked background, address-aliasing math
haw-submit-tx-to-sequencer50 KBbaked background flow
haw-bypassing-the-sequencer49 KBbaked background flow
haw-geth-sandwich2.7 KBtransparent canvas, opaque layers, bracket, legend chip

Note the floor: a baked-background diagram lands at ~50 KB regardless of complexity, because ~44 KB of it is the background. Judge a baked-background diagram against that number and a gradient-backdrop one against ~2 KB; they are different budgets, not a good and a bad result.

Their color choices predate the measured contrast table above. Copy their layout and structure, not their white-on-cyan text.

Current-generation exemplars

Copy these for colour and type; both pass check_contrast.py clean and both sit in public/img/. No gradient-backdrop exemplar is committed on this branch.

DiagramSizeCanvasBackdropShows
arb-chain-fee-lifecycle.svg53 KB1600×900baked backgroundtwo labeled lanes, three-role color key
arbitrum-chains-diagram.svg54 KB1600×900baked backgroundinlined brand logomarks, per-chain colors, tier gutter

Tooling: FOSS options considered (not yet adopted)

The generators in this skill hand-build SVG strings and tokenize code with a regex. Researched replacements, if this graduates to a tools/diagram_kit.py:

  • drawsvg (pip, actively maintained) — Pythonic SVG construction; replaces manual f-string assembly. Preferred over svgwrite (maintenance-only).
  • pygments + pygments-lexer-solidity (pip) — real Solidity/Rust lexer; use it as a tokenizer feeding the palenight color map above. Its SvgFormatter is experimental and lays out its own <text>, so tokenize only, render tspans yourself.
  • Auto-layout (ELK/dagre/Graphviz): skip. They impose their own geometry and fight the brand-precise, prose-aligned layouts this skill exists to produce; they're layout-only and still don't round corners. Reach for ELK only if a future diagram is a genuine large auto-layout graph.

See also

In this repo:

  • STYLE-GUIDE.md: editorial rules that govern diagram labels and alt text (see "Diagram labels are prose" above).
  • components/mdx.tsx: the component registry, where <ImageZoom> is registered for all MDX.
  • The img-*px width classes are no longer defined anywhere; they came from Docusaurus' src/css/custom.css, which this tree does not have. app/global.css caps prose images at 600px and data-wide on the image lifts the cap (CONTRIBUTE.md, "Image sizing"). Don't add new img-*px classes or per-image widths.
  • Finding the next candidate. Raster-in-SVG files declare themselves in the first few KB. As of origin/master this finds 26 files totalling 101.5 MB — the backlog this skill exists to work through:
for f in public/img/*.svg; do
  head -c 4000 "$f" | grep -q 'image/png;base64' && stat -f'%z %N' "$f"
done | sort -rn

(Scan 4 KB, not 400 bytes — the base64 payload starts after the draw.io preamble, so a short window silently reports zero hits.)

Standing team conventions this skill assumes:

  • Static concept art is hand-authored SVG. Never Mermaid or any text-DSL diagram tool — that is a standing preference, not a default.
  • Complex or interactive diagrams are React widgets under components/widgets/, not this skill.

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

Arbitrum L2 development best practices. Use when implementing bridges, Orbit chains, SDK integrations, or debugging cross-chain transactions. Triggers on Arbitrum, L2, rollup, bridge, retryable, Orbit, Nitro, or cross-chain contexts.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

OffchainLabs/arbitrum-docs2522026년 10월 24일 업데이트

Use when asked to rebrand, restyle, re-pace, resize, or re-render an existing explainer video, when a video's labels are baked-in raster pixels that cannot be edited, or when building a new animated explainer for the Arbitrum docs from a prompt or a doc section.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

OffchainLabs/arbitrum-docs2522026년 10월 24일 업데이트

cast

무료

Foundry's cast CLI for Ethereum interactions. Use when querying blockchain data, calling contracts, sending transactions, encoding/decoding data, or debugging on-chain issues. Triggers on "cast", "get balance", "call contract", "decode calldata", "check tx", "get block".

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

OffchainLabs/arbitrum-docs2522026년 10월 24일 업데이트

Run full documentation quality audit, covering sidebar orphans, unused-file review, MDX structure, internal links, glossary references, contract addresses, variables, formatting, frontmatter, tests, and types. Triggers on "audit docs", "check docs quality", "find problems", "content audit".

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

OffchainLabs/arbitrum-docs2522026년 10월 24일 업데이트

LLM Utility: Generate minimal project context map before starting any workflow. ALWAYS run this first at session start, before /prd, before /implement, or after context loss. Triggers on 'context prep', 'orient me', 'start', 'new session'.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

OffchainLabs/arbitrum-docs2522026년 10월 24일 업데이트

Create new Arbitrum development rules following best practices format. Use when capturing a reusable pattern, common mistake, or architectural decision. Triggers on "create rule", "add rule", "new rule", "document this pattern".

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

OffchainLabs/arbitrum-docs2522026년 10월 24일 업데이트

OffchainLabs의 스킬 모두 보기

이 스킬의 문제 신고