system-map
Skill0 starsChanged 3 months agoMITView on GitHub
Use when the user asks for a system map, an architecture or ecosystem diagram to show on screen, or says things like "build a widget", "system map", "show how X connects", "node diagram", "hub and spoke", "integration diagram", "interactive diagram for the deck", "sources in and deliverables out". Also use when updating, restyling, or changing the content of a system map built with this skill. Produces one self-contained dark .html file with a glowing core, curved connections, hover tooltips, and no build step.
5 other projects use a version of this.
Adopted by
Most recently taken up by
- rivendale/hsi-operatortheir own version, first seen Nov 7, 2026
- lifemixture/ai-orchestration-repotheir own version, first seen Oct 11, 2026
- tslateman/skillstheir own version, first seen Oct 11, 2026
- brian-bell/agent-skillstheir own version, first seen Sep 26, 2026
- mmaripanguec/dev-sdd-kittheir own version, first seen Jul 21, 2026
Other versions5 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 5
- mmaripanguec/dev-sdd-kit★ 2same name
- rivendale/hsi-operator★ 2same name
- brian-bell/agent-skills★ 1same name
- tslateman/skills★ 1same name
- lifemixture/ai-orchestration-repo★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repositoryOne other file it gives its agents.
- CLAUDE.mdCLAUDE.md
The file
Show its rules and text
We split this skill into the rules it gives an agent
16 rules. 0 are common, 8 are distinctive, and 8 are very specific to this project.
0
Common
Most of the 5 edited versions we read keep these.
8
Distinctive
This author's own choices. Worth a look.
8
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 27
Never write the widget from scratch: Always start from whichever of the four files in references/examples/ is closest to your content — …
- Distinctive: line 45
Ask the user which of the four examples is the closest match for their content (or pick for them if it's obvious from context), and read …
- Very specific: line 48
Ask which color direction they want — see references/palette.md for the four options.
- Distinctive: line 51
Ask whether they want a second language.
- Very specific: line 55
Run scripts/layout.py <left-count> <right-count> if the number of nodes changes.
- Distinctive: line 83
Every tooltip opens with a bold sentence that's the whole point, then one to three sentences of detail.
- Very specific: line 88
Keep the core's visible label short (one or two words).
- Distinctive: line 155
Default is one language (English) and zero language machinery.
- Distinctive: line 167
Only wrap strings that actually differ.
- Very specific: line 170
Add the display-toggle CSS (CODE is the language code from step 2):
- Very specific: line 181
Add the language buttons and the L keyboard shortcut:
- Very specific: line 227
Update the hint text and <title> to mention the language switch, and translate every wrapped string.
- Distinctive: line 245
Ask the user which direction fits before you build, or default to Neutral.
- Distinctive: line 249
All of these are real bugs from building the reference examples, found in a browser.
- Distinctive: line 272
Don't cherry-pick individual checks from here and run them by hand — the whole reason this file exists is that manual partial checks let …
- Very specific: line 287
Check C and D require group buttons to set b.__mdGuard = true where the mousedown guard is attached.
“Common” is measured: kept by at least 6 in 10 of the 5 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
HTML Widget
One system map on one screen: sources on the left, a glowing core in the middle, deliverables on the right, and a machinery box underneath. Hovering a node lights up its connection, dims the rest, and explains it in a tooltip card.
Built for a projector. One fixed 16:9 coordinate space that scales with pure CSS, so it looks the same at 1280×800 and 1920×1080. No build, no server, no external calls — a double-clickable file.
⛔ Only permitted workflow
Never write the widget from scratch. Always start from whichever of the
four files in references/examples/ is closest to your content — they are
finished, debugged starting points: the curve geometry, tooltip placement,
idle cycle, and scaling are all fixed through actual testing in a browser.
From scratch, you lose all of that, and every entry in Pitfalls comes back.
Steps 1–3 are questions to actually ask — batch them into one message — before touching any file, even when the request is a single terse instruction like "run the system-map skill on X" with no preferences stated. A prompt that doesn't mention a palette or a language is not the same as the user having no preference; it means you haven't asked yet.
| Rationalization | Reality |
| "The request was just 'run the skill', so speed matters more than asking" | One message with the open questions costs less than a wrong palette costs to redo |
| "No palette was mentioned, so they have no preference" | Unmentioned ≠ no preference. You haven't asked. |
| "It's obvious which palette fits the content" | Only the example match (step 1) can be inferred from content — palette and language are aesthetic choices, always ask |
- Ask the user which of the four examples is the closest match for their content (or pick for them if it's obvious from context), and read that file into context.
- Ask which color direction they want — see
references/palette.mdfor the four options. Default to Neutral only if, once asked, the user says they don't care. - Ask whether they want a second language. Default is English only, with no language-switching machinery at all. If they want a second language, see "Adding a second language" below.
- Clarify the content with the user (see Content model).
- Run
scripts/layout.py <left-count> <right-count>if the number of nodes changes. It prints finished coordinates,<g>blocks, and CSS rules. Use every block it prints, not just the curves. Change the curves without the nodes'top:values and the curves end up in one place, the nodes in another — the bug looks like "the curves are floating loose" and is easy to miss in a screenshot. - Swap in text, icons, and nodes. Keep all the CSS machinery.
- Run
scripts/selfcheck.jsin the browser at both 1920×1080 and 1280×800. Everything must beALL PASSbefore you ship it. See Verification.
Content model
| Zone | Role | Count that works |
| Left | What feeds the system — sources, inputs, integrations | 3–5 |
| Core | The one thing everything passes through | 1 |
| Right | What comes out — deliverables, artifacts, results | 3–5 |
| Bottom box | The machinery that makes it possible, in two rows | 3–4 and 4–6 |
Columns don't need equal node counts. The band is the same, so different density still reads as two equally tall blocks.
The bottom box's two rows carry a point the reference examples make on purpose: row 1 is the units, row 2 is the pattern that repeats in each of them. Use that structure when it fits, and collapse to one row when it doesn't.
Every tooltip opens with a bold sentence that's the whole point, then one to three sentences of detail. Fact-check every name and number against the source before shipping — a system map with a wrong detail is worse than no system map.
Keep the core's visible label short (one or two words). A long label widens
the core's bounding box enough to collide with the nearest tooltips — see
self-check H. Put the fuller description in the aria-label and the .ctip
text underneath instead.
Geometry
Everything is measured in a 1600×900 space. The scaling is pure CSS, no JS:
--u: calc(min(100vw, 177.78vh) / 100); /* 1u = 1% of the stage width */
.stage{width:min(100vw,177.78vh);aspect-ratio:16/9}The SVG layer uses viewBox="0 0 1600 900" and scales itself. Nodes are
positioned in %. All typography is set as calc(var(--u) * n).
Fixed anchor points: the core at (800, 395) with radius 58, the node band at y 200–590, the left column at x 56–368, the right column at x 1232–1544, the bottom box at y 645–872.
The column width of 312 (19.5%) is set by the longest label you actually
use, not by aesthetics. Check your longest label against it — self-check F
requires at least 8px of clearance, in every language you support. Need
longer labels, increase COL_W in layout.py and regenerate the curves —
don't shrink the font size, it's set for projector readability.
The curves are hand-drawn beziers between fixed points. That's why the
layout is fixed rather than responsive: a fluid layout would need JS that
measures the nodes and recomputes the curves on every resize. Let
layout.py compute them.
Tooltip placement
Tooltips are the part most likely to end up off-stage. The rules:
| Node | Classes | Why |
| Left column | inR | Points inward, toward the core |
| Right column | inL | Points inward, toward the core |
| Topmost in a column | + tTop | Centered would poke into the tagline |
| Bottommost in a column | + tBot | Centered would poke into the bottom box |
| Everyone else | none | Centered on the node |
| Bottom box row 1 | .r1 | Above the box, bottom: 100% + 4.2u |
| Bottom box row 2 | .r2 | Above the box, bottom: 100% + 10.2u |
| Outermost chip in a row | :first-child / :last-child | Anchored to its own edge instead of centered |
The bottom box's tooltips are centered on the chip, but the tooltip is 30u wide against a chip under 10u. At the edge of the row it then runs out past the node columns. The rule that fixes it, regardless of chip count:
.chip:first-child .tip{left:0;right:auto;transform:none}
.chip:last-child .tip{left:auto;right:0;transform:none}The bottom box's tooltip has to clear both the core's underside and the box's top edge. If it gets too tall, make it wider rather than moving it — a wider card gets shorter.
The tooltip covers the middle part of the curve it explains. That's intentional and matches the reference examples' behavior; the alternatives collide with something worse.
Adding a second language
Default is one language (English) and zero language machinery. If the user wants a second language, add this on top of an otherwise-finished widget:
- Set
data-lang="en"on.stage:<main class="stage" data-lang="en">. Wrap every piece of text that actually differs between the two languages in a pair of spans, using the second language's two-letter code (
no,fr,de, whatever the user asked for) instead ofen:<span class="tr tr-en">Customer request</span><span class="tr tr-CODE">…</span>Only wrap strings that actually differ. Folder names, filenames, and proper nouns are usually identical in both languages — leave them as plain text.
Add the display-toggle CSS (
CODEis the language code from step 2):.stage .tr{display:none} .stage[data-lang="en"] .tr-en{display:contents} .stage[data-lang="CODE"] .tr-CODE{display:contents}display:contentsis deliberate — the wrapper disappears from layout, so flex-gap on nodes anddisplay:blockon<b>inside tooltips behave the same in both languages.display:inlinewould add an extra flex item.Add the language buttons and the
Lkeyboard shortcut:<div class="lang"><button data-set="en">EN</button><button data-set="CODE">CODE-UPPER</button></div>.lang{position:absolute;left:2.4%;bottom:2.6%;display:flex;gap:calc(var(--u)*.35);z-index:30} .lang button{font-family:inherit;font-size:calc(var(--u)*.82);letter-spacing:.07em; padding:calc(var(--u)*.3) calc(var(--u)*.8);border-radius:calc(var(--u)*.38); border:1px solid rgba(255,255,255,.12);background:transparent;color:var(--txt-3); cursor:pointer;transition:.22s} .lang button:hover{color:var(--txt-2);border-color:rgba(255,255,255,.28)} .stage[data-lang="en"] .lang button[data-set="en"], .stage[data-lang="CODE"] .lang button[data-set="CODE"]{ color:var(--accent);border-color:rgba(var(--accent-rgb),.5);background:rgba(var(--accent-rgb),.10)} .lang button:focus-visible{outline:none;box-shadow:0 0 0 2px rgba(255,255,255,.4)}<script> var stage = document.querySelector('.stage'); function setLang(l) { stage.dataset.lang = l; document.documentElement.lang = l; } document.querySelectorAll('.lang button').forEach(function (b) { b.addEventListener('mousedown', function (e) { e.preventDefault(); }); b.__mdGuard = true; b.addEventListener('click', function () { setLang(b.dataset.set); }); }); addEventListener('keydown', function (e) { if (e.key === 'l' || e.key === 'L') { setLang(stage.dataset.lang === 'en' ? 'CODE' : 'en'); } }); </script>The
mousedown→preventDefault()guard matters: without it, a mouse click gives the clicked button focus, and the very next keypress makes the browser treat the session as keyboard-driven, so the focused button matches:focus-visibleand inherits the selected button's accent color — both languages end up looking selected.b.__mdGuard = trueis what lets self-check D confirm the guard is actually attached (event listeners are otherwise invisible from JS).- Update the hint text and
<title>to mention the language switch, and translate every wrapped string. - Re-run
scripts/selfcheck.js— it detects the.tr-CODEclasses automatically and runs checks E (language pairs) and F–H once per language. If you want automatic detection of untranslated leftover text, write your own probe for your specific two languages — the check isn't generic across arbitrary language pairs, so it isn't built in.
Palette
See references/palette.md for the four color directions (Neutral, Ember,
Signal, Volt) and their exact variable values. Every glow, shadow, and
gradient color in the stylesheet is written as var(--accent) or
rgba(var(--accent-rgb), alpha) (and the -2/-3 equivalents) — swap the
six variables in :root and the whole widget recolors, including the
gradient (wired through the .stopA/.stopB/.stopC classes on the
<stop> elements).
Ask the user which direction fits before you build, or default to Neutral.
Pitfalls
All of these are real bugs from building the reference examples, found in a browser. The right column shows which self-check catches them again.
| Symptom | Cause | Fix | Caught by |
| Tooltip text runs off the card, doesn't wrap | white-space:nowrap on .node/.chip inherited into .tip | .tip{white-space:normal;word-break:break-word} | G |
| The lit curve turns the wrong color | One gradient with gradientUnits="userSpaceOnUse" spanning the full width — left and right halves hit the wrong stops | Two gradients, #gL (x 300→800) and #gR (x 800→1300), both toward --accent at the core | — (visual, check the screenshot) |
| The pulse keeps running on dimmed curves during hover | Static opacity:0 doesn't stop a running animation | .stage:has(.node:hover) .cmt{animation:none;opacity:0} | — (visual, check the screenshot) |
| Node text wraps to two lines on a narrow screen | Rounding makes the text juuust not fit | white-space:nowrap on .node and a slightly smaller font size | F |
| Curves don't reach the nodes after the count changed | Only the <g> blocks from layout.py were pasted in, not the top: values | Use both blocks. Check that the number of .wire equals the number of .node | A + B |
| Both languages show at once in the tagline | A descendant selector like .hd span{display:block} also hits the .tr wrappers and overrides display:none | Use a child selector: .hd > span. Applies anywhere you style span inside a text container | — (fix manually; see "Adding a second language") |
| The core's label collides with a nearby tooltip | A long core label widens the .core-lbl bounding box far enough to overlap the closest node tooltips | Keep the visible core label to one or two words. Put the full name in aria-label and the .ctip text instead | H |
| The bottom box's tooltip lands on top of a node in the column | The card is centered on the chip and much wider than it. At the edge of the row it reaches over the column | Anchor the outermost chip to its own edge with :first-child / :last-child, see the table above | H |
| A label overflows the node, most visible after adding a second language | white-space:nowrap prevents wrapping, so text that's too long for one language runs out of the box without showing up in a tooltip sweep | Increase COL_W in layout.py and regenerate the curves. Measure the label in every language you support — check F | F |
Both language buttons light up after a mouse click followed by L | The click gave the button focus. The first keypress makes the browser treat the session as keyboard-driven, so the focused button matches :focus-visible and inherits the accent color from the focus rule | Give focus its own visual form — a box-shadow ring, not border-color. Also add mousedown → preventDefault() on the buttons, so a mouse click never moves focus | C + D |
| The widget ships in the default Neutral palette without the user ever being asked | A terse invocation ("run the skill on X") was read as license to skip the step-2 question instead of asking and defaulting only if told "no preference" | Ask steps 1–3 in one message before reading or writing any file, regardless of how terse the request was | — (process; verify the question was asked before any file was touched) |
Verification
Paste scripts/selfcheck.js into the browser console on the widget. Run
it at both 1920×1080 and 1280×800. ALL PASS is required before you ship
it.
Don't cherry-pick individual checks from here and run them by hand — the whole reason this file exists is that manual partial checks let three bugs through in a row. Run the whole file, at both resolutions.
| Check | Catches |
| A wires/nodes | Number of .wire ≠ number of .node |
| B wire attachment | A node moved without the curve following — half of layout.py's output was used |
| C focus/selected | A :focus-visible rule on a group button sets something other than outline/box-shadow/border-width. If it shares an expression with the selected state, two buttons light up alike the moment the user touches the keyboard |
| D focus trap | A group button without mousedown → preventDefault(), so a mouse click leaves focus behind |
| E language pairs | Only runs if the widget has a second language (auto-detected from .tr-CODE classes). Imbalance between the two codes, or a missing data-lang in the markup |
| F label width | Under 8px of clearance on a node label, in every language present |
| G tooltip | Text that doesn't wrap, or a card that ends up off-stage |
| H collision | A tooltip that covers a node, the core, the tagline, or the language switch |
Check C and D require group buttons to set b.__mdGuard = true where the
mousedown guard is attached. Without the flag, the test can't see it —
event listeners are otherwise invisible from JS.
Beyond the self-check, which is still manual: zero console errors, and fact-check every name and number in the tooltips against the source.
About the preview panel
Two things that look like bugs in the widget but are the panel:
- A mouse click lands in the wrong place. The tool reports one
coordinate and the page receives another. Use
read_page+ref, or test by keyboard. - Style changes on a single element don't re-layout.
el.style.top = …changes thestyleattribute withoutgetBoundingClientRect()moving. An injected<style>does work. If you want to sabotage layout to test it, do it through a stylesheet or by changing a curve'sd.
Files
| File | Role |
references/examples/agent-workflow.html | AI agent workflow, Signal palette, 4 sources / 5 outputs |
references/examples/business-integration.html | Business systems integration, Neutral palette, 3 / 4 |
references/examples/data-pipeline.html | Data/observability pipeline, Ember palette, 5 / 3 |
references/examples/hr-platform.html | HR/people-data platform, Volt palette, 4 / 4 |
references/palette.md | The four color directions and their variables |
scripts/layout.py | Computes node positions, bezier curves, and CSS rules. python3 layout.py 4 5 |
scripts/selfcheck.js | Eight invariants, auto-detecting single- vs two-language builds. Must give ALL PASS |
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

