feat(types): ship ten editorial diagram types in one release (#118)
Adds ten editorial diagram types with light, dark, and full-editorial examples; updates the taxonomy, gallery, descriptions, references, and synchronized plugin version; and adds maintainer/visual-proof contribution gates. Co-authored-by: Jacopo Vendramin <vendraminjacopo@gmail.com>
This commit is contained in:
co-authored by
Jacopo Vendramin
parent
5f1b6ddc66
commit
4691a2f40c
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "diagram-design",
|
||||
"metadata": {
|
||||
"description": "Create branded architecture, IT current-state, flowchart, sequence, state machine, ER/data model, timeline, swimlane, quadrant, radar/spider, loop/flywheel, nested, tree, org chart, layer stack, Venn, pyramid/funnel, treemap, bar, line, Gantt and scatter charts, high-level, process, medallion, data flow, DP integration, or DP security matrix diagrams as standalone HTML/SVG/PNG. Redraw .drawio/.drawio.png/.drawio.svg or Mermaid .mmd sources at a chosen size/detail; onboard brand tokens from a website; add semantic patterns, callouts, accessible motion, or sketchy/hand-drawn styling."
|
||||
"description": "Create branded architecture, IT current-state, flowchart, sequence, state machine, ER/data model, timeline, swimlane, quadrant, radar/spider, loop/flywheel, nested, tree, org chart, layer stack, Venn, pyramid/funnel, treemap, bar, line, Gantt and scatter charts, high-level, process, medallion, data flow, DP integration, DP security matrix, sankey, fishbone, Wardley map, kanban, user journey, deployment, dependency graph, UML class, story map, or database schema diagrams as standalone HTML/SVG/PNG. Redraw .drawio/.drawio.png/.drawio.svg or Mermaid .mmd sources at a chosen size/detail; onboard brand tokens from a website; add semantic patterns, callouts, accessible motion, or sketchy/hand-drawn styling."
|
||||
},
|
||||
"owner": {
|
||||
"name": "Cathryn Lavery"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "diagram-design",
|
||||
"description": "Create branded architecture, IT current-state, flowchart, sequence, state machine, ER/data model, timeline, swimlane, quadrant, radar/spider, loop/flywheel, nested, tree, org chart, layer stack, Venn, pyramid/funnel, treemap, bar, line, Gantt and scatter charts, high-level, process, medallion, data flow, DP integration, or DP security matrix diagrams as standalone HTML/SVG/PNG. Redraw .drawio/.drawio.png/.drawio.svg or Mermaid .mmd sources at a chosen size/detail; onboard brand tokens from a website; add semantic patterns, callouts, accessible motion, or sketchy/hand-drawn styling.",
|
||||
"version": "2.5.6",
|
||||
"description": "Create branded architecture, IT current-state, flowchart, sequence, state machine, ER/data model, timeline, swimlane, quadrant, radar/spider, loop/flywheel, nested, tree, org chart, layer stack, Venn, pyramid/funnel, treemap, bar, line, Gantt and scatter charts, high-level, process, medallion, data flow, DP integration, DP security matrix, sankey, fishbone, Wardley map, kanban, user journey, deployment, dependency graph, UML class, story map, or database schema diagrams as standalone HTML/SVG/PNG. Redraw .drawio/.drawio.png/.drawio.svg or Mermaid .mmd sources at a chosen size/detail; onboard brand tokens from a website; add semantic patterns, callouts, accessible motion, or sketchy/hand-drawn styling.",
|
||||
"version": "2.5.10",
|
||||
"author": {
|
||||
"name": "Cathryn Lavery",
|
||||
"url": "https://github.com/cathrynlavery"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "diagram-design",
|
||||
"description": "Create branded architecture, IT current-state, flowchart, sequence, state machine, ER/data model, timeline, swimlane, quadrant, radar/spider, loop/flywheel, nested, tree, org chart, layer stack, Venn, pyramid/funnel, treemap, bar, line, Gantt and scatter charts, high-level, process, medallion, data flow, DP integration, or DP security matrix diagrams as standalone HTML/SVG/PNG. Redraw .drawio/.drawio.png/.drawio.svg or Mermaid .mmd sources at a chosen size/detail; onboard brand tokens from a website; add semantic patterns, callouts, accessible motion, or sketchy/hand-drawn styling.",
|
||||
"version": "2.5.6",
|
||||
"description": "Create branded architecture, IT current-state, flowchart, sequence, state machine, ER/data model, timeline, swimlane, quadrant, radar/spider, loop/flywheel, nested, tree, org chart, layer stack, Venn, pyramid/funnel, treemap, bar, line, Gantt and scatter charts, high-level, process, medallion, data flow, DP integration, DP security matrix, sankey, fishbone, Wardley map, kanban, user journey, deployment, dependency graph, UML class, story map, or database schema diagrams as standalone HTML/SVG/PNG. Redraw .drawio/.drawio.png/.drawio.svg or Mermaid .mmd sources at a chosen size/detail; onboard brand tokens from a website; add semantic patterns, callouts, accessible motion, or sketchy/hand-drawn styling.",
|
||||
"version": "2.5.10",
|
||||
"author": {
|
||||
"name": "Cathryn Lavery",
|
||||
"url": "https://github.com/cathrynlavery"
|
||||
@@ -26,7 +26,7 @@
|
||||
"interface": {
|
||||
"displayName": "Diagram Design",
|
||||
"shortDescription": "Create editorial technical and product diagrams as standalone HTML.",
|
||||
"longDescription": "Create branded architecture, IT current-state, flowchart, sequence, state machine, ER/data model, timeline, swimlane, quadrant, radar/spider, loop/flywheel, nested, tree, org chart, layer stack, Venn, pyramid/funnel, treemap, bar, line, Gantt and scatter charts, high-level, process, medallion, data flow, DP integration, or DP security matrix diagrams as standalone HTML/SVG/PNG. Redraw .drawio/.drawio.png/.drawio.svg or Mermaid .mmd sources at a chosen size/detail; onboard brand tokens from a website; add semantic patterns, callouts, accessible motion, or sketchy/hand-drawn styling.",
|
||||
"longDescription": "Create branded architecture, IT current-state, flowchart, sequence, state machine, ER/data model, timeline, swimlane, quadrant, radar/spider, loop/flywheel, nested, tree, org chart, layer stack, Venn, pyramid/funnel, treemap, bar, line, Gantt and scatter charts, high-level, process, medallion, data flow, DP integration, DP security matrix, Sankey, fishbone, Wardley map, kanban, user journey, deployment, dependency graph, UML class, story map, or database schema diagrams as standalone HTML/SVG/PNG. Redraw .drawio/.drawio.png/.drawio.svg or Mermaid .mmd sources at a chosen size/detail; onboard brand tokens from a website; add semantic patterns, callouts, accessible motion, or sketchy/hand-drawn styling.",
|
||||
"developerName": "Cathryn Lavery",
|
||||
"category": "Productivity",
|
||||
"capabilities": [
|
||||
|
||||
@@ -2,6 +2,22 @@
|
||||
|
||||
<!-- One or two sentences: what changed and why. -->
|
||||
|
||||
## Visual proof
|
||||
|
||||
<!--
|
||||
Required when this PR adds or changes a diagram, example, or template.
|
||||
Attach rendered screenshots—not source snippets—for every changed variant. A new
|
||||
diagram type must show its light, dark, and full editorial examples. Use a small
|
||||
captioned table or contact sheet so reviewers can compare the whole set at a glance.
|
||||
Write "Not applicable" only when the PR has no visual output changes.
|
||||
-->
|
||||
|
||||
| Variant | Rendered preview |
|
||||
|---|---|
|
||||
| Light | |
|
||||
| Dark | |
|
||||
| Full editorial | |
|
||||
|
||||
## Validation gates
|
||||
|
||||
<!-- Mark what you ran and that it passed. CI runs all of these on the PR too. -->
|
||||
@@ -19,6 +35,7 @@
|
||||
- [ ] No new entries added to `scripts/lint-skin-baseline.txt`
|
||||
- [ ] Generated and source files are consistent (extractor ↔ verifier ↔ reference ↔ command)
|
||||
- [ ] Accessible SVG contract satisfied for new/changed examples
|
||||
- [ ] Rendered screenshots are attached for every new/changed diagram variant, or visual proof is marked not applicable
|
||||
- [ ] Code of Conduct respected
|
||||
|
||||
## Related issues
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 314 KiB |
@@ -0,0 +1,99 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"repository": "cathrynlavery/diagram-design",
|
||||
"base_branch": "main",
|
||||
"default_merge_method": "squash",
|
||||
"versioning": {
|
||||
"enabled": true,
|
||||
"strategy": "semver",
|
||||
"increment": "patch",
|
||||
"manifests": [
|
||||
{
|
||||
"path": ".claude-plugin/plugin.json",
|
||||
"json_pointer": "/version"
|
||||
},
|
||||
{
|
||||
"path": ".codex-plugin/plugin.json",
|
||||
"json_pointer": "/version"
|
||||
}
|
||||
],
|
||||
"require_synchronized": true,
|
||||
"require_unique_across_open_prs": true,
|
||||
"bump_command": "python3 scripts/bump-plugin-version.py"
|
||||
},
|
||||
"gates": {
|
||||
"local_commands": [
|
||||
"python3 scripts/test-plugin-package.py",
|
||||
"python3 scripts/verify-plugin-package.py origin/main",
|
||||
"npx --yes @anthropic-ai/claude-code@2.1.229 plugin validate . --strict",
|
||||
"python3 scripts/test-lint-a11y.py",
|
||||
"python3 scripts/verify-semantic-motion.py --markdown-only",
|
||||
"python3 scripts/verify-semantic-motion.py --example-only",
|
||||
"python3 scripts/verify-motion.py --shipped",
|
||||
"python3 scripts/test-build-icons-quoted-attributes.py",
|
||||
"python3 scripts/test-build-icons-devicon.py",
|
||||
"python3 scripts/test-fix-mojibake.py",
|
||||
"python3 scripts/lint-skin.py --all --baseline",
|
||||
"python3 scripts/verify-sequence-oauth.py",
|
||||
"python3 scripts/verify-drawio-import.py",
|
||||
"python3 scripts/test-verify-drawio-import.py",
|
||||
"python3 scripts/verify-mermaid-import.py",
|
||||
"python3 scripts/test-verify-motion.py",
|
||||
"python3 scripts/verify-docs-sync.py",
|
||||
"python3 scripts/test-verify-docs-sync.py",
|
||||
"python3 scripts/test-self-check.py",
|
||||
"python3 scripts/verify-geometry.py --all",
|
||||
"python3 scripts/test-verify-geometry.py",
|
||||
"python3 scripts/verify-treemap.py --all",
|
||||
"python3 scripts/test-verify-treemap.py",
|
||||
"python3 scripts/verify-dumbbell.py",
|
||||
"python3 scripts/test-verify-dumbbell.py",
|
||||
"python3 scripts/verify-slopegraph.py --all",
|
||||
"python3 scripts/test-verify-slopegraph.py",
|
||||
"python3 scripts/build-icons.py && git diff --ignore-space-at-eol --exit-code -- skills/diagram-design/assets/icons.html skills/diagram-design/references/primitive-icons.md"
|
||||
],
|
||||
"required_check_names": [],
|
||||
"review": {
|
||||
"provider": "none",
|
||||
"minimum_score": "",
|
||||
"require_same_head": true
|
||||
},
|
||||
"require_current_base": true,
|
||||
"require_conflict_free": true,
|
||||
"require_resolved_threads": true,
|
||||
"require_no_requested_changes": true,
|
||||
"post_merge_workflows": [
|
||||
"CI",
|
||||
"Deploy gallery to GitHub Pages"
|
||||
]
|
||||
},
|
||||
"issue_policy": {
|
||||
"read_complete_threads": true,
|
||||
"search_competing_prs": true,
|
||||
"reproduce_on_current_main": true,
|
||||
"inspect_history_for_intent": true,
|
||||
"max_maintainer_authored_prs_per_run": 1
|
||||
},
|
||||
"pr_policy": {
|
||||
"process_versioned_prs_serially": true,
|
||||
"inspect_first_time_fork_workflows": true,
|
||||
"split_large_or_mixed_scope": true,
|
||||
"run_complete_suite_on_exact_head": true,
|
||||
"wait_for_post_merge_before_next": true
|
||||
},
|
||||
"human_gates": [
|
||||
"product_priority",
|
||||
"large_or_mixed_scope",
|
||||
"first_time_workflow_approval",
|
||||
"ambiguous_issue_closure",
|
||||
"security_disclosure",
|
||||
"merge_authorization"
|
||||
],
|
||||
"report": {
|
||||
"path": ".context/maintainer/{timestamp}/report.md",
|
||||
"live_urls": [
|
||||
"https://cathrynlavery.github.io/diagram-design/"
|
||||
],
|
||||
"require_worklog": true
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -8,7 +8,7 @@ Please read [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) first. All contributions ar
|
||||
|
||||
## What this project is
|
||||
|
||||
Diagram Design is an agent skill (Claude Code, Codex, Pi) that produces editorial-quality diagrams as self-contained HTML files. The repo is documentation-first: `skills/diagram-design/SKILL.md` is the index, each of the 28 visual types has its own reference file, and the extractor scripts in `skills/diagram-design/scripts/` turn draw.io and Mermaid sources into a structured IR.
|
||||
Diagram Design is an agent skill (Claude Code, Codex, Pi) that produces editorial-quality diagrams as self-contained HTML files. The repo is documentation-first: `skills/diagram-design/SKILL.md` is the index, each of the 38 visual types has its own reference file, and the extractor scripts in `skills/diagram-design/scripts/` turn draw.io and Mermaid sources into a structured IR.
|
||||
|
||||
See [README.md](README.md) for the full picture, including the design system and the import/export flows.
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@
|
||||
|
||||
*New in 2.3: semantic system patterns and optional accessible motion, while static output stays the default.*
|
||||
|
||||
28 visual types. One agent skill for Claude Code, Codex, and Pi. Semantic patterns describe behavior separately from layout, so a queue, policy trace, or trust boundary can use the nearest existing type without expanding the type count. Static HTML remains the default; optional motion is available for ordered explanations. The skill also redraws draw.io or Mermaid sources at a chosen format, size, and detail level.
|
||||
*New in 2.5.10: ten more layout grammars — Sankey, fishbone, Wardley map, kanban, user journey, deployment, dependency graph, UML class, story map, and database schema.*
|
||||
|
||||
38 visual types. One agent skill for Claude Code, Codex, and Pi. Semantic patterns describe behavior separately from layout, so a queue, policy trace, or trust boundary can use the nearest existing type without expanding the type count. Static HTML remains the default; optional motion is available for ordered explanations. The skill also redraws draw.io or Mermaid sources at a chosen format, size, and detail level.
|
||||
|
||||
No Figma. No generic rounded boxes. No 30-minute color-picking sessions.
|
||||
|
||||
@@ -22,7 +24,7 @@ No Figma. No generic rounded boxes. No 30-minute color-picking sessions.
|
||||
|
||||
I write at [littlemight.com](https://littlemight.com?utm_source=diagram-design&utm_medium=readme&utm_campaign=github&utm_content=intro) (and run [BestSelf.co](https://bestself.co?utm_source=diagram-design&utm_medium=readme&utm_campaign=github&utm_content=intro) on the side). Every time I needed a diagram — an architecture sketch, a flowchart, a pyramid of what matters most — I'd ask Claude and get back a generic rounded-box thing that looked nothing like the rest of the site. I'd either fight with Figma for 30 minutes or just skip the diagram.
|
||||
|
||||
So I built a Claude Code skill for it. Twenty-eight visual types, editorial quality, matches your brand in 60 seconds by reading your website.
|
||||
So I built a Claude Code skill for it. Thirty-eight visual types, editorial quality, matches your brand in 60 seconds by reading your website.
|
||||
|
||||
> *The highest-quality move is usually deletion.* Every node earns its place. The accent color is reserved for the 1–2 things the reader should look at first. Target density: 4/10.
|
||||
|
||||
@@ -30,7 +32,7 @@ So I built a Claude Code skill for it. Twenty-eight visual types, editorial qual
|
||||
|
||||
## What it makes
|
||||
|
||||
All 28 visual types ship in three static variants: minimal light, minimal dark, and full-editorial. Open any of them directly in a browser. There is no build step, JavaScript, or external image dependency.
|
||||
All 38 visual types ship in three static variants: minimal light, minimal dark, and full-editorial. Open any of them directly in a browser. There is no build step, JavaScript, or external image dependency.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
@@ -85,7 +87,22 @@ All 28 visual types ship in three static variants: minimal light, minimal dark,
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
**Browse the live gallery:** [cathrynlavery.github.io/diagram-design](https://cathrynlavery.github.io/diagram-design/) — or open [`skills/diagram-design/assets/index.html`](skills/diagram-design/assets/index.html) locally to flip through all 28 diagrams with light / dark / full-editorial tabs.
|
||||
**New in 2.5.10 — ten layout grammars nothing else covered.** Open the examples directly or compare every light, dark, and full-editorial render in the [30-variant contact sheet](.github/pr-previews/editorial-diagrams-2.5.10.jpg).
|
||||
|
||||
| Type | Shows | Example |
|
||||
|---|---|---|
|
||||
| **Sankey** | A quantity splitting and merging across stages — band width is the amount | [`example-sankey.html`](skills/diagram-design/assets/example-sankey.html) |
|
||||
| **Fishbone** | Causes of one observed effect, grouped by category — root-cause analysis | [`example-fishbone.html`](skills/diagram-design/assets/example-fishbone.html) |
|
||||
| **Wardley map** | Value chain against evolution — what to build, what to buy, what is moving | [`example-wardley.html`](skills/diagram-design/assets/example-wardley.html) |
|
||||
| **Kanban** | Work-in-progress by state, with WIP limits and blocked items | [`example-kanban.html`](skills/diagram-design/assets/example-kanban.html) |
|
||||
| **User journey** | What a person does across stages of an experience, and how it feels | [`example-journey.html`](skills/diagram-design/assets/example-journey.html) |
|
||||
| **Deployment** | Where software runs — zones, hosts, artifacts, replicas, ports | [`example-deployment.html`](skills/diagram-design/assets/example-deployment.html) |
|
||||
| **Dependency graph** | What depends on what — fan-in and cycles a tree cannot express | [`example-dependency.html`](skills/diagram-design/assets/example-dependency.html) |
|
||||
| **UML class** | Classes with operations, inheritance and composition (other UML routes to existing types) | [`example-uml-class.html`](skills/diagram-design/assets/example-uml-class.html) |
|
||||
| **Story map** | Narrative backbone sliced into releases, with the release cut line | [`example-story-map.html`](skills/diagram-design/assets/example-story-map.html) |
|
||||
| **Database schema** | Physical tables: SQL types, constraints, indexes, column-level foreign keys | [`example-db-schema.html`](skills/diagram-design/assets/example-db-schema.html) |
|
||||
|
||||
**Browse the live gallery:** [cathrynlavery.github.io/diagram-design](https://cathrynlavery.github.io/diagram-design/) — or open [`skills/diagram-design/assets/index.html`](skills/diagram-design/assets/index.html) locally to flip through all 38 diagrams with light / dark / full-editorial tabs.
|
||||
|
||||
---
|
||||
|
||||
@@ -185,7 +202,7 @@ Every diagram template gives the inline SVG an accessible name and description:
|
||||
|
||||
### Manual override
|
||||
|
||||
Prefer to set tokens by hand? Open [`skills/diagram-design/references/style-guide.md`](skills/diagram-design/references/style-guide.md) and edit the table. Everything downstream reads from there — all 28 diagrams, the annotation primitive, and the gallery all inherit semantic role names (`accent`, not `#eb6c36`).
|
||||
Prefer to set tokens by hand? Open [`skills/diagram-design/references/style-guide.md`](skills/diagram-design/references/style-guide.md) and edit the table. Everything downstream reads from there — all 38 diagrams, the annotation primitive, and the gallery all inherit semantic role names (`accent`, not `#eb6c36`).
|
||||
|
||||
### First-run gate
|
||||
|
||||
@@ -206,7 +223,7 @@ The profile library is shared across Claude Code, Codex, and Pi. Use `/diagram-d
|
||||
## Quickstart
|
||||
|
||||
```bash
|
||||
# From a cloned checkout, open the gallery to see all 28 diagrams
|
||||
# From a cloned checkout, open the gallery to see all 38 diagrams
|
||||
open skills/diagram-design/assets/index.html # macOS
|
||||
xdg-open skills/diagram-design/assets/index.html # Linux
|
||||
|
||||
@@ -360,6 +377,16 @@ diagram-design/
|
||||
│ │ ├── type-layers.md
|
||||
│ │ ├── type-venn.md
|
||||
│ │ ├── type-pyramid.md
|
||||
│ │ ├── type-sankey.md
|
||||
│ │ ├── type-fishbone.md
|
||||
│ │ ├── type-wardley.md
|
||||
│ │ ├── type-kanban.md
|
||||
│ │ ├── type-journey.md
|
||||
│ │ ├── type-deployment.md
|
||||
│ │ ├── type-dependency.md
|
||||
│ │ ├── type-uml-class.md
|
||||
│ │ ├── type-story-map.md
|
||||
│ │ ├── type-db-schema.md
|
||||
│ │ ├── primitive-annotation.md
|
||||
│ │ ├── primitive-sketchy.md
|
||||
│ │ └── primitive-terminal.md
|
||||
@@ -370,7 +397,7 @@ diagram-design/
|
||||
│ └── assets/
|
||||
│ ├── index.html — live gallery, tabbed
|
||||
│ ├── template*.html — scaffolds for new diagrams
|
||||
│ ├── example-<type>.html — 3 variants × 28 types
|
||||
│ ├── example-<type>.html — 3 variants × 38 types
|
||||
│ ├── example-loop-terminal.html
|
||||
│ ├── example-quadrant-consultant.html
|
||||
│ ├── example-import-drawio.html
|
||||
@@ -429,7 +456,7 @@ At startup, the agent sees only the skill name and description. When a request m
|
||||
| "Give me a terminal / CLI-window version" | `SKILL.md` + `references/primitive-terminal.md` |
|
||||
| "Redraw this .drawio file for my deck" | `SKILL.md` + `references/import-drawio.md` + `references/output-spec.md` + the chosen type's reference |
|
||||
| "Redraw this Mermaid block for my deck" | `SKILL.md` + `references/import-mermaid.md` + `references/output-spec.md` + the chosen type's reference |
|
||||
| Routine static diagram-making (any of the 28 visual types) | Only `SKILL.md` + that one type's reference |
|
||||
| Routine static diagram-making (any of the 38 visual types) | Only `SKILL.md` + that one type's reference |
|
||||
|
||||
No matter how many types exist, the agent only reads the one you need. Add a new type tomorrow and nothing else changes.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ Accepts `.drawio`, `.drawio.xml`, `.xml`, `.drawio.png`, and `.drawio.svg`.
|
||||
- `--detail=balanced` · `--audience=mixed`.
|
||||
- `--variant=light` — the minimal light template.
|
||||
- A single-page file selects its only page; a multi-page file lists pages and asks which to use.
|
||||
- Type is chosen from the extracted structure; `--type` forces one of the 27.
|
||||
- Type is chosen from the extracted structure; `--type` forces one of the 37.
|
||||
|
||||
## Flags
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ Accepts `.mmd`, `.mermaid`, and Markdown files containing fenced `mermaid` block
|
||||
- `--detail=balanced` · `--audience=mixed`.
|
||||
- `--variant=light` — the minimal light template.
|
||||
- A single diagram selects diagram 0; a multi-block Markdown file lists blocks and asks which to use.
|
||||
- Type is chosen from the extracted grammar and structure; `--type` forces one of the 27.
|
||||
- Type is chosen from the extracted grammar and structure; `--type` forces one of the 37.
|
||||
|
||||
## Flags
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# ADR 0002 — Semantic patterns never expand the 27-type taxonomy
|
||||
# ADR 0002 — Semantic patterns never expand the visual-type taxonomy
|
||||
|
||||
**Status:** accepted (v2.3)
|
||||
|
||||
@@ -8,7 +8,7 @@ Auditing behavior-rich figures (queues, policy traces, trust boundaries) showed
|
||||
|
||||
## Decision
|
||||
|
||||
Behavior is a separate axis. The seven semantic patterns in `references/semantic-patterns.md` each route to the **nearest existing visual type** for layout; a pattern owns semantic primitives and a tighter budget, never a second layout grammar. The visual-type count stays at 27 unless a genuinely new *layout* grammar appears.
|
||||
Behavior is a separate axis. The seven semantic patterns in `references/semantic-patterns.md` each route to the **nearest existing visual type** for layout; a pattern owns semantic primitives and a tighter budget, never a second layout grammar. The visual-type count only moves when a genuinely new *layout* grammar appears (it did in v2.5.10 — see [ADR 0007](0007-new-layout-grammars.md), 28 → 38).
|
||||
|
||||
## Consequences
|
||||
|
||||
@@ -21,3 +21,5 @@ Behavior is a separate axis. The seven semantic patterns in `references/semantic
|
||||
**2026-08-18 — the count is 28.** Treemap was admitted under the escape clause above: recursive area subdivision is a layout grammar no existing type provides (bar encodes with length, nested with containment and no quantity, pyramid with rank). It shipped the full §10 set, and the counters named above moved 27 → 28 together with the prose.
|
||||
|
||||
The decision itself is unchanged — semantic patterns still never add a type, and the count still moves only for a new *layout* grammar. What this amendment records is the procedure: the two counters are this ADR's enforcement, so a PR that edits them without amending this file has quietly made itself the authority. Amend here in the same PR, or the number in the test is just whatever the last contributor typed.
|
||||
|
||||
**2026-08-19 — the count is 38.** Ten additional visual grammars were admitted under the same escape clause: Sankey, fishbone, Wardley map, kanban, user journey, deployment, dependency graph, UML class, story map, and database schema. The per-type argument is in [ADR 0007](0007-new-layout-grammars.md); each ships the full §10 set, and the two counters move 28 → 38 together with the prose.
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# ADR 0007 — Ten new layout grammars (28 → 38 visual types)
|
||||
|
||||
**Status:** accepted (v2.5.10)
|
||||
|
||||
## Context
|
||||
|
||||
ADR 0002 fixed the taxonomy at 27 and set the bar for growth: a new type is justified only when a **genuinely new layout grammar** appears, not when a new *behavior* does — behavior is a semantic pattern routed onto an existing type.
|
||||
|
||||
Three coverage audits ran against that bar. The first compared the skill to the Mermaid 2026 taxonomy (29 diagram types) plus current practitioner writing. The second worked through a 20-item request list covering architecture, flows, API/integration, database, and process figures. The third covered UML, story mapping, and physical database diagrams. Between them, ten figures turned out to be undrawable with any existing grammar.
|
||||
|
||||
## Decision
|
||||
|
||||
Add ten types, each with the full §10 shipping set (type reference + light/dark/full examples + gallery tab + routing row + budget row):
|
||||
|
||||
| Type | The grammar that did not exist | Nearest existing type, and why it fails |
|
||||
|---|---|---|
|
||||
| **Sankey** | Band width encodes a quantity that splits and merges | Pyramid shows drop-off but not splits/merges; process shows steps, not amounts |
|
||||
| **Fishbone** | Angled category bones converging on one effect | Tree is parent→children, not cause→effect at a fixed angle with sub-cause ticks |
|
||||
| **Wardley map** | Two ordinal axes (value chain × evolution) with dependency links and movement | Quadrant positions items but has no dependency chain and no evolution bands |
|
||||
| **Kanban** | State census in columns with WIP limits and **no connectors** | Swimlane is lanes plus a flow crossing them; a board deliberately has no flow |
|
||||
| **User journey** | Stage grid plus a sentiment curve as the load-bearing element | Timeline places events on an axis; line charts plot data, neither carries per-stage rows |
|
||||
| **Deployment** | Physical placement: zones containing hosts containing versioned artifacts, with replica counts and protocol:port paths | Architecture is logical components and relations; high-level is one fixed stack shape, not an arbitrary environment topology |
|
||||
| **Dependency graph** | Ranked DAG where a node has **many parents** and a **cycle** is representable | Tree structurally forbids both — a single parent per node, and no back-edges |
|
||||
| **UML class** | Three-compartment boxes with an operations list, plus the typed relationship vocabulary where the arrowhead carries the meaning (triangle, filled diamond, hollow diamond) | ER has no operations compartment and no inheritance or ownership semantics |
|
||||
| **Story map** | Narrative-ordered backbone crossed by release slices, with a release cut line | Kanban's columns are state, not narrative order, and it does not slice; journey carries sentiment, not scope |
|
||||
| **Database schema** | Foreign keys that anchor **column row to column row**, with SQL types, constraint chips, and index compartments | ER joins *boxes* and stops at cardinality — it cannot point at a column |
|
||||
|
||||
## Rejected, with reason
|
||||
|
||||
Kept out on purpose, so the next audit does not relitigate them.
|
||||
|
||||
**Already covered by an existing type — the request names a use case, not a grammar:**
|
||||
|
||||
| Requested | Covered by |
|
||||
|---|---|
|
||||
| System context, component, API interaction | **Architecture** (a boundary or a zoom level, not a new grammar) |
|
||||
| UML sequence, request lifecycle, sequence/state combination | **Sequence** |
|
||||
| UML state machine | **State** |
|
||||
| UML activity, activity diagram | **Swimlane** + **flowchart** — the fork/join bar alone does not earn a type |
|
||||
| UML component, UML deployment | **Architecture**, **deployment** |
|
||||
| Data flow diagram (DFD) | **Data flow** |
|
||||
| Event flow (producer → broker → consumers) | **Data flow** via the fan-in queue / bottleneck semantic pattern (ADR 0002) |
|
||||
| Integration diagram | **DP integration** |
|
||||
| Data model, conceptual ER | **ER** |
|
||||
| Decision tree | **Flowchart** |
|
||||
|
||||
**Rejected on the ADR 0002 bar or on editorial fit:**
|
||||
|
||||
- **C4 (context / container / component)** — a zoom-level convention over the architecture grammar, not a new one. Semantic-pattern territory.
|
||||
- **AI-agent / RAG architecture** — same: boxes and arrows with a decision loop. A pattern over architecture, not a layout.
|
||||
- **Mindmap** — radial restatement of tree. Same information, same grammar, different projection.
|
||||
- **UML use case** — stick-figure actors and ovals inside a system boundary. A real grammar, but the ovals carry no structure and the figure rarely survives an editorial cut.
|
||||
- **Pie / donut** — the repository's own rule ("lists of anything → a table") already rejects it.
|
||||
- **Git graph and packet / bit-field** — real grammars, but narrow audiences and weak editorial fit. Revisit only on demand.
|
||||
- **Treemap** — originally rejected here, then overtaken by the implementation admitted in #87 under ADR 0002's escape clause. The 28 → 38 count includes it.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The verifiable count moves to 38. `verify-docs-sync.py` and `verify-semantic-motion.py` both hardcode it, by design: adding a type must be a conscious edit, not a silent drift.
|
||||
- Four of the ten carry a documented, narrow exemption from §6 rule 1 (orthogonal elbows): sankey ribbons, fishbone bones, wardley dependency links, journey sentiment curves. Each exemption is scoped in its own type reference and covers that element only. The other six obey §6 in full; kanban and story map simply have no connectors.
|
||||
- **ER's scope was narrowed, not duplicated.** `type-er.md` now states that it is entity-level and points at `type-db-schema.md` for the physical schema. Without that edit the two types would have overlapping "Best for" claims and the router would be ambiguous.
|
||||
- The ADR 0004 byte cap became binding. Adding these types required trimming SKILL.md body prose: the §11 import consequences were compressed, the terminal-variant and typography paragraphs were tightened, and §4's six connector anti-pattern rows collapsed into one row pointing at §6 — that table was the third statement of rules already given in full in §6 and checked one by one in §9. `SKILL.md` sits at ~39.2 KB against the 40 KB cap. The next type must be paid for the same way, never by trimming the frontmatter description.
|
||||
- Per-type budget rows stayed in SKILL.md §7 rather than moving into the type references, because several older references (venn, pyramid, layers, ER, swimlane, timeline) do not state their own limits. Moving the rows out would have lost those numbers.
|
||||
- `docs/screenshots/` has no PNGs for the new types yet; the README lists them in a text table instead of the image grid until the screenshots are generated.
|
||||
@@ -402,7 +402,7 @@ def main() -> int:
|
||||
missing_guide = directory / "missing-guide.md"
|
||||
missing_guide.write_text(
|
||||
original_skill.read_text(encoding="utf-8").replace(
|
||||
"### Visual-type guide (28)", "### Visual guide"
|
||||
"### Visual-type guide (38)", "### Visual guide"
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
@@ -410,7 +410,7 @@ def main() -> int:
|
||||
markdown_errors = semantic_module.verify_markdown()
|
||||
finally:
|
||||
semantic_module.SKILL = original_skill
|
||||
if not any("must contain the 28-row visual-type guide" in error for error in markdown_errors):
|
||||
if not any("must contain the 38-row visual-type guide" in error for error in markdown_errors):
|
||||
raise AssertionError(f"missing visual-guide anchor was accepted: {markdown_errors}")
|
||||
print("OK: missing visual-type guide anchor is rejected")
|
||||
|
||||
|
||||
@@ -72,8 +72,8 @@ def check_description(errors: list[str]) -> None:
|
||||
errors.append("SKILL.md frontmatter description is missing")
|
||||
return
|
||||
types = selection_table_types(markdown)
|
||||
if len(types) != 28:
|
||||
errors.append(f"expected 28 visual types in the selection table; found {len(types)}")
|
||||
if len(types) != 38:
|
||||
errors.append(f"expected 38 visual types in the selection table; found {len(types)}")
|
||||
for name in types:
|
||||
key = normalized(name)
|
||||
key = DESCRIPTION_ALIASES.get(key, key)
|
||||
|
||||
@@ -191,18 +191,18 @@ def verify_markdown() -> list[str]:
|
||||
if "Selection: semantic pattern, then visual type" not in skill:
|
||||
errors.append("SKILL.md must choose semantic pattern before visual type")
|
||||
router_position = skill.find("semantic-patterns.md")
|
||||
guide_position = skill.find("### Visual-type guide (28)")
|
||||
guide_position = skill.find("### Visual-type guide (38)")
|
||||
if router_position < 0:
|
||||
errors.append("SKILL.md must link to semantic-patterns.md")
|
||||
if guide_position < 0:
|
||||
errors.append("SKILL.md must contain the 28-row visual-type guide")
|
||||
errors.append("SKILL.md must contain the 38-row visual-type guide")
|
||||
if router_position >= 0 and guide_position >= 0 and router_position > guide_position:
|
||||
errors.append("semantic-pattern router must precede the visual-type guide")
|
||||
|
||||
visual_guide = section(skill, "### Visual-type guide (28)", "Rules of thumb:")
|
||||
visual_guide = section(skill, "### Visual-type guide (38)", "Rules of thumb:")
|
||||
visual_rows = re.findall(r"^\|.*\[type-[^)]+\.md\]", visual_guide, re.MULTILINE)
|
||||
if len(visual_rows) != 28:
|
||||
errors.append(f"visual-type guide must preserve 28 rows; found {len(visual_rows)}")
|
||||
if len(visual_rows) != 38:
|
||||
errors.append(f"visual-type guide must preserve 38 rows; found {len(visual_rows)}")
|
||||
|
||||
for index, name in enumerate(PATTERN_NAMES, 1):
|
||||
if name not in skill:
|
||||
@@ -419,7 +419,7 @@ def main() -> int:
|
||||
print(f"- {error}")
|
||||
return 1
|
||||
if not args.example_only:
|
||||
print("OK: 7 semantic patterns route independently to the preserved 28 visual types")
|
||||
print("OK: 7 semantic patterns route independently to the preserved 38 visual types")
|
||||
print("OK: animation modes, primitives, static fallback, and accessibility contract")
|
||||
if not args.markdown_only:
|
||||
print("OK: policy-trace example controls, structure, reduced motion, and unique IDs")
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
---
|
||||
name: diagram-design
|
||||
description: Create branded architecture, IT current-state, flowchart, sequence, state machine, ER/data model, timeline, swimlane, quadrant, radar/spider, loop/flywheel, nested, tree, org chart, layer stack, Venn, pyramid/funnel, treemap, bar, line, Gantt and scatter charts, high-level, process, medallion, data flow, DP integration, or DP security matrix diagrams as standalone HTML/SVG/PNG. Redraw .drawio/.drawio.png/.drawio.svg or Mermaid .mmd sources at a chosen size/detail; onboard brand tokens from a website; add semantic patterns, callouts, accessible motion, or sketchy/hand-drawn styling.
|
||||
description: Create branded architecture, IT current-state, flowchart, sequence, state machine, ER/data model, timeline, swimlane, quadrant, radar/spider, loop/flywheel, nested, tree, org chart, layer stack, Venn, pyramid/funnel, treemap, bar, line, Gantt and scatter charts, high-level, process, medallion, data flow, DP integration, DP security matrix, Sankey, fishbone, Wardley map, kanban, user journey, deployment, dependency graph, UML class, story map, or database schema diagrams as standalone HTML/SVG/PNG. Redraw .drawio/.drawio.png/.drawio.svg or Mermaid .mmd sources at a chosen size/detail; onboard brand tokens from a website; add semantic patterns, callouts, accessible motion, or sketchy/hand-drawn styling.
|
||||
license: MIT
|
||||
metadata:
|
||||
version: "2.4"
|
||||
version: "2.5"
|
||||
---
|
||||
|
||||
# Diagram Design
|
||||
|
||||
Create visual diagrams as self-contained HTML files with inline SVG and CSS, following an opinionated editorial design system.
|
||||
|
||||
Twenty-eight visual types. Semantic patterns describe behavior independently; type references describe layout. Details load from `references/` only when selected.
|
||||
Thirty-eight visual types. Semantic patterns describe behavior independently; type references describe layout. Details load from `references/` only when selected.
|
||||
|
||||
---
|
||||
|
||||
@@ -49,7 +49,7 @@ Applied to schematics:
|
||||
|
||||
## 2. When to Use
|
||||
|
||||
Use for any of the 28 visual types (§3) when a reader will learn more from a visual than from prose, a table, or a bulleted list.
|
||||
Use for any of the 38 visual types (§3) when a reader will learn more from a visual than from prose, a table, or a bulleted list.
|
||||
|
||||
**Don't use for:**
|
||||
|
||||
@@ -78,7 +78,7 @@ When behavior, state, enforcement, or risk carries the meaning, first load [`ref
|
||||
|
||||
The pattern owns semantic primitives and its tighter budget; the type owns layout grammar. Use [`references/animation.md`](references/animation.md) only when motion is requested or materially clarifies ordered change; static remains the default.
|
||||
|
||||
### Visual-type guide (28)
|
||||
### Visual-type guide (38)
|
||||
|
||||
| If you're showing… | Use | Reference |
|
||||
|---|---|---|
|
||||
@@ -110,6 +110,16 @@ The pattern owns semantic primitives and its tighter budget; the type owns layou
|
||||
| Role-scoped data flow: who does what at each pipeline step | **Data flow** | [type-data-flow.md](references/type-data-flow.md) |
|
||||
| Integration topology of a data platform — sources → core → consumers | **DP integration** | [type-dp-integration.md](references/type-dp-integration.md) |
|
||||
| Per-role / per-component access permissions matrix | **DP security matrix** | [type-dp-security-matrix.md](references/type-dp-security-matrix.md) |
|
||||
| A quantity splitting and merging across stages, band width = amount | **Sankey** | [type-sankey.md](references/type-sankey.md) |
|
||||
| Causes of one observed effect, grouped by category (root-cause analysis) | **Fishbone** | [type-fishbone.md](references/type-fishbone.md) |
|
||||
| Value chain against evolution — what to build, buy, and what is moving | **Wardley map** | [type-wardley.md](references/type-wardley.md) |
|
||||
| Work-in-progress by state, with WIP limits and blocked items | **Kanban** | [type-kanban.md](references/type-kanban.md) |
|
||||
| What a person does across stages of an experience, and how it feels | **User journey** | [type-journey.md](references/type-journey.md) |
|
||||
| Where software runs — zones, hosts, artifacts, replicas, ports | **Deployment** | [type-deployment.md](references/type-deployment.md) |
|
||||
| What depends on what, with fan-in and cycles a tree cannot express | **Dependency graph** | [type-dependency.md](references/type-dependency.md) |
|
||||
| Classes with operations, inheritance, composition (other UML routes elsewhere) | **UML class** | [type-uml-class.md](references/type-uml-class.md) |
|
||||
| Narrative backbone sliced into releases, with the cut line | **Story map** | [type-story-map.md](references/type-story-map.md) |
|
||||
| Physical tables: SQL types, constraints, indexes, column-level FKs | **Database schema** | [type-db-schema.md](references/type-db-schema.md) |
|
||||
|
||||
Rules of thumb:
|
||||
|
||||
@@ -142,12 +152,7 @@ These mark "AI slop" schematics of any type:
|
||||
| `rounded-2xl` on boxes | Max radius 6–10px or none |
|
||||
| Coral on every "important" node | Coral is 1–2 editorial accents, not a signaling system |
|
||||
| Reproducing Mermaid's renderer layout | Imports automatic spacing and routing instead of making an editorial layout |
|
||||
| Diagonal / slanted connectors between off-axis nodes | Rounded right-angle (orthogonal) elbows are mandatory — see §6 Mandatory connector rules |
|
||||
| Arrow label sitting on or touching its connector | Label must have a 6–10px gap above the line so the connector stays visible |
|
||||
| Arrow label mask overlapping a node box | Nodes paint after labels — the fill clips the text into a fragment on the border. See §6 rule 6 |
|
||||
| Two connectors overlapping or running on the same path | Each connection must be independently traceable — bridge crossings, offset parallels |
|
||||
| Two connectors sharing a single attach point on a box | Fan attach points along the edge (≥12px apart) so every arrow is clearly distinct — see §6 rule 4 |
|
||||
| Connector routed behind a non-endpoint box without need | Reroute around intervening boxes; the dashed-transit exception (§6 rule 5) only applies when an unavoidable intervening box sits on the direct path |
|
||||
| Any breach of the six §6 connector rules | Diagonal slants, labels touching their stroke, masks clipped by a later node, overlapping paths, shared attach points, transit behind a non-endpoint box — each is an automatic fail; §6 states them in full |
|
||||
|
||||
Type-specific anti-patterns live in each `references/type-*.md`.
|
||||
|
||||
@@ -193,7 +198,7 @@ Type-specific anti-patterns live in each `references/type-*.md`.
|
||||
- **Arrow label** — Geist Mono, 8px — annotation on arrows
|
||||
- **Editorial aside** — Instrument Serif *italic*, 14px — callouts only
|
||||
|
||||
**Mono is for technical content.** Names are Geist sans. Page title is Instrument Serif. Italic Instrument Serif is reserved for annotation callouts. Never JetBrains Mono as a blanket "dev" font.
|
||||
**Mono is for technical content only** — never as a blanket "dev" font, and never JetBrains Mono.
|
||||
|
||||
```html
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
@@ -383,6 +388,16 @@ Quick check: if a coordinate ends in 1, 2, 3, 5, 6, 7, 9 — fix it.
|
||||
| Max series (line chart) | 5 |
|
||||
| Max tasks (Gantt) | 12 |
|
||||
| Max points (scatter plot) | 30 |
|
||||
| Max stages / nodes / flows (sankey) | 3 / 8 / 12 |
|
||||
| Max categories (fishbone) | 6 bones, 3 sub-causes each |
|
||||
| Max components / links (wardley) | 9 / 12, 2 movement arrows |
|
||||
| Max columns / cards (kanban) | 5 / 12 total, 4 per column |
|
||||
| Max stages / rows (user journey) | 6 / 3, 2 pain markers |
|
||||
| Max zones / nodes / paths (deployment) | 3 / 6 / 8, 9 artifacts |
|
||||
| Max nodes / edges (dependency) | 9 / 14, 4 ranks, 1 cycle |
|
||||
| Max classes / relationships (UML class) | 7 / 8, 5 members per compartment |
|
||||
| Max activities / slices / cards (story map) | 5 / 3 / 12 |
|
||||
| Max tables / columns / FKs (db schema) | 5 / 8 shown / 6 |
|
||||
| Max annotation callouts | 2 |
|
||||
| Max motion (optional) | 8 steps, 12 marked items, 2 simultaneous items — see [animation.md](references/animation.md) |
|
||||
|
||||
@@ -493,7 +508,7 @@ Every diagram ships in three variants (see `assets/`):
|
||||
|
||||
**Sketchy variant** (optional, applied to any of the above) — see [primitive-sketchy.md](references/primitive-sketchy.md). SVG turbulence filter wobbles strokes for a hand-drawn feel. Good for essays, not for technical docs.
|
||||
|
||||
**Terminal variant** (optional, replaces any of the above) — see [primitive-terminal.md](references/primitive-terminal.md). `template-terminal.html`, `example-<type>-terminal.html`. Charcoal-black CLI-window chrome, monospace type, one red-orange accent. Good for dev-tool / CLI-product posts and technical social cards; not brand-tokenized, so skip it for onboarded/brand-matched output.
|
||||
**Terminal variant** (optional, replaces any of the above) — see [primitive-terminal.md](references/primitive-terminal.md). `template-terminal.html`, `example-<type>-terminal.html`. Charcoal CLI-window chrome, monospace, one red-orange accent. Good for dev-tool posts; not brand-tokenized, so skip it for onboarded output.
|
||||
|
||||
**Animation** (optional presentation layer) — see [animation.md](references/animation.md). Modes are `none` (default), `reveal`, `step`, and `loop`; motion never changes the static meaning or raises the complexity budget.
|
||||
|
||||
@@ -531,10 +546,7 @@ Every imported diagram is shaped by four decisions. Full spec in [`references/ou
|
||||
| **Detail** | `faithful` (≤24 nodes, zoned) · `balanced` (≤12) · `simplified` (≤7) | `balanced` |
|
||||
| **Audience** | `engineer` · `mixed` · `executive` — governs wording, not count | `mixed` |
|
||||
|
||||
Two consequences worth remembering here:
|
||||
|
||||
- The size preset sets the `viewBox` **and** the type ramp. A slide gets 16px node names, not 12px — scaling the canvas without scaling the type is how projected diagrams end up unreadable.
|
||||
- `faithful` is the one documented exemption from the §7 complexity budget, and it's conditional: above 9 nodes the layout must be zoned, above 24 it must split into overview + detail. The connector rules in §6 never relax.
|
||||
Two consequences: the size preset sets the `viewBox` **and** the type ramp (a slide gets 16px node names, not 12px), and `faithful` is the only exemption from the §7 budget — conditional, zoned above 9 nodes, split above 24. The §6 connector rules never relax.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Commerce schema · database schema</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #2d3142;
|
||||
--color-ink: #f5f5f5;
|
||||
--color-muted: #bfc0c0;
|
||||
--color-accent: #f08a59;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1240px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 960px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Database schema · Diagram Design</p>
|
||||
<h1>Commerce schema · public + billing</h1>
|
||||
|
||||
<svg viewBox="0 0 1080 560" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="db-schema-dark-title db-schema-dark-desc">
|
||||
<title id="db-schema-dark-title">Commerce schema · public + billing</title>
|
||||
<desc id="db-schema-dark-desc">Physical database schema diagram showing five commerce tables with column-level foreign keys, SQL types, and constraint chips, including a cascading delete from orders into order_items and a billing schema group.</desc>
|
||||
<defs>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#bfc0c0"/></marker>
|
||||
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#f08a59"/></marker>
|
||||
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#2e5aa8"/></marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#2d3142"/>
|
||||
|
||||
<!-- BILLING schema group (drawn first so tables paint over it) -->
|
||||
<rect x="744" y="284" width="272" height="152" rx="8" fill="rgba(245,245,245,0.02)" stroke="rgba(245,245,245,0.20)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<text x="756" y="300" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">BILLING</text>
|
||||
|
||||
<!-- FK connectors (drawn before boxes) -->
|
||||
<!-- FK1: orders.customer_id -> customers.id · ON DELETE RESTRICT -->
|
||||
<path d="M 400,104 H 348 Q 340,104 340,112 Q 340,120 332,120 H 280" fill="none" stroke="#bfc0c0" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- FK2: order_items.order_id -> orders.id · ON DELETE CASCADE (focal) -->
|
||||
<path d="M 640,384 H 692 Q 700,384 700,376 V 80 Q 700,72 692,72 H 640" fill="none" stroke="#f08a59" stroke-width="1.2" marker-end="url(#arrow-accent)"/>
|
||||
<!-- FK3: order_items.product_id -> products.id · ON DELETE RESTRICT -->
|
||||
<path d="M 640,408 H 708 Q 716,408 716,400 V 88 Q 716,80 724,80 H 760" fill="none" stroke="#bfc0c0" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- FK4: invoices.order_id -> orders.id · ON DELETE RESTRICT -->
|
||||
<path d="M 760,384 H 692 Q 684,384 684,376 V 96 Q 684,88 676,88 H 640" fill="none" stroke="#bfc0c0" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- FK labels (masked, 6-10px gap above/beside their connector) -->
|
||||
<rect x="284" y="82" width="112" height="12" rx="2" fill="#2d3142"/>
|
||||
<text x="340" y="91" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">ON DELETE RESTRICT</text>
|
||||
|
||||
<rect x="648" y="392" width="88" height="12" rx="2" fill="#2d3142"/>
|
||||
<text x="692" y="401" fill="#f08a59" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">ON DELETE CASCADE</text>
|
||||
|
||||
<rect x="724" y="252" width="112" height="12" rx="2" fill="#2d3142"/>
|
||||
<text x="780" y="261" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">ON DELETE RESTRICT</text>
|
||||
|
||||
<rect x="564" y="270" width="112" height="12" rx="2" fill="#2d3142"/>
|
||||
<text x="620" y="279" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">ON DELETE RESTRICT</text>
|
||||
|
||||
<!-- Table: public.customers -->
|
||||
<rect x="40" y="80" width="240" height="100" rx="6" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<rect x="40" y="80" width="240" height="28" rx="6" fill="rgba(245,245,245,0.04)" stroke="none"/>
|
||||
<rect x="40" y="100" width="240" height="8" fill="rgba(245,245,245,0.04)"/>
|
||||
<line x1="40" y1="108" x2="280" y2="108" stroke="rgba(245,245,245,0.22)" stroke-width="1"/>
|
||||
<text x="52" y="98" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">public.customers</text>
|
||||
<rect x="228" y="88" width="40" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="248" y="97" fill="#f5f5f5" opacity="0.8" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">TABLE</text>
|
||||
|
||||
<rect x="40" y="108" width="240" height="24" fill="rgba(245,245,245,0.02)"/>
|
||||
<text x="52" y="124" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">id</text>
|
||||
<rect x="160" y="116" width="20" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.35)" stroke-width="0.8"/>
|
||||
<text x="170" y="125" fill="#f5f5f5" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">PK</text>
|
||||
<text x="268" y="124" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<text x="52" y="148" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">email</text>
|
||||
<rect x="160" y="140" width="20" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.35)" stroke-width="0.8"/>
|
||||
<text x="170" y="149" fill="#f5f5f5" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">UQ</text>
|
||||
<rect x="184" y="140" width="20" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.35)" stroke-width="0.8"/>
|
||||
<text x="194" y="149" fill="#f5f5f5" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="268" y="148" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
|
||||
<rect x="40" y="156" width="240" height="24" fill="rgba(245,245,245,0.02)"/>
|
||||
<text x="52" y="172" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">created_at</text>
|
||||
<rect x="160" y="164" width="20" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.35)" stroke-width="0.8"/>
|
||||
<text x="170" y="173" fill="#f5f5f5" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="268" y="172" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">timestamptz</text>
|
||||
|
||||
<!-- Table: public.orders -->
|
||||
<rect x="400" y="40" width="240" height="200" rx="6" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<rect x="400" y="40" width="240" height="28" rx="6" fill="rgba(245,245,245,0.04)" stroke="none"/>
|
||||
<rect x="400" y="60" width="240" height="8" fill="rgba(245,245,245,0.04)"/>
|
||||
<line x1="400" y1="68" x2="640" y2="68" stroke="rgba(245,245,245,0.22)" stroke-width="1"/>
|
||||
<text x="412" y="58" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">public.orders</text>
|
||||
<rect x="588" y="48" width="40" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="608" y="57" fill="#f5f5f5" opacity="0.8" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">TABLE</text>
|
||||
|
||||
<rect x="400" y="68" width="240" height="24" fill="rgba(245,245,245,0.02)"/>
|
||||
<text x="412" y="84" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">id</text>
|
||||
<rect x="520" y="76" width="20" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="85" fill="#f5f5f5" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">PK</text>
|
||||
<text x="628" y="84" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<text x="412" y="108" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">customer_id</text>
|
||||
<rect x="520" y="100" width="20" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="109" fill="#f5f5f5" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">FK</text>
|
||||
<rect x="544" y="100" width="20" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.35)" stroke-width="0.8"/>
|
||||
<text x="554" y="109" fill="#f5f5f5" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="628" y="108" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<rect x="400" y="116" width="240" height="24" fill="rgba(245,245,245,0.02)"/>
|
||||
<text x="412" y="132" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">status</text>
|
||||
<rect x="520" y="124" width="20" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="133" fill="#f5f5f5" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="628" y="132" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
|
||||
<text x="412" y="156" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">total</text>
|
||||
<rect x="520" y="148" width="20" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="157" fill="#f5f5f5" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="628" y="156" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">numeric(12,2)</text>
|
||||
|
||||
<rect x="400" y="164" width="240" height="24" fill="rgba(245,245,245,0.02)"/>
|
||||
<text x="412" y="180" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">placed_at</text>
|
||||
<rect x="520" y="172" width="20" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="181" fill="#f5f5f5" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="628" y="180" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">timestamptz</text>
|
||||
|
||||
<line x1="400" y1="188" x2="640" y2="188" stroke="rgba(245,245,245,0.12)" stroke-width="0.8"/>
|
||||
<text x="412" y="202" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">INDEXES</text>
|
||||
<text x="412" y="220" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">idx_orders_customer_id</text>
|
||||
<text x="412" y="236" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">idx_orders_status</text>
|
||||
|
||||
<!-- Table: public.order_items (focal header — accent-tint, cascade target) -->
|
||||
<rect x="400" y="320" width="240" height="148" rx="6" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<rect x="400" y="320" width="240" height="28" rx="6" fill="rgba(240,138,89,0.08)" stroke="none"/>
|
||||
<rect x="400" y="340" width="240" height="8" fill="rgba(240,138,89,0.08)"/>
|
||||
<line x1="400" y1="348" x2="640" y2="348" stroke="rgba(245,245,245,0.22)" stroke-width="1"/>
|
||||
<text x="412" y="338" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">public.order_items</text>
|
||||
<rect x="588" y="328" width="40" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="608" y="337" fill="#f5f5f5" opacity="0.8" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">TABLE</text>
|
||||
|
||||
<rect x="400" y="348" width="240" height="24" fill="rgba(245,245,245,0.02)"/>
|
||||
<text x="412" y="364" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">id</text>
|
||||
<rect x="520" y="356" width="20" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="365" fill="#f5f5f5" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">PK</text>
|
||||
<text x="628" y="364" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<text x="412" y="388" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">order_id</text>
|
||||
<rect x="520" y="380" width="20" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="389" fill="#f5f5f5" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">FK</text>
|
||||
<rect x="544" y="380" width="20" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.35)" stroke-width="0.8"/>
|
||||
<text x="554" y="389" fill="#f5f5f5" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="628" y="388" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<rect x="400" y="396" width="240" height="24" fill="rgba(245,245,245,0.02)"/>
|
||||
<text x="412" y="412" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">product_id</text>
|
||||
<rect x="520" y="404" width="20" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="413" fill="#f5f5f5" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">FK</text>
|
||||
<rect x="544" y="404" width="20" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.35)" stroke-width="0.8"/>
|
||||
<text x="554" y="413" fill="#f5f5f5" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="628" y="412" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<text x="412" y="436" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">qty</text>
|
||||
<rect x="520" y="428" width="20" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="437" fill="#f5f5f5" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="628" y="436" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">integer</text>
|
||||
|
||||
<rect x="400" y="444" width="240" height="24" fill="rgba(245,245,245,0.02)"/>
|
||||
<text x="412" y="460" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">unit_price</text>
|
||||
<rect x="520" y="452" width="20" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="461" fill="#f5f5f5" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="628" y="460" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">numeric(12,2)</text>
|
||||
|
||||
<!-- Table: public.products -->
|
||||
<rect x="760" y="40" width="240" height="184" rx="6" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<rect x="760" y="40" width="240" height="28" rx="6" fill="rgba(245,245,245,0.04)" stroke="none"/>
|
||||
<rect x="760" y="60" width="240" height="8" fill="rgba(245,245,245,0.04)"/>
|
||||
<line x1="760" y1="68" x2="1000" y2="68" stroke="rgba(245,245,245,0.22)" stroke-width="1"/>
|
||||
<text x="772" y="58" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">public.products</text>
|
||||
<rect x="948" y="48" width="40" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="968" y="57" fill="#f5f5f5" opacity="0.8" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">TABLE</text>
|
||||
|
||||
<rect x="760" y="68" width="240" height="24" fill="rgba(245,245,245,0.02)"/>
|
||||
<text x="772" y="84" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">id</text>
|
||||
<rect x="880" y="76" width="20" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.35)" stroke-width="0.8"/>
|
||||
<text x="890" y="85" fill="#f5f5f5" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">PK</text>
|
||||
<text x="988" y="84" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<text x="772" y="108" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">sku</text>
|
||||
<rect x="880" y="100" width="20" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.35)" stroke-width="0.8"/>
|
||||
<text x="890" y="109" fill="#f5f5f5" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">UQ</text>
|
||||
<rect x="904" y="100" width="20" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.35)" stroke-width="0.8"/>
|
||||
<text x="914" y="109" fill="#f5f5f5" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="988" y="108" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
|
||||
<rect x="760" y="116" width="240" height="24" fill="rgba(245,245,245,0.02)"/>
|
||||
<text x="772" y="132" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">name</text>
|
||||
<rect x="880" y="124" width="20" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.35)" stroke-width="0.8"/>
|
||||
<text x="890" y="133" fill="#f5f5f5" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="988" y="132" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
|
||||
<text x="772" y="156" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">price</text>
|
||||
<rect x="880" y="148" width="20" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.35)" stroke-width="0.8"/>
|
||||
<text x="890" y="157" fill="#f5f5f5" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="988" y="156" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">numeric(12,2)</text>
|
||||
|
||||
<rect x="760" y="164" width="240" height="24" fill="rgba(245,245,245,0.02)"/>
|
||||
<text x="772" y="180" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">+ 3 more columns</text>
|
||||
|
||||
<line x1="760" y1="188" x2="1000" y2="188" stroke="rgba(245,245,245,0.12)" stroke-width="0.8"/>
|
||||
<text x="772" y="202" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">INDEXES</text>
|
||||
<text x="772" y="220" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">uq_products_sku</text>
|
||||
|
||||
<!-- Table: billing.invoices (inside BILLING group) -->
|
||||
<rect x="760" y="320" width="240" height="100" rx="6" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<rect x="760" y="320" width="240" height="28" rx="6" fill="rgba(245,245,245,0.04)" stroke="none"/>
|
||||
<rect x="760" y="340" width="240" height="8" fill="rgba(245,245,245,0.04)"/>
|
||||
<line x1="760" y1="348" x2="1000" y2="348" stroke="rgba(245,245,245,0.22)" stroke-width="1"/>
|
||||
<text x="772" y="338" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">billing.invoices</text>
|
||||
<rect x="948" y="328" width="40" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="968" y="337" fill="#f5f5f5" opacity="0.8" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">TABLE</text>
|
||||
|
||||
<rect x="760" y="348" width="240" height="24" fill="rgba(245,245,245,0.02)"/>
|
||||
<text x="772" y="364" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">id</text>
|
||||
<rect x="880" y="356" width="20" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.35)" stroke-width="0.8"/>
|
||||
<text x="890" y="365" fill="#f5f5f5" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">PK</text>
|
||||
<text x="988" y="364" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<text x="772" y="388" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">order_id</text>
|
||||
<rect x="880" y="380" width="20" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.35)" stroke-width="0.8"/>
|
||||
<text x="890" y="389" fill="#f5f5f5" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">FK</text>
|
||||
<rect x="904" y="380" width="20" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.35)" stroke-width="0.8"/>
|
||||
<text x="914" y="389" fill="#f5f5f5" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="988" y="388" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<rect x="760" y="396" width="240" height="24" fill="rgba(245,245,245,0.02)"/>
|
||||
<text x="772" y="412" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">issued_at</text>
|
||||
<rect x="880" y="404" width="20" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.35)" stroke-width="0.8"/>
|
||||
<text x="890" y="413" fill="#f5f5f5" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="988" y="412" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">timestamptz</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="500" x2="1040" y2="500" stroke="rgba(245,245,245,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="516" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="40" y="532" width="20" height="12" rx="2" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<text x="68" y="540" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Table</text>
|
||||
|
||||
<rect x="180" y="532" width="20" height="12" rx="2" fill="rgba(245,245,245,0.02)" stroke="rgba(245,245,245,0.35)" stroke-width="0.8" stroke-dasharray="3,3"/>
|
||||
<text x="208" y="540" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Schema group</text>
|
||||
|
||||
<rect x="340" y="532" width="20" height="12" rx="2" fill="none" stroke="rgba(245,245,245,0.35)" stroke-width="0.8"/>
|
||||
<text x="350" y="541" fill="#f5f5f5" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">PK</text>
|
||||
<text x="368" y="540" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Constraint chip</text>
|
||||
|
||||
<line x1="500" y1="538" x2="520" y2="538" stroke="#bfc0c0" stroke-width="1"/>
|
||||
<text x="528" y="540" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Foreign key (column to column)</text>
|
||||
|
||||
<line x1="740" y1="538" x2="760" y2="538" stroke="#f08a59" stroke-width="1.2"/>
|
||||
<text x="768" y="540" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Destructive delete (CASCADE)</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,305 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Commerce schema · database schema</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root { --color-paper:#f5f5f5; --color-paper-2:#ececec; --color-ink:#2d3142; --color-muted:#4f5d75; --color-soft:#7a8399; --color-rule:rgba(45,49,66,0.12); --color-accent:#eb6c36; --color-link:#2e5aa8; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
|
||||
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
|
||||
.container { max-width: 1240px; margin: 0 auto; }
|
||||
.header { margin-bottom: 2.5rem; }
|
||||
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
|
||||
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
|
||||
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 62ch; }
|
||||
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
|
||||
svg { width: 100%; min-width: 960px; display: block; }
|
||||
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
|
||||
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
|
||||
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
|
||||
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
|
||||
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(45,49,66,0.08); }
|
||||
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
|
||||
.card-dot.ink { background: var(--color-ink); } .card-dot.muted { background: var(--color-muted); } .card-dot.coral { background: var(--color-accent); }
|
||||
.card h3 { font-size: 0.875rem; font-weight: 600; }
|
||||
.card p, .card ul { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
|
||||
.card li { margin-bottom: 0.3rem; padding-left: 0.875rem; position: relative; }
|
||||
.card li::before { content: '—'; position: absolute; left: 0; color: rgba(45,49,66,0.25); font-size: 0.75rem; }
|
||||
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(45,49,66,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<p class="header-eyebrow">Database schema · Diagram Design</p>
|
||||
<h1>Commerce schema · public + billing</h1>
|
||||
<p class="subtitle">The physical schema behind checkout: five tables, real SQL types, and foreign keys that anchor column to column, not box to box. The dashed group marks the non-default `billing` schema.</p>
|
||||
</div>
|
||||
|
||||
<div class="diagram-container">
|
||||
<svg viewBox="0 0 1080 560" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="db-schema-full-title db-schema-full-desc">
|
||||
<title id="db-schema-full-title">Commerce schema · public + billing</title>
|
||||
<desc id="db-schema-full-desc">Physical database schema diagram showing five commerce tables with column-level foreign keys, SQL types, and constraint chips, including a cascading delete from orders into order_items and a billing schema group.</desc>
|
||||
<defs>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#4f5d75"/></marker>
|
||||
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#eb6c36"/></marker>
|
||||
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#2e5aa8"/></marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f5f5"/>
|
||||
|
||||
<!-- BILLING schema group (drawn first so tables paint over it) -->
|
||||
<rect x="744" y="284" width="272" height="152" rx="8" fill="rgba(45,49,66,0.02)" stroke="rgba(45,49,66,0.20)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<text x="756" y="300" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">BILLING</text>
|
||||
|
||||
<!-- FK connectors (drawn before boxes) -->
|
||||
<!-- FK1: orders.customer_id -> customers.id · ON DELETE RESTRICT -->
|
||||
<path d="M 400,104 H 348 Q 340,104 340,112 Q 340,120 332,120 H 280" fill="none" stroke="#4f5d75" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- FK2: order_items.order_id -> orders.id · ON DELETE CASCADE (focal) -->
|
||||
<path d="M 640,384 H 692 Q 700,384 700,376 V 80 Q 700,72 692,72 H 640" fill="none" stroke="#eb6c36" stroke-width="1.2" marker-end="url(#arrow-accent)"/>
|
||||
<!-- FK3: order_items.product_id -> products.id · ON DELETE RESTRICT -->
|
||||
<path d="M 640,408 H 708 Q 716,408 716,400 V 88 Q 716,80 724,80 H 760" fill="none" stroke="#4f5d75" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- FK4: invoices.order_id -> orders.id · ON DELETE RESTRICT -->
|
||||
<path d="M 760,384 H 692 Q 684,384 684,376 V 96 Q 684,88 676,88 H 640" fill="none" stroke="#4f5d75" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- FK labels (masked, 6-10px gap above/beside their connector) -->
|
||||
<rect x="284" y="82" width="112" height="12" rx="2" fill="#f5f5f5"/>
|
||||
<text x="340" y="91" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">ON DELETE RESTRICT</text>
|
||||
|
||||
<rect x="648" y="392" width="88" height="12" rx="2" fill="#f5f5f5"/>
|
||||
<text x="692" y="401" fill="#eb6c36" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">ON DELETE CASCADE</text>
|
||||
|
||||
<rect x="724" y="252" width="112" height="12" rx="2" fill="#f5f5f5"/>
|
||||
<text x="780" y="261" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">ON DELETE RESTRICT</text>
|
||||
|
||||
<rect x="564" y="270" width="112" height="12" rx="2" fill="#f5f5f5"/>
|
||||
<text x="620" y="279" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">ON DELETE RESTRICT</text>
|
||||
|
||||
<!-- Table: public.customers -->
|
||||
<rect x="40" y="80" width="240" height="100" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="40" y="80" width="240" height="28" rx="6" fill="rgba(45,49,66,0.04)" stroke="none"/>
|
||||
<rect x="40" y="100" width="240" height="8" fill="rgba(45,49,66,0.04)"/>
|
||||
<line x1="40" y1="108" x2="280" y2="108" stroke="rgba(45,49,66,0.22)" stroke-width="1"/>
|
||||
<text x="52" y="98" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">public.customers</text>
|
||||
<rect x="228" y="88" width="40" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="248" y="97" fill="#2d3142" opacity="0.8" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">TABLE</text>
|
||||
|
||||
<rect x="40" y="108" width="240" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="52" y="124" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">id</text>
|
||||
<rect x="160" y="116" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="170" y="125" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">PK</text>
|
||||
<text x="268" y="124" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<text x="52" y="148" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">email</text>
|
||||
<rect x="160" y="140" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="170" y="149" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">UQ</text>
|
||||
<rect x="184" y="140" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="194" y="149" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="268" y="148" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
|
||||
<rect x="40" y="156" width="240" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="52" y="172" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">created_at</text>
|
||||
<rect x="160" y="164" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="170" y="173" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="268" y="172" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">timestamptz</text>
|
||||
|
||||
<!-- Table: public.orders -->
|
||||
<rect x="400" y="40" width="240" height="200" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="400" y="40" width="240" height="28" rx="6" fill="rgba(45,49,66,0.04)" stroke="none"/>
|
||||
<rect x="400" y="60" width="240" height="8" fill="rgba(45,49,66,0.04)"/>
|
||||
<line x1="400" y1="68" x2="640" y2="68" stroke="rgba(45,49,66,0.22)" stroke-width="1"/>
|
||||
<text x="412" y="58" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">public.orders</text>
|
||||
<rect x="588" y="48" width="40" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="608" y="57" fill="#2d3142" opacity="0.8" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">TABLE</text>
|
||||
|
||||
<rect x="400" y="68" width="240" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="412" y="84" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">id</text>
|
||||
<rect x="520" y="76" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="85" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">PK</text>
|
||||
<text x="628" y="84" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<text x="412" y="108" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">customer_id</text>
|
||||
<rect x="520" y="100" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="109" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">FK</text>
|
||||
<rect x="544" y="100" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="554" y="109" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="628" y="108" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<rect x="400" y="116" width="240" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="412" y="132" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">status</text>
|
||||
<rect x="520" y="124" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="133" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="628" y="132" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
|
||||
<text x="412" y="156" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">total</text>
|
||||
<rect x="520" y="148" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="157" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="628" y="156" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">numeric(12,2)</text>
|
||||
|
||||
<rect x="400" y="164" width="240" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="412" y="180" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">placed_at</text>
|
||||
<rect x="520" y="172" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="181" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="628" y="180" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">timestamptz</text>
|
||||
|
||||
<line x1="400" y1="188" x2="640" y2="188" stroke="rgba(45,49,66,0.12)" stroke-width="0.8"/>
|
||||
<text x="412" y="202" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">INDEXES</text>
|
||||
<text x="412" y="220" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">idx_orders_customer_id</text>
|
||||
<text x="412" y="236" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">idx_orders_status</text>
|
||||
|
||||
<!-- Table: public.order_items (focal header — accent-tint, cascade target) -->
|
||||
<rect x="400" y="320" width="240" height="148" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="400" y="320" width="240" height="28" rx="6" fill="rgba(235,108,54,0.08)" stroke="none"/>
|
||||
<rect x="400" y="340" width="240" height="8" fill="rgba(235,108,54,0.08)"/>
|
||||
<line x1="400" y1="348" x2="640" y2="348" stroke="rgba(45,49,66,0.22)" stroke-width="1"/>
|
||||
<text x="412" y="338" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">public.order_items</text>
|
||||
<rect x="588" y="328" width="40" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="608" y="337" fill="#2d3142" opacity="0.8" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">TABLE</text>
|
||||
|
||||
<rect x="400" y="348" width="240" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="412" y="364" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">id</text>
|
||||
<rect x="520" y="356" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="365" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">PK</text>
|
||||
<text x="628" y="364" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<text x="412" y="388" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">order_id</text>
|
||||
<rect x="520" y="380" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="389" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">FK</text>
|
||||
<rect x="544" y="380" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="554" y="389" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="628" y="388" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<rect x="400" y="396" width="240" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="412" y="412" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">product_id</text>
|
||||
<rect x="520" y="404" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="413" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">FK</text>
|
||||
<rect x="544" y="404" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="554" y="413" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="628" y="412" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<text x="412" y="436" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">qty</text>
|
||||
<rect x="520" y="428" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="437" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="628" y="436" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">integer</text>
|
||||
|
||||
<rect x="400" y="444" width="240" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="412" y="460" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">unit_price</text>
|
||||
<rect x="520" y="452" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="461" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="628" y="460" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">numeric(12,2)</text>
|
||||
|
||||
<!-- Table: public.products -->
|
||||
<rect x="760" y="40" width="240" height="184" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="760" y="40" width="240" height="28" rx="6" fill="rgba(45,49,66,0.04)" stroke="none"/>
|
||||
<rect x="760" y="60" width="240" height="8" fill="rgba(45,49,66,0.04)"/>
|
||||
<line x1="760" y1="68" x2="1000" y2="68" stroke="rgba(45,49,66,0.22)" stroke-width="1"/>
|
||||
<text x="772" y="58" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">public.products</text>
|
||||
<rect x="948" y="48" width="40" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="968" y="57" fill="#2d3142" opacity="0.8" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">TABLE</text>
|
||||
|
||||
<rect x="760" y="68" width="240" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="772" y="84" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">id</text>
|
||||
<rect x="880" y="76" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="890" y="85" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">PK</text>
|
||||
<text x="988" y="84" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<text x="772" y="108" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">sku</text>
|
||||
<rect x="880" y="100" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="890" y="109" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">UQ</text>
|
||||
<rect x="904" y="100" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="914" y="109" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="988" y="108" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
|
||||
<rect x="760" y="116" width="240" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="772" y="132" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">name</text>
|
||||
<rect x="880" y="124" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="890" y="133" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="988" y="132" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
|
||||
<text x="772" y="156" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">price</text>
|
||||
<rect x="880" y="148" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="890" y="157" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="988" y="156" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">numeric(12,2)</text>
|
||||
|
||||
<rect x="760" y="164" width="240" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="772" y="180" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">+ 3 more columns</text>
|
||||
|
||||
<line x1="760" y1="188" x2="1000" y2="188" stroke="rgba(45,49,66,0.12)" stroke-width="0.8"/>
|
||||
<text x="772" y="202" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">INDEXES</text>
|
||||
<text x="772" y="220" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">uq_products_sku</text>
|
||||
|
||||
<!-- Table: billing.invoices (inside BILLING group) -->
|
||||
<rect x="760" y="320" width="240" height="100" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="760" y="320" width="240" height="28" rx="6" fill="rgba(45,49,66,0.04)" stroke="none"/>
|
||||
<rect x="760" y="340" width="240" height="8" fill="rgba(45,49,66,0.04)"/>
|
||||
<line x1="760" y1="348" x2="1000" y2="348" stroke="rgba(45,49,66,0.22)" stroke-width="1"/>
|
||||
<text x="772" y="338" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">billing.invoices</text>
|
||||
<rect x="948" y="328" width="40" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="968" y="337" fill="#2d3142" opacity="0.8" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">TABLE</text>
|
||||
|
||||
<rect x="760" y="348" width="240" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="772" y="364" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">id</text>
|
||||
<rect x="880" y="356" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="890" y="365" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">PK</text>
|
||||
<text x="988" y="364" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<text x="772" y="388" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">order_id</text>
|
||||
<rect x="880" y="380" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="890" y="389" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">FK</text>
|
||||
<rect x="904" y="380" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="914" y="389" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="988" y="388" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<rect x="760" y="396" width="240" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="772" y="412" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">issued_at</text>
|
||||
<rect x="880" y="404" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="890" y="413" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="988" y="412" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">timestamptz</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="500" x2="1040" y2="500" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="516" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="40" y="532" width="20" height="12" rx="2" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="68" y="540" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Table</text>
|
||||
|
||||
<rect x="180" y="532" width="20" height="12" rx="2" fill="rgba(45,49,66,0.02)" stroke="rgba(45,49,66,0.35)" stroke-width="0.8" stroke-dasharray="3,3"/>
|
||||
<text x="208" y="540" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Schema group</text>
|
||||
|
||||
<rect x="340" y="532" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="350" y="541" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">PK</text>
|
||||
<text x="368" y="540" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Constraint chip</text>
|
||||
|
||||
<line x1="500" y1="538" x2="520" y2="538" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="528" y="540" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Foreign key (column to column)</text>
|
||||
|
||||
<line x1="740" y1="538" x2="760" y2="538" stroke="#eb6c36" stroke-width="1.2"/>
|
||||
<text x="768" y="540" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Destructive delete (CASCADE)</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<p class="eyebrow">THE DESTRUCTIVE EDGE</p>
|
||||
<div class="card-header"><span class="card-dot coral"></span><h3>Deleting an order cascades</h3></div>
|
||||
<p>`order_items.order_id` is the one `ON DELETE CASCADE` in this schema — remove an order and its line items go with it. Every other foreign key is `RESTRICT`.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot ink"></span><h3>Column to column, not box to box</h3></div>
|
||||
<ul><li>Every FK anchors to a specific row on both ends</li><li>Fixed 24px row height keeps anchors predictable</li><li>The `ON DELETE` label is the point of the edge</li></ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot muted"></span><h3>Scoped to the subsystem</h3></div>
|
||||
<p>`products` truncates to a `+ 3 more columns` row rather than dumping every column. This is checkout, not a `\d+` of the whole database.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span>Commerce schema · database schema model</span>
|
||||
<span>example · diagram design</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,302 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Commerce schema · database schema</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #f5f5f5;
|
||||
--color-ink: #2d3142;
|
||||
--color-muted: #4f5d75;
|
||||
--color-accent: #eb6c36;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1240px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 960px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Database schema · Diagram Design</p>
|
||||
<h1>Commerce schema · public + billing</h1>
|
||||
|
||||
<svg viewBox="0 0 1080 560" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="db-schema-title db-schema-desc">
|
||||
<title id="db-schema-title">Commerce schema · public + billing</title>
|
||||
<desc id="db-schema-desc">Physical database schema diagram showing five commerce tables with column-level foreign keys, SQL types, and constraint chips, including a cascading delete from orders into order_items and a billing schema group.</desc>
|
||||
<defs>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#4f5d75"/></marker>
|
||||
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#eb6c36"/></marker>
|
||||
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#2e5aa8"/></marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f5f5"/>
|
||||
|
||||
<!-- BILLING schema group (drawn first so tables paint over it) -->
|
||||
<rect x="744" y="284" width="272" height="152" rx="8" fill="rgba(45,49,66,0.02)" stroke="rgba(45,49,66,0.20)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<text x="756" y="300" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">BILLING</text>
|
||||
|
||||
<!-- FK connectors (drawn before boxes) -->
|
||||
<!-- FK1: orders.customer_id -> customers.id · ON DELETE RESTRICT -->
|
||||
<path d="M 400,104 H 348 Q 340,104 340,112 Q 340,120 332,120 H 280" fill="none" stroke="#4f5d75" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- FK2: order_items.order_id -> orders.id · ON DELETE CASCADE (focal) -->
|
||||
<path d="M 640,384 H 692 Q 700,384 700,376 V 80 Q 700,72 692,72 H 640" fill="none" stroke="#eb6c36" stroke-width="1.2" marker-end="url(#arrow-accent)"/>
|
||||
<!-- FK3: order_items.product_id -> products.id · ON DELETE RESTRICT -->
|
||||
<path d="M 640,408 H 708 Q 716,408 716,400 V 88 Q 716,80 724,80 H 760" fill="none" stroke="#4f5d75" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- FK4: invoices.order_id -> orders.id · ON DELETE RESTRICT -->
|
||||
<path d="M 760,384 H 692 Q 684,384 684,376 V 96 Q 684,88 676,88 H 640" fill="none" stroke="#4f5d75" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- FK labels (masked, 6-10px gap above/beside their connector) -->
|
||||
<rect x="284" y="82" width="112" height="12" rx="2" fill="#f5f5f5"/>
|
||||
<text x="340" y="91" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">ON DELETE RESTRICT</text>
|
||||
|
||||
<rect x="648" y="392" width="88" height="12" rx="2" fill="#f5f5f5"/>
|
||||
<text x="692" y="401" fill="#eb6c36" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">ON DELETE CASCADE</text>
|
||||
|
||||
<rect x="724" y="252" width="112" height="12" rx="2" fill="#f5f5f5"/>
|
||||
<text x="780" y="261" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">ON DELETE RESTRICT</text>
|
||||
|
||||
<rect x="564" y="270" width="112" height="12" rx="2" fill="#f5f5f5"/>
|
||||
<text x="620" y="279" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">ON DELETE RESTRICT</text>
|
||||
|
||||
<!-- Table: public.customers -->
|
||||
<rect x="40" y="80" width="240" height="100" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="40" y="80" width="240" height="28" rx="6" fill="rgba(45,49,66,0.04)" stroke="none"/>
|
||||
<rect x="40" y="100" width="240" height="8" fill="rgba(45,49,66,0.04)"/>
|
||||
<line x1="40" y1="108" x2="280" y2="108" stroke="rgba(45,49,66,0.22)" stroke-width="1"/>
|
||||
<text x="52" y="98" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">public.customers</text>
|
||||
<rect x="228" y="88" width="40" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="248" y="97" fill="#2d3142" opacity="0.8" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">TABLE</text>
|
||||
|
||||
<rect x="40" y="108" width="240" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="52" y="124" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">id</text>
|
||||
<rect x="160" y="116" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="170" y="125" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">PK</text>
|
||||
<text x="268" y="124" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<text x="52" y="148" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">email</text>
|
||||
<rect x="160" y="140" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="170" y="149" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">UQ</text>
|
||||
<rect x="184" y="140" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="194" y="149" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="268" y="148" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
|
||||
<rect x="40" y="156" width="240" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="52" y="172" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">created_at</text>
|
||||
<rect x="160" y="164" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="170" y="173" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="268" y="172" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">timestamptz</text>
|
||||
|
||||
<!-- Table: public.orders -->
|
||||
<rect x="400" y="40" width="240" height="200" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="400" y="40" width="240" height="28" rx="6" fill="rgba(45,49,66,0.04)" stroke="none"/>
|
||||
<rect x="400" y="60" width="240" height="8" fill="rgba(45,49,66,0.04)"/>
|
||||
<line x1="400" y1="68" x2="640" y2="68" stroke="rgba(45,49,66,0.22)" stroke-width="1"/>
|
||||
<text x="412" y="58" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">public.orders</text>
|
||||
<rect x="588" y="48" width="40" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="608" y="57" fill="#2d3142" opacity="0.8" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">TABLE</text>
|
||||
|
||||
<rect x="400" y="68" width="240" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="412" y="84" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">id</text>
|
||||
<rect x="520" y="76" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="85" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">PK</text>
|
||||
<text x="628" y="84" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<text x="412" y="108" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">customer_id</text>
|
||||
<rect x="520" y="100" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="109" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">FK</text>
|
||||
<rect x="544" y="100" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="554" y="109" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="628" y="108" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<rect x="400" y="116" width="240" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="412" y="132" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">status</text>
|
||||
<rect x="520" y="124" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="133" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="628" y="132" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
|
||||
<text x="412" y="156" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">total</text>
|
||||
<rect x="520" y="148" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="157" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="628" y="156" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">numeric(12,2)</text>
|
||||
|
||||
<rect x="400" y="164" width="240" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="412" y="180" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">placed_at</text>
|
||||
<rect x="520" y="172" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="181" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="628" y="180" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">timestamptz</text>
|
||||
|
||||
<line x1="400" y1="188" x2="640" y2="188" stroke="rgba(45,49,66,0.12)" stroke-width="0.8"/>
|
||||
<text x="412" y="202" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">INDEXES</text>
|
||||
<text x="412" y="220" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">idx_orders_customer_id</text>
|
||||
<text x="412" y="236" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">idx_orders_status</text>
|
||||
|
||||
<!-- Table: public.order_items (focal header — accent-tint, cascade target) -->
|
||||
<rect x="400" y="320" width="240" height="148" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="400" y="320" width="240" height="28" rx="6" fill="rgba(235,108,54,0.08)" stroke="none"/>
|
||||
<rect x="400" y="340" width="240" height="8" fill="rgba(235,108,54,0.08)"/>
|
||||
<line x1="400" y1="348" x2="640" y2="348" stroke="rgba(45,49,66,0.22)" stroke-width="1"/>
|
||||
<text x="412" y="338" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">public.order_items</text>
|
||||
<rect x="588" y="328" width="40" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="608" y="337" fill="#2d3142" opacity="0.8" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">TABLE</text>
|
||||
|
||||
<rect x="400" y="348" width="240" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="412" y="364" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">id</text>
|
||||
<rect x="520" y="356" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="365" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">PK</text>
|
||||
<text x="628" y="364" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<text x="412" y="388" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">order_id</text>
|
||||
<rect x="520" y="380" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="389" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">FK</text>
|
||||
<rect x="544" y="380" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="554" y="389" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="628" y="388" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<rect x="400" y="396" width="240" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="412" y="412" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">product_id</text>
|
||||
<rect x="520" y="404" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="413" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">FK</text>
|
||||
<rect x="544" y="404" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="554" y="413" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="628" y="412" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<text x="412" y="436" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">qty</text>
|
||||
<rect x="520" y="428" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="437" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="628" y="436" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">integer</text>
|
||||
|
||||
<rect x="400" y="444" width="240" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="412" y="460" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">unit_price</text>
|
||||
<rect x="520" y="452" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="530" y="461" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="628" y="460" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">numeric(12,2)</text>
|
||||
|
||||
<!-- Table: public.products -->
|
||||
<rect x="760" y="40" width="240" height="184" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="760" y="40" width="240" height="28" rx="6" fill="rgba(45,49,66,0.04)" stroke="none"/>
|
||||
<rect x="760" y="60" width="240" height="8" fill="rgba(45,49,66,0.04)"/>
|
||||
<line x1="760" y1="68" x2="1000" y2="68" stroke="rgba(45,49,66,0.22)" stroke-width="1"/>
|
||||
<text x="772" y="58" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">public.products</text>
|
||||
<rect x="948" y="48" width="40" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="968" y="57" fill="#2d3142" opacity="0.8" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">TABLE</text>
|
||||
|
||||
<rect x="760" y="68" width="240" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="772" y="84" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">id</text>
|
||||
<rect x="880" y="76" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="890" y="85" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">PK</text>
|
||||
<text x="988" y="84" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<text x="772" y="108" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">sku</text>
|
||||
<rect x="880" y="100" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="890" y="109" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">UQ</text>
|
||||
<rect x="904" y="100" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="914" y="109" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="988" y="108" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
|
||||
<rect x="760" y="116" width="240" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="772" y="132" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">name</text>
|
||||
<rect x="880" y="124" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="890" y="133" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="988" y="132" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">text</text>
|
||||
|
||||
<text x="772" y="156" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">price</text>
|
||||
<rect x="880" y="148" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="890" y="157" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="988" y="156" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">numeric(12,2)</text>
|
||||
|
||||
<rect x="760" y="164" width="240" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="772" y="180" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">+ 3 more columns</text>
|
||||
|
||||
<line x1="760" y1="188" x2="1000" y2="188" stroke="rgba(45,49,66,0.12)" stroke-width="0.8"/>
|
||||
<text x="772" y="202" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">INDEXES</text>
|
||||
<text x="772" y="220" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">uq_products_sku</text>
|
||||
|
||||
<!-- Table: billing.invoices (inside BILLING group) -->
|
||||
<rect x="760" y="320" width="240" height="100" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="760" y="320" width="240" height="28" rx="6" fill="rgba(45,49,66,0.04)" stroke="none"/>
|
||||
<rect x="760" y="340" width="240" height="8" fill="rgba(45,49,66,0.04)"/>
|
||||
<line x1="760" y1="348" x2="1000" y2="348" stroke="rgba(45,49,66,0.22)" stroke-width="1"/>
|
||||
<text x="772" y="338" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">billing.invoices</text>
|
||||
<rect x="948" y="328" width="40" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="968" y="337" fill="#2d3142" opacity="0.8" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">TABLE</text>
|
||||
|
||||
<rect x="760" y="348" width="240" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="772" y="364" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">id</text>
|
||||
<rect x="880" y="356" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="890" y="365" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">PK</text>
|
||||
<text x="988" y="364" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<text x="772" y="388" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">order_id</text>
|
||||
<rect x="880" y="380" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="890" y="389" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">FK</text>
|
||||
<rect x="904" y="380" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="914" y="389" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="988" y="388" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">uuid</text>
|
||||
|
||||
<rect x="760" y="396" width="240" height="24" fill="rgba(45,49,66,0.02)"/>
|
||||
<text x="772" y="412" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">issued_at</text>
|
||||
<rect x="880" y="404" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="890" y="413" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">NN</text>
|
||||
<text x="988" y="412" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">timestamptz</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="500" x2="1040" y2="500" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="516" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="40" y="532" width="20" height="12" rx="2" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="68" y="540" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Table</text>
|
||||
|
||||
<rect x="180" y="532" width="20" height="12" rx="2" fill="rgba(45,49,66,0.02)" stroke="rgba(45,49,66,0.35)" stroke-width="0.8" stroke-dasharray="3,3"/>
|
||||
<text x="208" y="540" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Schema group</text>
|
||||
|
||||
<rect x="340" y="532" width="20" height="12" rx="2" fill="none" stroke="rgba(45,49,66,0.35)" stroke-width="0.8"/>
|
||||
<text x="350" y="541" fill="#2d3142" opacity="0.75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">PK</text>
|
||||
<text x="368" y="540" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Constraint chip</text>
|
||||
|
||||
<line x1="500" y1="538" x2="520" y2="538" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="528" y="540" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Foreign key (column to column)</text>
|
||||
|
||||
<line x1="740" y1="538" x2="760" y2="538" stroke="#eb6c36" stroke-width="1.2"/>
|
||||
<text x="768" y="540" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Destructive delete (CASCADE)</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,224 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TypeScript monorepo dependency graph · Dependency</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #2d3142;
|
||||
--color-ink: #f5f5f5;
|
||||
--color-muted: #bfc0c0;
|
||||
--color-accent: #f08a59;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Dependency · Diagram Design</p>
|
||||
<h1>TypeScript monorepo dependency graph</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 600" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="dependency-dark-title dependency-dark-desc">
|
||||
<title id="dependency-dark-title">TypeScript monorepo dependency graph</title>
|
||||
<desc id="dependency-dark-desc">Ranked dependency graph of a TypeScript monorepo showing shared-types as the highest fan-in package with four dependents, and one back-edge cycle from utils into shared-types.</desc>
|
||||
<defs>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#bfc0c0"/>
|
||||
</marker>
|
||||
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#f08a59"/>
|
||||
</marker>
|
||||
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#6a95d8"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#2d3142"/>
|
||||
|
||||
<!-- Rank labels (left margin) -->
|
||||
<text x="40" y="108" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">RANK 0</text>
|
||||
<text x="40" y="228" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">RANK 1</text>
|
||||
<text x="40" y="348" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">RANK 2</text>
|
||||
<text x="40" y="468" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">RANK 3</text>
|
||||
|
||||
<!-- Connectors drawn before boxes -->
|
||||
<!-- web -> api (straight, same column) -->
|
||||
<line x1="400" y1="136" x2="400" y2="200" stroke="#bfc0c0" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- web -> ui-kit (elbow, bridged over the web->api crossing at x=400) -->
|
||||
<path d="M 360,136 V 152 Q 360,160 368,160 H 392 a 8,8 0 0,1 16,0 H 552 Q 560,160 560,168 V 200" fill="none" stroke="#bfc0c0" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- admin -> ui-kit (straight, same column) -->
|
||||
<line x1="600" y1="136" x2="600" y2="200" stroke="#bfc0c0" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- admin -> api (elbow, bridged over the web->ui-kit crossing) -->
|
||||
<path d="M 640,136 V 168 Q 640,176 632,176 H 568 a 8,8 0 0,0 -16,0 H 448 Q 440,176 440,184 V 200" fill="none" stroke="#bfc0c0" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- api -> shared-types (straight, same column) -->
|
||||
<line x1="400" y1="256" x2="400" y2="320" stroke="#bfc0c0" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- api -> db (elbow, routed at y=304 - 16px below the ui-kit->shared-types elbow at y=288 so the two never run colinear) -->
|
||||
<path d="M 440,256 V 296 Q 440,304 448,304 H 592 Q 600,304 600,312 V 320" fill="none" stroke="#bfc0c0" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- ui-kit -> shared-types (elbow, bridges the api->shared-types crossing at x=400 and the api->db vertical at x=440) -->
|
||||
<path d="M 572,256 V 280 Q 572,288 564,288 H 448 a 8,8 0 0,0 -16,0 H 408 a 8,8 0 0,0 -16,0 H 368 Q 360,288 360,296 V 320" fill="none" stroke="#bfc0c0" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- ui-kit -> tokens (elbow, routed around the outside via the left margin, at y=272 - 16px clear of the ui-kit->shared-types elbow at y=288 so the two never run parallel closer than 12px; bridges the ui-kit->shared-types vertical at x=572, the api->shared-types crossing at x=400, and the api->db vertical at x=440) -->
|
||||
<path d="M 628,256 V 264 Q 628,272 620,272 H 580 a 8,8 0 0,0 -16,0 H 448 a 8,8 0 0,0 -16,0 H 408 a 8,8 0 0,0 -16,0 H 188 Q 180,272 180,280 V 460 Q 180,468 188,468 H 220" fill="none" stroke="#bfc0c0" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- db -> shared-types (straight, same row) -->
|
||||
<line x1="520" y1="348" x2="480" y2="348" stroke="#bfc0c0" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- shared-types -> utils (elbow) -->
|
||||
<path d="M 372,376 V 400 Q 372,408 380,408 H 492 Q 500,408 500,416 V 440" fill="none" stroke="#bfc0c0" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- shared-types -> zod (elbow) -->
|
||||
<path d="M 428,376 V 388 Q 428,396 436,396 H 692 Q 700,396 700,404 V 440" fill="none" stroke="#bfc0c0" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- Cycle back-edge: utils -> shared-types, routed below the tokens/utils row and above the legend hairline -->
|
||||
<!-- Hop on the accent (dashed, less-important) stroke where it crosses the ui-kit->tokens connector at (196,468) -->
|
||||
<path d="M 440,496 V 500 Q 440,508 432,508 H 204 Q 196,508 196,500 V 476 a 8,8 0 0,0 0,-16 V 356 Q 196,348 204,348 H 320" fill="none" stroke="#f08a59" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow-accent)"/>
|
||||
<rect x="204" y="504" width="40" height="12" rx="2" fill="#2d3142"/>
|
||||
<text x="224" y="513" fill="#f08a59" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">CYCLE</text>
|
||||
|
||||
<!-- web -->
|
||||
<rect x="320" y="80" width="160" height="56" rx="6" fill="#2d3142"/>
|
||||
<rect x="320" y="80" width="160" height="56" rx="6" fill="rgba(245,245,245,0.04)" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<rect x="328" y="86" width="28" height="12" rx="2" fill="transparent" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="342" y="95" fill="#f5f5f5" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">APP</text>
|
||||
<rect x="444" y="88" width="28" height="12" rx="2" fill="transparent" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="458" y="97" fill="#f5f5f5" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">0 IN</text>
|
||||
<text x="400" y="114" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">web</text>
|
||||
<text x="400" y="130" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">apps/web</text>
|
||||
|
||||
<!-- admin -->
|
||||
<rect x="520" y="80" width="160" height="56" rx="6" fill="#2d3142"/>
|
||||
<rect x="520" y="80" width="160" height="56" rx="6" fill="rgba(245,245,245,0.04)" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<rect x="528" y="86" width="28" height="12" rx="2" fill="transparent" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="542" y="95" fill="#f5f5f5" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">APP</text>
|
||||
<rect x="644" y="88" width="28" height="12" rx="2" fill="transparent" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="658" y="97" fill="#f5f5f5" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">0 IN</text>
|
||||
<text x="600" y="114" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">admin</text>
|
||||
<text x="600" y="130" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">apps/admin</text>
|
||||
|
||||
<!-- api -->
|
||||
<rect x="320" y="200" width="160" height="56" rx="6" fill="#2d3142"/>
|
||||
<rect x="320" y="200" width="160" height="56" rx="6" fill="rgba(245,245,245,0.04)" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<rect x="328" y="206" width="28" height="12" rx="2" fill="transparent" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="342" y="215" fill="#f5f5f5" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">SVC</text>
|
||||
<rect x="444" y="208" width="28" height="12" rx="2" fill="transparent" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="458" y="217" fill="#f5f5f5" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">2 IN</text>
|
||||
<text x="400" y="234" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">api</text>
|
||||
<text x="400" y="250" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">services/api</text>
|
||||
|
||||
<!-- ui-kit -->
|
||||
<rect x="520" y="200" width="160" height="56" rx="6" fill="#2d3142"/>
|
||||
<rect x="520" y="200" width="160" height="56" rx="6" fill="rgba(245,245,245,0.04)" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<rect x="528" y="206" width="28" height="12" rx="2" fill="transparent" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="542" y="215" fill="#f5f5f5" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">PKG</text>
|
||||
<rect x="644" y="208" width="28" height="12" rx="2" fill="transparent" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="658" y="217" fill="#f5f5f5" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">2 IN</text>
|
||||
<text x="600" y="234" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">ui-kit</text>
|
||||
<text x="600" y="250" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">packages/ui-kit</text>
|
||||
|
||||
<!-- shared-types (highest fan-in) -->
|
||||
<rect x="320" y="320" width="160" height="56" rx="6" fill="#2d3142"/>
|
||||
<rect x="320" y="320" width="160" height="56" rx="6" fill="rgba(245,245,245,0.04)" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<rect x="328" y="326" width="28" height="12" rx="2" fill="transparent" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="342" y="335" fill="#f5f5f5" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">PKG</text>
|
||||
<rect x="444" y="328" width="28" height="12" rx="2" fill="transparent" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="458" y="337" fill="#f5f5f5" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">4 IN</text>
|
||||
<text x="400" y="354" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">shared-types</text>
|
||||
<text x="400" y="370" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">packages/shared-types</text>
|
||||
|
||||
<!-- db -->
|
||||
<rect x="520" y="320" width="160" height="56" rx="6" fill="#2d3142"/>
|
||||
<rect x="520" y="320" width="160" height="56" rx="6" fill="rgba(245,245,245,0.04)" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<rect x="528" y="326" width="28" height="12" rx="2" fill="transparent" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="542" y="335" fill="#f5f5f5" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">SVC</text>
|
||||
<rect x="644" y="328" width="28" height="12" rx="2" fill="transparent" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="658" y="337" fill="#f5f5f5" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">1 IN</text>
|
||||
<text x="600" y="354" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">db</text>
|
||||
<text x="600" y="370" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">services/db</text>
|
||||
|
||||
<!-- tokens (leaf) -->
|
||||
<rect x="220" y="440" width="160" height="56" rx="6" fill="#2d3142"/>
|
||||
<rect x="220" y="440" width="160" height="56" rx="6" fill="rgba(245,245,245,0.05)" stroke="#bfc0c0" stroke-width="0.8"/>
|
||||
<rect x="228" y="446" width="28" height="12" rx="2" fill="transparent" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="242" y="455" fill="#f5f5f5" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">PKG</text>
|
||||
<rect x="344" y="448" width="28" height="12" rx="2" fill="transparent" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="358" y="457" fill="#f5f5f5" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">1 IN</text>
|
||||
<text x="300" y="474" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">tokens</text>
|
||||
<text x="300" y="490" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">packages/tokens</text>
|
||||
|
||||
<!-- utils (holds the outgoing cycle edge) -->
|
||||
<rect x="420" y="440" width="160" height="56" rx="6" fill="#2d3142"/>
|
||||
<rect x="420" y="440" width="160" height="56" rx="6" fill="rgba(245,245,245,0.04)" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<rect x="428" y="446" width="28" height="12" rx="2" fill="transparent" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="442" y="455" fill="#f5f5f5" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">PKG</text>
|
||||
<rect x="544" y="448" width="28" height="12" rx="2" fill="transparent" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="558" y="457" fill="#f5f5f5" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">1 IN</text>
|
||||
<text x="500" y="474" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">utils</text>
|
||||
<text x="500" y="490" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">packages/utils</text>
|
||||
|
||||
<!-- zod (external) -->
|
||||
<rect x="620" y="440" width="160" height="56" rx="6" fill="#2d3142"/>
|
||||
<rect x="620" y="440" width="160" height="56" rx="6" fill="rgba(245,245,245,0.03)" stroke="rgba(245,245,245,0.30)" stroke-width="1"/>
|
||||
<rect x="628" y="446" width="28" height="12" rx="2" fill="transparent" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="642" y="455" fill="#f5f5f5" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EXT</text>
|
||||
<rect x="744" y="448" width="28" height="12" rx="2" fill="transparent" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="758" y="457" fill="#f5f5f5" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">1 IN</text>
|
||||
<text x="700" y="474" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">zod</text>
|
||||
<text x="700" y="490" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">v3.23 · npm</text>
|
||||
|
||||
<!-- Legend strip -->
|
||||
<line x1="40" y1="524" x2="960" y2="524" stroke="rgba(245,245,245,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="540" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="40" y="556" width="16" height="12" rx="2" fill="rgba(245,245,245,0.04)" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<text x="64" y="565" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Internal package</text>
|
||||
|
||||
<rect x="192" y="556" width="16" height="12" rx="2" fill="rgba(245,245,245,0.03)" stroke="rgba(245,245,245,0.30)" stroke-width="1"/>
|
||||
<text x="216" y="565" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">External / third-party</text>
|
||||
|
||||
<rect x="380" y="556" width="16" height="12" rx="2" fill="rgba(245,245,245,0.05)" stroke="#bfc0c0" stroke-width="0.8"/>
|
||||
<text x="404" y="565" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Leaf (no outgoing)</text>
|
||||
|
||||
<line x1="552" y1="562" x2="580" y2="562" stroke="#f08a59" stroke-width="1.2" stroke-dasharray="5,4"/>
|
||||
<text x="588" y="565" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Cycle (back-edge)</text>
|
||||
|
||||
<text x="700" y="565" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Badges show fan-in — a tree can't express convergence.</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,227 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TypeScript monorepo dependency graph · Dependency</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root { --color-paper:#f5f5f5; --color-paper-2:#ececec; --color-ink:#2d3142; --color-muted:#4f5d75; --color-soft:#7a8399; --color-rule:rgba(45,49,66,0.12); --color-accent:#eb6c36; --color-link:#2e5aa8; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
|
||||
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.header { margin-bottom: 2.5rem; }
|
||||
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
|
||||
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
|
||||
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
|
||||
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
|
||||
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
|
||||
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
|
||||
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
|
||||
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(45,49,66,0.08); }
|
||||
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
|
||||
.card-dot.ink { background: var(--color-ink); } .card-dot.muted { background: var(--color-muted); } .card-dot.coral { background: var(--color-accent); }
|
||||
.card h3 { font-size: 0.875rem; font-weight: 600; }
|
||||
.card p, .card ul { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
|
||||
.card li { margin-bottom: 0.3rem; padding-left: 0.875rem; position: relative; }
|
||||
.card li::before { content: '—'; position: absolute; left: 0; color: rgba(45,49,66,0.25); font-size: 0.75rem; }
|
||||
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(45,49,66,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<p class="header-eyebrow">Dependency · Diagram Design</p>
|
||||
<h1>TypeScript monorepo dependency graph</h1>
|
||||
<p class="subtitle">Nine packages, ranked by dependency depth. A tree can't show what matters here — that <code>shared-types</code> has four dependents, or that <code>utils</code> depends back on it. Coral is spent on exactly that cycle.</p>
|
||||
</div>
|
||||
|
||||
<div class="diagram-container">
|
||||
<svg viewBox="0 0 1000 600" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="dependency-full-title dependency-full-desc">
|
||||
<title id="dependency-full-title">TypeScript monorepo dependency graph</title>
|
||||
<desc id="dependency-full-desc">Ranked dependency graph of a TypeScript monorepo showing shared-types as the highest fan-in package with four dependents, and one back-edge cycle from utils into shared-types.</desc>
|
||||
<defs>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#4f5d75"/>
|
||||
</marker>
|
||||
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#eb6c36"/>
|
||||
</marker>
|
||||
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#2e5aa8"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f5f5"/>
|
||||
|
||||
<!-- Rank labels (left margin) -->
|
||||
<text x="40" y="108" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">RANK 0</text>
|
||||
<text x="40" y="228" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">RANK 1</text>
|
||||
<text x="40" y="348" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">RANK 2</text>
|
||||
<text x="40" y="468" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">RANK 3</text>
|
||||
|
||||
<!-- Connectors drawn before boxes -->
|
||||
<!-- web -> api (straight, same column) -->
|
||||
<line x1="400" y1="136" x2="400" y2="200" stroke="#4f5d75" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- web -> ui-kit (elbow, bridged over the web->api crossing at x=400) -->
|
||||
<path d="M 360,136 V 152 Q 360,160 368,160 H 392 a 8,8 0 0,1 16,0 H 552 Q 560,160 560,168 V 200" fill="none" stroke="#4f5d75" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- admin -> ui-kit (straight, same column) -->
|
||||
<line x1="600" y1="136" x2="600" y2="200" stroke="#4f5d75" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- admin -> api (elbow, bridged over the web->ui-kit crossing) -->
|
||||
<path d="M 640,136 V 168 Q 640,176 632,176 H 568 a 8,8 0 0,0 -16,0 H 448 Q 440,176 440,184 V 200" fill="none" stroke="#4f5d75" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- api -> shared-types (straight, same column) -->
|
||||
<line x1="400" y1="256" x2="400" y2="320" stroke="#4f5d75" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- api -> db (elbow, routed at y=304 - 16px below the ui-kit->shared-types elbow at y=288 so the two never run colinear) -->
|
||||
<path d="M 440,256 V 296 Q 440,304 448,304 H 592 Q 600,304 600,312 V 320" fill="none" stroke="#4f5d75" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- ui-kit -> shared-types (elbow, bridges the api->shared-types crossing at x=400 and the api->db vertical at x=440) -->
|
||||
<path d="M 572,256 V 280 Q 572,288 564,288 H 448 a 8,8 0 0,0 -16,0 H 408 a 8,8 0 0,0 -16,0 H 368 Q 360,288 360,296 V 320" fill="none" stroke="#4f5d75" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- ui-kit -> tokens (elbow, routed around the outside via the left margin, at y=272 - 16px clear of the ui-kit->shared-types elbow at y=288 so the two never run parallel closer than 12px; bridges the ui-kit->shared-types vertical at x=572, the api->shared-types crossing at x=400, and the api->db vertical at x=440) -->
|
||||
<path d="M 628,256 V 264 Q 628,272 620,272 H 580 a 8,8 0 0,0 -16,0 H 448 a 8,8 0 0,0 -16,0 H 408 a 8,8 0 0,0 -16,0 H 188 Q 180,272 180,280 V 460 Q 180,468 188,468 H 220" fill="none" stroke="#4f5d75" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- db -> shared-types (straight, same row) -->
|
||||
<line x1="520" y1="348" x2="480" y2="348" stroke="#4f5d75" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- shared-types -> utils (elbow) -->
|
||||
<path d="M 372,376 V 400 Q 372,408 380,408 H 492 Q 500,408 500,416 V 440" fill="none" stroke="#4f5d75" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- shared-types -> zod (elbow) -->
|
||||
<path d="M 428,376 V 388 Q 428,396 436,396 H 692 Q 700,396 700,404 V 440" fill="none" stroke="#4f5d75" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- Cycle back-edge: utils -> shared-types, routed below the tokens/utils row and above the legend hairline -->
|
||||
<!-- Hop on the accent (dashed, less-important) stroke where it crosses the ui-kit->tokens connector at (196,468) -->
|
||||
<path d="M 440,496 V 500 Q 440,508 432,508 H 204 Q 196,508 196,500 V 476 a 8,8 0 0,0 0,-16 V 356 Q 196,348 204,348 H 320" fill="none" stroke="#eb6c36" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow-accent)"/>
|
||||
<rect x="204" y="504" width="40" height="12" rx="2" fill="#f5f5f5"/>
|
||||
<text x="224" y="513" fill="#eb6c36" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">CYCLE</text>
|
||||
|
||||
<!-- web -->
|
||||
<rect x="320" y="80" width="160" height="56" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="320" y="80" width="160" height="56" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="328" y="86" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="342" y="95" fill="#2d3142" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">APP</text>
|
||||
<rect x="444" y="88" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="458" y="97" fill="#2d3142" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">0 IN</text>
|
||||
<text x="400" y="114" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">web</text>
|
||||
<text x="400" y="130" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">apps/web</text>
|
||||
|
||||
<!-- admin -->
|
||||
<rect x="520" y="80" width="160" height="56" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="520" y="80" width="160" height="56" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="528" y="86" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="542" y="95" fill="#2d3142" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">APP</text>
|
||||
<rect x="644" y="88" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="658" y="97" fill="#2d3142" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">0 IN</text>
|
||||
<text x="600" y="114" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">admin</text>
|
||||
<text x="600" y="130" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">apps/admin</text>
|
||||
|
||||
<!-- api -->
|
||||
<rect x="320" y="200" width="160" height="56" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="320" y="200" width="160" height="56" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="328" y="206" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="342" y="215" fill="#2d3142" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">SVC</text>
|
||||
<rect x="444" y="208" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="458" y="217" fill="#2d3142" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">2 IN</text>
|
||||
<text x="400" y="234" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">api</text>
|
||||
<text x="400" y="250" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">services/api</text>
|
||||
|
||||
<!-- ui-kit -->
|
||||
<rect x="520" y="200" width="160" height="56" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="520" y="200" width="160" height="56" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="528" y="206" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="542" y="215" fill="#2d3142" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">PKG</text>
|
||||
<rect x="644" y="208" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="658" y="217" fill="#2d3142" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">2 IN</text>
|
||||
<text x="600" y="234" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">ui-kit</text>
|
||||
<text x="600" y="250" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">packages/ui-kit</text>
|
||||
|
||||
<!-- shared-types (highest fan-in) -->
|
||||
<rect x="320" y="320" width="160" height="56" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="320" y="320" width="160" height="56" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="328" y="326" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="342" y="335" fill="#2d3142" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">PKG</text>
|
||||
<rect x="444" y="328" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="458" y="337" fill="#2d3142" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">4 IN</text>
|
||||
<text x="400" y="354" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">shared-types</text>
|
||||
<text x="400" y="370" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">packages/shared-types</text>
|
||||
|
||||
<!-- db -->
|
||||
<rect x="520" y="320" width="160" height="56" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="520" y="320" width="160" height="56" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="528" y="326" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="542" y="335" fill="#2d3142" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">SVC</text>
|
||||
<rect x="644" y="328" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="658" y="337" fill="#2d3142" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">1 IN</text>
|
||||
<text x="600" y="354" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">db</text>
|
||||
<text x="600" y="370" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">services/db</text>
|
||||
|
||||
<!-- tokens (leaf) -->
|
||||
<rect x="220" y="440" width="160" height="56" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="220" y="440" width="160" height="56" rx="6" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<rect x="228" y="446" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="242" y="455" fill="#2d3142" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">PKG</text>
|
||||
<rect x="344" y="448" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="358" y="457" fill="#2d3142" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">1 IN</text>
|
||||
<text x="300" y="474" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">tokens</text>
|
||||
<text x="300" y="490" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">packages/tokens</text>
|
||||
|
||||
<!-- utils (holds the outgoing cycle edge) -->
|
||||
<rect x="420" y="440" width="160" height="56" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="420" y="440" width="160" height="56" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="428" y="446" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="442" y="455" fill="#2d3142" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">PKG</text>
|
||||
<rect x="544" y="448" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="558" y="457" fill="#2d3142" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">1 IN</text>
|
||||
<text x="500" y="474" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">utils</text>
|
||||
<text x="500" y="490" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">packages/utils</text>
|
||||
|
||||
<!-- zod (external) -->
|
||||
<rect x="620" y="440" width="160" height="56" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="620" y="440" width="160" height="56" rx="6" fill="rgba(45,49,66,0.03)" stroke="rgba(45,49,66,0.30)" stroke-width="1"/>
|
||||
<rect x="628" y="446" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="642" y="455" fill="#2d3142" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EXT</text>
|
||||
<rect x="744" y="448" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="758" y="457" fill="#2d3142" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">1 IN</text>
|
||||
<text x="700" y="474" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">zod</text>
|
||||
<text x="700" y="490" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">v3.23 · npm</text>
|
||||
|
||||
<!-- Legend strip -->
|
||||
<line x1="40" y1="524" x2="960" y2="524" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="540" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="40" y="556" width="16" height="12" rx="2" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="64" y="565" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Internal package</text>
|
||||
|
||||
<rect x="192" y="556" width="16" height="12" rx="2" fill="rgba(45,49,66,0.03)" stroke="rgba(45,49,66,0.30)" stroke-width="1"/>
|
||||
<text x="216" y="565" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">External / third-party</text>
|
||||
|
||||
<rect x="380" y="556" width="16" height="12" rx="2" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<text x="404" y="565" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Leaf (no outgoing)</text>
|
||||
|
||||
<line x1="552" y1="562" x2="580" y2="562" stroke="#eb6c36" stroke-width="1.2" stroke-dasharray="5,4"/>
|
||||
<text x="588" y="565" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Cycle (back-edge)</text>
|
||||
|
||||
<text x="700" y="565" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Badges show fan-in — a tree can't express convergence.</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<p class="eyebrow">THE HEADLINE</p>
|
||||
<div class="card-header"><span class="card-dot coral"></span><h3>One cycle, spent deliberately</h3></div>
|
||||
<p>Coral appears exactly twice: the dashed back-edge from <code>utils</code> into <code>shared-types</code>, and its <code>CYCLE</code> label. Everything else — including the four-dependent fan-in on <code>shared-types</code> — stays ink and muted, because only the cycle is the editorial point.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot ink"></span><h3>Why not a tree</h3></div>
|
||||
<ul><li>shared-types has four parents, not one</li><li>utils depends back on its own ancestor</li><li>a tree can render neither honestly</li></ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot muted"></span><h3>Read the badges</h3></div>
|
||||
<p>The small <code>N IN</code> chip on every node is the fan-in count. It's the fastest way to spot where a change ripples widest before reading a single arrow.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span>typescript monorepo dependency graph</span>
|
||||
<span>example · diagram design</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,224 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TypeScript monorepo dependency graph · Dependency</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #f5f5f5;
|
||||
--color-ink: #2d3142;
|
||||
--color-muted: #4f5d75;
|
||||
--color-accent: #eb6c36;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Dependency · Diagram Design</p>
|
||||
<h1>TypeScript monorepo dependency graph</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 600" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="dependency-title dependency-desc">
|
||||
<title id="dependency-title">TypeScript monorepo dependency graph</title>
|
||||
<desc id="dependency-desc">Ranked dependency graph of a TypeScript monorepo showing shared-types as the highest fan-in package with four dependents, and one back-edge cycle from utils into shared-types.</desc>
|
||||
<defs>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#4f5d75"/>
|
||||
</marker>
|
||||
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#eb6c36"/>
|
||||
</marker>
|
||||
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#2e5aa8"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f5f5"/>
|
||||
|
||||
<!-- Rank labels (left margin) -->
|
||||
<text x="40" y="108" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">RANK 0</text>
|
||||
<text x="40" y="228" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">RANK 1</text>
|
||||
<text x="40" y="348" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">RANK 2</text>
|
||||
<text x="40" y="468" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">RANK 3</text>
|
||||
|
||||
<!-- Connectors drawn before boxes -->
|
||||
<!-- web -> api (straight, same column) -->
|
||||
<line x1="400" y1="136" x2="400" y2="200" stroke="#4f5d75" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- web -> ui-kit (elbow, bridged over the web->api crossing at x=400) -->
|
||||
<path d="M 360,136 V 152 Q 360,160 368,160 H 392 a 8,8 0 0,1 16,0 H 552 Q 560,160 560,168 V 200" fill="none" stroke="#4f5d75" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- admin -> ui-kit (straight, same column) -->
|
||||
<line x1="600" y1="136" x2="600" y2="200" stroke="#4f5d75" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- admin -> api (elbow, bridged over the web->ui-kit crossing) -->
|
||||
<path d="M 640,136 V 168 Q 640,176 632,176 H 568 a 8,8 0 0,0 -16,0 H 448 Q 440,176 440,184 V 200" fill="none" stroke="#4f5d75" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- api -> shared-types (straight, same column) -->
|
||||
<line x1="400" y1="256" x2="400" y2="320" stroke="#4f5d75" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- api -> db (elbow, routed at y=304 - 16px below the ui-kit->shared-types elbow at y=288 so the two never run colinear) -->
|
||||
<path d="M 440,256 V 296 Q 440,304 448,304 H 592 Q 600,304 600,312 V 320" fill="none" stroke="#4f5d75" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- ui-kit -> shared-types (elbow, bridges the api->shared-types crossing at x=400 and the api->db vertical at x=440) -->
|
||||
<path d="M 572,256 V 280 Q 572,288 564,288 H 448 a 8,8 0 0,0 -16,0 H 408 a 8,8 0 0,0 -16,0 H 368 Q 360,288 360,296 V 320" fill="none" stroke="#4f5d75" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- ui-kit -> tokens (elbow, routed around the outside via the left margin, at y=272 - 16px clear of the ui-kit->shared-types elbow at y=288 so the two never run parallel closer than 12px; bridges the ui-kit->shared-types vertical at x=572, the api->shared-types crossing at x=400, and the api->db vertical at x=440) -->
|
||||
<path d="M 628,256 V 264 Q 628,272 620,272 H 580 a 8,8 0 0,0 -16,0 H 448 a 8,8 0 0,0 -16,0 H 408 a 8,8 0 0,0 -16,0 H 188 Q 180,272 180,280 V 460 Q 180,468 188,468 H 220" fill="none" stroke="#4f5d75" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- db -> shared-types (straight, same row) -->
|
||||
<line x1="520" y1="348" x2="480" y2="348" stroke="#4f5d75" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- shared-types -> utils (elbow) -->
|
||||
<path d="M 372,376 V 400 Q 372,408 380,408 H 492 Q 500,408 500,416 V 440" fill="none" stroke="#4f5d75" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
<!-- shared-types -> zod (elbow) -->
|
||||
<path d="M 428,376 V 388 Q 428,396 436,396 H 692 Q 700,396 700,404 V 440" fill="none" stroke="#4f5d75" stroke-width="1" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- Cycle back-edge: utils -> shared-types, routed below the tokens/utils row and above the legend hairline -->
|
||||
<!-- Hop on the accent (dashed, less-important) stroke where it crosses the ui-kit->tokens connector at (196,468) -->
|
||||
<path d="M 440,496 V 500 Q 440,508 432,508 H 204 Q 196,508 196,500 V 476 a 8,8 0 0,0 0,-16 V 356 Q 196,348 204,348 H 320" fill="none" stroke="#eb6c36" stroke-width="1.2" stroke-dasharray="5,4" marker-end="url(#arrow-accent)"/>
|
||||
<rect x="204" y="504" width="40" height="12" rx="2" fill="#f5f5f5"/>
|
||||
<text x="224" y="513" fill="#eb6c36" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">CYCLE</text>
|
||||
|
||||
<!-- web -->
|
||||
<rect x="320" y="80" width="160" height="56" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="320" y="80" width="160" height="56" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="328" y="86" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="342" y="95" fill="#2d3142" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">APP</text>
|
||||
<rect x="444" y="88" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="458" y="97" fill="#2d3142" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">0 IN</text>
|
||||
<text x="400" y="114" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">web</text>
|
||||
<text x="400" y="130" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">apps/web</text>
|
||||
|
||||
<!-- admin -->
|
||||
<rect x="520" y="80" width="160" height="56" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="520" y="80" width="160" height="56" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="528" y="86" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="542" y="95" fill="#2d3142" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">APP</text>
|
||||
<rect x="644" y="88" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="658" y="97" fill="#2d3142" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">0 IN</text>
|
||||
<text x="600" y="114" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">admin</text>
|
||||
<text x="600" y="130" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">apps/admin</text>
|
||||
|
||||
<!-- api -->
|
||||
<rect x="320" y="200" width="160" height="56" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="320" y="200" width="160" height="56" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="328" y="206" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="342" y="215" fill="#2d3142" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">SVC</text>
|
||||
<rect x="444" y="208" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="458" y="217" fill="#2d3142" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">2 IN</text>
|
||||
<text x="400" y="234" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">api</text>
|
||||
<text x="400" y="250" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">services/api</text>
|
||||
|
||||
<!-- ui-kit -->
|
||||
<rect x="520" y="200" width="160" height="56" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="520" y="200" width="160" height="56" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="528" y="206" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="542" y="215" fill="#2d3142" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">PKG</text>
|
||||
<rect x="644" y="208" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="658" y="217" fill="#2d3142" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">2 IN</text>
|
||||
<text x="600" y="234" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">ui-kit</text>
|
||||
<text x="600" y="250" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">packages/ui-kit</text>
|
||||
|
||||
<!-- shared-types (highest fan-in) -->
|
||||
<rect x="320" y="320" width="160" height="56" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="320" y="320" width="160" height="56" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="328" y="326" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="342" y="335" fill="#2d3142" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">PKG</text>
|
||||
<rect x="444" y="328" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="458" y="337" fill="#2d3142" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">4 IN</text>
|
||||
<text x="400" y="354" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">shared-types</text>
|
||||
<text x="400" y="370" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">packages/shared-types</text>
|
||||
|
||||
<!-- db -->
|
||||
<rect x="520" y="320" width="160" height="56" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="520" y="320" width="160" height="56" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="528" y="326" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="542" y="335" fill="#2d3142" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">SVC</text>
|
||||
<rect x="644" y="328" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="658" y="337" fill="#2d3142" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">1 IN</text>
|
||||
<text x="600" y="354" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">db</text>
|
||||
<text x="600" y="370" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">services/db</text>
|
||||
|
||||
<!-- tokens (leaf) -->
|
||||
<rect x="220" y="440" width="160" height="56" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="220" y="440" width="160" height="56" rx="6" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<rect x="228" y="446" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="242" y="455" fill="#2d3142" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">PKG</text>
|
||||
<rect x="344" y="448" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="358" y="457" fill="#2d3142" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">1 IN</text>
|
||||
<text x="300" y="474" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">tokens</text>
|
||||
<text x="300" y="490" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">packages/tokens</text>
|
||||
|
||||
<!-- utils (holds the outgoing cycle edge) -->
|
||||
<rect x="420" y="440" width="160" height="56" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="420" y="440" width="160" height="56" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="428" y="446" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="442" y="455" fill="#2d3142" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">PKG</text>
|
||||
<rect x="544" y="448" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="558" y="457" fill="#2d3142" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">1 IN</text>
|
||||
<text x="500" y="474" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">utils</text>
|
||||
<text x="500" y="490" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">packages/utils</text>
|
||||
|
||||
<!-- zod (external) -->
|
||||
<rect x="620" y="440" width="160" height="56" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="620" y="440" width="160" height="56" rx="6" fill="rgba(45,49,66,0.03)" stroke="rgba(45,49,66,0.30)" stroke-width="1"/>
|
||||
<rect x="628" y="446" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="642" y="455" fill="#2d3142" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EXT</text>
|
||||
<rect x="744" y="448" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="758" y="457" fill="#2d3142" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">1 IN</text>
|
||||
<text x="700" y="474" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">zod</text>
|
||||
<text x="700" y="490" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">v3.23 · npm</text>
|
||||
|
||||
<!-- Legend strip -->
|
||||
<line x1="40" y1="524" x2="960" y2="524" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="540" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="40" y="556" width="16" height="12" rx="2" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="64" y="565" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Internal package</text>
|
||||
|
||||
<rect x="192" y="556" width="16" height="12" rx="2" fill="rgba(45,49,66,0.03)" stroke="rgba(45,49,66,0.30)" stroke-width="1"/>
|
||||
<text x="216" y="565" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">External / third-party</text>
|
||||
|
||||
<rect x="380" y="556" width="16" height="12" rx="2" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<text x="404" y="565" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Leaf (no outgoing)</text>
|
||||
|
||||
<line x1="552" y1="562" x2="580" y2="562" stroke="#eb6c36" stroke-width="1.2" stroke-dasharray="5,4"/>
|
||||
<text x="588" y="565" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Cycle (back-edge)</text>
|
||||
|
||||
<text x="700" y="565" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Badges show fan-in — a tree can't express convergence.</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,190 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Checkout service · Deployment</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #2d3142;
|
||||
--color-ink: #f5f5f5;
|
||||
--color-muted: #bfc0c0;
|
||||
--color-accent: #f08a59;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 880px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Deployment · Diagram Design</p>
|
||||
<h1>Checkout service, production</h1>
|
||||
|
||||
<svg viewBox="0 0 1040 416" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="deployment-dark-title deployment-dark-desc">
|
||||
<title id="deployment-dark-title">Checkout service, production</title>
|
||||
<desc id="deployment-dark-desc">Deployment diagram placing the checkout service across an edge CDN zone, a production Kubernetes zone running ingress and API pods, and a data zone with a primary Postgres instance replicating to a standby.</desc>
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(245,245,245,0.10)"/>
|
||||
</pattern>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#bfc0c0"/></marker>
|
||||
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#f08a59"/></marker>
|
||||
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#6a95d8"/></marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#2d3142"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Zones (drawn first, before arrows and nodes) -->
|
||||
<rect x="40" y="40" width="208" height="280" rx="8" fill="rgba(245,245,245,0.03)" stroke="rgba(245,245,245,0.10)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<rect x="56" y="44" width="48" height="12" fill="#2d3142"/>
|
||||
<text x="80" y="53" fill="rgba(245,245,245,0.35)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">EDGE</text>
|
||||
|
||||
<rect x="280" y="40" width="280" height="280" rx="8" fill="rgba(245,245,245,0.03)" stroke="rgba(245,245,245,0.10)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<rect x="296" y="44" width="140" height="12" fill="#2d3142"/>
|
||||
<text x="366" y="53" fill="rgba(245,245,245,0.35)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">PROD / EU-WEST-1</text>
|
||||
|
||||
<rect x="600" y="40" width="240" height="280" rx="8" fill="rgba(245,245,245,0.03)" stroke="rgba(245,245,245,0.10)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<rect x="616" y="44" width="40" height="12" fill="#2d3142"/>
|
||||
<text x="636" y="53" fill="rgba(245,245,245,0.35)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">DATA</text>
|
||||
|
||||
<!-- Network paths (behind nodes) -->
|
||||
<path d="M 224,196 H 288 Q 296,196 296,188 V 128 Q 296,120 304,120" fill="none" stroke="#6a95d8" stroke-width="1.2" marker-end="url(#arrow-link)"/>
|
||||
<line x1="420" y1="164" x2="420" y2="196" stroke="#bfc0c0" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<path d="M 536,252 H 572 Q 580,252 580,244 V 128 Q 580,120 588,120 H 624" fill="none" stroke="#6a95d8" stroke-width="1.2" marker-end="url(#arrow-link)"/>
|
||||
<line x1="720" y1="164" x2="720" y2="212" stroke="#f08a59" stroke-width="1" stroke-dasharray="5,4" marker-end="url(#arrow-accent)"/>
|
||||
|
||||
<!-- Path labels -->
|
||||
<rect x="224" y="176" width="64" height="12" rx="2" fill="#2d3142"/>
|
||||
<text x="256" y="185" fill="#6a95d8" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">HTTPS:443</text>
|
||||
|
||||
<rect x="428" y="174" width="64" height="12" rx="2" fill="#2d3142"/>
|
||||
<text x="460" y="183" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">HTTP:8080</text>
|
||||
|
||||
<rect x="588" y="180" width="64" height="12" rx="2" fill="#2d3142"/>
|
||||
<text x="620" y="189" fill="#6a95d8" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">TLS:5432</text>
|
||||
|
||||
<rect x="728" y="182" width="72" height="12" rx="2" fill="#2d3142"/>
|
||||
<text x="764" y="191" fill="#f08a59" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">WAL STREAM</text>
|
||||
|
||||
<!-- Node: CDN (edge-cache) -->
|
||||
<rect x="64" y="156" width="160" height="84" rx="6" fill="#2d3142"/>
|
||||
<rect x="64" y="156" width="160" height="84" rx="6" fill="rgba(245,245,245,0.03)" stroke="rgba(245,245,245,0.30)" stroke-width="1"/>
|
||||
<rect x="72" y="164" width="32" height="12" rx="2" fill="transparent" stroke="rgba(245,245,245,0.22)" stroke-width="0.8"/>
|
||||
<text x="88" y="173" fill="#8e98ac" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CDN</text>
|
||||
<text x="144" y="196" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Cloudflare</text>
|
||||
<rect x="72" y="208" width="144" height="24" rx="4" fill="rgba(245,245,245,0.05)" stroke="#bfc0c0" stroke-width="0.8"/>
|
||||
<text x="80" y="224" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">edge-cache</text>
|
||||
<text x="208" y="224" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">v1.2</text>
|
||||
|
||||
<!-- Node: ingress -->
|
||||
<rect x="304" y="80" width="232" height="84" rx="6" fill="#2d3142"/>
|
||||
<rect x="304" y="80" width="232" height="84" rx="6" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<rect x="312" y="88" width="32" height="12" rx="2" fill="transparent" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="328" y="97" fill="#f5f5f5" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">POD</text>
|
||||
<text x="420" y="120" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">ingress</text>
|
||||
<rect x="312" y="132" width="216" height="24" rx="4" fill="rgba(245,245,245,0.05)" stroke="#bfc0c0" stroke-width="0.8"/>
|
||||
<text x="320" y="148" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">nginx-ingress</text>
|
||||
<text x="520" y="148" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">v1.11</text>
|
||||
|
||||
<!-- Node: app (replica x3) -->
|
||||
<rect x="304" y="196" width="232" height="116" rx="6" fill="#2d3142"/>
|
||||
<rect x="304" y="196" width="232" height="116" rx="6" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<rect x="312" y="204" width="32" height="12" rx="2" fill="transparent" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="328" y="213" fill="#f5f5f5" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">POD</text>
|
||||
<rect x="500" y="204" width="28" height="12" rx="2" fill="transparent" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="514" y="213" fill="#f5f5f5" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">x3</text>
|
||||
<text x="420" y="236" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">app</text>
|
||||
<rect x="312" y="248" width="216" height="24" rx="4" fill="rgba(245,245,245,0.05)" stroke="#bfc0c0" stroke-width="0.8"/>
|
||||
<text x="320" y="264" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">checkout-api</text>
|
||||
<text x="520" y="264" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">v2.4.1</text>
|
||||
<rect x="312" y="280" width="216" height="24" rx="4" fill="rgba(245,245,245,0.05)" stroke="#bfc0c0" stroke-width="0.8"/>
|
||||
<text x="320" y="296" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">sidecar-otel</text>
|
||||
<text x="520" y="296" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">v0.9</text>
|
||||
|
||||
<!-- Node: rds-primary (focal) -->
|
||||
<rect x="624" y="80" width="192" height="84" rx="6" fill="#2d3142"/>
|
||||
<rect x="624" y="80" width="192" height="84" rx="6" fill="rgba(240,138,89,0.08)" stroke="#f08a59" stroke-width="1"/>
|
||||
<rect x="632" y="88" width="64" height="12" rx="2" fill="transparent" stroke="rgba(240,138,89,0.50)" stroke-width="0.8"/>
|
||||
<text x="664" y="97" fill="#f08a59" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">MANAGED</text>
|
||||
<text x="720" y="120" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">rds-primary</text>
|
||||
<rect x="632" y="132" width="176" height="24" rx="4" fill="rgba(245,245,245,0.05)" stroke="#bfc0c0" stroke-width="0.8"/>
|
||||
<text x="640" y="148" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">postgres</text>
|
||||
<text x="800" y="148" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">16.2</text>
|
||||
|
||||
<!-- Node: rds-standby -->
|
||||
<rect x="624" y="212" width="192" height="84" rx="6" fill="#2d3142"/>
|
||||
<rect x="624" y="212" width="192" height="84" rx="6" fill="rgba(245,245,245,0.05)" stroke="#bfc0c0" stroke-width="1"/>
|
||||
<rect x="632" y="220" width="64" height="12" rx="2" fill="transparent" stroke="rgba(191,192,192,0.50)" stroke-width="0.8"/>
|
||||
<text x="664" y="229" fill="#bfc0c0" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">MANAGED</text>
|
||||
<text x="720" y="252" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">rds-standby</text>
|
||||
<rect x="632" y="264" width="176" height="24" rx="4" fill="rgba(245,245,245,0.05)" stroke="#bfc0c0" stroke-width="0.8"/>
|
||||
<text x="640" y="280" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">postgres</text>
|
||||
<text x="800" y="280" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">16.2</text>
|
||||
|
||||
<!-- Legend strip -->
|
||||
<line x1="40" y1="340" x2="1000" y2="340" stroke="rgba(245,245,245,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="356" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="40" y="372" width="14" height="10" rx="2" fill="rgba(240,138,89,0.08)" stroke="#f08a59" stroke-width="1"/>
|
||||
<text x="60" y="380" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Focal / primary</text>
|
||||
|
||||
<rect x="180" y="372" width="14" height="10" rx="2" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<text x="200" y="380" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Infra node</text>
|
||||
|
||||
<rect x="320" y="372" width="14" height="10" rx="2" fill="rgba(245,245,245,0.05)" stroke="#bfc0c0" stroke-width="1"/>
|
||||
<text x="340" y="380" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Store / standby</text>
|
||||
|
||||
<rect x="460" y="372" width="14" height="10" rx="2" fill="rgba(245,245,245,0.03)" stroke="rgba(245,245,245,0.30)" stroke-width="1"/>
|
||||
<text x="480" y="380" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Managed / CDN</text>
|
||||
|
||||
<line x1="600" y1="376" x2="628" y2="376" stroke="#6a95d8" stroke-width="1.2" marker-end="url(#arrow-link)"/>
|
||||
<text x="636" y="380" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Cross-zone</text>
|
||||
|
||||
<line x1="740" y1="376" x2="768" y2="376" stroke="#f08a59" stroke-width="1" stroke-dasharray="5,4" marker-end="url(#arrow-accent)"/>
|
||||
<text x="776" y="380" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Replication</text>
|
||||
|
||||
<line x1="880" y1="376" x2="908" y2="376" stroke="#bfc0c0" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<text x="916" y="380" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">In-zone call</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,202 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Checkout service · Deployment</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #f5f5f5; --color-paper-2: #ececec;
|
||||
--color-ink: #2d3142; --color-muted: #4f5d75; --color-soft: #7a8399;
|
||||
--color-rule: rgba(45,49,66,0.12);
|
||||
--color-accent: #eb6c36; --color-accent-tint: rgba(235,108,54,0.08);
|
||||
--color-link: #2e5aa8;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.header { margin-bottom: 2.5rem; }
|
||||
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
|
||||
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
|
||||
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
|
||||
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
|
||||
svg { width: 100%; min-width: 880px; display: block; }
|
||||
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
|
||||
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
|
||||
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
|
||||
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
|
||||
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(45,49,66,0.08); }
|
||||
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
|
||||
.card-dot.ink { background: var(--color-ink); } .card-dot.muted { background: var(--color-muted); } .card-dot.coral { background: var(--color-accent); }
|
||||
.card h3 { font-size: 0.875rem; font-weight: 600; letter-spacing: -0.005em; }
|
||||
.card p, .card ul { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
|
||||
.card li { margin-bottom: 0.3rem; padding-left: 0.875rem; position: relative; }
|
||||
.card li::before { content: '—'; position: absolute; left: 0; color: rgba(45,49,66,0.25); font-size: 0.75rem; }
|
||||
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(45,49,66,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<p class="header-eyebrow">Deployment · Diagram Design</p>
|
||||
<h1>Checkout service, production</h1>
|
||||
<p class="subtitle">Edge caching absorbs the reads, three ingress-fronted API pods handle the writes, and a single Postgres primary streams WAL to a standby in the data zone.</p>
|
||||
</div>
|
||||
|
||||
<div class="diagram-container">
|
||||
<svg viewBox="0 0 1040 416" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="deployment-full-title deployment-full-desc">
|
||||
<title id="deployment-full-title">Checkout service, production</title>
|
||||
<desc id="deployment-full-desc">Deployment diagram placing the checkout service across an edge CDN zone, a production Kubernetes zone running ingress and API pods, and a data zone with a primary Postgres instance replicating to a standby.</desc>
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(45,49,66,0.10)"/>
|
||||
</pattern>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#4f5d75"/></marker>
|
||||
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#eb6c36"/></marker>
|
||||
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#2e5aa8"/></marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f5f5"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Zones (drawn first, before arrows and nodes) -->
|
||||
<rect x="40" y="40" width="208" height="280" rx="8" fill="rgba(45,49,66,0.02)" stroke="rgba(45,49,66,0.20)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<rect x="56" y="44" width="48" height="12" fill="#f5f5f5"/>
|
||||
<text x="80" y="53" fill="rgba(45,49,66,0.40)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">EDGE</text>
|
||||
|
||||
<rect x="280" y="40" width="280" height="280" rx="8" fill="rgba(45,49,66,0.02)" stroke="rgba(45,49,66,0.20)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<rect x="296" y="44" width="140" height="12" fill="#f5f5f5"/>
|
||||
<text x="366" y="53" fill="rgba(45,49,66,0.40)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">PROD / EU-WEST-1</text>
|
||||
|
||||
<rect x="600" y="40" width="240" height="280" rx="8" fill="rgba(45,49,66,0.02)" stroke="rgba(45,49,66,0.20)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<rect x="616" y="44" width="40" height="12" fill="#f5f5f5"/>
|
||||
<text x="636" y="53" fill="rgba(45,49,66,0.40)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">DATA</text>
|
||||
|
||||
<!-- Network paths (behind nodes) -->
|
||||
<path d="M 224,196 H 288 Q 296,196 296,188 V 128 Q 296,120 304,120" fill="none" stroke="#2e5aa8" stroke-width="1.2" marker-end="url(#arrow-link)"/>
|
||||
<line x1="420" y1="164" x2="420" y2="196" stroke="#4f5d75" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<path d="M 536,252 H 572 Q 580,252 580,244 V 128 Q 580,120 588,120 H 624" fill="none" stroke="#2e5aa8" stroke-width="1.2" marker-end="url(#arrow-link)"/>
|
||||
<line x1="720" y1="164" x2="720" y2="212" stroke="#eb6c36" stroke-width="1" stroke-dasharray="5,4" marker-end="url(#arrow-accent)"/>
|
||||
|
||||
<!-- Path labels -->
|
||||
<rect x="224" y="176" width="64" height="12" rx="2" fill="#f5f5f5"/>
|
||||
<text x="256" y="185" fill="#2e5aa8" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">HTTPS:443</text>
|
||||
|
||||
<rect x="428" y="174" width="64" height="12" rx="2" fill="#f5f5f5"/>
|
||||
<text x="460" y="183" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">HTTP:8080</text>
|
||||
|
||||
<rect x="588" y="180" width="64" height="12" rx="2" fill="#f5f5f5"/>
|
||||
<text x="620" y="189" fill="#2e5aa8" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">TLS:5432</text>
|
||||
|
||||
<rect x="728" y="182" width="72" height="12" rx="2" fill="#f5f5f5"/>
|
||||
<text x="764" y="191" fill="#eb6c36" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">WAL STREAM</text>
|
||||
|
||||
<!-- Node: CDN (edge-cache) -->
|
||||
<rect x="64" y="156" width="160" height="84" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="64" y="156" width="160" height="84" rx="6" fill="rgba(45,49,66,0.03)" stroke="rgba(45,49,66,0.30)" stroke-width="1"/>
|
||||
<rect x="72" y="164" width="32" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.22)" stroke-width="0.8"/>
|
||||
<text x="88" y="173" fill="#7a8399" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CDN</text>
|
||||
<text x="144" y="196" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Cloudflare</text>
|
||||
<rect x="72" y="208" width="144" height="24" rx="4" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<text x="80" y="224" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">edge-cache</text>
|
||||
<text x="208" y="224" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">v1.2</text>
|
||||
|
||||
<!-- Node: ingress -->
|
||||
<rect x="304" y="80" width="232" height="84" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="304" y="80" width="232" height="84" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="312" y="88" width="32" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="328" y="97" fill="#2d3142" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">POD</text>
|
||||
<text x="420" y="120" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">ingress</text>
|
||||
<rect x="312" y="132" width="216" height="24" rx="4" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<text x="320" y="148" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">nginx-ingress</text>
|
||||
<text x="520" y="148" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">v1.11</text>
|
||||
|
||||
<!-- Node: app (replica x3) -->
|
||||
<rect x="304" y="196" width="232" height="116" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="304" y="196" width="232" height="116" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="312" y="204" width="32" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="328" y="213" fill="#2d3142" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">POD</text>
|
||||
<rect x="500" y="204" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="514" y="213" fill="#2d3142" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">x3</text>
|
||||
<text x="420" y="236" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">app</text>
|
||||
<rect x="312" y="248" width="216" height="24" rx="4" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<text x="320" y="264" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">checkout-api</text>
|
||||
<text x="520" y="264" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">v2.4.1</text>
|
||||
<rect x="312" y="280" width="216" height="24" rx="4" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<text x="320" y="296" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">sidecar-otel</text>
|
||||
<text x="520" y="296" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">v0.9</text>
|
||||
|
||||
<!-- Node: rds-primary (focal) -->
|
||||
<rect x="624" y="80" width="192" height="84" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="624" y="80" width="192" height="84" rx="6" fill="rgba(235,108,54,0.08)" stroke="#eb6c36" stroke-width="1"/>
|
||||
<rect x="632" y="88" width="64" height="12" rx="2" fill="transparent" stroke="rgba(235,108,54,0.50)" stroke-width="0.8"/>
|
||||
<text x="664" y="97" fill="#eb6c36" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">MANAGED</text>
|
||||
<text x="720" y="120" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">rds-primary</text>
|
||||
<rect x="632" y="132" width="176" height="24" rx="4" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<text x="640" y="148" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">postgres</text>
|
||||
<text x="800" y="148" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">16.2</text>
|
||||
|
||||
<!-- Node: rds-standby -->
|
||||
<rect x="624" y="212" width="192" height="84" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="624" y="212" width="192" height="84" rx="6" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="1"/>
|
||||
<rect x="632" y="220" width="64" height="12" rx="2" fill="transparent" stroke="rgba(79,93,117,0.50)" stroke-width="0.8"/>
|
||||
<text x="664" y="229" fill="#4f5d75" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">MANAGED</text>
|
||||
<text x="720" y="252" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">rds-standby</text>
|
||||
<rect x="632" y="264" width="176" height="24" rx="4" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<text x="640" y="280" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">postgres</text>
|
||||
<text x="800" y="280" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">16.2</text>
|
||||
|
||||
<!-- Legend strip -->
|
||||
<line x1="40" y1="340" x2="1000" y2="340" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="356" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="40" y="372" width="14" height="10" rx="2" fill="rgba(235,108,54,0.08)" stroke="#eb6c36" stroke-width="1"/>
|
||||
<text x="60" y="380" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Focal / primary</text>
|
||||
|
||||
<rect x="180" y="372" width="14" height="10" rx="2" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="200" y="380" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Infra node</text>
|
||||
|
||||
<rect x="320" y="372" width="14" height="10" rx="2" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="340" y="380" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Store / standby</text>
|
||||
|
||||
<rect x="460" y="372" width="14" height="10" rx="2" fill="rgba(45,49,66,0.03)" stroke="rgba(45,49,66,0.30)" stroke-width="1"/>
|
||||
<text x="480" y="380" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Managed / CDN</text>
|
||||
|
||||
<line x1="600" y1="376" x2="628" y2="376" stroke="#2e5aa8" stroke-width="1.2" marker-end="url(#arrow-link)"/>
|
||||
<text x="636" y="380" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Cross-zone</text>
|
||||
|
||||
<line x1="740" y1="376" x2="768" y2="376" stroke="#eb6c36" stroke-width="1" stroke-dasharray="5,4" marker-end="url(#arrow-accent)"/>
|
||||
<text x="776" y="380" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Replication</text>
|
||||
|
||||
<line x1="880" y1="376" x2="908" y2="376" stroke="#4f5d75" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<text x="916" y="380" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">In-zone call</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<p class="eyebrow">THE HEADLINE</p>
|
||||
<div class="card-header"><span class="card-dot coral"></span><h3>One primary, one point of failure</h3></div>
|
||||
<p>Every write lands on `rds-primary`. The standby streams WAL continuously but isn't hot — a primary failure is the one scenario this topology hasn't automated away yet.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot ink"></span><h3>Three replicas behind one ingress</h3></div>
|
||||
<ul><li>`checkout-api` runs 3 pod replicas</li><li>`sidecar-otel` ships traces from the same pod</li><li>nginx-ingress fans requests across all three</li></ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot muted"></span><h3>Edge absorbs static reads</h3></div>
|
||||
<p>Cloudflare's edge cache serves cached responses directly; only cache misses and writes reach the ingress zone.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span>checkout service · deployment</span>
|
||||
<span>example · diagram design</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,190 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Checkout service · Deployment</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #f5f5f5;
|
||||
--color-ink: #2d3142;
|
||||
--color-muted: #4f5d75;
|
||||
--color-accent: #eb6c36;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 880px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Deployment · Diagram Design</p>
|
||||
<h1>Checkout service, production</h1>
|
||||
|
||||
<svg viewBox="0 0 1040 416" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="deployment-title deployment-desc">
|
||||
<title id="deployment-title">Checkout service, production</title>
|
||||
<desc id="deployment-desc">Deployment diagram placing the checkout service across an edge CDN zone, a production Kubernetes zone running ingress and API pods, and a data zone with a primary Postgres instance replicating to a standby.</desc>
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(45,49,66,0.10)"/>
|
||||
</pattern>
|
||||
<marker id="arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#4f5d75"/></marker>
|
||||
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#eb6c36"/></marker>
|
||||
<marker id="arrow-link" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto"><polygon points="0 0, 8 3, 0 6" fill="#2e5aa8"/></marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f5f5"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Zones (drawn first, before arrows and nodes) -->
|
||||
<rect x="40" y="40" width="208" height="280" rx="8" fill="rgba(45,49,66,0.02)" stroke="rgba(45,49,66,0.20)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<rect x="56" y="44" width="48" height="12" fill="#f5f5f5"/>
|
||||
<text x="80" y="53" fill="rgba(45,49,66,0.40)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">EDGE</text>
|
||||
|
||||
<rect x="280" y="40" width="280" height="280" rx="8" fill="rgba(45,49,66,0.02)" stroke="rgba(45,49,66,0.20)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<rect x="296" y="44" width="140" height="12" fill="#f5f5f5"/>
|
||||
<text x="366" y="53" fill="rgba(45,49,66,0.40)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">PROD / EU-WEST-1</text>
|
||||
|
||||
<rect x="600" y="40" width="240" height="280" rx="8" fill="rgba(45,49,66,0.02)" stroke="rgba(45,49,66,0.20)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<rect x="616" y="44" width="40" height="12" fill="#f5f5f5"/>
|
||||
<text x="636" y="53" fill="rgba(45,49,66,0.40)" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">DATA</text>
|
||||
|
||||
<!-- Network paths (behind nodes) -->
|
||||
<path d="M 224,196 H 288 Q 296,196 296,188 V 128 Q 296,120 304,120" fill="none" stroke="#2e5aa8" stroke-width="1.2" marker-end="url(#arrow-link)"/>
|
||||
<line x1="420" y1="164" x2="420" y2="196" stroke="#4f5d75" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<path d="M 536,252 H 572 Q 580,252 580,244 V 128 Q 580,120 588,120 H 624" fill="none" stroke="#2e5aa8" stroke-width="1.2" marker-end="url(#arrow-link)"/>
|
||||
<line x1="720" y1="164" x2="720" y2="212" stroke="#eb6c36" stroke-width="1" stroke-dasharray="5,4" marker-end="url(#arrow-accent)"/>
|
||||
|
||||
<!-- Path labels -->
|
||||
<rect x="224" y="176" width="64" height="12" rx="2" fill="#f5f5f5"/>
|
||||
<text x="256" y="185" fill="#2e5aa8" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">HTTPS:443</text>
|
||||
|
||||
<rect x="428" y="174" width="64" height="12" rx="2" fill="#f5f5f5"/>
|
||||
<text x="460" y="183" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">HTTP:8080</text>
|
||||
|
||||
<rect x="588" y="180" width="64" height="12" rx="2" fill="#f5f5f5"/>
|
||||
<text x="620" y="189" fill="#2e5aa8" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">TLS:5432</text>
|
||||
|
||||
<rect x="728" y="182" width="72" height="12" rx="2" fill="#f5f5f5"/>
|
||||
<text x="764" y="191" fill="#eb6c36" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">WAL STREAM</text>
|
||||
|
||||
<!-- Node: CDN (edge-cache) -->
|
||||
<rect x="64" y="156" width="160" height="84" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="64" y="156" width="160" height="84" rx="6" fill="rgba(45,49,66,0.03)" stroke="rgba(45,49,66,0.30)" stroke-width="1"/>
|
||||
<rect x="72" y="164" width="32" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.22)" stroke-width="0.8"/>
|
||||
<text x="88" y="173" fill="#7a8399" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">CDN</text>
|
||||
<text x="144" y="196" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Cloudflare</text>
|
||||
<rect x="72" y="208" width="144" height="24" rx="4" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<text x="80" y="224" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">edge-cache</text>
|
||||
<text x="208" y="224" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">v1.2</text>
|
||||
|
||||
<!-- Node: ingress -->
|
||||
<rect x="304" y="80" width="232" height="84" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="304" y="80" width="232" height="84" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="312" y="88" width="32" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="328" y="97" fill="#2d3142" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">POD</text>
|
||||
<text x="420" y="120" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">ingress</text>
|
||||
<rect x="312" y="132" width="216" height="24" rx="4" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<text x="320" y="148" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">nginx-ingress</text>
|
||||
<text x="520" y="148" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">v1.11</text>
|
||||
|
||||
<!-- Node: app (replica x3) -->
|
||||
<rect x="304" y="196" width="232" height="116" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="304" y="196" width="232" height="116" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<rect x="312" y="204" width="32" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="328" y="213" fill="#2d3142" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">POD</text>
|
||||
<rect x="500" y="204" width="28" height="12" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="514" y="213" fill="#2d3142" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">x3</text>
|
||||
<text x="420" y="236" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">app</text>
|
||||
<rect x="312" y="248" width="216" height="24" rx="4" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<text x="320" y="264" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">checkout-api</text>
|
||||
<text x="520" y="264" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">v2.4.1</text>
|
||||
<rect x="312" y="280" width="216" height="24" rx="4" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<text x="320" y="296" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">sidecar-otel</text>
|
||||
<text x="520" y="296" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">v0.9</text>
|
||||
|
||||
<!-- Node: rds-primary (focal) -->
|
||||
<rect x="624" y="80" width="192" height="84" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="624" y="80" width="192" height="84" rx="6" fill="rgba(235,108,54,0.08)" stroke="#eb6c36" stroke-width="1"/>
|
||||
<rect x="632" y="88" width="64" height="12" rx="2" fill="transparent" stroke="rgba(235,108,54,0.50)" stroke-width="0.8"/>
|
||||
<text x="664" y="97" fill="#eb6c36" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">MANAGED</text>
|
||||
<text x="720" y="120" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">rds-primary</text>
|
||||
<rect x="632" y="132" width="176" height="24" rx="4" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<text x="640" y="148" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">postgres</text>
|
||||
<text x="800" y="148" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">16.2</text>
|
||||
|
||||
<!-- Node: rds-standby -->
|
||||
<rect x="624" y="212" width="192" height="84" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="624" y="212" width="192" height="84" rx="6" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="1"/>
|
||||
<rect x="632" y="220" width="64" height="12" rx="2" fill="transparent" stroke="rgba(79,93,117,0.50)" stroke-width="0.8"/>
|
||||
<text x="664" y="229" fill="#4f5d75" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">MANAGED</text>
|
||||
<text x="720" y="252" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">rds-standby</text>
|
||||
<rect x="632" y="264" width="176" height="24" rx="4" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<text x="640" y="280" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">postgres</text>
|
||||
<text x="800" y="280" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">16.2</text>
|
||||
|
||||
<!-- Legend strip -->
|
||||
<line x1="40" y1="340" x2="1000" y2="340" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="356" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="40" y="372" width="14" height="10" rx="2" fill="rgba(235,108,54,0.08)" stroke="#eb6c36" stroke-width="1"/>
|
||||
<text x="60" y="380" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Focal / primary</text>
|
||||
|
||||
<rect x="180" y="372" width="14" height="10" rx="2" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="200" y="380" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Infra node</text>
|
||||
|
||||
<rect x="320" y="372" width="14" height="10" rx="2" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="340" y="380" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Store / standby</text>
|
||||
|
||||
<rect x="460" y="372" width="14" height="10" rx="2" fill="rgba(45,49,66,0.03)" stroke="rgba(45,49,66,0.30)" stroke-width="1"/>
|
||||
<text x="480" y="380" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Managed / CDN</text>
|
||||
|
||||
<line x1="600" y1="376" x2="628" y2="376" stroke="#2e5aa8" stroke-width="1.2" marker-end="url(#arrow-link)"/>
|
||||
<text x="636" y="380" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Cross-zone</text>
|
||||
|
||||
<line x1="740" y1="376" x2="768" y2="376" stroke="#eb6c36" stroke-width="1" stroke-dasharray="5,4" marker-end="url(#arrow-accent)"/>
|
||||
<text x="776" y="380" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Replication</text>
|
||||
|
||||
<line x1="880" y1="376" x2="908" y2="376" stroke="#4f5d75" stroke-width="1.2" marker-end="url(#arrow)"/>
|
||||
<text x="916" y="380" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">In-zone call</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,152 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Checkout p99 latency · Root-cause fishbone</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #2d3142;
|
||||
--color-ink: #f5f5f5;
|
||||
--color-muted: #bfc0c0;
|
||||
--color-accent: #f08a59;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1320px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 1080px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Fishbone · Diagram Design</p>
|
||||
<h1>Checkout p99 latency · Root-cause fishbone</h1>
|
||||
|
||||
<svg viewBox="-40 0 1480 600" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="fishbone-dark-title fishbone-dark-desc">
|
||||
<title id="fishbone-dark-title">Checkout p99 latency · Root-cause fishbone</title>
|
||||
<desc id="fishbone-dark-desc">Fishbone diagram tracing a checkout latency incident to five candidate cause categories, with a missing database index under Data confirmed as the root cause.</desc>
|
||||
<defs>
|
||||
<marker id="fishbone-arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#f5f5f5"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect x="-40" y="0" width="1480" height="600" fill="#2d3142"/>
|
||||
|
||||
<!-- Spine -->
|
||||
<line x1="120" y1="320" x2="1200" y2="320" stroke="#f5f5f5" stroke-width="1.2" marker-end="url(#fishbone-arrow)"/>
|
||||
|
||||
<!-- Bones -->
|
||||
<line x1="880" y1="320" x2="784" y2="152" stroke="#f5f5f5" stroke-width="1.2"/>
|
||||
<line x1="720" y1="320" x2="624" y2="488" stroke="#f08a59" stroke-width="1.2"/>
|
||||
<line x1="560" y1="320" x2="464" y2="152" stroke="#f5f5f5" stroke-width="1.2"/>
|
||||
<line x1="400" y1="320" x2="304" y2="488" stroke="#f5f5f5" stroke-width="1.2"/>
|
||||
<line x1="240" y1="320" x2="144" y2="152" stroke="#f5f5f5" stroke-width="1.2"/>
|
||||
|
||||
<!-- Sub-cause ticks + labels · Deploy -->
|
||||
<line x1="848" y1="264" x2="816" y2="264" stroke="#bfc0c0" stroke-width="1"/>
|
||||
<text x="812" y="260" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">canary skipped</text>
|
||||
<line x1="816" y1="208" x2="784" y2="208" stroke="#bfc0c0" stroke-width="1"/>
|
||||
<text x="780" y="204" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">config drift</text>
|
||||
|
||||
<!-- Sub-cause ticks + labels · Data (root cause) -->
|
||||
<line x1="688" y1="376" x2="656" y2="376" stroke="#bfc0c0" stroke-width="1"/>
|
||||
<text x="652" y="388" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">index missing on orders.created_at</text>
|
||||
<line x1="656" y1="432" x2="624" y2="432" stroke="#bfc0c0" stroke-width="1"/>
|
||||
<text x="620" y="444" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">table 3x forecast</text>
|
||||
|
||||
<!-- Sub-cause ticks + labels · Runtime -->
|
||||
<line x1="528" y1="264" x2="496" y2="264" stroke="#bfc0c0" stroke-width="1"/>
|
||||
<text x="492" y="260" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">connection pool cap 20</text>
|
||||
<line x1="496" y1="208" x2="464" y2="208" stroke="#bfc0c0" stroke-width="1"/>
|
||||
<text x="460" y="204" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">GC pauses</text>
|
||||
|
||||
<!-- Sub-cause ticks + labels · Observability -->
|
||||
<line x1="368" y1="376" x2="336" y2="376" stroke="#bfc0c0" stroke-width="1"/>
|
||||
<text x="332" y="388" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">no p99 alert</text>
|
||||
<line x1="336" y1="432" x2="304" y2="432" stroke="#bfc0c0" stroke-width="1"/>
|
||||
<text x="300" y="444" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">dashboard sampled at 5m</text>
|
||||
|
||||
<!-- Sub-cause tick + label · People -->
|
||||
<line x1="192" y1="236" x2="160" y2="236" stroke="#bfc0c0" stroke-width="1"/>
|
||||
<text x="156" y="232" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">on-call handover mid-incident</text>
|
||||
|
||||
<!-- Category tag boxes -->
|
||||
<rect x="724" y="116" width="120" height="28" rx="4" fill="#2d3142" stroke="#f5f5f5" stroke-width="0.8"/>
|
||||
<text x="784" y="134" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">DEPLOY</text>
|
||||
|
||||
<rect x="564" y="496" width="120" height="28" rx="4" fill="rgba(240,138,89,0.10)" stroke="#f08a59" stroke-width="0.8"/>
|
||||
<text x="624" y="514" fill="#f08a59" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">DATA</text>
|
||||
|
||||
<rect x="404" y="116" width="120" height="28" rx="4" fill="#2d3142" stroke="#f5f5f5" stroke-width="0.8"/>
|
||||
<text x="464" y="134" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">RUNTIME</text>
|
||||
|
||||
<rect x="244" y="496" width="120" height="28" rx="4" fill="#2d3142" stroke="#f5f5f5" stroke-width="0.8"/>
|
||||
<text x="304" y="514" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">OBSERVABILITY</text>
|
||||
|
||||
<rect x="84" y="116" width="120" height="28" rx="4" fill="#2d3142" stroke="#f5f5f5" stroke-width="0.8"/>
|
||||
<text x="144" y="134" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">PEOPLE</text>
|
||||
|
||||
<!-- Effect box -->
|
||||
<rect x="1200" y="272" width="200" height="96" rx="6" fill="#2d3142"/>
|
||||
<rect x="1200" y="272" width="200" height="96" rx="6" fill="rgba(240,138,89,0.10)" stroke="#f08a59" stroke-width="1"/>
|
||||
<rect x="1208" y="280" width="48" height="12" rx="2" fill="transparent" stroke="rgba(240,138,89,0.40)" stroke-width="0.8"/>
|
||||
<text x="1232" y="289" fill="#f08a59" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EFFECT</text>
|
||||
<text x="1300" y="324" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Checkout p99 latency</text>
|
||||
<text x="1300" y="340" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">4x for 90 minutes</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="560" x2="1400" y2="560" stroke="rgba(245,245,245,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="576" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<line x1="40" y1="588" x2="56" y2="588" stroke="#f5f5f5" stroke-width="1.2"/>
|
||||
<text x="64" y="592" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Category (bone)</text>
|
||||
|
||||
<line x1="260" y1="588" x2="276" y2="588" stroke="#f08a59" stroke-width="1.2"/>
|
||||
<text x="284" y="592" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Confirmed root cause</text>
|
||||
|
||||
<line x1="480" y1="588" x2="496" y2="588" stroke="#bfc0c0" stroke-width="1"/>
|
||||
<text x="504" y="592" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Sub-cause</text>
|
||||
|
||||
<rect x="700" y="584" width="16" height="8" fill="rgba(240,138,89,0.10)" stroke="#f08a59" stroke-width="1"/>
|
||||
<text x="724" y="592" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Observed effect</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,146 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Checkout p99 latency · Root-cause fishbone</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root { --color-paper:#f5f5f5; --color-paper-2:#ececec; --color-ink:#2d3142; --color-muted:#4f5d75; --color-soft:#7a8399; --color-rule:rgba(45,49,66,0.12); --color-accent:#eb6c36; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
|
||||
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
|
||||
.container { max-width: 1320px; margin: 0 auto; }
|
||||
.header { margin-bottom: 2.5rem; }
|
||||
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
|
||||
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
|
||||
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 62ch; }
|
||||
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
|
||||
svg { width: 100%; min-width: 1080px; display: block; }
|
||||
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
|
||||
@media (max-width: 900px) { .cards { grid-template-columns: 1fr 1fr; } }
|
||||
@media (max-width: 600px) { .cards { grid-template-columns: 1fr; } }
|
||||
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
|
||||
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
|
||||
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(45,49,66,0.08); }
|
||||
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
|
||||
.card-dot.coral { background: var(--color-accent); }
|
||||
.card-dot.ink { background: var(--color-ink); }
|
||||
.card-dot.soft { background: var(--color-soft); }
|
||||
.card h3 { font-size: 0.875rem; font-weight: 600; }
|
||||
.card p { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; }
|
||||
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(45,49,66,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<p class="header-eyebrow">Fishbone · Diagram Design</p>
|
||||
<h1>Checkout p99 latency · Root-cause fishbone</h1>
|
||||
<p class="subtitle">Five candidate cause categories investigated after a 90-minute checkout latency incident. The confirmed root cause carries the diagram's only accent — everything else stays in the neutral ink/muted palette so the reader's eye lands on the answer, not the investigation.</p>
|
||||
</div>
|
||||
|
||||
<div class="diagram-container">
|
||||
<svg viewBox="-40 0 1480 600" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="fishbone-full-title fishbone-full-desc">
|
||||
<title id="fishbone-full-title">Checkout p99 latency · Root-cause fishbone</title>
|
||||
<desc id="fishbone-full-desc">Fishbone diagram tracing a checkout latency incident to five candidate cause categories, with a missing database index under Data confirmed as the root cause.</desc>
|
||||
<defs>
|
||||
<marker id="fishbone-arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#2d3142"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect x="-40" y="0" width="1480" height="600" fill="#f5f5f5"/>
|
||||
|
||||
<line x1="120" y1="320" x2="1200" y2="320" stroke="#2d3142" stroke-width="1.2" marker-end="url(#fishbone-arrow)"/>
|
||||
|
||||
<line x1="880" y1="320" x2="784" y2="152" stroke="#2d3142" stroke-width="1.2"/>
|
||||
<line x1="720" y1="320" x2="624" y2="488" stroke="#eb6c36" stroke-width="1.2"/>
|
||||
<line x1="560" y1="320" x2="464" y2="152" stroke="#2d3142" stroke-width="1.2"/>
|
||||
<line x1="400" y1="320" x2="304" y2="488" stroke="#2d3142" stroke-width="1.2"/>
|
||||
<line x1="240" y1="320" x2="144" y2="152" stroke="#2d3142" stroke-width="1.2"/>
|
||||
|
||||
<line x1="848" y1="264" x2="816" y2="264" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="812" y="260" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">canary skipped</text>
|
||||
<line x1="816" y1="208" x2="784" y2="208" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="780" y="204" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">config drift</text>
|
||||
|
||||
<line x1="688" y1="376" x2="656" y2="376" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="652" y="388" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">index missing on orders.created_at</text>
|
||||
<line x1="656" y1="432" x2="624" y2="432" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="620" y="444" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">table 3x forecast</text>
|
||||
|
||||
<line x1="528" y1="264" x2="496" y2="264" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="492" y="260" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">connection pool cap 20</text>
|
||||
<line x1="496" y1="208" x2="464" y2="208" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="460" y="204" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">GC pauses</text>
|
||||
|
||||
<line x1="368" y1="376" x2="336" y2="376" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="332" y="388" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">no p99 alert</text>
|
||||
<line x1="336" y1="432" x2="304" y2="432" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="300" y="444" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">dashboard sampled at 5m</text>
|
||||
|
||||
<line x1="192" y1="236" x2="160" y2="236" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="156" y="232" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">on-call handover mid-incident</text>
|
||||
|
||||
<rect x="724" y="116" width="120" height="28" rx="4" fill="#f5f5f5" stroke="#2d3142" stroke-width="0.8"/>
|
||||
<text x="784" y="134" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">DEPLOY</text>
|
||||
|
||||
<rect x="564" y="496" width="120" height="28" rx="4" fill="rgba(235,108,54,0.08)" stroke="#eb6c36" stroke-width="0.8"/>
|
||||
<text x="624" y="514" fill="#eb6c36" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">DATA</text>
|
||||
|
||||
<rect x="404" y="116" width="120" height="28" rx="4" fill="#f5f5f5" stroke="#2d3142" stroke-width="0.8"/>
|
||||
<text x="464" y="134" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">RUNTIME</text>
|
||||
|
||||
<rect x="244" y="496" width="120" height="28" rx="4" fill="#f5f5f5" stroke="#2d3142" stroke-width="0.8"/>
|
||||
<text x="304" y="514" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">OBSERVABILITY</text>
|
||||
|
||||
<rect x="84" y="116" width="120" height="28" rx="4" fill="#f5f5f5" stroke="#2d3142" stroke-width="0.8"/>
|
||||
<text x="144" y="134" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">PEOPLE</text>
|
||||
|
||||
<rect x="1200" y="272" width="200" height="96" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="1200" y="272" width="200" height="96" rx="6" fill="rgba(235,108,54,0.08)" stroke="#eb6c36" stroke-width="1"/>
|
||||
<rect x="1208" y="280" width="48" height="12" rx="2" fill="transparent" stroke="rgba(235,108,54,0.40)" stroke-width="0.8"/>
|
||||
<text x="1232" y="289" fill="#eb6c36" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EFFECT</text>
|
||||
<text x="1300" y="324" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Checkout p99 latency</text>
|
||||
<text x="1300" y="340" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">4x for 90 minutes</text>
|
||||
|
||||
<line x1="40" y1="560" x2="1400" y2="560" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="576" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<line x1="40" y1="588" x2="56" y2="588" stroke="#2d3142" stroke-width="1.2"/>
|
||||
<text x="64" y="592" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Category (bone)</text>
|
||||
|
||||
<line x1="260" y1="588" x2="276" y2="588" stroke="#eb6c36" stroke-width="1.2"/>
|
||||
<text x="284" y="592" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Confirmed root cause</text>
|
||||
|
||||
<line x1="480" y1="588" x2="496" y2="588" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="504" y="592" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Sub-cause</text>
|
||||
|
||||
<rect x="700" y="584" width="16" height="8" fill="rgba(235,108,54,0.08)" stroke="#eb6c36" stroke-width="1"/>
|
||||
<text x="724" y="592" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Observed effect</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<p class="eyebrow">ROOT CAUSE</p>
|
||||
<div class="card-header"><span class="card-dot coral"></span><h3>Data — missing index</h3></div>
|
||||
<p>No index on <code>orders.created_at</code>; the query planner fell back to a sequential scan once the table crossed 3x its forecast size. The single confirmed cause — every other category was ruled out.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot ink"></span><h3>Contributing categories</h3></div>
|
||||
<p>Deploy (canary skipped, config drift), Runtime (pool cap 20, GC pauses), and Observability (no p99 alert, 5-minute dashboard sampling) all slowed detection but didn't cause the regression themselves.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot soft"></span><h3>People</h3></div>
|
||||
<p>A mid-incident on-call handover cost roughly ten minutes of context — worth fixing, not the root cause.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span>checkout p99 latency · root-cause fishbone</span>
|
||||
<span>example · diagram design</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,152 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Checkout p99 latency · Root-cause fishbone</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #f5f5f5;
|
||||
--color-ink: #2d3142;
|
||||
--color-muted: #4f5d75;
|
||||
--color-accent: #eb6c36;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1320px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 1080px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Fishbone · Diagram Design</p>
|
||||
<h1>Checkout p99 latency · Root-cause fishbone</h1>
|
||||
|
||||
<svg viewBox="-40 0 1480 600" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="fishbone-title fishbone-desc">
|
||||
<title id="fishbone-title">Checkout p99 latency · Root-cause fishbone</title>
|
||||
<desc id="fishbone-desc">Fishbone diagram tracing a checkout latency incident to five candidate cause categories, with a missing database index under Data confirmed as the root cause.</desc>
|
||||
<defs>
|
||||
<marker id="fishbone-arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#2d3142"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect x="-40" y="0" width="1480" height="600" fill="#f5f5f5"/>
|
||||
|
||||
<!-- Spine -->
|
||||
<line x1="120" y1="320" x2="1200" y2="320" stroke="#2d3142" stroke-width="1.2" marker-end="url(#fishbone-arrow)"/>
|
||||
|
||||
<!-- Bones -->
|
||||
<line x1="880" y1="320" x2="784" y2="152" stroke="#2d3142" stroke-width="1.2"/>
|
||||
<line x1="720" y1="320" x2="624" y2="488" stroke="#eb6c36" stroke-width="1.2"/>
|
||||
<line x1="560" y1="320" x2="464" y2="152" stroke="#2d3142" stroke-width="1.2"/>
|
||||
<line x1="400" y1="320" x2="304" y2="488" stroke="#2d3142" stroke-width="1.2"/>
|
||||
<line x1="240" y1="320" x2="144" y2="152" stroke="#2d3142" stroke-width="1.2"/>
|
||||
|
||||
<!-- Sub-cause ticks + labels · Deploy -->
|
||||
<line x1="848" y1="264" x2="816" y2="264" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="812" y="260" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">canary skipped</text>
|
||||
<line x1="816" y1="208" x2="784" y2="208" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="780" y="204" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">config drift</text>
|
||||
|
||||
<!-- Sub-cause ticks + labels · Data (root cause) -->
|
||||
<line x1="688" y1="376" x2="656" y2="376" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="652" y="388" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">index missing on orders.created_at</text>
|
||||
<line x1="656" y1="432" x2="624" y2="432" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="620" y="444" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">table 3x forecast</text>
|
||||
|
||||
<!-- Sub-cause ticks + labels · Runtime -->
|
||||
<line x1="528" y1="264" x2="496" y2="264" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="492" y="260" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">connection pool cap 20</text>
|
||||
<line x1="496" y1="208" x2="464" y2="208" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="460" y="204" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">GC pauses</text>
|
||||
|
||||
<!-- Sub-cause ticks + labels · Observability -->
|
||||
<line x1="368" y1="376" x2="336" y2="376" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="332" y="388" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">no p99 alert</text>
|
||||
<line x1="336" y1="432" x2="304" y2="432" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="300" y="444" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">dashboard sampled at 5m</text>
|
||||
|
||||
<!-- Sub-cause tick + label · People -->
|
||||
<line x1="192" y1="236" x2="160" y2="236" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="156" y="232" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">on-call handover mid-incident</text>
|
||||
|
||||
<!-- Category tag boxes -->
|
||||
<rect x="724" y="116" width="120" height="28" rx="4" fill="#f5f5f5" stroke="#2d3142" stroke-width="0.8"/>
|
||||
<text x="784" y="134" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">DEPLOY</text>
|
||||
|
||||
<rect x="564" y="496" width="120" height="28" rx="4" fill="rgba(235,108,54,0.08)" stroke="#eb6c36" stroke-width="0.8"/>
|
||||
<text x="624" y="514" fill="#eb6c36" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">DATA</text>
|
||||
|
||||
<rect x="404" y="116" width="120" height="28" rx="4" fill="#f5f5f5" stroke="#2d3142" stroke-width="0.8"/>
|
||||
<text x="464" y="134" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">RUNTIME</text>
|
||||
|
||||
<rect x="244" y="496" width="120" height="28" rx="4" fill="#f5f5f5" stroke="#2d3142" stroke-width="0.8"/>
|
||||
<text x="304" y="514" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">OBSERVABILITY</text>
|
||||
|
||||
<rect x="84" y="116" width="120" height="28" rx="4" fill="#f5f5f5" stroke="#2d3142" stroke-width="0.8"/>
|
||||
<text x="144" y="134" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">PEOPLE</text>
|
||||
|
||||
<!-- Effect box -->
|
||||
<rect x="1200" y="272" width="200" height="96" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="1200" y="272" width="200" height="96" rx="6" fill="rgba(235,108,54,0.08)" stroke="#eb6c36" stroke-width="1"/>
|
||||
<rect x="1208" y="280" width="48" height="12" rx="2" fill="transparent" stroke="rgba(235,108,54,0.40)" stroke-width="0.8"/>
|
||||
<text x="1232" y="289" fill="#eb6c36" font-size="7" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">EFFECT</text>
|
||||
<text x="1300" y="324" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Checkout p99 latency</text>
|
||||
<text x="1300" y="340" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">4x for 90 minutes</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="560" x2="1400" y2="560" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="576" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<line x1="40" y1="588" x2="56" y2="588" stroke="#2d3142" stroke-width="1.2"/>
|
||||
<text x="64" y="592" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Category (bone)</text>
|
||||
|
||||
<line x1="260" y1="588" x2="276" y2="588" stroke="#eb6c36" stroke-width="1.2"/>
|
||||
<text x="284" y="592" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Confirmed root cause</text>
|
||||
|
||||
<line x1="480" y1="588" x2="496" y2="588" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="504" y="592" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Sub-cause</text>
|
||||
|
||||
<rect x="700" y="584" width="16" height="8" fill="rgba(235,108,54,0.08)" stroke="#eb6c36" stroke-width="1"/>
|
||||
<text x="724" y="592" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Observed effect</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,148 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Trial to paid: the first week · User journey map</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #2d3142;
|
||||
--color-ink: #f5f5f5;
|
||||
--color-muted: #bfc0c0;
|
||||
--color-accent: #f08a59;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1280px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 960px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">User journey · Diagram Design</p>
|
||||
<h1>Trial to paid: the first week</h1>
|
||||
|
||||
<svg viewBox="0 0 1200 520" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="journey-dark-title journey-dark-desc">
|
||||
<title id="journey-dark-title">Trial to paid: the first week</title>
|
||||
<desc id="journey-dark-desc">User journey map of a new trial user across five stages, showing actions, touchpoints, and a sentiment curve that dips at the usage-limit stage before recovering at upgrade.</desc>
|
||||
|
||||
<rect width="100%" height="100%" fill="#2d3142"/>
|
||||
|
||||
<!-- Stage headers -->
|
||||
<text x="164" y="40" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">STAGE 1</text>
|
||||
<text x="164" y="60" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Sign up</text>
|
||||
|
||||
<text x="388" y="40" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">STAGE 2</text>
|
||||
<text x="388" y="60" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">First run</text>
|
||||
|
||||
<text x="612" y="40" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">STAGE 3</text>
|
||||
<text x="612" y="60" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Invite team</text>
|
||||
|
||||
<text x="836" y="40" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">STAGE 4</text>
|
||||
<text x="836" y="60" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Hit the limit</text>
|
||||
|
||||
<text x="1060" y="40" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">STAGE 5</text>
|
||||
<text x="1060" y="60" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Upgrade</text>
|
||||
|
||||
<!-- Sentiment band: reference hairlines -->
|
||||
<line x1="64" y1="100" x2="1160" y2="100" stroke="rgba(245,245,245,0.10)" stroke-width="0.8"/>
|
||||
<line x1="64" y1="180" x2="1160" y2="180" stroke="rgba(245,245,245,0.10)" stroke-width="0.8"/>
|
||||
<line x1="64" y1="260" x2="1160" y2="260" stroke="rgba(245,245,245,0.10)" stroke-width="0.8"/>
|
||||
<text x="56" y="104" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.10em">HIGH</text>
|
||||
<text x="56" y="184" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.10em">NEUTRAL</text>
|
||||
<text x="56" y="264" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.10em">LOW</text>
|
||||
|
||||
<!-- Sentiment curve (data curve — not a connector, §6 rule 1 exempt) -->
|
||||
<polyline points="164,100 388,180 612,100" fill="none" stroke="#bfc0c0" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<polyline points="612,100 836,260" fill="none" stroke="#f08a59" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<polyline points="836,260 1060,180" fill="none" stroke="#bfc0c0" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
|
||||
<circle cx="164" cy="100" r="5" fill="#bfc0c0"/>
|
||||
<circle cx="388" cy="180" r="5" fill="#bfc0c0"/>
|
||||
<circle cx="612" cy="100" r="5" fill="#bfc0c0"/>
|
||||
<circle cx="836" cy="260" r="5" fill="#f08a59"/>
|
||||
<circle cx="1060" cy="180" r="5" fill="#bfc0c0"/>
|
||||
|
||||
<!-- Row separators (full plot width) -->
|
||||
<line x1="64" y1="280" x2="1160" y2="280" stroke="rgba(245,245,245,0.10)" stroke-width="0.8"/>
|
||||
<line x1="64" y1="376" x2="1160" y2="376" stroke="rgba(245,245,245,0.10)" stroke-width="0.8"/>
|
||||
|
||||
<!-- Row labels -->
|
||||
<text x="56" y="328" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.14em">ACTIONS</text>
|
||||
<text x="72" y="408" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.14em">TOUCHPOINTS</text>
|
||||
|
||||
<!-- Actions -->
|
||||
<text x="164" y="300" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif" text-anchor="middle">Create workspace</text>
|
||||
<text x="388" y="300" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif" text-anchor="middle">Import first dataset</text>
|
||||
<text x="612" y="300" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif" text-anchor="middle">Add 3 teammates</text>
|
||||
<text x="836" y="300" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif" text-anchor="middle">Export blocked mid-report</text>
|
||||
<text x="1060" y="300" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif" text-anchor="middle">Pick annual plan</text>
|
||||
|
||||
<!-- Pain markers (Hit the limit only) -->
|
||||
<rect x="744" y="312" width="184" height="16" rx="2" fill="rgba(240,138,89,0.10)" stroke="rgba(240,138,89,0.50)" stroke-width="0.8" stroke-dasharray="3,3"/>
|
||||
<text x="836" y="324" fill="#f08a59" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.04em">NO WARNING AT 80%</text>
|
||||
|
||||
<rect x="744" y="332" width="184" height="16" rx="2" fill="rgba(240,138,89,0.10)" stroke="rgba(240,138,89,0.50)" stroke-width="0.8" stroke-dasharray="3,3"/>
|
||||
<text x="836" y="344" fill="#f08a59" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.02em">PRICING PAGE IS 3 CLICKS AWAY</text>
|
||||
|
||||
<!-- Touchpoints -->
|
||||
<text x="164" y="412" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">signup form</text>
|
||||
<text x="388" y="412" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">onboarding wizard</text>
|
||||
<text x="612" y="412" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">email invite</text>
|
||||
<text x="836" y="412" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">in-app modal</text>
|
||||
<text x="1060" y="412" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">billing page</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="464" x2="1160" y2="464" stroke="rgba(245,245,245,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="480" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<line x1="40" y1="496" x2="56" y2="496" stroke="#bfc0c0" stroke-width="1.5"/>
|
||||
<circle cx="48" cy="496" r="3" fill="#bfc0c0"/>
|
||||
<text x="64" y="500" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Sentiment curve</text>
|
||||
|
||||
<line x1="220" y1="496" x2="236" y2="496" stroke="#f08a59" stroke-width="1.5"/>
|
||||
<circle cx="228" cy="496" r="3" fill="#f08a59"/>
|
||||
<text x="244" y="500" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Trough stage</text>
|
||||
|
||||
<rect x="384" y="492" width="16" height="8" rx="2" fill="rgba(240,138,89,0.10)" stroke="rgba(240,138,89,0.50)" stroke-width="0.8" stroke-dasharray="3,3"/>
|
||||
<text x="408" y="500" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Pain marker</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,142 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Trial to paid: the first week · User journey map</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root { --color-paper:#f5f5f5; --color-paper-2:#ececec; --color-ink:#2d3142; --color-muted:#4f5d75; --color-soft:#7a8399; --color-rule:rgba(45,49,66,0.12); --color-accent:#eb6c36; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
|
||||
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
|
||||
.container { max-width: 1280px; margin: 0 auto; }
|
||||
.header { margin-bottom: 2.5rem; }
|
||||
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
|
||||
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
|
||||
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 60ch; }
|
||||
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
|
||||
svg { width: 100%; min-width: 960px; display: block; }
|
||||
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
|
||||
@media (max-width: 900px) { .cards { grid-template-columns: 1fr; } }
|
||||
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
|
||||
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
|
||||
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(45,49,66,0.08); }
|
||||
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
|
||||
.card-dot.coral { background: var(--color-accent); }
|
||||
.card-dot.ink { background: var(--color-ink); }
|
||||
.card-dot.muted { background: var(--color-muted); }
|
||||
.card h3 { font-size: 0.875rem; font-weight: 600; }
|
||||
.card p { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; }
|
||||
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(45,49,66,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<p class="header-eyebrow">User journey · Diagram Design</p>
|
||||
<h1>Trial to paid: the first week</h1>
|
||||
<p class="subtitle">One persona, five stages from signup to upgrade. The sentiment curve is the point: a confident start, a dip when the free-tier export limit hits with no warning, and a recovery once the team upgrades.</p>
|
||||
</div>
|
||||
|
||||
<div class="diagram-container">
|
||||
<svg viewBox="0 0 1200 520" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="journey-full-title journey-full-desc">
|
||||
<title id="journey-full-title">Trial to paid: the first week</title>
|
||||
<desc id="journey-full-desc">User journey map of a new trial user across five stages, showing actions, touchpoints, and a sentiment curve that dips at the usage-limit stage before recovering at upgrade.</desc>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f5f5"/>
|
||||
|
||||
<text x="164" y="40" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">STAGE 1</text>
|
||||
<text x="164" y="60" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Sign up</text>
|
||||
|
||||
<text x="388" y="40" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">STAGE 2</text>
|
||||
<text x="388" y="60" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">First run</text>
|
||||
|
||||
<text x="612" y="40" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">STAGE 3</text>
|
||||
<text x="612" y="60" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Invite team</text>
|
||||
|
||||
<text x="836" y="40" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">STAGE 4</text>
|
||||
<text x="836" y="60" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Hit the limit</text>
|
||||
|
||||
<text x="1060" y="40" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">STAGE 5</text>
|
||||
<text x="1060" y="60" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Upgrade</text>
|
||||
|
||||
<line x1="64" y1="100" x2="1160" y2="100" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<line x1="64" y1="180" x2="1160" y2="180" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<line x1="64" y1="260" x2="1160" y2="260" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<text x="56" y="104" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.10em">HIGH</text>
|
||||
<text x="56" y="184" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.10em">NEUTRAL</text>
|
||||
<text x="56" y="264" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.10em">LOW</text>
|
||||
|
||||
<polyline points="164,100 388,180 612,100" fill="none" stroke="#4f5d75" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<polyline points="612,100 836,260" fill="none" stroke="#eb6c36" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<polyline points="836,260 1060,180" fill="none" stroke="#4f5d75" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
|
||||
<circle cx="164" cy="100" r="5" fill="#4f5d75"/>
|
||||
<circle cx="388" cy="180" r="5" fill="#4f5d75"/>
|
||||
<circle cx="612" cy="100" r="5" fill="#4f5d75"/>
|
||||
<circle cx="836" cy="260" r="5" fill="#eb6c36"/>
|
||||
<circle cx="1060" cy="180" r="5" fill="#4f5d75"/>
|
||||
|
||||
<line x1="64" y1="280" x2="1160" y2="280" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<line x1="64" y1="376" x2="1160" y2="376" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
|
||||
<text x="56" y="328" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.14em">ACTIONS</text>
|
||||
<text x="72" y="408" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.14em">TOUCHPOINTS</text>
|
||||
|
||||
<text x="164" y="300" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif" text-anchor="middle">Create workspace</text>
|
||||
<text x="388" y="300" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif" text-anchor="middle">Import first dataset</text>
|
||||
<text x="612" y="300" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif" text-anchor="middle">Add 3 teammates</text>
|
||||
<text x="836" y="300" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif" text-anchor="middle">Export blocked mid-report</text>
|
||||
<text x="1060" y="300" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif" text-anchor="middle">Pick annual plan</text>
|
||||
|
||||
<rect x="744" y="312" width="184" height="16" rx="2" fill="rgba(235,108,54,0.08)" stroke="rgba(235,108,54,0.50)" stroke-width="0.8" stroke-dasharray="3,3"/>
|
||||
<text x="836" y="324" fill="#eb6c36" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.04em">NO WARNING AT 80%</text>
|
||||
|
||||
<rect x="744" y="332" width="184" height="16" rx="2" fill="rgba(235,108,54,0.08)" stroke="rgba(235,108,54,0.50)" stroke-width="0.8" stroke-dasharray="3,3"/>
|
||||
<text x="836" y="344" fill="#eb6c36" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.02em">PRICING PAGE IS 3 CLICKS AWAY</text>
|
||||
|
||||
<text x="164" y="412" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">signup form</text>
|
||||
<text x="388" y="412" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">onboarding wizard</text>
|
||||
<text x="612" y="412" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">email invite</text>
|
||||
<text x="836" y="412" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">in-app modal</text>
|
||||
<text x="1060" y="412" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">billing page</text>
|
||||
|
||||
<line x1="40" y1="464" x2="1160" y2="464" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="480" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<line x1="40" y1="496" x2="56" y2="496" stroke="#4f5d75" stroke-width="1.5"/>
|
||||
<circle cx="48" cy="496" r="3" fill="#4f5d75"/>
|
||||
<text x="64" y="500" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Sentiment curve</text>
|
||||
|
||||
<line x1="220" y1="496" x2="236" y2="496" stroke="#eb6c36" stroke-width="1.5"/>
|
||||
<circle cx="228" cy="496" r="3" fill="#eb6c36"/>
|
||||
<text x="244" y="500" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Trough stage</text>
|
||||
|
||||
<rect x="384" y="492" width="16" height="8" rx="2" fill="rgba(235,108,54,0.08)" stroke="rgba(235,108,54,0.50)" stroke-width="0.8" stroke-dasharray="3,3"/>
|
||||
<text x="408" y="500" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Pain marker</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<p class="eyebrow">TROUGH</p>
|
||||
<div class="card-header"><span class="card-dot coral"></span><h3>Hit the limit</h3></div>
|
||||
<p>The export cap arrives with zero warning at 80% usage, and the only way out is a pricing page that's three clicks from the blocked screen. This is the one stage worth fixing before anything else.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot ink"></span><h3>Recovery, not just relief</h3></div>
|
||||
<p>Sentiment climbs back to neutral at upgrade rather than back to high — the friction is remembered. Track this cohort's 90-day retention separately from users who never hit the limit.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot muted"></span><h3>Early signal</h3></div>
|
||||
<p>Sign up and invite team both read high. The trial's first impression is working; the problem is entirely mid-funnel.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span>trial to paid · user journey map</span>
|
||||
<span>example · diagram design</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,148 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Trial to paid: the first week · User journey map</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #f5f5f5;
|
||||
--color-ink: #2d3142;
|
||||
--color-muted: #4f5d75;
|
||||
--color-accent: #eb6c36;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1280px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 960px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">User journey · Diagram Design</p>
|
||||
<h1>Trial to paid: the first week</h1>
|
||||
|
||||
<svg viewBox="0 0 1200 520" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="journey-title journey-desc">
|
||||
<title id="journey-title">Trial to paid: the first week</title>
|
||||
<desc id="journey-desc">User journey map of a new trial user across five stages, showing actions, touchpoints, and a sentiment curve that dips at the usage-limit stage before recovering at upgrade.</desc>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f5f5"/>
|
||||
|
||||
<!-- Stage headers -->
|
||||
<text x="164" y="40" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">STAGE 1</text>
|
||||
<text x="164" y="60" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Sign up</text>
|
||||
|
||||
<text x="388" y="40" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">STAGE 2</text>
|
||||
<text x="388" y="60" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">First run</text>
|
||||
|
||||
<text x="612" y="40" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">STAGE 3</text>
|
||||
<text x="612" y="60" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Invite team</text>
|
||||
|
||||
<text x="836" y="40" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">STAGE 4</text>
|
||||
<text x="836" y="60" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Hit the limit</text>
|
||||
|
||||
<text x="1060" y="40" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">STAGE 5</text>
|
||||
<text x="1060" y="60" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Upgrade</text>
|
||||
|
||||
<!-- Sentiment band: reference hairlines -->
|
||||
<line x1="64" y1="100" x2="1160" y2="100" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<line x1="64" y1="180" x2="1160" y2="180" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<line x1="64" y1="260" x2="1160" y2="260" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<text x="56" y="104" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.10em">HIGH</text>
|
||||
<text x="56" y="184" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.10em">NEUTRAL</text>
|
||||
<text x="56" y="264" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.10em">LOW</text>
|
||||
|
||||
<!-- Sentiment curve (data curve — not a connector, §6 rule 1 exempt) -->
|
||||
<polyline points="164,100 388,180 612,100" fill="none" stroke="#4f5d75" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<polyline points="612,100 836,260" fill="none" stroke="#eb6c36" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
<polyline points="836,260 1060,180" fill="none" stroke="#4f5d75" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
|
||||
<circle cx="164" cy="100" r="5" fill="#4f5d75"/>
|
||||
<circle cx="388" cy="180" r="5" fill="#4f5d75"/>
|
||||
<circle cx="612" cy="100" r="5" fill="#4f5d75"/>
|
||||
<circle cx="836" cy="260" r="5" fill="#eb6c36"/>
|
||||
<circle cx="1060" cy="180" r="5" fill="#4f5d75"/>
|
||||
|
||||
<!-- Row separators (full plot width) -->
|
||||
<line x1="64" y1="280" x2="1160" y2="280" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<line x1="64" y1="376" x2="1160" y2="376" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
|
||||
<!-- Row labels -->
|
||||
<text x="56" y="328" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.14em">ACTIONS</text>
|
||||
<text x="72" y="408" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.14em">TOUCHPOINTS</text>
|
||||
|
||||
<!-- Actions -->
|
||||
<text x="164" y="300" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif" text-anchor="middle">Create workspace</text>
|
||||
<text x="388" y="300" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif" text-anchor="middle">Import first dataset</text>
|
||||
<text x="612" y="300" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif" text-anchor="middle">Add 3 teammates</text>
|
||||
<text x="836" y="300" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif" text-anchor="middle">Export blocked mid-report</text>
|
||||
<text x="1060" y="300" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif" text-anchor="middle">Pick annual plan</text>
|
||||
|
||||
<!-- Pain markers (Hit the limit only) -->
|
||||
<rect x="744" y="312" width="184" height="16" rx="2" fill="rgba(235,108,54,0.08)" stroke="rgba(235,108,54,0.50)" stroke-width="0.8" stroke-dasharray="3,3"/>
|
||||
<text x="836" y="324" fill="#eb6c36" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.04em">NO WARNING AT 80%</text>
|
||||
|
||||
<rect x="744" y="332" width="184" height="16" rx="2" fill="rgba(235,108,54,0.08)" stroke="rgba(235,108,54,0.50)" stroke-width="0.8" stroke-dasharray="3,3"/>
|
||||
<text x="836" y="344" fill="#eb6c36" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.02em">PRICING PAGE IS 3 CLICKS AWAY</text>
|
||||
|
||||
<!-- Touchpoints -->
|
||||
<text x="164" y="412" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">signup form</text>
|
||||
<text x="388" y="412" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">onboarding wizard</text>
|
||||
<text x="612" y="412" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">email invite</text>
|
||||
<text x="836" y="412" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">in-app modal</text>
|
||||
<text x="1060" y="412" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">billing page</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="464" x2="1160" y2="464" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="480" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<line x1="40" y1="496" x2="56" y2="496" stroke="#4f5d75" stroke-width="1.5"/>
|
||||
<circle cx="48" cy="496" r="3" fill="#4f5d75"/>
|
||||
<text x="64" y="500" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Sentiment curve</text>
|
||||
|
||||
<line x1="220" y1="496" x2="236" y2="496" stroke="#eb6c36" stroke-width="1.5"/>
|
||||
<circle cx="228" cy="496" r="3" fill="#eb6c36"/>
|
||||
<text x="244" y="500" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Trough stage</text>
|
||||
|
||||
<rect x="384" y="492" width="16" height="8" rx="2" fill="rgba(235,108,54,0.08)" stroke="rgba(235,108,54,0.50)" stroke-width="0.8" stroke-dasharray="3,3"/>
|
||||
<text x="408" y="500" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Pain marker</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,174 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Platform team board · Kanban</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #2d3142;
|
||||
--color-ink: #f5f5f5;
|
||||
--color-muted: #bfc0c0;
|
||||
--color-accent: #f08a59;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Kanban · Diagram Design</p>
|
||||
<h1>Platform team board</h1>
|
||||
|
||||
<svg viewBox="0 0 1120 440" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="kanban-dark-title kanban-dark-desc">
|
||||
<title id="kanban-dark-title">Platform team board · Kanban</title>
|
||||
<desc id="kanban-dark-desc">Kanban board showing a platform team's work-in-progress across backlog, in-progress, review, and done columns, with the in-progress column over its WIP limit and one blocked card awaiting a vendor maintenance window.</desc>
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(245,245,245,0.10)"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#2d3142"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Column backgrounds -->
|
||||
<rect x="32" y="40" width="240" height="324" rx="8" fill="rgba(245,245,245,0.02)"/>
|
||||
<rect x="304" y="40" width="240" height="324" rx="8" fill="rgba(245,245,245,0.02)"/>
|
||||
<rect x="576" y="40" width="240" height="324" rx="8" fill="rgba(245,245,245,0.02)"/>
|
||||
<rect x="848" y="40" width="240" height="324" rx="8" fill="rgba(245,245,245,0.02)"/>
|
||||
|
||||
<!-- Column headers -->
|
||||
<text x="48" y="60" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Backlog</text>
|
||||
<text x="320" y="60" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">In progress</text>
|
||||
<text x="592" y="60" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Review</text>
|
||||
<text x="864" y="60" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Done</text>
|
||||
|
||||
<!-- WIP chips (rx=2, rectangular — never a pill) -->
|
||||
<rect x="216" y="48" width="40" height="16" rx="2" fill="transparent" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="236" y="59" fill="rgba(245,245,245,0.8)" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">3</text>
|
||||
|
||||
<rect x="488" y="48" width="40" height="16" rx="2" fill="transparent" stroke="#f08a59" stroke-width="0.8"/>
|
||||
<text x="508" y="59" fill="#f08a59" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">4/3</text>
|
||||
|
||||
<rect x="760" y="48" width="40" height="16" rx="2" fill="transparent" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="780" y="59" fill="rgba(245,245,245,0.8)" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">2/3</text>
|
||||
|
||||
<rect x="1032" y="48" width="40" height="16" rx="2" fill="transparent" stroke="rgba(245,245,245,0.40)" stroke-width="0.8"/>
|
||||
<text x="1052" y="59" fill="rgba(245,245,245,0.8)" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">2</text>
|
||||
|
||||
<!-- Header rules -->
|
||||
<line x1="32" y1="76" x2="272" y2="76" stroke="rgba(245,245,245,0.20)" stroke-width="1"/>
|
||||
<line x1="304" y1="76" x2="544" y2="76" stroke="rgba(245,245,245,0.20)" stroke-width="1"/>
|
||||
<line x1="576" y1="76" x2="816" y2="76" stroke="rgba(245,245,245,0.20)" stroke-width="1"/>
|
||||
<line x1="848" y1="76" x2="1088" y2="76" stroke="rgba(245,245,245,0.20)" stroke-width="1"/>
|
||||
|
||||
<!-- Backlog: 3 default cards -->
|
||||
<rect x="48" y="88" width="208" height="56" rx="6" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<text x="60" y="110" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">API rate limiting</text>
|
||||
<text x="60" y="126" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">AVA-201 · priya</text>
|
||||
|
||||
<rect x="48" y="156" width="208" height="56" rx="6" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<text x="60" y="178" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Terraform module cleanup</text>
|
||||
<text x="60" y="194" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">AVA-205 · leo</text>
|
||||
|
||||
<rect x="48" y="224" width="208" height="56" rx="6" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<text x="60" y="246" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Onboarding docs refresh</text>
|
||||
<text x="60" y="262" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">AVA-209 · maya</text>
|
||||
|
||||
<!-- In progress: blocked + 3 default (over limit) -->
|
||||
<rect x="320" y="88" width="208" height="56" rx="6" fill="rgba(240,138,89,0.10)" stroke="#f08a59" stroke-width="1" stroke-dasharray="4,4"/>
|
||||
<rect x="320" y="88" width="4" height="56" fill="#f08a59"/>
|
||||
<text x="336" y="110" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Postgres 16 upgrade</text>
|
||||
<text x="336" y="126" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">AVA-118 · sam</text>
|
||||
|
||||
<rect x="320" y="156" width="208" height="56" rx="6" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<text x="332" y="178" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">K8s node pool migration</text>
|
||||
<text x="332" y="194" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">AVA-142 · priya</text>
|
||||
|
||||
<rect x="320" y="224" width="208" height="56" rx="6" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<text x="332" y="246" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Auth token rotation</text>
|
||||
<text x="332" y="262" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">AVA-150 · leo</text>
|
||||
|
||||
<rect x="320" y="292" width="208" height="56" rx="6" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<text x="332" y="314" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Observability dashboards</text>
|
||||
<text x="332" y="330" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">AVA-161 · dara</text>
|
||||
|
||||
<!-- Review: default + waiting/external -->
|
||||
<rect x="592" y="88" width="208" height="56" rx="6" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<text x="604" y="110" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Feature flag cleanup</text>
|
||||
<text x="604" y="126" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">AVA-176 · nadia</text>
|
||||
|
||||
<rect x="592" y="156" width="208" height="56" rx="6" fill="rgba(245,245,245,0.02)" stroke="rgba(245,245,245,0.20)" stroke-width="1" stroke-dasharray="4,3"/>
|
||||
<text x="604" y="178" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Vendor SSO integration</text>
|
||||
<text x="604" y="194" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">AVA-183 · sam</text>
|
||||
|
||||
<!-- Done: 2 done cards -->
|
||||
<rect x="864" y="88" width="208" height="56" rx="6" fill="rgba(245,245,245,0.05)" stroke="#bfc0c0" stroke-width="1"/>
|
||||
<text x="876" y="110" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Log retention policy</text>
|
||||
<text x="876" y="126" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">AVA-090 · leo</text>
|
||||
|
||||
<rect x="864" y="156" width="208" height="56" rx="6" fill="rgba(245,245,245,0.05)" stroke="#bfc0c0" stroke-width="1"/>
|
||||
<text x="876" y="178" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">CI cache warmup</text>
|
||||
<text x="876" y="194" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">AVA-097 · maya</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="32" y1="388" x2="1088" y2="388" stroke="rgba(245,245,245,0.10)" stroke-width="0.8"/>
|
||||
<text x="32" y="404" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="32" y="416" width="16" height="8" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<text x="56" y="424" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Default</text>
|
||||
|
||||
<rect x="232" y="416" width="16" height="8" fill="rgba(240,138,89,0.10)" stroke="#f08a59" stroke-width="1" stroke-dasharray="4,4"/>
|
||||
<text x="256" y="424" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Blocked</text>
|
||||
|
||||
<rect x="432" y="416" width="16" height="8" fill="rgba(245,245,245,0.02)" stroke="rgba(245,245,245,0.20)" stroke-width="1" stroke-dasharray="4,3"/>
|
||||
<text x="456" y="424" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Waiting / external</text>
|
||||
|
||||
<rect x="632" y="416" width="16" height="8" fill="rgba(245,245,245,0.05)" stroke="#bfc0c0" stroke-width="1"/>
|
||||
<text x="656" y="424" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Done</text>
|
||||
|
||||
<rect x="832" y="416" width="16" height="8" rx="2" fill="transparent" stroke="#f08a59" stroke-width="0.8"/>
|
||||
<text x="856" y="424" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Over WIP limit</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,178 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Platform team board · Kanban</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root { --color-paper:#f5f5f5; --color-paper-2:#ececec; --color-ink:#2d3142; --color-muted:#4f5d75; --color-soft:#7a8399; --color-rule:rgba(45,49,66,0.12); --color-accent:#eb6c36; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
|
||||
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.header { margin-bottom: 2.5rem; }
|
||||
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
|
||||
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
|
||||
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
|
||||
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
|
||||
@media (max-width: 980px) { .cards { grid-template-columns: 1fr 1fr; } }
|
||||
@media (max-width: 600px) { .cards { grid-template-columns: 1fr; } }
|
||||
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
|
||||
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
|
||||
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(45,49,66,0.08); }
|
||||
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
|
||||
.card-dot.coral { background: var(--color-accent); }
|
||||
.card-dot.ink { background: var(--color-ink); }
|
||||
.card-dot.muted { background: var(--color-muted); }
|
||||
.card h3 { font-size: 0.875rem; font-weight: 600; }
|
||||
.card p { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; }
|
||||
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(45,49,66,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<p class="header-eyebrow">Kanban · Diagram Design</p>
|
||||
<h1>Platform team board</h1>
|
||||
<p class="subtitle">A state census of platform-team work, not a flow — no connectors, just where each ticket sits and whether a column is over its WIP limit. In progress is running one over budget, and one ticket is stuck on a vendor maintenance window.</p>
|
||||
</div>
|
||||
|
||||
<div class="diagram-container">
|
||||
<svg viewBox="0 0 1120 440" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="kanban-full-title kanban-full-desc">
|
||||
<title id="kanban-full-title">Platform team board · Kanban</title>
|
||||
<desc id="kanban-full-desc">Kanban board showing a platform team's work-in-progress across backlog, in-progress, review, and done columns, with the in-progress column over its WIP limit and one blocked card awaiting a vendor maintenance window.</desc>
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(45,49,66,0.10)"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f5f5"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Column backgrounds -->
|
||||
<rect x="32" y="40" width="240" height="324" rx="8" fill="rgba(45,49,66,0.02)"/>
|
||||
<rect x="304" y="40" width="240" height="324" rx="8" fill="rgba(45,49,66,0.02)"/>
|
||||
<rect x="576" y="40" width="240" height="324" rx="8" fill="rgba(45,49,66,0.02)"/>
|
||||
<rect x="848" y="40" width="240" height="324" rx="8" fill="rgba(45,49,66,0.02)"/>
|
||||
|
||||
<!-- Column headers -->
|
||||
<text x="48" y="60" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Backlog</text>
|
||||
<text x="320" y="60" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">In progress</text>
|
||||
<text x="592" y="60" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Review</text>
|
||||
<text x="864" y="60" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Done</text>
|
||||
|
||||
<!-- WIP chips (rx=2, rectangular — never a pill) -->
|
||||
<rect x="216" y="48" width="40" height="16" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="236" y="59" fill="rgba(45,49,66,0.8)" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">3</text>
|
||||
|
||||
<rect x="488" y="48" width="40" height="16" rx="2" fill="transparent" stroke="#eb6c36" stroke-width="0.8"/>
|
||||
<text x="508" y="59" fill="#eb6c36" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">4/3</text>
|
||||
|
||||
<rect x="760" y="48" width="40" height="16" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="780" y="59" fill="rgba(45,49,66,0.8)" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">2/3</text>
|
||||
|
||||
<rect x="1032" y="48" width="40" height="16" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="1052" y="59" fill="rgba(45,49,66,0.8)" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">2</text>
|
||||
|
||||
<!-- Header rules -->
|
||||
<line x1="32" y1="76" x2="272" y2="76" stroke="rgba(45,49,66,0.20)" stroke-width="1"/>
|
||||
<line x1="304" y1="76" x2="544" y2="76" stroke="rgba(45,49,66,0.20)" stroke-width="1"/>
|
||||
<line x1="576" y1="76" x2="816" y2="76" stroke="rgba(45,49,66,0.20)" stroke-width="1"/>
|
||||
<line x1="848" y1="76" x2="1088" y2="76" stroke="rgba(45,49,66,0.20)" stroke-width="1"/>
|
||||
|
||||
<!-- Backlog: 3 default cards -->
|
||||
<rect x="48" y="88" width="208" height="56" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="60" y="110" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">API rate limiting</text>
|
||||
<text x="60" y="126" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">AVA-201 · priya</text>
|
||||
|
||||
<rect x="48" y="156" width="208" height="56" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="60" y="178" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Terraform module cleanup</text>
|
||||
<text x="60" y="194" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">AVA-205 · leo</text>
|
||||
|
||||
<rect x="48" y="224" width="208" height="56" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="60" y="246" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Onboarding docs refresh</text>
|
||||
<text x="60" y="262" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">AVA-209 · maya</text>
|
||||
|
||||
<!-- In progress: blocked + 3 default (over limit) -->
|
||||
<rect x="320" y="88" width="208" height="56" rx="6" fill="rgba(235,108,54,0.05)" stroke="#eb6c36" stroke-width="1" stroke-dasharray="4,4"/>
|
||||
<rect x="320" y="88" width="4" height="56" fill="#eb6c36"/>
|
||||
<text x="336" y="110" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Postgres 16 upgrade</text>
|
||||
<text x="336" y="126" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">AVA-118 · sam</text>
|
||||
|
||||
<rect x="320" y="156" width="208" height="56" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="332" y="178" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">K8s node pool migration</text>
|
||||
<text x="332" y="194" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">AVA-142 · priya</text>
|
||||
|
||||
<rect x="320" y="224" width="208" height="56" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="332" y="246" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Auth token rotation</text>
|
||||
<text x="332" y="262" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">AVA-150 · leo</text>
|
||||
|
||||
<rect x="320" y="292" width="208" height="56" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="332" y="314" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Observability dashboards</text>
|
||||
<text x="332" y="330" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">AVA-161 · dara</text>
|
||||
|
||||
<!-- Review: default + waiting/external -->
|
||||
<rect x="592" y="88" width="208" height="56" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="604" y="110" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Feature flag cleanup</text>
|
||||
<text x="604" y="126" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">AVA-176 · nadia</text>
|
||||
|
||||
<rect x="592" y="156" width="208" height="56" rx="6" fill="rgba(45,49,66,0.02)" stroke="rgba(45,49,66,0.20)" stroke-width="1" stroke-dasharray="4,3"/>
|
||||
<text x="604" y="178" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Vendor SSO integration</text>
|
||||
<text x="604" y="194" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">AVA-183 · sam</text>
|
||||
|
||||
<!-- Done: 2 done cards -->
|
||||
<rect x="864" y="88" width="208" height="56" rx="6" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="876" y="110" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Log retention policy</text>
|
||||
<text x="876" y="126" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">AVA-090 · leo</text>
|
||||
|
||||
<rect x="864" y="156" width="208" height="56" rx="6" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="876" y="178" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">CI cache warmup</text>
|
||||
<text x="876" y="194" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">AVA-097 · maya</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="32" y1="388" x2="1088" y2="388" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<text x="32" y="404" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="32" y="416" width="16" height="8" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="56" y="424" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Default</text>
|
||||
|
||||
<rect x="232" y="416" width="16" height="8" fill="rgba(235,108,54,0.05)" stroke="#eb6c36" stroke-width="1" stroke-dasharray="4,4"/>
|
||||
<text x="256" y="424" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Blocked</text>
|
||||
|
||||
<rect x="432" y="416" width="16" height="8" fill="rgba(45,49,66,0.02)" stroke="rgba(45,49,66,0.20)" stroke-width="1" stroke-dasharray="4,3"/>
|
||||
<text x="456" y="424" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Waiting / external</text>
|
||||
|
||||
<rect x="632" y="416" width="16" height="8" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="656" y="424" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Done</text>
|
||||
|
||||
<rect x="832" y="416" width="16" height="8" rx="2" fill="transparent" stroke="#eb6c36" stroke-width="0.8"/>
|
||||
<text x="856" y="424" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Over WIP limit</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<p class="eyebrow">OVER LIMIT</p>
|
||||
<div class="card-header"><span class="card-dot coral"></span><h3>In progress · 4/3</h3></div>
|
||||
<p>One ticket over the stated cap, and the reason isn't throughput — it's <em>Postgres 16 upgrade</em>, blocked on a vendor maintenance window. Pulling more work in won't help until that window opens.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot ink"></span><h3>Review · 2/3</h3></div>
|
||||
<p>Under limit, but one of the two cards is waiting on an external vendor SSO integration rather than internal review time — worth tracking separately from cards actually being reviewed.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot muted"></span><h3>Done</h3></div>
|
||||
<p>Capped at two visible entries; older completions roll off the board rather than accumulating indefinitely.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span>platform team board · kanban</span>
|
||||
<span>example · diagram design</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,174 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Platform team board · Kanban</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #f5f5f5;
|
||||
--color-ink: #2d3142;
|
||||
--color-muted: #4f5d75;
|
||||
--color-accent: #eb6c36;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Kanban · Diagram Design</p>
|
||||
<h1>Platform team board</h1>
|
||||
|
||||
<svg viewBox="0 0 1120 440" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="kanban-title kanban-desc">
|
||||
<title id="kanban-title">Platform team board · Kanban</title>
|
||||
<desc id="kanban-desc">Kanban board showing a platform team's work-in-progress across backlog, in-progress, review, and done columns, with the in-progress column over its WIP limit and one blocked card awaiting a vendor maintenance window.</desc>
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(45,49,66,0.10)"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f5f5"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Column backgrounds -->
|
||||
<rect x="32" y="40" width="240" height="324" rx="8" fill="rgba(45,49,66,0.02)"/>
|
||||
<rect x="304" y="40" width="240" height="324" rx="8" fill="rgba(45,49,66,0.02)"/>
|
||||
<rect x="576" y="40" width="240" height="324" rx="8" fill="rgba(45,49,66,0.02)"/>
|
||||
<rect x="848" y="40" width="240" height="324" rx="8" fill="rgba(45,49,66,0.02)"/>
|
||||
|
||||
<!-- Column headers -->
|
||||
<text x="48" y="60" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Backlog</text>
|
||||
<text x="320" y="60" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">In progress</text>
|
||||
<text x="592" y="60" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Review</text>
|
||||
<text x="864" y="60" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Done</text>
|
||||
|
||||
<!-- WIP chips (rx=2, rectangular — never a pill) -->
|
||||
<rect x="216" y="48" width="40" height="16" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="236" y="59" fill="rgba(45,49,66,0.8)" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">3</text>
|
||||
|
||||
<rect x="488" y="48" width="40" height="16" rx="2" fill="transparent" stroke="#eb6c36" stroke-width="0.8"/>
|
||||
<text x="508" y="59" fill="#eb6c36" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">4/3</text>
|
||||
|
||||
<rect x="760" y="48" width="40" height="16" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="780" y="59" fill="rgba(45,49,66,0.8)" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">2/3</text>
|
||||
|
||||
<rect x="1032" y="48" width="40" height="16" rx="2" fill="transparent" stroke="rgba(45,49,66,0.40)" stroke-width="0.8"/>
|
||||
<text x="1052" y="59" fill="rgba(45,49,66,0.8)" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle">2</text>
|
||||
|
||||
<!-- Header rules -->
|
||||
<line x1="32" y1="76" x2="272" y2="76" stroke="rgba(45,49,66,0.20)" stroke-width="1"/>
|
||||
<line x1="304" y1="76" x2="544" y2="76" stroke="rgba(45,49,66,0.20)" stroke-width="1"/>
|
||||
<line x1="576" y1="76" x2="816" y2="76" stroke="rgba(45,49,66,0.20)" stroke-width="1"/>
|
||||
<line x1="848" y1="76" x2="1088" y2="76" stroke="rgba(45,49,66,0.20)" stroke-width="1"/>
|
||||
|
||||
<!-- Backlog: 3 default cards -->
|
||||
<rect x="48" y="88" width="208" height="56" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="60" y="110" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">API rate limiting</text>
|
||||
<text x="60" y="126" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">AVA-201 · priya</text>
|
||||
|
||||
<rect x="48" y="156" width="208" height="56" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="60" y="178" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Terraform module cleanup</text>
|
||||
<text x="60" y="194" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">AVA-205 · leo</text>
|
||||
|
||||
<rect x="48" y="224" width="208" height="56" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="60" y="246" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Onboarding docs refresh</text>
|
||||
<text x="60" y="262" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">AVA-209 · maya</text>
|
||||
|
||||
<!-- In progress: blocked + 3 default (over limit) -->
|
||||
<rect x="320" y="88" width="208" height="56" rx="6" fill="rgba(235,108,54,0.05)" stroke="#eb6c36" stroke-width="1" stroke-dasharray="4,4"/>
|
||||
<rect x="320" y="88" width="4" height="56" fill="#eb6c36"/>
|
||||
<text x="336" y="110" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Postgres 16 upgrade</text>
|
||||
<text x="336" y="126" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">AVA-118 · sam</text>
|
||||
|
||||
<rect x="320" y="156" width="208" height="56" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="332" y="178" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">K8s node pool migration</text>
|
||||
<text x="332" y="194" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">AVA-142 · priya</text>
|
||||
|
||||
<rect x="320" y="224" width="208" height="56" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="332" y="246" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Auth token rotation</text>
|
||||
<text x="332" y="262" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">AVA-150 · leo</text>
|
||||
|
||||
<rect x="320" y="292" width="208" height="56" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="332" y="314" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Observability dashboards</text>
|
||||
<text x="332" y="330" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">AVA-161 · dara</text>
|
||||
|
||||
<!-- Review: default + waiting/external -->
|
||||
<rect x="592" y="88" width="208" height="56" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="604" y="110" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Feature flag cleanup</text>
|
||||
<text x="604" y="126" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">AVA-176 · nadia</text>
|
||||
|
||||
<rect x="592" y="156" width="208" height="56" rx="6" fill="rgba(45,49,66,0.02)" stroke="rgba(45,49,66,0.20)" stroke-width="1" stroke-dasharray="4,3"/>
|
||||
<text x="604" y="178" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Vendor SSO integration</text>
|
||||
<text x="604" y="194" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">AVA-183 · sam</text>
|
||||
|
||||
<!-- Done: 2 done cards -->
|
||||
<rect x="864" y="88" width="208" height="56" rx="6" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="876" y="110" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Log retention policy</text>
|
||||
<text x="876" y="126" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">AVA-090 · leo</text>
|
||||
|
||||
<rect x="864" y="156" width="208" height="56" rx="6" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="876" y="178" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">CI cache warmup</text>
|
||||
<text x="876" y="194" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">AVA-097 · maya</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="32" y1="388" x2="1088" y2="388" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<text x="32" y="404" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="32" y="416" width="16" height="8" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="56" y="424" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Default</text>
|
||||
|
||||
<rect x="232" y="416" width="16" height="8" fill="rgba(235,108,54,0.05)" stroke="#eb6c36" stroke-width="1" stroke-dasharray="4,4"/>
|
||||
<text x="256" y="424" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Blocked</text>
|
||||
|
||||
<rect x="432" y="416" width="16" height="8" fill="rgba(45,49,66,0.02)" stroke="rgba(45,49,66,0.20)" stroke-width="1" stroke-dasharray="4,3"/>
|
||||
<text x="456" y="424" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Waiting / external</text>
|
||||
|
||||
<rect x="632" y="416" width="16" height="8" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="656" y="424" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Done</text>
|
||||
|
||||
<rect x="832" y="416" width="16" height="8" rx="2" fill="transparent" stroke="#eb6c36" stroke-width="0.8"/>
|
||||
<text x="856" y="424" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Over WIP limit</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,150 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CI compute budget · Sankey</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #2d3142;
|
||||
--color-ink: #f5f5f5;
|
||||
--color-muted: #bfc0c0;
|
||||
--color-accent: #f08a59;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Sankey · Diagram Design</p>
|
||||
<h1>CI compute budget · a month of pipeline minutes</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 560" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="sankey-dark-title sankey-dark-desc">
|
||||
<title id="sankey-dark-title">CI compute budget · a month of pipeline minutes</title>
|
||||
<desc id="sankey-dark-desc">Sankey diagram showing 12,000 monthly CI minutes splitting into unit test, E2E, build, and lint stages, then merging into passed, failed, and flaked outcomes, with the flaky-rerun path highlighted as the one accent flow.</desc>
|
||||
|
||||
<rect width="100%" height="100%" fill="#2d3142"/>
|
||||
|
||||
<!-- Column headers -->
|
||||
<text x="124" y="76" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">BUDGET</text>
|
||||
<text x="504" y="76" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">TEST STAGE</text>
|
||||
<text x="884" y="76" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">OUTCOME</text>
|
||||
|
||||
<!-- Ribbons: col1 -> col2. Both control points sit on the midpoint x, so every band
|
||||
meets its node bar horizontally at each end rather than arriving at a slant. -->
|
||||
<path d="M132,120 C316,120 316,120 500,120 L500,224 C316,224 316,224 132,224 Z" fill="rgba(191,192,192,0.22)"/>
|
||||
<path d="M132,224 C316,224 316,256 500,256 L500,336 C316,336 316,304 132,304 Z" fill="rgba(191,192,192,0.22)"/>
|
||||
<path d="M132,304 C316,304 316,368 500,368 L500,408 C316,408 316,344 132,344 Z" fill="rgba(191,192,192,0.22)"/>
|
||||
<path d="M132,344 C316,344 316,440 500,440 L500,456 C316,456 316,360 132,360 Z" fill="rgba(191,192,192,0.22)"/>
|
||||
|
||||
<!-- Ribbons: col2 -> col3, ordinary. Each source stacks its outgoing bands in target
|
||||
order and each target stacks its incoming bands in source order, which is what
|
||||
keeps every bar exactly saturated. Four sources feeding three outcomes cannot be
|
||||
drawn crossing-free; what remains is the minimum. -->
|
||||
<path d="M512,136 C696,136 696,148 880,148 L880,236 C696,236 696,224 512,224 Z" fill="rgba(191,192,192,0.22)"/>
|
||||
<path d="M512,260 C696,260 696,236 880,236 L880,312 C696,312 696,336 512,336 Z" fill="rgba(191,192,192,0.22)"/>
|
||||
<path d="M512,368 C696,368 696,312 880,312 L880,328 C696,328 696,384 512,384 Z" fill="rgba(191,192,192,0.22)"/>
|
||||
<path d="M512,384 C696,384 696,344 880,344 L880,368 C696,368 696,408 512,408 Z" fill="rgba(191,192,192,0.22)"/>
|
||||
<path d="M512,440 C696,440 696,328 880,328 L880,336 C696,336 696,448 512,448 Z" fill="rgba(191,192,192,0.22)"/>
|
||||
<path d="M512,448 C696,448 696,368 880,368 L880,376 C696,376 696,456 512,456 Z" fill="rgba(191,192,192,0.22)"/>
|
||||
|
||||
<!-- Ribbons: col2 -> col3, flaky-rerun path (the one accent flow) -->
|
||||
<path d="M512,120 C696,120 696,120 880,120 L880,136 C696,136 696,136 512,136 Z" fill="rgba(240,138,89,0.28)"/>
|
||||
<path d="M512,256 C696,256 696,136 880,136 L880,140 C696,140 696,260 512,260 Z" fill="rgba(240,138,89,0.28)"/>
|
||||
|
||||
<!-- Nodes -->
|
||||
<rect x="120" y="120" width="12" height="240" fill="#f5f5f5"/>
|
||||
<rect x="500" y="120" width="12" height="104" fill="#f5f5f5"/>
|
||||
<rect x="500" y="256" width="12" height="80" fill="#f5f5f5"/>
|
||||
<rect x="500" y="368" width="12" height="40" fill="#f5f5f5"/>
|
||||
<rect x="500" y="440" width="12" height="16" fill="#f5f5f5"/>
|
||||
<rect x="880" y="120" width="12" height="20" fill="#f5f5f5"/>
|
||||
<rect x="880" y="148" width="12" height="188" fill="#f5f5f5"/>
|
||||
<rect x="880" y="344" width="12" height="32" fill="#f5f5f5"/>
|
||||
|
||||
<!-- Labels: column 1, outside, anchored end -->
|
||||
<text x="104" y="236" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="end">CI minutes</text>
|
||||
<text x="104" y="252" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">12,000 min</text>
|
||||
|
||||
<!-- Labels: column 2, centred in the gutter above the bar, where the bands separate. -->
|
||||
|
||||
<text x="504" y="96" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Unit tests</text>
|
||||
<text x="504" y="112" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">5,200 min</text>
|
||||
|
||||
<text x="504" y="236" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">E2E</text>
|
||||
<text x="504" y="252" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">4,000 min</text>
|
||||
|
||||
<text x="504" y="348" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Build</text>
|
||||
<text x="504" y="364" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">2,000 min</text>
|
||||
|
||||
<text x="504" y="420" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Lint</text>
|
||||
<text x="504" y="436" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">800 min</text>
|
||||
|
||||
<!-- Labels: column 3, outside, anchored start -->
|
||||
|
||||
<text x="908" y="124" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="start">Flaked</text>
|
||||
<text x="908" y="136" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="start">1,000 min</text>
|
||||
|
||||
<text x="908" y="236" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="start">Passed</text>
|
||||
<text x="908" y="252" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="start">9,400 min</text>
|
||||
|
||||
<text x="908" y="356" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="start">Failed</text>
|
||||
<text x="908" y="372" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace" text-anchor="start">1,600 min</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="492" x2="960" y2="492" stroke="rgba(245,245,245,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="508" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="40" y="520" width="16" height="8" fill="rgba(191,192,192,0.22)" stroke="#bfc0c0" stroke-width="1"/>
|
||||
<text x="64" y="528" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Flow · stage routing</text>
|
||||
|
||||
<rect x="220" y="520" width="16" height="8" fill="rgba(240,138,89,0.28)" stroke="#f08a59" stroke-width="1"/>
|
||||
<text x="244" y="528" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Flaked path · reruns</text>
|
||||
|
||||
<rect x="420" y="520" width="16" height="8" fill="#f5f5f5"/>
|
||||
<text x="444" y="528" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Node · stage total</text>
|
||||
|
||||
<text x="600" y="528" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Two coral ribbons — flaky reruns burn ~8% of the monthly CI budget.</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,151 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CI compute budget · Sankey</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root { --color-paper:#f5f5f5; --color-paper-2:#ececec; --color-ink:#2d3142; --color-muted:#4f5d75; --color-soft:#7a8399; --color-rule:rgba(45,49,66,0.12); --color-accent:#eb6c36; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
|
||||
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.header { margin-bottom: 2.5rem; }
|
||||
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
|
||||
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
|
||||
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
|
||||
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
|
||||
@media (max-width: 900px) { .cards { grid-template-columns: 1fr; } }
|
||||
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
|
||||
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
|
||||
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(45,49,66,0.08); }
|
||||
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
|
||||
.card-dot.coral { background: var(--color-accent); }
|
||||
.card-dot.ink { background: var(--color-ink); }
|
||||
.card-dot.muted { background: var(--color-muted); }
|
||||
.card h3 { font-size: 0.875rem; font-weight: 600; }
|
||||
.card p { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; }
|
||||
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(45,49,66,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<p class="header-eyebrow">Sankey · Diagram Design</p>
|
||||
<h1>CI compute budget · a month of pipeline minutes</h1>
|
||||
<p class="subtitle">12,000 CI minutes split across four pipeline stages, then merge into three outcomes. Band thickness is the data — the flaky-rerun path is the only colored ribbon, because it's the line item worth cutting.</p>
|
||||
</div>
|
||||
|
||||
<div class="diagram-container">
|
||||
<svg viewBox="0 0 1000 560" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="sankey-full-title sankey-full-desc">
|
||||
<title id="sankey-full-title">CI compute budget · a month of pipeline minutes</title>
|
||||
<desc id="sankey-full-desc">Sankey diagram showing 12,000 monthly CI minutes splitting into unit test, E2E, build, and lint stages, then merging into passed, failed, and flaked outcomes, with the flaky-rerun path highlighted as the one accent flow.</desc>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f5f5"/>
|
||||
|
||||
<text x="124" y="76" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">BUDGET</text>
|
||||
<text x="504" y="76" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">TEST STAGE</text>
|
||||
<text x="884" y="76" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">OUTCOME</text>
|
||||
|
||||
<!-- Ribbons: col1 -> col2. Both control points sit on the midpoint x, so every band
|
||||
meets its node bar horizontally at each end rather than arriving at a slant. -->
|
||||
<path d="M132,120 C316,120 316,120 500,120 L500,224 C316,224 316,224 132,224 Z" fill="rgba(79,93,117,0.18)"/>
|
||||
<path d="M132,224 C316,224 316,256 500,256 L500,336 C316,336 316,304 132,304 Z" fill="rgba(79,93,117,0.18)"/>
|
||||
<path d="M132,304 C316,304 316,368 500,368 L500,408 C316,408 316,344 132,344 Z" fill="rgba(79,93,117,0.18)"/>
|
||||
<path d="M132,344 C316,344 316,440 500,440 L500,456 C316,456 316,360 132,360 Z" fill="rgba(79,93,117,0.18)"/>
|
||||
|
||||
<!-- Ribbons: col2 -> col3, ordinary. Each source stacks its outgoing bands in target
|
||||
order and each target stacks its incoming bands in source order, which is what
|
||||
keeps every bar exactly saturated. Four sources feeding three outcomes cannot be
|
||||
drawn crossing-free; what remains is the minimum. -->
|
||||
<path d="M512,136 C696,136 696,148 880,148 L880,236 C696,236 696,224 512,224 Z" fill="rgba(79,93,117,0.18)"/>
|
||||
<path d="M512,260 C696,260 696,236 880,236 L880,312 C696,312 696,336 512,336 Z" fill="rgba(79,93,117,0.18)"/>
|
||||
<path d="M512,368 C696,368 696,312 880,312 L880,328 C696,328 696,384 512,384 Z" fill="rgba(79,93,117,0.18)"/>
|
||||
<path d="M512,384 C696,384 696,344 880,344 L880,368 C696,368 696,408 512,408 Z" fill="rgba(79,93,117,0.18)"/>
|
||||
<path d="M512,440 C696,440 696,328 880,328 L880,336 C696,336 696,448 512,448 Z" fill="rgba(79,93,117,0.18)"/>
|
||||
<path d="M512,448 C696,448 696,368 880,368 L880,376 C696,376 696,456 512,456 Z" fill="rgba(79,93,117,0.18)"/>
|
||||
|
||||
<!-- Ribbons: col2 -> col3, flaky-rerun path (the one accent flow) -->
|
||||
<path d="M512,120 C696,120 696,120 880,120 L880,136 C696,136 696,136 512,136 Z" fill="rgba(235,108,54,0.28)"/>
|
||||
<path d="M512,256 C696,256 696,136 880,136 L880,140 C696,140 696,260 512,260 Z" fill="rgba(235,108,54,0.28)"/>
|
||||
|
||||
<!-- Nodes -->
|
||||
<rect x="120" y="120" width="12" height="240" fill="#2d3142"/>
|
||||
<rect x="500" y="120" width="12" height="104" fill="#2d3142"/>
|
||||
<rect x="500" y="256" width="12" height="80" fill="#2d3142"/>
|
||||
<rect x="500" y="368" width="12" height="40" fill="#2d3142"/>
|
||||
<rect x="500" y="440" width="12" height="16" fill="#2d3142"/>
|
||||
<rect x="880" y="120" width="12" height="20" fill="#2d3142"/>
|
||||
<rect x="880" y="148" width="12" height="188" fill="#2d3142"/>
|
||||
<rect x="880" y="344" width="12" height="32" fill="#2d3142"/>
|
||||
|
||||
<!-- Labels: column 1, outside, anchored end -->
|
||||
<text x="104" y="236" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="end">CI minutes</text>
|
||||
<text x="104" y="252" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">12,000 min</text>
|
||||
|
||||
<!-- Labels: column 2, centred in the gutter above the bar, where the bands separate. -->
|
||||
|
||||
<text x="504" y="96" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Unit tests</text>
|
||||
<text x="504" y="112" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">5,200 min</text>
|
||||
|
||||
<text x="504" y="236" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">E2E</text>
|
||||
<text x="504" y="252" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">4,000 min</text>
|
||||
|
||||
<text x="504" y="348" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Build</text>
|
||||
<text x="504" y="364" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">2,000 min</text>
|
||||
|
||||
<text x="504" y="420" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Lint</text>
|
||||
<text x="504" y="436" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">800 min</text>
|
||||
|
||||
<!-- Labels: column 3, outside, anchored start -->
|
||||
|
||||
<text x="908" y="124" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="start">Flaked</text>
|
||||
<text x="908" y="136" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="start">1,000 min</text>
|
||||
|
||||
<text x="908" y="236" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="start">Passed</text>
|
||||
<text x="908" y="252" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="start">9,400 min</text>
|
||||
|
||||
<text x="908" y="356" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="start">Failed</text>
|
||||
<text x="908" y="372" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="start">1,600 min</text>
|
||||
|
||||
<line x1="40" y1="492" x2="960" y2="492" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="508" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="40" y="520" width="16" height="8" fill="rgba(79,93,117,0.18)" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="64" y="528" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Flow · stage routing</text>
|
||||
|
||||
<rect x="220" y="520" width="16" height="8" fill="rgba(235,108,54,0.28)" stroke="#eb6c36" stroke-width="1"/>
|
||||
<text x="244" y="528" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Flaked path · reruns</text>
|
||||
|
||||
<rect x="420" y="520" width="16" height="8" fill="#2d3142"/>
|
||||
<text x="444" y="528" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Node · stage total</text>
|
||||
|
||||
<text x="600" y="528" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Two coral ribbons — flaky reruns burn ~8% of the monthly CI budget.</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<p class="eyebrow">EDITORIAL POINT</p>
|
||||
<div class="card-header"><span class="card-dot coral"></span><h3>Flaky reruns</h3></div>
|
||||
<p>1,000 of the 12,000 monthly minutes — about 8% of the whole budget — go to tests that failed once and passed on rerun. That's the one line item this diagram argues for cutting.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot ink"></span><h3>Where the minutes go</h3></div>
|
||||
<p>Unit tests and E2E together are three-quarters of the budget (9,200 of 12,000 minutes). Build and lint are comparatively cheap at 1,900 and 900.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot muted"></span><h3>Net outcome</h3></div>
|
||||
<p>9,400 minutes pass clean, 1,600 fail for real. The remaining 1,000 are pure overhead — neither signal nor failure, just retries.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span>ci compute budget · sankey</span>
|
||||
<span>example · diagram design</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,150 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CI compute budget · Sankey</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #f5f5f5;
|
||||
--color-ink: #2d3142;
|
||||
--color-muted: #4f5d75;
|
||||
--color-accent: #eb6c36;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Sankey · Diagram Design</p>
|
||||
<h1>CI compute budget · a month of pipeline minutes</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 560" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="sankey-title sankey-desc">
|
||||
<title id="sankey-title">CI compute budget · a month of pipeline minutes</title>
|
||||
<desc id="sankey-desc">Sankey diagram showing 12,000 monthly CI minutes splitting into unit test, E2E, build, and lint stages, then merging into passed, failed, and flaked outcomes, with the flaky-rerun path highlighted as the one accent flow.</desc>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f5f5"/>
|
||||
|
||||
<!-- Column headers -->
|
||||
<text x="124" y="76" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">BUDGET</text>
|
||||
<text x="504" y="76" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">TEST STAGE</text>
|
||||
<text x="884" y="76" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">OUTCOME</text>
|
||||
|
||||
<!-- Ribbons: col1 -> col2. Both control points sit on the midpoint x, so every band
|
||||
meets its node bar horizontally at each end rather than arriving at a slant. -->
|
||||
<path d="M132,120 C316,120 316,120 500,120 L500,224 C316,224 316,224 132,224 Z" fill="rgba(79,93,117,0.18)"/>
|
||||
<path d="M132,224 C316,224 316,256 500,256 L500,336 C316,336 316,304 132,304 Z" fill="rgba(79,93,117,0.18)"/>
|
||||
<path d="M132,304 C316,304 316,368 500,368 L500,408 C316,408 316,344 132,344 Z" fill="rgba(79,93,117,0.18)"/>
|
||||
<path d="M132,344 C316,344 316,440 500,440 L500,456 C316,456 316,360 132,360 Z" fill="rgba(79,93,117,0.18)"/>
|
||||
|
||||
<!-- Ribbons: col2 -> col3, ordinary. Each source stacks its outgoing bands in target
|
||||
order and each target stacks its incoming bands in source order, which is what
|
||||
keeps every bar exactly saturated. Four sources feeding three outcomes cannot be
|
||||
drawn crossing-free; what remains is the minimum. -->
|
||||
<path d="M512,136 C696,136 696,148 880,148 L880,236 C696,236 696,224 512,224 Z" fill="rgba(79,93,117,0.18)"/>
|
||||
<path d="M512,260 C696,260 696,236 880,236 L880,312 C696,312 696,336 512,336 Z" fill="rgba(79,93,117,0.18)"/>
|
||||
<path d="M512,368 C696,368 696,312 880,312 L880,328 C696,328 696,384 512,384 Z" fill="rgba(79,93,117,0.18)"/>
|
||||
<path d="M512,384 C696,384 696,344 880,344 L880,368 C696,368 696,408 512,408 Z" fill="rgba(79,93,117,0.18)"/>
|
||||
<path d="M512,440 C696,440 696,328 880,328 L880,336 C696,336 696,448 512,448 Z" fill="rgba(79,93,117,0.18)"/>
|
||||
<path d="M512,448 C696,448 696,368 880,368 L880,376 C696,376 696,456 512,456 Z" fill="rgba(79,93,117,0.18)"/>
|
||||
|
||||
<!-- Ribbons: col2 -> col3, flaky-rerun path (the one accent flow) -->
|
||||
<path d="M512,120 C696,120 696,120 880,120 L880,136 C696,136 696,136 512,136 Z" fill="rgba(235,108,54,0.28)"/>
|
||||
<path d="M512,256 C696,256 696,136 880,136 L880,140 C696,140 696,260 512,260 Z" fill="rgba(235,108,54,0.28)"/>
|
||||
|
||||
<!-- Nodes -->
|
||||
<rect x="120" y="120" width="12" height="240" fill="#2d3142"/>
|
||||
<rect x="500" y="120" width="12" height="104" fill="#2d3142"/>
|
||||
<rect x="500" y="256" width="12" height="80" fill="#2d3142"/>
|
||||
<rect x="500" y="368" width="12" height="40" fill="#2d3142"/>
|
||||
<rect x="500" y="440" width="12" height="16" fill="#2d3142"/>
|
||||
<rect x="880" y="120" width="12" height="20" fill="#2d3142"/>
|
||||
<rect x="880" y="148" width="12" height="188" fill="#2d3142"/>
|
||||
<rect x="880" y="344" width="12" height="32" fill="#2d3142"/>
|
||||
|
||||
<!-- Labels: column 1, outside, anchored end -->
|
||||
<text x="104" y="236" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="end">CI minutes</text>
|
||||
<text x="104" y="252" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="end">12,000 min</text>
|
||||
|
||||
<!-- Labels: column 2, centred in the gutter above the bar, where the bands separate. -->
|
||||
|
||||
<text x="504" y="96" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Unit tests</text>
|
||||
<text x="504" y="112" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">5,200 min</text>
|
||||
|
||||
<text x="504" y="236" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">E2E</text>
|
||||
<text x="504" y="252" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">4,000 min</text>
|
||||
|
||||
<text x="504" y="348" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Build</text>
|
||||
<text x="504" y="364" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">2,000 min</text>
|
||||
|
||||
<text x="504" y="420" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Lint</text>
|
||||
<text x="504" y="436" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="middle">800 min</text>
|
||||
|
||||
<!-- Labels: column 3, outside, anchored start -->
|
||||
|
||||
<text x="908" y="124" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="start">Flaked</text>
|
||||
<text x="908" y="136" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="start">1,000 min</text>
|
||||
|
||||
<text x="908" y="236" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="start">Passed</text>
|
||||
<text x="908" y="252" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="start">9,400 min</text>
|
||||
|
||||
<text x="908" y="356" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="start">Failed</text>
|
||||
<text x="908" y="372" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace" text-anchor="start">1,600 min</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="492" x2="960" y2="492" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="508" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="40" y="520" width="16" height="8" fill="rgba(79,93,117,0.18)" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="64" y="528" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Flow · stage routing</text>
|
||||
|
||||
<rect x="220" y="520" width="16" height="8" fill="rgba(235,108,54,0.28)" stroke="#eb6c36" stroke-width="1"/>
|
||||
<text x="244" y="528" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Flaked path · reruns</text>
|
||||
|
||||
<rect x="420" y="520" width="16" height="8" fill="#2d3142"/>
|
||||
<text x="444" y="528" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Node · stage total</text>
|
||||
|
||||
<text x="600" y="528" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">Two coral ribbons — flaky reruns burn ~8% of the monthly CI budget.</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,184 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Reporting, first release · User story map</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #2d3142;
|
||||
--color-ink: #f5f5f5;
|
||||
--color-muted: #bfc0c0;
|
||||
--color-accent: #f08a59;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">User story map · Diagram Design</p>
|
||||
<h1>Reporting, first release</h1>
|
||||
|
||||
<svg viewBox="0 0 1032 540" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="story-map-dark-title story-map-dark-desc">
|
||||
<title id="story-map-dark-title">Reporting, first release · User story map</title>
|
||||
<desc id="story-map-dark-desc">User story map for a reporting tool's first release: four narrative activities from finding data to trusting it, walking-skeleton steps beneath each, and three release slices sliced by an MVP release-cut line, with the row-level-permissions story flagged as the riskiest bet in Release 2.</desc>
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(245,245,245,0.10)"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#2d3142"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Release slice bands (alternating fill) -->
|
||||
<rect x="32" y="216" width="968" height="80" fill="rgba(245,245,245,0.02)"/>
|
||||
<rect x="32" y="376" width="968" height="80" fill="rgba(245,245,245,0.02)"/>
|
||||
|
||||
<text x="48" y="260" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">MVP</text>
|
||||
<text x="48" y="340" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">RELEASE 2</text>
|
||||
<text x="48" y="420" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">LATER</text>
|
||||
|
||||
<!-- Column separators: tie each card row to its activity column -->
|
||||
<line x1="116" y1="108" x2="116" y2="456" stroke="rgba(245,245,245,0.10)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<line x1="340" y1="108" x2="340" y2="456" stroke="rgba(245,245,245,0.10)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<line x1="564" y1="108" x2="564" y2="456" stroke="rgba(245,245,245,0.10)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<line x1="788" y1="108" x2="788" y2="456" stroke="rgba(245,245,245,0.10)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<line x1="1012" y1="108" x2="1012" y2="456" stroke="rgba(245,245,245,0.10)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
|
||||
<!-- Backbone: 4 activities, narrative left-to-right order -->
|
||||
<rect x="128" y="40" width="200" height="64" rx="6" fill="rgba(245,245,245,0.05)" stroke="#bfc0c0" stroke-width="1"/>
|
||||
<text x="228" y="68" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Find the data</text>
|
||||
<text x="228" y="88" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">ACTIVITY 1</text>
|
||||
|
||||
<rect x="352" y="40" width="200" height="64" rx="6" fill="rgba(245,245,245,0.05)" stroke="#bfc0c0" stroke-width="1"/>
|
||||
<text x="452" y="68" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Build the report</text>
|
||||
<text x="452" y="88" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">ACTIVITY 2</text>
|
||||
|
||||
<rect x="576" y="40" width="200" height="64" rx="6" fill="rgba(245,245,245,0.05)" stroke="#bfc0c0" stroke-width="1"/>
|
||||
<text x="676" y="68" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Share it</text>
|
||||
<text x="676" y="88" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">ACTIVITY 3</text>
|
||||
|
||||
<rect x="800" y="40" width="200" height="64" rx="6" fill="rgba(245,245,245,0.05)" stroke="#bfc0c0" stroke-width="1"/>
|
||||
<text x="900" y="68" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Trust it</text>
|
||||
<text x="900" y="88" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">ACTIVITY 4</text>
|
||||
|
||||
<!-- Walking skeleton: user steps under each activity -->
|
||||
<rect x="144" y="120" width="168" height="32" rx="4" fill="#393e53" stroke="#f5f5f5" stroke-width="0.8"/>
|
||||
<text x="156" y="140" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">Search datasets</text>
|
||||
<rect x="144" y="160" width="168" height="32" rx="4" fill="#393e53" stroke="#f5f5f5" stroke-width="0.8"/>
|
||||
<text x="156" y="180" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">Preview rows</text>
|
||||
|
||||
<rect x="368" y="120" width="168" height="32" rx="4" fill="#393e53" stroke="#f5f5f5" stroke-width="0.8"/>
|
||||
<text x="380" y="140" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">Pick columns</text>
|
||||
<rect x="368" y="160" width="168" height="32" rx="4" fill="#393e53" stroke="#f5f5f5" stroke-width="0.8"/>
|
||||
<text x="380" y="180" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">Add a chart</text>
|
||||
|
||||
<rect x="592" y="120" width="168" height="32" rx="4" fill="#393e53" stroke="#f5f5f5" stroke-width="0.8"/>
|
||||
<text x="604" y="140" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">Send a link</text>
|
||||
|
||||
<rect x="816" y="120" width="168" height="32" rx="4" fill="#393e53" stroke="#f5f5f5" stroke-width="0.8"/>
|
||||
<text x="828" y="140" fill="#f5f5f5" font-size="12" font-family="'Geist', sans-serif">Check freshness</text>
|
||||
|
||||
<!-- MVP row story cards -->
|
||||
<rect x="144" y="232" width="168" height="48" rx="4" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<text x="156" y="250" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Keyword search</text>
|
||||
<text x="156" y="266" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">RPT-101 · 3pt</text>
|
||||
|
||||
<rect x="368" y="232" width="168" height="48" rx="4" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<text x="380" y="250" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Table + one chart</text>
|
||||
<text x="380" y="266" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">RPT-102 · 5pt</text>
|
||||
|
||||
<rect x="592" y="232" width="168" height="48" rx="4" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<text x="604" y="250" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Public link</text>
|
||||
<text x="604" y="266" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">RPT-103 · 2pt</text>
|
||||
|
||||
<rect x="816" y="232" width="168" height="48" rx="4" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<text x="828" y="250" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Last-updated stamp</text>
|
||||
<text x="828" y="266" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">RPT-104 · 1pt</text>
|
||||
|
||||
<!-- Release cut line — accent element 1 -->
|
||||
<line x1="32" y1="296" x2="1000" y2="296" stroke="#f08a59" stroke-width="1.5"/>
|
||||
<rect x="736" y="280" width="104" height="8" rx="2" fill="#2d3142"/>
|
||||
<text x="788" y="285" fill="#f08a59" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.10em">RELEASE CUT</text>
|
||||
|
||||
<!-- Release 2 row story cards -->
|
||||
<rect x="144" y="312" width="168" height="48" rx="4" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<text x="156" y="330" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Saved filters</text>
|
||||
<text x="156" y="346" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">RPT-110 · 3pt</text>
|
||||
|
||||
<rect x="592" y="312" width="168" height="48" rx="4" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<text x="604" y="330" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Scheduled email</text>
|
||||
<text x="604" y="346" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">RPT-112 · 5pt</text>
|
||||
|
||||
<!-- Riskiest story — accent element 2 -->
|
||||
<rect x="816" y="312" width="168" height="48" rx="4" fill="rgba(240,138,89,0.10)" stroke="#f08a59" stroke-width="1" stroke-dasharray="4,4"/>
|
||||
<text x="828" y="330" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Row-level permissions</text>
|
||||
<text x="828" y="346" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">RPT-114 · 3pt</text>
|
||||
<rect x="936" y="336" width="40" height="12" rx="2" fill="transparent" stroke="#f08a59" stroke-width="0.8"/>
|
||||
<text x="956" y="345" fill="#f08a59" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">RISK</text>
|
||||
|
||||
<!-- Later row story cards -->
|
||||
<rect x="144" y="392" width="168" height="48" rx="4" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<text x="156" y="410" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Natural-language query</text>
|
||||
<text x="156" y="426" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">RPT-120 · 8pt</text>
|
||||
|
||||
<rect x="816" y="392" width="168" height="48" rx="4" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<text x="828" y="410" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Alerting on anomalies</text>
|
||||
<text x="828" y="426" fill="#bfc0c0" font-size="9" font-family="'Geist Mono', monospace">RPT-121 · 5pt</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="32" y1="480" x2="1000" y2="480" stroke="rgba(245,245,245,0.10)" stroke-width="0.8"/>
|
||||
<text x="32" y="496" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="32" y="508" width="16" height="8" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<text x="56" y="516" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Story card</text>
|
||||
|
||||
<rect x="232" y="508" width="16" height="8" fill="rgba(240,138,89,0.10)" stroke="#f08a59" stroke-width="1" stroke-dasharray="4,4"/>
|
||||
<text x="256" y="516" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Highest risk</text>
|
||||
|
||||
<line x1="432" y1="512" x2="464" y2="512" stroke="#f08a59" stroke-width="1.5"/>
|
||||
<text x="480" y="516" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Release cut</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,188 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Reporting, first release · User story map</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root { --color-paper:#f5f5f5; --color-paper-2:#ececec; --color-ink:#2d3142; --color-muted:#4f5d75; --color-soft:#7a8399; --color-rule:rgba(45,49,66,0.12); --color-accent:#eb6c36; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
|
||||
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.header { margin-bottom: 2.5rem; }
|
||||
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
|
||||
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
|
||||
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
|
||||
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
|
||||
@media (max-width: 980px) { .cards { grid-template-columns: 1fr 1fr; } }
|
||||
@media (max-width: 600px) { .cards { grid-template-columns: 1fr; } }
|
||||
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
|
||||
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
|
||||
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(45,49,66,0.08); }
|
||||
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
|
||||
.card-dot.coral { background: var(--color-accent); }
|
||||
.card-dot.ink { background: var(--color-ink); }
|
||||
.card-dot.muted { background: var(--color-muted); }
|
||||
.card h3 { font-size: 0.875rem; font-weight: 600; }
|
||||
.card p { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; }
|
||||
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(45,49,66,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<p class="header-eyebrow">User story map · Diagram Design</p>
|
||||
<h1>Reporting, first release</h1>
|
||||
<p class="subtitle">Narrative order runs left to right — find the data, build the report, share it, trust it — while priority runs top to bottom. The release-cut line under MVP is the editorial point: everything above it ships first, and the riskiest bet, row-level permissions, lands in Release 2.</p>
|
||||
</div>
|
||||
|
||||
<div class="diagram-container">
|
||||
<svg viewBox="0 0 1032 540" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="story-map-full-title story-map-full-desc">
|
||||
<title id="story-map-full-title">Reporting, first release · User story map</title>
|
||||
<desc id="story-map-full-desc">User story map for a reporting tool's first release: four narrative activities from finding data to trusting it, walking-skeleton steps beneath each, and three release slices sliced by an MVP release-cut line, with the row-level-permissions story flagged as the riskiest bet in Release 2.</desc>
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(45,49,66,0.10)"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f5f5"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Release slice bands (alternating fill) -->
|
||||
<rect x="32" y="216" width="968" height="80" fill="rgba(45,49,66,0.02)"/>
|
||||
<rect x="32" y="376" width="968" height="80" fill="rgba(45,49,66,0.02)"/>
|
||||
|
||||
<text x="48" y="260" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">MVP</text>
|
||||
<text x="48" y="340" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">RELEASE 2</text>
|
||||
<text x="48" y="420" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">LATER</text>
|
||||
|
||||
<!-- Column separators: tie each card row to its activity column -->
|
||||
<line x1="116" y1="108" x2="116" y2="456" stroke="rgba(45,49,66,0.10)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<line x1="340" y1="108" x2="340" y2="456" stroke="rgba(45,49,66,0.10)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<line x1="564" y1="108" x2="564" y2="456" stroke="rgba(45,49,66,0.10)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<line x1="788" y1="108" x2="788" y2="456" stroke="rgba(45,49,66,0.10)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<line x1="1012" y1="108" x2="1012" y2="456" stroke="rgba(45,49,66,0.10)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
|
||||
<!-- Backbone: 4 activities, narrative left-to-right order -->
|
||||
<rect x="128" y="40" width="200" height="64" rx="6" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="228" y="68" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Find the data</text>
|
||||
<text x="228" y="88" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">ACTIVITY 1</text>
|
||||
|
||||
<rect x="352" y="40" width="200" height="64" rx="6" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="452" y="68" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Build the report</text>
|
||||
<text x="452" y="88" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">ACTIVITY 2</text>
|
||||
|
||||
<rect x="576" y="40" width="200" height="64" rx="6" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="676" y="68" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Share it</text>
|
||||
<text x="676" y="88" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">ACTIVITY 3</text>
|
||||
|
||||
<rect x="800" y="40" width="200" height="64" rx="6" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="900" y="68" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Trust it</text>
|
||||
<text x="900" y="88" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">ACTIVITY 4</text>
|
||||
|
||||
<!-- Walking skeleton: user steps under each activity -->
|
||||
<rect x="144" y="120" width="168" height="32" rx="4" fill="#ffffff" stroke="#2d3142" stroke-width="0.8"/>
|
||||
<text x="156" y="140" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">Search datasets</text>
|
||||
<rect x="144" y="160" width="168" height="32" rx="4" fill="#ffffff" stroke="#2d3142" stroke-width="0.8"/>
|
||||
<text x="156" y="180" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">Preview rows</text>
|
||||
|
||||
<rect x="368" y="120" width="168" height="32" rx="4" fill="#ffffff" stroke="#2d3142" stroke-width="0.8"/>
|
||||
<text x="380" y="140" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">Pick columns</text>
|
||||
<rect x="368" y="160" width="168" height="32" rx="4" fill="#ffffff" stroke="#2d3142" stroke-width="0.8"/>
|
||||
<text x="380" y="180" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">Add a chart</text>
|
||||
|
||||
<rect x="592" y="120" width="168" height="32" rx="4" fill="#ffffff" stroke="#2d3142" stroke-width="0.8"/>
|
||||
<text x="604" y="140" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">Send a link</text>
|
||||
|
||||
<rect x="816" y="120" width="168" height="32" rx="4" fill="#ffffff" stroke="#2d3142" stroke-width="0.8"/>
|
||||
<text x="828" y="140" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">Check freshness</text>
|
||||
|
||||
<!-- MVP row story cards -->
|
||||
<rect x="144" y="232" width="168" height="48" rx="4" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="156" y="250" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Keyword search</text>
|
||||
<text x="156" y="266" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">RPT-101 · 3pt</text>
|
||||
|
||||
<rect x="368" y="232" width="168" height="48" rx="4" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="380" y="250" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Table + one chart</text>
|
||||
<text x="380" y="266" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">RPT-102 · 5pt</text>
|
||||
|
||||
<rect x="592" y="232" width="168" height="48" rx="4" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="604" y="250" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Public link</text>
|
||||
<text x="604" y="266" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">RPT-103 · 2pt</text>
|
||||
|
||||
<rect x="816" y="232" width="168" height="48" rx="4" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="828" y="250" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Last-updated stamp</text>
|
||||
<text x="828" y="266" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">RPT-104 · 1pt</text>
|
||||
|
||||
<!-- Release cut line — accent element 1 -->
|
||||
<line x1="32" y1="296" x2="1000" y2="296" stroke="#eb6c36" stroke-width="1.5"/>
|
||||
<rect x="736" y="280" width="104" height="8" rx="2" fill="#f5f5f5"/>
|
||||
<text x="788" y="285" fill="#eb6c36" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.10em">RELEASE CUT</text>
|
||||
|
||||
<!-- Release 2 row story cards -->
|
||||
<rect x="144" y="312" width="168" height="48" rx="4" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="156" y="330" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Saved filters</text>
|
||||
<text x="156" y="346" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">RPT-110 · 3pt</text>
|
||||
|
||||
<rect x="592" y="312" width="168" height="48" rx="4" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="604" y="330" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Scheduled email</text>
|
||||
<text x="604" y="346" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">RPT-112 · 5pt</text>
|
||||
|
||||
<!-- Riskiest story — accent element 2 -->
|
||||
<rect x="816" y="312" width="168" height="48" rx="4" fill="rgba(235,108,54,0.05)" stroke="#eb6c36" stroke-width="1" stroke-dasharray="4,4"/>
|
||||
<text x="828" y="330" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Row-level permissions</text>
|
||||
<text x="828" y="346" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">RPT-114 · 3pt</text>
|
||||
<rect x="936" y="336" width="40" height="12" rx="2" fill="transparent" stroke="#eb6c36" stroke-width="0.8"/>
|
||||
<text x="956" y="345" fill="#eb6c36" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">RISK</text>
|
||||
|
||||
<!-- Later row story cards -->
|
||||
<rect x="144" y="392" width="168" height="48" rx="4" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="156" y="410" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Natural-language query</text>
|
||||
<text x="156" y="426" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">RPT-120 · 8pt</text>
|
||||
|
||||
<rect x="816" y="392" width="168" height="48" rx="4" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="828" y="410" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Alerting on anomalies</text>
|
||||
<text x="828" y="426" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">RPT-121 · 5pt</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="32" y1="480" x2="1000" y2="480" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<text x="32" y="496" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="32" y="508" width="16" height="8" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="56" y="516" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Story card</text>
|
||||
|
||||
<rect x="232" y="508" width="16" height="8" fill="rgba(235,108,54,0.05)" stroke="#eb6c36" stroke-width="1" stroke-dasharray="4,4"/>
|
||||
<text x="256" y="516" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Highest risk</text>
|
||||
|
||||
<line x1="432" y1="512" x2="464" y2="512" stroke="#eb6c36" stroke-width="1.5"/>
|
||||
<text x="480" y="516" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Release cut</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<p class="eyebrow">RELEASE CUT</p>
|
||||
<div class="card-header"><span class="card-dot coral"></span><h3>MVP · 4 stories</h3></div>
|
||||
<p>Keyword search, a table with one chart, a public link, and a last-updated stamp — enough to answer "what is the whole story" without shipping every activity's full depth. Everything below the cut line waits.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot ink"></span><h3>Release 2 · 3 stories</h3></div>
|
||||
<p>Saved filters and scheduled email round out the workflow, but row-level permissions is the riskiest bet in the map — untested access-control logic sitting right after the cut, not buried in "Later".</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot muted"></span><h3>Later · 2 stories</h3></div>
|
||||
<p>Natural-language query and anomaly alerting are named and ordered, not promised — three slices is the ceiling before nobody believes the sequencing anymore.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span>reporting, first release · user story map</span>
|
||||
<span>example · diagram design</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,184 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Reporting, first release · User story map</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #f5f5f5;
|
||||
--color-ink: #2d3142;
|
||||
--color-muted: #4f5d75;
|
||||
--color-accent: #eb6c36;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">User story map · Diagram Design</p>
|
||||
<h1>Reporting, first release</h1>
|
||||
|
||||
<svg viewBox="0 0 1032 540" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="story-map-title story-map-desc">
|
||||
<title id="story-map-title">Reporting, first release · User story map</title>
|
||||
<desc id="story-map-desc">User story map for a reporting tool's first release: four narrative activities from finding data to trusting it, walking-skeleton steps beneath each, and three release slices sliced by an MVP release-cut line, with the row-level-permissions story flagged as the riskiest bet in Release 2.</desc>
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(45,49,66,0.10)"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f5f5"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Release slice bands (alternating fill) -->
|
||||
<rect x="32" y="216" width="968" height="80" fill="rgba(45,49,66,0.02)"/>
|
||||
<rect x="32" y="376" width="968" height="80" fill="rgba(45,49,66,0.02)"/>
|
||||
|
||||
<text x="48" y="260" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">MVP</text>
|
||||
<text x="48" y="340" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">RELEASE 2</text>
|
||||
<text x="48" y="420" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.14em">LATER</text>
|
||||
|
||||
<!-- Column separators: tie each card row to its activity column -->
|
||||
<line x1="116" y1="108" x2="116" y2="456" stroke="rgba(45,49,66,0.10)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<line x1="340" y1="108" x2="340" y2="456" stroke="rgba(45,49,66,0.10)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<line x1="564" y1="108" x2="564" y2="456" stroke="rgba(45,49,66,0.10)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<line x1="788" y1="108" x2="788" y2="456" stroke="rgba(45,49,66,0.10)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<line x1="1012" y1="108" x2="1012" y2="456" stroke="rgba(45,49,66,0.10)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
|
||||
<!-- Backbone: 4 activities, narrative left-to-right order -->
|
||||
<rect x="128" y="40" width="200" height="64" rx="6" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="228" y="68" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Find the data</text>
|
||||
<text x="228" y="88" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">ACTIVITY 1</text>
|
||||
|
||||
<rect x="352" y="40" width="200" height="64" rx="6" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="452" y="68" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Build the report</text>
|
||||
<text x="452" y="88" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">ACTIVITY 2</text>
|
||||
|
||||
<rect x="576" y="40" width="200" height="64" rx="6" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="676" y="68" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Share it</text>
|
||||
<text x="676" y="88" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">ACTIVITY 3</text>
|
||||
|
||||
<rect x="800" y="40" width="200" height="64" rx="6" fill="rgba(45,49,66,0.05)" stroke="#4f5d75" stroke-width="1"/>
|
||||
<text x="900" y="68" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Trust it</text>
|
||||
<text x="900" y="88" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.14em">ACTIVITY 4</text>
|
||||
|
||||
<!-- Walking skeleton: user steps under each activity -->
|
||||
<rect x="144" y="120" width="168" height="32" rx="4" fill="#ffffff" stroke="#2d3142" stroke-width="0.8"/>
|
||||
<text x="156" y="140" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">Search datasets</text>
|
||||
<rect x="144" y="160" width="168" height="32" rx="4" fill="#ffffff" stroke="#2d3142" stroke-width="0.8"/>
|
||||
<text x="156" y="180" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">Preview rows</text>
|
||||
|
||||
<rect x="368" y="120" width="168" height="32" rx="4" fill="#ffffff" stroke="#2d3142" stroke-width="0.8"/>
|
||||
<text x="380" y="140" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">Pick columns</text>
|
||||
<rect x="368" y="160" width="168" height="32" rx="4" fill="#ffffff" stroke="#2d3142" stroke-width="0.8"/>
|
||||
<text x="380" y="180" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">Add a chart</text>
|
||||
|
||||
<rect x="592" y="120" width="168" height="32" rx="4" fill="#ffffff" stroke="#2d3142" stroke-width="0.8"/>
|
||||
<text x="604" y="140" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">Send a link</text>
|
||||
|
||||
<rect x="816" y="120" width="168" height="32" rx="4" fill="#ffffff" stroke="#2d3142" stroke-width="0.8"/>
|
||||
<text x="828" y="140" fill="#2d3142" font-size="12" font-family="'Geist', sans-serif">Check freshness</text>
|
||||
|
||||
<!-- MVP row story cards -->
|
||||
<rect x="144" y="232" width="168" height="48" rx="4" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="156" y="250" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Keyword search</text>
|
||||
<text x="156" y="266" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">RPT-101 · 3pt</text>
|
||||
|
||||
<rect x="368" y="232" width="168" height="48" rx="4" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="380" y="250" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Table + one chart</text>
|
||||
<text x="380" y="266" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">RPT-102 · 5pt</text>
|
||||
|
||||
<rect x="592" y="232" width="168" height="48" rx="4" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="604" y="250" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Public link</text>
|
||||
<text x="604" y="266" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">RPT-103 · 2pt</text>
|
||||
|
||||
<rect x="816" y="232" width="168" height="48" rx="4" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="828" y="250" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Last-updated stamp</text>
|
||||
<text x="828" y="266" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">RPT-104 · 1pt</text>
|
||||
|
||||
<!-- Release cut line — accent element 1 -->
|
||||
<line x1="32" y1="296" x2="1000" y2="296" stroke="#eb6c36" stroke-width="1.5"/>
|
||||
<rect x="736" y="280" width="104" height="8" rx="2" fill="#f5f5f5"/>
|
||||
<text x="788" y="285" fill="#eb6c36" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.10em">RELEASE CUT</text>
|
||||
|
||||
<!-- Release 2 row story cards -->
|
||||
<rect x="144" y="312" width="168" height="48" rx="4" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="156" y="330" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Saved filters</text>
|
||||
<text x="156" y="346" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">RPT-110 · 3pt</text>
|
||||
|
||||
<rect x="592" y="312" width="168" height="48" rx="4" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="604" y="330" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Scheduled email</text>
|
||||
<text x="604" y="346" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">RPT-112 · 5pt</text>
|
||||
|
||||
<!-- Riskiest story — accent element 2 -->
|
||||
<rect x="816" y="312" width="168" height="48" rx="4" fill="rgba(235,108,54,0.05)" stroke="#eb6c36" stroke-width="1" stroke-dasharray="4,4"/>
|
||||
<text x="828" y="330" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Row-level permissions</text>
|
||||
<text x="828" y="346" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">RPT-114 · 3pt</text>
|
||||
<rect x="936" y="336" width="40" height="12" rx="2" fill="transparent" stroke="#eb6c36" stroke-width="0.8"/>
|
||||
<text x="956" y="345" fill="#eb6c36" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">RISK</text>
|
||||
|
||||
<!-- Later row story cards -->
|
||||
<rect x="144" y="392" width="168" height="48" rx="4" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="156" y="410" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Natural-language query</text>
|
||||
<text x="156" y="426" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">RPT-120 · 8pt</text>
|
||||
|
||||
<rect x="816" y="392" width="168" height="48" rx="4" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="828" y="410" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif">Alerting on anomalies</text>
|
||||
<text x="828" y="426" fill="#4f5d75" font-size="9" font-family="'Geist Mono', monospace">RPT-121 · 5pt</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="32" y1="480" x2="1000" y2="480" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<text x="32" y="496" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<rect x="32" y="508" width="16" height="8" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="56" y="516" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Story card</text>
|
||||
|
||||
<rect x="232" y="508" width="16" height="8" fill="rgba(235,108,54,0.05)" stroke="#eb6c36" stroke-width="1" stroke-dasharray="4,4"/>
|
||||
<text x="256" y="516" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Highest risk</text>
|
||||
|
||||
<line x1="432" y1="512" x2="464" y2="512" stroke="#eb6c36" stroke-width="1.5"/>
|
||||
<text x="480" y="516" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Release cut</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,188 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Payments domain · UML class diagram</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #2d3142;
|
||||
--color-ink: #f5f5f5;
|
||||
--color-muted: #bfc0c0;
|
||||
--color-accent: #f08a59;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1240px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">UML class · Diagram Design</p>
|
||||
<h1>Payments domain · class diagram</h1>
|
||||
|
||||
<svg viewBox="0 0 980 664" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="uml-class-dark-title uml-class-dark-desc">
|
||||
<title id="uml-class-dark-title">Payments domain · UML class diagram</title>
|
||||
<desc id="uml-class-dark-desc">Class diagram showing Card and BankTransfer realizing a PaymentMethod interface, PaymentService depending on PaymentMethod, and Order composed of OrderLine while associated with Customer.</desc>
|
||||
<defs>
|
||||
<marker id="uml-triangle" markerWidth="16" markerHeight="12" refX="15" refY="6" orient="auto" markerUnits="userSpaceOnUse">
|
||||
<polygon points="0 0, 16 6, 0 12" fill="#2d3142" stroke="#f5f5f5" stroke-width="1"/>
|
||||
</marker>
|
||||
<marker id="uml-diamond-filled" markerWidth="18" markerHeight="10" refX="0" refY="5" orient="auto" markerUnits="userSpaceOnUse">
|
||||
<polygon points="0 5, 9 0, 18 5, 9 10" fill="#f5f5f5"/>
|
||||
</marker>
|
||||
<marker id="uml-diamond-hollow" markerWidth="18" markerHeight="10" refX="0" refY="5" orient="auto" markerUnits="userSpaceOnUse">
|
||||
<polygon points="0 5, 9 0, 18 5, 9 10" fill="#2d3142" stroke="#f5f5f5" stroke-width="1"/>
|
||||
</marker>
|
||||
<marker id="uml-open" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="userSpaceOnUse">
|
||||
<path d="M0,0 L9,4 L0,8" fill="none" stroke="#bfc0c0" stroke-width="1"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#2d3142"/>
|
||||
|
||||
<!-- Relationships (drawn before boxes) -->
|
||||
<!-- Card realizes PaymentMethod -->
|
||||
<line x1="480" y1="240" x2="480" y2="140" stroke="#f08a59" stroke-width="1" stroke-dasharray="5,4" marker-end="url(#uml-triangle)"/>
|
||||
<!-- BankTransfer realizes PaymentMethod -->
|
||||
<line x1="760" y1="240" x2="760" y2="140" stroke="#f08a59" stroke-width="1" stroke-dasharray="5,4" marker-end="url(#uml-triangle)"/>
|
||||
<!-- PaymentService depends on PaymentMethod -->
|
||||
<line x1="320" y1="100" x2="420" y2="100" stroke="#bfc0c0" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#uml-open)"/>
|
||||
<!-- Order composed of OrderLine -->
|
||||
<line x1="260" y1="480" x2="380" y2="480" stroke="#bfc0c0" stroke-width="1" marker-start="url(#uml-diamond-filled)"/>
|
||||
<!-- Order associates Customer (routed below to clear OrderLine) -->
|
||||
<path d="M 160,540 V 572 Q 160,580 168,580 H 752 Q 760,580 760,572 V 540" fill="none" stroke="#bfc0c0" stroke-width="1" marker-end="url(#uml-open)"/>
|
||||
|
||||
<!-- Dependency label -->
|
||||
<rect x="352" y="80" width="32" height="12" rx="2" fill="#2d3142"/>
|
||||
<text x="368" y="89" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">USES</text>
|
||||
|
||||
<!-- Composition multiplicities -->
|
||||
<rect x="288" y="474" width="12" height="12" rx="2" fill="#2d3142"/>
|
||||
<text x="294" y="484" fill="#f5f5f5" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">1</text>
|
||||
<rect x="348" y="474" width="24" height="12" rx="2" fill="#2d3142"/>
|
||||
<text x="360" y="484" fill="#f5f5f5" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">1..*</text>
|
||||
|
||||
<!-- Association multiplicities -->
|
||||
<rect x="164" y="548" width="28" height="12" rx="2" fill="#2d3142"/>
|
||||
<text x="178" y="557" fill="#f5f5f5" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">0..*</text>
|
||||
<rect x="752" y="548" width="16" height="12" rx="2" fill="#2d3142"/>
|
||||
<text x="760" y="557" fill="#f5f5f5" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">1</text>
|
||||
|
||||
<!-- Class: PaymentMethod (interface, focal) -->
|
||||
<rect x="420" y="40" width="360" height="100" rx="6" fill="rgba(240,138,89,0.04)" stroke="#f08a59" stroke-width="1"/>
|
||||
<rect x="420" y="40" width="360" height="40" rx="6" fill="rgba(240,138,89,0.10)"/>
|
||||
<rect x="420" y="72" width="360" height="8" fill="rgba(240,138,89,0.10)"/>
|
||||
<line x1="420" y1="80" x2="780" y2="80" stroke="rgba(240,138,89,0.40)" stroke-width="1"/>
|
||||
<text x="600" y="56" fill="#f08a59" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">«interface»</text>
|
||||
<text x="600" y="72" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">PaymentMethod</text>
|
||||
<text x="436" y="100" fill="#f5f5f5" font-size="9" font-family="'Geist Mono', monospace">+ authorize(amount: Money): AuthResult</text>
|
||||
<text x="436" y="120" fill="#f5f5f5" font-size="9" font-family="'Geist Mono', monospace">+ capture(ref: String): void</text>
|
||||
|
||||
<!-- Class: Card -->
|
||||
<rect x="380" y="240" width="200" height="120" rx="6" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<line x1="380" y1="280" x2="580" y2="280" stroke="rgba(245,245,245,0.22)" stroke-width="1"/>
|
||||
<text x="480" y="264" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Card</text>
|
||||
<text x="396" y="300" fill="#f5f5f5" font-size="9" font-family="'Geist Mono', monospace">- number: String</text>
|
||||
<text x="396" y="320" fill="#f5f5f5" font-size="9" font-family="'Geist Mono', monospace">- expiry: Date</text>
|
||||
<text x="396" y="340" fill="#f5f5f5" font-size="9" font-family="'Geist Mono', monospace">- cvv: String</text>
|
||||
|
||||
<!-- Class: BankTransfer -->
|
||||
<rect x="660" y="240" width="200" height="100" rx="6" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<line x1="660" y1="280" x2="860" y2="280" stroke="rgba(245,245,245,0.22)" stroke-width="1"/>
|
||||
<text x="760" y="264" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">BankTransfer</text>
|
||||
<text x="676" y="300" fill="#f5f5f5" font-size="9" font-family="'Geist Mono', monospace">- iban: String</text>
|
||||
<text x="676" y="320" fill="#f5f5f5" font-size="9" font-family="'Geist Mono', monospace">- accountHolder: String</text>
|
||||
|
||||
<!-- Class: PaymentService -->
|
||||
<rect x="40" y="60" width="280" height="80" rx="6" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<line x1="40" y1="100" x2="320" y2="100" stroke="rgba(245,245,245,0.22)" stroke-width="1"/>
|
||||
<text x="180" y="84" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">PaymentService</text>
|
||||
<text x="56" y="120" fill="#f5f5f5" font-size="9" font-family="'Geist Mono', monospace">+ charge(order: Order): Receipt</text>
|
||||
|
||||
<!-- Class: Order -->
|
||||
<rect x="60" y="420" width="200" height="120" rx="6" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<line x1="60" y1="460" x2="260" y2="460" stroke="rgba(245,245,245,0.22)" stroke-width="1"/>
|
||||
<text x="160" y="444" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Order</text>
|
||||
<text x="76" y="480" fill="#f5f5f5" font-size="9" font-family="'Geist Mono', monospace">- id: String</text>
|
||||
<text x="76" y="500" fill="#f5f5f5" font-size="9" font-family="'Geist Mono', monospace">- placedAt: Date</text>
|
||||
<text x="76" y="520" fill="#f5f5f5" font-size="9" font-family="'Geist Mono', monospace">- total: Money</text>
|
||||
|
||||
<!-- Class: OrderLine -->
|
||||
<rect x="380" y="420" width="200" height="120" rx="6" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<line x1="380" y1="460" x2="580" y2="460" stroke="rgba(245,245,245,0.22)" stroke-width="1"/>
|
||||
<text x="480" y="444" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">OrderLine</text>
|
||||
<text x="396" y="480" fill="#f5f5f5" font-size="9" font-family="'Geist Mono', monospace">- sku: String</text>
|
||||
<text x="396" y="500" fill="#f5f5f5" font-size="9" font-family="'Geist Mono', monospace">- qty: Int</text>
|
||||
<text x="396" y="520" fill="#f5f5f5" font-size="9" font-family="'Geist Mono', monospace">- price: Money</text>
|
||||
|
||||
<!-- Class: Customer -->
|
||||
<rect x="660" y="420" width="200" height="120" rx="6" fill="#393e53" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<line x1="660" y1="460" x2="860" y2="460" stroke="rgba(245,245,245,0.22)" stroke-width="1"/>
|
||||
<text x="760" y="444" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Customer</text>
|
||||
<text x="676" y="480" fill="#f5f5f5" font-size="9" font-family="'Geist Mono', monospace">- id: String</text>
|
||||
<text x="676" y="500" fill="#f5f5f5" font-size="9" font-family="'Geist Mono', monospace">- name: String</text>
|
||||
<text x="676" y="520" fill="#f5f5f5" font-size="9" font-family="'Geist Mono', monospace">- email: String</text>
|
||||
|
||||
<!-- Legend: relationship vocabulary -->
|
||||
<line x1="40" y1="604" x2="940" y2="604" stroke="rgba(245,245,245,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="620" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND · RELATIONSHIPS</text>
|
||||
|
||||
<line x1="40" y1="636" x2="68" y2="636" stroke="#f5f5f5" stroke-width="1" marker-end="url(#uml-triangle)"/>
|
||||
<text x="76" y="640" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Inheritance</text>
|
||||
|
||||
<line x1="188" y1="636" x2="216" y2="636" stroke="#f5f5f5" stroke-width="1" stroke-dasharray="5,4" marker-end="url(#uml-triangle)"/>
|
||||
<text x="224" y="640" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Realization</text>
|
||||
|
||||
<line x1="336" y1="636" x2="364" y2="636" stroke="#f5f5f5" stroke-width="1" marker-start="url(#uml-diamond-filled)"/>
|
||||
<text x="372" y="640" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Composition · owns</text>
|
||||
|
||||
<line x1="484" y1="636" x2="512" y2="636" stroke="#f5f5f5" stroke-width="1" marker-start="url(#uml-diamond-hollow)"/>
|
||||
<text x="520" y="640" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Aggregation · has</text>
|
||||
|
||||
<line x1="632" y1="636" x2="660" y2="636" stroke="#bfc0c0" stroke-width="1" marker-end="url(#uml-open)"/>
|
||||
<text x="668" y="640" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Association</text>
|
||||
|
||||
<line x1="780" y1="636" x2="808" y2="636" stroke="#bfc0c0" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#uml-open)"/>
|
||||
<text x="816" y="640" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Dependency · uses</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,191 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Payments domain · UML class diagram</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root { --color-paper:#f5f5f5; --color-paper-2:#ececec; --color-ink:#2d3142; --color-muted:#4f5d75; --color-soft:#7a8399; --color-rule:rgba(45,49,66,0.12); --color-accent:#eb6c36; --color-link:#2e5aa8; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
|
||||
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
|
||||
.container { max-width: 1240px; margin: 0 auto; }
|
||||
.header { margin-bottom: 2.5rem; }
|
||||
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
|
||||
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
|
||||
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 60ch; }
|
||||
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
|
||||
@media (max-width: 820px) { .cards { grid-template-columns: 1fr; } }
|
||||
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
|
||||
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
|
||||
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(45,49,66,0.08); }
|
||||
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
|
||||
.card-dot.ink { background: var(--color-ink); } .card-dot.muted { background: var(--color-muted); } .card-dot.coral { background: var(--color-accent); }
|
||||
.card h3 { font-size: 0.875rem; font-weight: 600; }
|
||||
.card p, .card ul { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; list-style: none; }
|
||||
.card li { margin-bottom: 0.3rem; padding-left: 0.875rem; position: relative; }
|
||||
.card li::before { content: '—'; position: absolute; left: 0; color: rgba(45,49,66,0.25); font-size: 0.75rem; }
|
||||
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(45,49,66,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<p class="header-eyebrow">UML class · Diagram Design</p>
|
||||
<h1>Payments domain · class diagram</h1>
|
||||
<p class="subtitle">PaymentMethod is the contract two payment methods realize. Order composes its line items and references a customer. PaymentService only depends on the interface, never the concrete methods.</p>
|
||||
</div>
|
||||
|
||||
<div class="diagram-container">
|
||||
<svg viewBox="0 0 980 664" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="uml-class-full-title uml-class-full-desc">
|
||||
<title id="uml-class-full-title">Payments domain · UML class diagram</title>
|
||||
<desc id="uml-class-full-desc">Class diagram showing Card and BankTransfer realizing a PaymentMethod interface, PaymentService depending on PaymentMethod, and Order composed of OrderLine while associated with Customer.</desc>
|
||||
<defs>
|
||||
<marker id="uml-triangle" markerWidth="16" markerHeight="12" refX="15" refY="6" orient="auto" markerUnits="userSpaceOnUse">
|
||||
<polygon points="0 0, 16 6, 0 12" fill="#f5f5f5" stroke="#2d3142" stroke-width="1"/>
|
||||
</marker>
|
||||
<marker id="uml-diamond-filled" markerWidth="18" markerHeight="10" refX="0" refY="5" orient="auto" markerUnits="userSpaceOnUse">
|
||||
<polygon points="0 5, 9 0, 18 5, 9 10" fill="#2d3142"/>
|
||||
</marker>
|
||||
<marker id="uml-diamond-hollow" markerWidth="18" markerHeight="10" refX="0" refY="5" orient="auto" markerUnits="userSpaceOnUse">
|
||||
<polygon points="0 5, 9 0, 18 5, 9 10" fill="#f5f5f5" stroke="#2d3142" stroke-width="1"/>
|
||||
</marker>
|
||||
<marker id="uml-open" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="userSpaceOnUse">
|
||||
<path d="M0,0 L9,4 L0,8" fill="none" stroke="#4f5d75" stroke-width="1"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f5f5"/>
|
||||
|
||||
<!-- Relationships (drawn before boxes) -->
|
||||
<!-- Card realizes PaymentMethod -->
|
||||
<line x1="480" y1="240" x2="480" y2="140" stroke="#eb6c36" stroke-width="1" stroke-dasharray="5,4" marker-end="url(#uml-triangle)"/>
|
||||
<!-- BankTransfer realizes PaymentMethod -->
|
||||
<line x1="760" y1="240" x2="760" y2="140" stroke="#eb6c36" stroke-width="1" stroke-dasharray="5,4" marker-end="url(#uml-triangle)"/>
|
||||
<!-- PaymentService depends on PaymentMethod -->
|
||||
<line x1="320" y1="100" x2="420" y2="100" stroke="#4f5d75" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#uml-open)"/>
|
||||
<!-- Order composed of OrderLine -->
|
||||
<line x1="260" y1="480" x2="380" y2="480" stroke="#4f5d75" stroke-width="1" marker-start="url(#uml-diamond-filled)"/>
|
||||
<!-- Order associates Customer (routed below to clear OrderLine) -->
|
||||
<path d="M 160,540 V 572 Q 160,580 168,580 H 752 Q 760,580 760,572 V 540" fill="none" stroke="#4f5d75" stroke-width="1" marker-end="url(#uml-open)"/>
|
||||
|
||||
<!-- Dependency label -->
|
||||
<rect x="352" y="80" width="32" height="12" rx="2" fill="#f5f5f5"/>
|
||||
<text x="368" y="89" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">USES</text>
|
||||
|
||||
<!-- Composition multiplicities -->
|
||||
<rect x="288" y="474" width="12" height="12" rx="2" fill="#f5f5f5"/>
|
||||
<text x="294" y="484" fill="#2d3142" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">1</text>
|
||||
<rect x="348" y="474" width="24" height="12" rx="2" fill="#f5f5f5"/>
|
||||
<text x="360" y="484" fill="#2d3142" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">1..*</text>
|
||||
|
||||
<!-- Association multiplicities -->
|
||||
<rect x="164" y="548" width="28" height="12" rx="2" fill="#f5f5f5"/>
|
||||
<text x="178" y="557" fill="#2d3142" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">0..*</text>
|
||||
<rect x="752" y="548" width="16" height="12" rx="2" fill="#f5f5f5"/>
|
||||
<text x="760" y="557" fill="#2d3142" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">1</text>
|
||||
|
||||
<!-- Class: PaymentMethod (interface, focal) -->
|
||||
<rect x="420" y="40" width="360" height="100" rx="6" fill="rgba(235,108,54,0.04)" stroke="#eb6c36" stroke-width="1"/>
|
||||
<rect x="420" y="40" width="360" height="40" rx="6" fill="rgba(235,108,54,0.10)"/>
|
||||
<rect x="420" y="72" width="360" height="8" fill="rgba(235,108,54,0.10)"/>
|
||||
<line x1="420" y1="80" x2="780" y2="80" stroke="rgba(235,108,54,0.40)" stroke-width="1"/>
|
||||
<text x="600" y="56" fill="#eb6c36" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">«interface»</text>
|
||||
<text x="600" y="72" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">PaymentMethod</text>
|
||||
<text x="436" y="100" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">+ authorize(amount: Money): AuthResult</text>
|
||||
<text x="436" y="120" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">+ capture(ref: String): void</text>
|
||||
|
||||
<!-- Class: Card -->
|
||||
<rect x="380" y="240" width="200" height="120" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<line x1="380" y1="280" x2="580" y2="280" stroke="rgba(45,49,66,0.22)" stroke-width="1"/>
|
||||
<text x="480" y="264" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Card</text>
|
||||
<text x="396" y="300" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">- number: String</text>
|
||||
<text x="396" y="320" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">- expiry: Date</text>
|
||||
<text x="396" y="340" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">- cvv: String</text>
|
||||
|
||||
<!-- Class: BankTransfer -->
|
||||
<rect x="660" y="240" width="200" height="100" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<line x1="660" y1="280" x2="860" y2="280" stroke="rgba(45,49,66,0.22)" stroke-width="1"/>
|
||||
<text x="760" y="264" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">BankTransfer</text>
|
||||
<text x="676" y="300" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">- iban: String</text>
|
||||
<text x="676" y="320" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">- accountHolder: String</text>
|
||||
|
||||
<!-- Class: PaymentService -->
|
||||
<rect x="40" y="60" width="280" height="80" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<line x1="40" y1="100" x2="320" y2="100" stroke="rgba(45,49,66,0.22)" stroke-width="1"/>
|
||||
<text x="180" y="84" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">PaymentService</text>
|
||||
<text x="56" y="120" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">+ charge(order: Order): Receipt</text>
|
||||
|
||||
<!-- Class: Order -->
|
||||
<rect x="60" y="420" width="200" height="120" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<line x1="60" y1="460" x2="260" y2="460" stroke="rgba(45,49,66,0.22)" stroke-width="1"/>
|
||||
<text x="160" y="444" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Order</text>
|
||||
<text x="76" y="480" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">- id: String</text>
|
||||
<text x="76" y="500" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">- placedAt: Date</text>
|
||||
<text x="76" y="520" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">- total: Money</text>
|
||||
|
||||
<!-- Class: OrderLine -->
|
||||
<rect x="380" y="420" width="200" height="120" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<line x1="380" y1="460" x2="580" y2="460" stroke="rgba(45,49,66,0.22)" stroke-width="1"/>
|
||||
<text x="480" y="444" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">OrderLine</text>
|
||||
<text x="396" y="480" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">- sku: String</text>
|
||||
<text x="396" y="500" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">- qty: Int</text>
|
||||
<text x="396" y="520" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">- price: Money</text>
|
||||
|
||||
<!-- Class: Customer -->
|
||||
<rect x="660" y="420" width="200" height="120" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<line x1="660" y1="460" x2="860" y2="460" stroke="rgba(45,49,66,0.22)" stroke-width="1"/>
|
||||
<text x="760" y="444" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Customer</text>
|
||||
<text x="676" y="480" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">- id: String</text>
|
||||
<text x="676" y="500" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">- name: String</text>
|
||||
<text x="676" y="520" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">- email: String</text>
|
||||
|
||||
<!-- Legend: relationship vocabulary -->
|
||||
<line x1="40" y1="604" x2="940" y2="604" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="620" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND · RELATIONSHIPS</text>
|
||||
|
||||
<line x1="40" y1="636" x2="68" y2="636" stroke="#2d3142" stroke-width="1" marker-end="url(#uml-triangle)"/>
|
||||
<text x="76" y="640" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Inheritance</text>
|
||||
|
||||
<line x1="188" y1="636" x2="216" y2="636" stroke="#2d3142" stroke-width="1" stroke-dasharray="5,4" marker-end="url(#uml-triangle)"/>
|
||||
<text x="224" y="640" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Realization</text>
|
||||
|
||||
<line x1="336" y1="636" x2="364" y2="636" stroke="#2d3142" stroke-width="1" marker-start="url(#uml-diamond-filled)"/>
|
||||
<text x="372" y="640" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Composition · owns</text>
|
||||
|
||||
<line x1="484" y1="636" x2="512" y2="636" stroke="#2d3142" stroke-width="1" marker-start="url(#uml-diamond-hollow)"/>
|
||||
<text x="520" y="640" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Aggregation · has</text>
|
||||
|
||||
<line x1="632" y1="636" x2="660" y2="636" stroke="#4f5d75" stroke-width="1" marker-end="url(#uml-open)"/>
|
||||
<text x="668" y="640" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Association</text>
|
||||
|
||||
<line x1="780" y1="636" x2="808" y2="636" stroke="#4f5d75" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#uml-open)"/>
|
||||
<text x="816" y="640" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Dependency · uses</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<p class="eyebrow">THE HEADLINE</p>
|
||||
<div class="card-header"><span class="card-dot coral"></span><h3>PaymentMethod is the contract</h3></div>
|
||||
<p>Card and BankTransfer both realize the interface — the dashed line with the hollow triangle. PaymentService only ever talks to the interface, never to a concrete method.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot ink"></span><h3>Composition means it dies with the whole</h3></div>
|
||||
<ul><li>OrderLine has no life outside an Order</li><li>Filled diamond sits at Order, the owner</li><li>Order also just references Customer — no shared lifecycle there</li></ul>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot muted"></span><h3>What's deliberately left out</h3></div>
|
||||
<p>No getters or setters, no full attribute lists. Every member shown earns its place in the argument this diagram is making.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span>Payments domain · UML class diagram</span>
|
||||
<span>example · diagram design</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,188 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Payments domain · UML class diagram</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #f5f5f5;
|
||||
--color-ink: #2d3142;
|
||||
--color-muted: #4f5d75;
|
||||
--color-accent: #eb6c36;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1240px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">UML class · Diagram Design</p>
|
||||
<h1>Payments domain · class diagram</h1>
|
||||
|
||||
<svg viewBox="0 0 980 664" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="uml-class-title uml-class-desc">
|
||||
<title id="uml-class-title">Payments domain · UML class diagram</title>
|
||||
<desc id="uml-class-desc">Class diagram showing Card and BankTransfer realizing a PaymentMethod interface, PaymentService depending on PaymentMethod, and Order composed of OrderLine while associated with Customer.</desc>
|
||||
<defs>
|
||||
<marker id="uml-triangle" markerWidth="16" markerHeight="12" refX="15" refY="6" orient="auto" markerUnits="userSpaceOnUse">
|
||||
<polygon points="0 0, 16 6, 0 12" fill="#f5f5f5" stroke="#2d3142" stroke-width="1"/>
|
||||
</marker>
|
||||
<marker id="uml-diamond-filled" markerWidth="18" markerHeight="10" refX="0" refY="5" orient="auto" markerUnits="userSpaceOnUse">
|
||||
<polygon points="0 5, 9 0, 18 5, 9 10" fill="#2d3142"/>
|
||||
</marker>
|
||||
<marker id="uml-diamond-hollow" markerWidth="18" markerHeight="10" refX="0" refY="5" orient="auto" markerUnits="userSpaceOnUse">
|
||||
<polygon points="0 5, 9 0, 18 5, 9 10" fill="#f5f5f5" stroke="#2d3142" stroke-width="1"/>
|
||||
</marker>
|
||||
<marker id="uml-open" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="userSpaceOnUse">
|
||||
<path d="M0,0 L9,4 L0,8" fill="none" stroke="#4f5d75" stroke-width="1"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f5f5"/>
|
||||
|
||||
<!-- Relationships (drawn before boxes) -->
|
||||
<!-- Card realizes PaymentMethod -->
|
||||
<line x1="480" y1="240" x2="480" y2="140" stroke="#eb6c36" stroke-width="1" stroke-dasharray="5,4" marker-end="url(#uml-triangle)"/>
|
||||
<!-- BankTransfer realizes PaymentMethod -->
|
||||
<line x1="760" y1="240" x2="760" y2="140" stroke="#eb6c36" stroke-width="1" stroke-dasharray="5,4" marker-end="url(#uml-triangle)"/>
|
||||
<!-- PaymentService depends on PaymentMethod -->
|
||||
<line x1="320" y1="100" x2="420" y2="100" stroke="#4f5d75" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#uml-open)"/>
|
||||
<!-- Order composed of OrderLine -->
|
||||
<line x1="260" y1="480" x2="380" y2="480" stroke="#4f5d75" stroke-width="1" marker-start="url(#uml-diamond-filled)"/>
|
||||
<!-- Order associates Customer (routed below to clear OrderLine) -->
|
||||
<path d="M 160,540 V 572 Q 160,580 168,580 H 752 Q 760,580 760,572 V 540" fill="none" stroke="#4f5d75" stroke-width="1" marker-end="url(#uml-open)"/>
|
||||
|
||||
<!-- Dependency label -->
|
||||
<rect x="352" y="80" width="32" height="12" rx="2" fill="#f5f5f5"/>
|
||||
<text x="368" y="89" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.06em">USES</text>
|
||||
|
||||
<!-- Composition multiplicities -->
|
||||
<rect x="288" y="474" width="12" height="12" rx="2" fill="#f5f5f5"/>
|
||||
<text x="294" y="484" fill="#2d3142" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">1</text>
|
||||
<rect x="348" y="474" width="24" height="12" rx="2" fill="#f5f5f5"/>
|
||||
<text x="360" y="484" fill="#2d3142" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">1..*</text>
|
||||
|
||||
<!-- Association multiplicities -->
|
||||
<rect x="164" y="548" width="28" height="12" rx="2" fill="#f5f5f5"/>
|
||||
<text x="178" y="557" fill="#2d3142" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">0..*</text>
|
||||
<rect x="752" y="548" width="16" height="12" rx="2" fill="#f5f5f5"/>
|
||||
<text x="760" y="557" fill="#2d3142" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" font-weight="600">1</text>
|
||||
|
||||
<!-- Class: PaymentMethod (interface, focal) -->
|
||||
<rect x="420" y="40" width="360" height="100" rx="6" fill="rgba(235,108,54,0.04)" stroke="#eb6c36" stroke-width="1"/>
|
||||
<rect x="420" y="40" width="360" height="40" rx="6" fill="rgba(235,108,54,0.10)"/>
|
||||
<rect x="420" y="72" width="360" height="8" fill="rgba(235,108,54,0.10)"/>
|
||||
<line x1="420" y1="80" x2="780" y2="80" stroke="rgba(235,108,54,0.40)" stroke-width="1"/>
|
||||
<text x="600" y="56" fill="#eb6c36" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.08em">«interface»</text>
|
||||
<text x="600" y="72" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">PaymentMethod</text>
|
||||
<text x="436" y="100" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">+ authorize(amount: Money): AuthResult</text>
|
||||
<text x="436" y="120" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">+ capture(ref: String): void</text>
|
||||
|
||||
<!-- Class: Card -->
|
||||
<rect x="380" y="240" width="200" height="120" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<line x1="380" y1="280" x2="580" y2="280" stroke="rgba(45,49,66,0.22)" stroke-width="1"/>
|
||||
<text x="480" y="264" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Card</text>
|
||||
<text x="396" y="300" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">- number: String</text>
|
||||
<text x="396" y="320" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">- expiry: Date</text>
|
||||
<text x="396" y="340" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">- cvv: String</text>
|
||||
|
||||
<!-- Class: BankTransfer -->
|
||||
<rect x="660" y="240" width="200" height="100" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<line x1="660" y1="280" x2="860" y2="280" stroke="rgba(45,49,66,0.22)" stroke-width="1"/>
|
||||
<text x="760" y="264" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">BankTransfer</text>
|
||||
<text x="676" y="300" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">- iban: String</text>
|
||||
<text x="676" y="320" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">- accountHolder: String</text>
|
||||
|
||||
<!-- Class: PaymentService -->
|
||||
<rect x="40" y="60" width="280" height="80" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<line x1="40" y1="100" x2="320" y2="100" stroke="rgba(45,49,66,0.22)" stroke-width="1"/>
|
||||
<text x="180" y="84" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">PaymentService</text>
|
||||
<text x="56" y="120" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">+ charge(order: Order): Receipt</text>
|
||||
|
||||
<!-- Class: Order -->
|
||||
<rect x="60" y="420" width="200" height="120" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<line x1="60" y1="460" x2="260" y2="460" stroke="rgba(45,49,66,0.22)" stroke-width="1"/>
|
||||
<text x="160" y="444" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Order</text>
|
||||
<text x="76" y="480" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">- id: String</text>
|
||||
<text x="76" y="500" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">- placedAt: Date</text>
|
||||
<text x="76" y="520" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">- total: Money</text>
|
||||
|
||||
<!-- Class: OrderLine -->
|
||||
<rect x="380" y="420" width="200" height="120" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<line x1="380" y1="460" x2="580" y2="460" stroke="rgba(45,49,66,0.22)" stroke-width="1"/>
|
||||
<text x="480" y="444" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">OrderLine</text>
|
||||
<text x="396" y="480" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">- sku: String</text>
|
||||
<text x="396" y="500" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">- qty: Int</text>
|
||||
<text x="396" y="520" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">- price: Money</text>
|
||||
|
||||
<!-- Class: Customer -->
|
||||
<rect x="660" y="420" width="200" height="120" rx="6" fill="#ffffff" stroke="#2d3142" stroke-width="1"/>
|
||||
<line x1="660" y1="460" x2="860" y2="460" stroke="rgba(45,49,66,0.22)" stroke-width="1"/>
|
||||
<text x="760" y="444" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Customer</text>
|
||||
<text x="676" y="480" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">- id: String</text>
|
||||
<text x="676" y="500" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">- name: String</text>
|
||||
<text x="676" y="520" fill="#2d3142" font-size="9" font-family="'Geist Mono', monospace">- email: String</text>
|
||||
|
||||
<!-- Legend: relationship vocabulary -->
|
||||
<line x1="40" y1="604" x2="940" y2="604" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="620" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND · RELATIONSHIPS</text>
|
||||
|
||||
<line x1="40" y1="636" x2="68" y2="636" stroke="#2d3142" stroke-width="1" marker-end="url(#uml-triangle)"/>
|
||||
<text x="76" y="640" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Inheritance</text>
|
||||
|
||||
<line x1="188" y1="636" x2="216" y2="636" stroke="#2d3142" stroke-width="1" stroke-dasharray="5,4" marker-end="url(#uml-triangle)"/>
|
||||
<text x="224" y="640" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Realization</text>
|
||||
|
||||
<line x1="336" y1="636" x2="364" y2="636" stroke="#2d3142" stroke-width="1" marker-start="url(#uml-diamond-filled)"/>
|
||||
<text x="372" y="640" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Composition · owns</text>
|
||||
|
||||
<line x1="484" y1="636" x2="512" y2="636" stroke="#2d3142" stroke-width="1" marker-start="url(#uml-diamond-hollow)"/>
|
||||
<text x="520" y="640" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Aggregation · has</text>
|
||||
|
||||
<line x1="632" y1="636" x2="660" y2="636" stroke="#4f5d75" stroke-width="1" marker-end="url(#uml-open)"/>
|
||||
<text x="668" y="640" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Association</text>
|
||||
|
||||
<line x1="780" y1="636" x2="808" y2="636" stroke="#4f5d75" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#uml-open)"/>
|
||||
<text x="816" y="640" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Dependency · uses</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,153 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI assistant product · Wardley map</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #2d3142;
|
||||
--color-ink: #f5f5f5;
|
||||
--color-muted: #bfc0c0;
|
||||
--color-accent: #f08a59;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Wardley map · Diagram Design</p>
|
||||
<h1>AI assistant product · Wardley map</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 460" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="wardley-dark-title wardley-dark-desc">
|
||||
<title id="wardley-dark-title">AI assistant product · Wardley map</title>
|
||||
<desc id="wardley-dark-desc">Wardley map plotting an AI assistant product's value chain from the user-facing need down to raw infrastructure against genesis-to-commodity evolution, with agent orchestration commoditising toward the product band.</desc>
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(245,245,245,0.10)"/>
|
||||
</pattern>
|
||||
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#f08a59"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#2d3142"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Axis lines (value chain vertical, evolution horizontal) -->
|
||||
<line x1="120" y1="60" x2="120" y2="340" stroke="rgba(245,245,245,0.20)" stroke-width="1"/>
|
||||
<line x1="120" y1="340" x2="920" y2="340" stroke="rgba(245,245,245,0.20)" stroke-width="1"/>
|
||||
|
||||
<!-- Evolution band separators -->
|
||||
<line x1="320" y1="60" x2="320" y2="340" stroke="rgba(245,245,245,0.10)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<line x1="520" y1="60" x2="520" y2="340" stroke="rgba(245,245,245,0.10)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<line x1="720" y1="60" x2="720" y2="340" stroke="rgba(245,245,245,0.10)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
|
||||
<!-- Band labels -->
|
||||
<text x="220" y="356" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.18em">GENESIS</text>
|
||||
<text x="420" y="356" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.18em">CUSTOM-BUILT</text>
|
||||
<text x="620" y="356" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.18em">PRODUCT</text>
|
||||
<text x="820" y="356" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.18em">COMMODITY</text>
|
||||
|
||||
<!-- Value-chain axis labels (stacked lines, never writing-mode) -->
|
||||
<text x="112" y="44" fill="#f5f5f5" font-size="9" font-weight="500" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.14em">VISIBLE TO</text>
|
||||
<text x="112" y="56" fill="#f5f5f5" font-size="9" font-weight="500" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.14em">THE USER</text>
|
||||
<text x="112" y="352" fill="#f5f5f5" font-size="9" font-weight="500" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.14em">INVISIBLE</text>
|
||||
|
||||
<!-- Dependency links (exempt from the orthogonal-elbow rule — position is the data) -->
|
||||
<line x1="420" y1="92" x2="620" y2="124" stroke="#bfc0c0" stroke-width="0.8"/>
|
||||
<line x1="620" y1="124" x2="420" y2="156" stroke="#bfc0c0" stroke-width="0.8"/>
|
||||
<line x1="420" y1="156" x2="220" y2="188" stroke="#bfc0c0" stroke-width="0.8"/>
|
||||
<line x1="420" y1="156" x2="620" y2="220" stroke="#bfc0c0" stroke-width="0.8"/>
|
||||
<line x1="420" y1="156" x2="680" y2="252" stroke="#bfc0c0" stroke-width="0.8"/>
|
||||
<line x1="620" y1="220" x2="820" y2="316" stroke="#bfc0c0" stroke-width="0.8"/>
|
||||
<line x1="680" y1="252" x2="820" y2="284" stroke="#bfc0c0" stroke-width="0.8"/>
|
||||
<line x1="220" y1="188" x2="680" y2="252" stroke="#bfc0c0" stroke-width="0.8"/>
|
||||
|
||||
<!-- Movement arrow (the 2nd accent element) -->
|
||||
<line x1="428" y1="156" x2="504" y2="156" stroke="#f08a59" stroke-width="1.5" stroke-dasharray="5,4" marker-end="url(#arrow-accent)"/>
|
||||
|
||||
<!-- Components -->
|
||||
<circle cx="420" cy="92" r="6" fill="#2d3142" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<text x="420" y="80" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Answer in the flow of work</text>
|
||||
|
||||
<circle cx="620" cy="124" r="6" fill="#2d3142" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<text x="620" y="112" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Chat UI</text>
|
||||
|
||||
<!-- Moving component (the 1st accent element) -->
|
||||
<circle cx="420" cy="156" r="6" fill="rgba(240,138,89,0.10)" stroke="#f08a59" stroke-width="1.5"/>
|
||||
<text x="420" y="144" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Agent orchestration</text>
|
||||
|
||||
<circle cx="220" cy="188" r="6" fill="#2d3142" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<text x="220" y="176" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Eval harness</text>
|
||||
|
||||
<circle cx="620" cy="220" r="6" fill="#2d3142" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<text x="620" y="208" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Vector store</text>
|
||||
|
||||
<circle cx="680" cy="252" r="6" fill="#2d3142" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<text x="680" y="240" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">LLM API</text>
|
||||
|
||||
<circle cx="820" cy="284" r="6" fill="#2d3142" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<text x="820" y="272" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">GPU compute</text>
|
||||
|
||||
<circle cx="820" cy="316" r="6" fill="#2d3142" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<text x="836" y="320" fill="#f5f5f5" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="start">Object storage</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="396" x2="960" y2="396" stroke="rgba(245,245,245,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="412" fill="#bfc0c0" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<circle cx="48" cy="428" r="4" fill="#2d3142" stroke="#f5f5f5" stroke-width="1"/>
|
||||
<text x="64" y="432" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Component</text>
|
||||
|
||||
<line x1="220" y1="428" x2="248" y2="428" stroke="#bfc0c0" stroke-width="0.8"/>
|
||||
<text x="256" y="432" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Dependency</text>
|
||||
|
||||
<circle cx="404" cy="428" r="4" fill="rgba(240,138,89,0.10)" stroke="#f08a59" stroke-width="1.5"/>
|
||||
<line x1="416" y1="428" x2="440" y2="428" stroke="#f08a59" stroke-width="1.5" stroke-dasharray="5,4"/>
|
||||
<text x="448" y="432" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif">Evolving</text>
|
||||
|
||||
<text x="560" y="432" fill="#bfc0c0" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">One coral. Position along evolution is the signal — the arrow marks what's about to commoditise.</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,146 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI assistant product · Wardley map</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root { --color-paper:#f5f5f5; --color-paper-2:#ececec; --color-ink:#2d3142; --color-muted:#4f5d75; --color-soft:#7a8399; --color-rule:rgba(45,49,66,0.12); --color-accent:#eb6c36; --font-sans:'Geist',system-ui,sans-serif; --font-serif:'Instrument Serif',serif; --font-mono:'Geist Mono',ui-monospace,monospace; }
|
||||
body { font-family: var(--font-sans); background: var(--color-paper); min-height: 100vh; padding: 3rem 2rem; color: var(--color-ink); }
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
.header { margin-bottom: 2.5rem; }
|
||||
.header-eyebrow { font-family: var(--font-mono); font-size: 0.66rem; font-weight: 500; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.75rem; }
|
||||
h1 { font-family: var(--font-serif); font-size: clamp(1.75rem, 3vw + 1rem, 2.5rem); font-weight: 400; letter-spacing: -0.02em; line-height: 1.1; margin-bottom: 0.5rem; }
|
||||
.subtitle { font-size: 1rem; line-height: 1.55; color: var(--color-muted); max-width: 58ch; }
|
||||
.diagram-container { background: var(--color-paper-2); border-radius: 8px; border: 1px solid var(--color-rule); padding: 1.5rem; overflow-x: auto; }
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
.cards { display: grid; grid-template-columns: 1.1fr 1fr 0.9fr; gap: 1rem; margin-top: 1.5rem; }
|
||||
@media (max-width: 900px) { .cards { grid-template-columns: 1fr 1fr; } }
|
||||
@media (max-width: 600px) { .cards { grid-template-columns: 1fr; } }
|
||||
.card { background: #fff; border-radius: 6px; border: 1px solid var(--color-rule); padding: 1.25rem; }
|
||||
.card .eyebrow { font-family: var(--font-mono); font-size: 0.5rem; letter-spacing: 0.18em; text-transform: uppercase; color: var(--color-muted); margin-bottom: 0.5rem; }
|
||||
.card-header { display: flex; align-items: center; gap: 0.6rem; margin-bottom: 0.875rem; padding-bottom: 0.875rem; border-bottom: 1px solid rgba(45,49,66,0.08); }
|
||||
.card-dot { width: 7px; height: 7px; border-radius: 50%; }
|
||||
.card-dot.coral { background: var(--color-accent); }
|
||||
.card-dot.ink { background: var(--color-ink); }
|
||||
.card-dot.muted { background: var(--color-muted); }
|
||||
.card h3 { font-size: 0.875rem; font-weight: 600; }
|
||||
.card p { color: var(--color-muted); font-size: 0.8125rem; line-height: 1.55; }
|
||||
.footer { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid rgba(45,49,66,0.10); font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.06em; color: var(--color-soft); display: flex; justify-content: space-between; flex-wrap: wrap; gap: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<p class="header-eyebrow">Wardley map · Diagram Design</p>
|
||||
<h1>AI assistant product · Wardley map</h1>
|
||||
<p class="subtitle">The value chain behind an AI assistant, from the user-facing need down to raw infrastructure, positioned against how evolved each component is. The one arrow is the map's point: agent orchestration is commoditising, so betting engineering time on a bespoke orchestrator is a shrinking advantage.</p>
|
||||
</div>
|
||||
|
||||
<div class="diagram-container">
|
||||
<svg viewBox="0 0 1000 460" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="wardley-full-title wardley-full-desc">
|
||||
<title id="wardley-full-title">AI assistant product · Wardley map</title>
|
||||
<desc id="wardley-full-desc">Wardley map plotting an AI assistant product's value chain from the user-facing need down to raw infrastructure against genesis-to-commodity evolution, with agent orchestration commoditising toward the product band.</desc>
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(45,49,66,0.10)"/>
|
||||
</pattern>
|
||||
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#eb6c36"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f5f5"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<line x1="120" y1="60" x2="120" y2="340" stroke="rgba(45,49,66,0.20)" stroke-width="1"/>
|
||||
<line x1="120" y1="340" x2="920" y2="340" stroke="rgba(45,49,66,0.20)" stroke-width="1"/>
|
||||
|
||||
<line x1="320" y1="60" x2="320" y2="340" stroke="rgba(45,49,66,0.10)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<line x1="520" y1="60" x2="520" y2="340" stroke="rgba(45,49,66,0.10)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<line x1="720" y1="60" x2="720" y2="340" stroke="rgba(45,49,66,0.10)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
|
||||
<text x="220" y="356" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.18em">GENESIS</text>
|
||||
<text x="420" y="356" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.18em">CUSTOM-BUILT</text>
|
||||
<text x="620" y="356" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.18em">PRODUCT</text>
|
||||
<text x="820" y="356" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.18em">COMMODITY</text>
|
||||
|
||||
<text x="112" y="44" fill="#2d3142" font-size="9" font-weight="500" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.14em">VISIBLE TO</text>
|
||||
<text x="112" y="56" fill="#2d3142" font-size="9" font-weight="500" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.14em">THE USER</text>
|
||||
<text x="112" y="352" fill="#2d3142" font-size="9" font-weight="500" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.14em">INVISIBLE</text>
|
||||
|
||||
<line x1="420" y1="92" x2="620" y2="124" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<line x1="620" y1="124" x2="420" y2="156" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<line x1="420" y1="156" x2="220" y2="188" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<line x1="420" y1="156" x2="620" y2="220" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<line x1="420" y1="156" x2="680" y2="252" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<line x1="620" y1="220" x2="820" y2="316" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<line x1="680" y1="252" x2="820" y2="284" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<line x1="220" y1="188" x2="680" y2="252" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
|
||||
<line x1="428" y1="156" x2="504" y2="156" stroke="#eb6c36" stroke-width="1.5" stroke-dasharray="5,4" marker-end="url(#arrow-accent)"/>
|
||||
|
||||
<circle cx="420" cy="92" r="6" fill="#f5f5f5" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="420" y="80" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Answer in the flow of work</text>
|
||||
|
||||
<circle cx="620" cy="124" r="6" fill="#f5f5f5" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="620" y="112" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Chat UI</text>
|
||||
|
||||
<circle cx="420" cy="156" r="6" fill="rgba(235,108,54,0.08)" stroke="#eb6c36" stroke-width="1.5"/>
|
||||
<text x="420" y="144" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Agent orchestration</text>
|
||||
|
||||
<circle cx="220" cy="188" r="6" fill="#f5f5f5" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="220" y="176" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Eval harness</text>
|
||||
|
||||
<circle cx="620" cy="220" r="6" fill="#f5f5f5" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="620" y="208" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Vector store</text>
|
||||
|
||||
<circle cx="680" cy="252" r="6" fill="#f5f5f5" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="680" y="240" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">LLM API</text>
|
||||
|
||||
<circle cx="820" cy="284" r="6" fill="#f5f5f5" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="820" y="272" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">GPU compute</text>
|
||||
|
||||
<circle cx="820" cy="316" r="6" fill="#f5f5f5" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="836" y="320" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="start">Object storage</text>
|
||||
|
||||
<line x1="40" y1="396" x2="960" y2="396" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="412" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<circle cx="48" cy="428" r="4" fill="#f5f5f5" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="64" y="432" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Component</text>
|
||||
|
||||
<line x1="220" y1="428" x2="248" y2="428" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<text x="256" y="432" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Dependency</text>
|
||||
|
||||
<circle cx="404" cy="428" r="4" fill="rgba(235,108,54,0.08)" stroke="#eb6c36" stroke-width="1.5"/>
|
||||
<line x1="416" y1="428" x2="440" y2="428" stroke="#eb6c36" stroke-width="1.5" stroke-dasharray="5,4"/>
|
||||
<text x="448" y="432" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Evolving</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<p class="eyebrow">MOVEMENT</p>
|
||||
<div class="card-header"><span class="card-dot coral"></span><h3>Agent orchestration</h3></div>
|
||||
<p>Still custom-built today, but the open-source orchestration frameworks maturing this year push it toward product. Betting differentiation here has a shrinking half-life.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot ink"></span><h3>Eval harness</h3></div>
|
||||
<p>Genesis-stage and specific to this product's failure modes. No off-the-shelf harness captures what "good" means for this assistant yet — worth the custom build.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header"><span class="card-dot muted"></span><h3>GPU compute · Object storage</h3></div>
|
||||
<p>Fully commoditised. Buy, don't build — any engineering time spent here is time not spent on the parts of the chain that are still genesis or custom-built.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<span>ai assistant product · wardley map</span>
|
||||
<span>example · diagram design</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,153 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI assistant product · Wardley map</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
:root {
|
||||
--color-paper: #f5f5f5;
|
||||
--color-ink: #2d3142;
|
||||
--color-muted: #4f5d75;
|
||||
--color-accent: #eb6c36;
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-sans);
|
||||
background: var(--color-paper);
|
||||
color: var(--color-ink);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
|
||||
.frame { max-width: 1200px; width: 100%; }
|
||||
|
||||
.eyebrow {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-serif);
|
||||
font-size: clamp(1.5rem, 2.4vw + 0.75rem, 2rem);
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.15;
|
||||
color: var(--color-ink);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
svg { width: 100%; min-width: 900px; display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="frame">
|
||||
<p class="eyebrow">Wardley map · Diagram Design</p>
|
||||
<h1>AI assistant product · Wardley map</h1>
|
||||
|
||||
<svg viewBox="0 0 1000 460" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="wardley-title wardley-desc">
|
||||
<title id="wardley-title">AI assistant product · Wardley map</title>
|
||||
<desc id="wardley-desc">Wardley map plotting an AI assistant product's value chain from the user-facing need down to raw infrastructure against genesis-to-commodity evolution, with agent orchestration commoditising toward the product band.</desc>
|
||||
<defs>
|
||||
<pattern id="dots" width="22" height="22" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.9" fill="rgba(45,49,66,0.10)"/>
|
||||
</pattern>
|
||||
<marker id="arrow-accent" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#eb6c36"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="100%" height="100%" fill="#f5f5f5"/>
|
||||
<rect width="100%" height="100%" fill="url(#dots)" opacity="0.55"/>
|
||||
|
||||
<!-- Axis lines (value chain vertical, evolution horizontal) -->
|
||||
<line x1="120" y1="60" x2="120" y2="340" stroke="rgba(45,49,66,0.20)" stroke-width="1"/>
|
||||
<line x1="120" y1="340" x2="920" y2="340" stroke="rgba(45,49,66,0.20)" stroke-width="1"/>
|
||||
|
||||
<!-- Evolution band separators -->
|
||||
<line x1="320" y1="60" x2="320" y2="340" stroke="rgba(45,49,66,0.10)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<line x1="520" y1="60" x2="520" y2="340" stroke="rgba(45,49,66,0.10)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
<line x1="720" y1="60" x2="720" y2="340" stroke="rgba(45,49,66,0.10)" stroke-width="0.8" stroke-dasharray="4,4"/>
|
||||
|
||||
<!-- Band labels -->
|
||||
<text x="220" y="356" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.18em">GENESIS</text>
|
||||
<text x="420" y="356" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.18em">CUSTOM-BUILT</text>
|
||||
<text x="620" y="356" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.18em">PRODUCT</text>
|
||||
<text x="820" y="356" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" text-anchor="middle" letter-spacing="0.18em">COMMODITY</text>
|
||||
|
||||
<!-- Value-chain axis labels (stacked lines, never writing-mode) -->
|
||||
<text x="112" y="44" fill="#2d3142" font-size="9" font-weight="500" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.14em">VISIBLE TO</text>
|
||||
<text x="112" y="56" fill="#2d3142" font-size="9" font-weight="500" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.14em">THE USER</text>
|
||||
<text x="112" y="352" fill="#2d3142" font-size="9" font-weight="500" font-family="'Geist Mono', monospace" text-anchor="end" letter-spacing="0.14em">INVISIBLE</text>
|
||||
|
||||
<!-- Dependency links (exempt from the orthogonal-elbow rule — position is the data) -->
|
||||
<line x1="420" y1="92" x2="620" y2="124" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<line x1="620" y1="124" x2="420" y2="156" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<line x1="420" y1="156" x2="220" y2="188" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<line x1="420" y1="156" x2="620" y2="220" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<line x1="420" y1="156" x2="680" y2="252" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<line x1="620" y1="220" x2="820" y2="316" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<line x1="680" y1="252" x2="820" y2="284" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<line x1="220" y1="188" x2="680" y2="252" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
|
||||
<!-- Movement arrow (the 2nd accent element) -->
|
||||
<line x1="428" y1="156" x2="504" y2="156" stroke="#eb6c36" stroke-width="1.5" stroke-dasharray="5,4" marker-end="url(#arrow-accent)"/>
|
||||
|
||||
<!-- Components -->
|
||||
<circle cx="420" cy="92" r="6" fill="#f5f5f5" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="420" y="80" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Answer in the flow of work</text>
|
||||
|
||||
<circle cx="620" cy="124" r="6" fill="#f5f5f5" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="620" y="112" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Chat UI</text>
|
||||
|
||||
<!-- Moving component (the 1st accent element) -->
|
||||
<circle cx="420" cy="156" r="6" fill="rgba(235,108,54,0.08)" stroke="#eb6c36" stroke-width="1.5"/>
|
||||
<text x="420" y="144" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Agent orchestration</text>
|
||||
|
||||
<circle cx="220" cy="188" r="6" fill="#f5f5f5" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="220" y="176" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Eval harness</text>
|
||||
|
||||
<circle cx="620" cy="220" r="6" fill="#f5f5f5" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="620" y="208" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">Vector store</text>
|
||||
|
||||
<circle cx="680" cy="252" r="6" fill="#f5f5f5" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="680" y="240" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">LLM API</text>
|
||||
|
||||
<circle cx="820" cy="284" r="6" fill="#f5f5f5" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="820" y="272" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="middle">GPU compute</text>
|
||||
|
||||
<circle cx="820" cy="316" r="6" fill="#f5f5f5" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="836" y="320" fill="#2d3142" font-size="12" font-weight="600" font-family="'Geist', sans-serif" text-anchor="start">Object storage</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<line x1="40" y1="396" x2="960" y2="396" stroke="rgba(45,49,66,0.10)" stroke-width="0.8"/>
|
||||
<text x="40" y="412" fill="#4f5d75" font-size="8" font-family="'Geist Mono', monospace" letter-spacing="0.18em">LEGEND</text>
|
||||
|
||||
<circle cx="48" cy="428" r="4" fill="#f5f5f5" stroke="#2d3142" stroke-width="1"/>
|
||||
<text x="64" y="432" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Component</text>
|
||||
|
||||
<line x1="220" y1="428" x2="248" y2="428" stroke="#4f5d75" stroke-width="0.8"/>
|
||||
<text x="256" y="432" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Dependency</text>
|
||||
|
||||
<circle cx="404" cy="428" r="4" fill="rgba(235,108,54,0.08)" stroke="#eb6c36" stroke-width="1.5"/>
|
||||
<line x1="416" y1="428" x2="440" y2="428" stroke="#eb6c36" stroke-width="1.5" stroke-dasharray="5,4"/>
|
||||
<text x="448" y="432" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif">Evolving</text>
|
||||
|
||||
<text x="560" y="432" fill="#4f5d75" font-size="8.5" font-family="'Geist', sans-serif" font-style="italic">One coral. Position along evolution is the signal — the arrow marks what's about to commoditise.</text>
|
||||
</svg>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -288,29 +288,59 @@
|
||||
<button class="tab new" data-type="it-state">
|
||||
<span class="eyebrow">33</span>IT current-state
|
||||
</button>
|
||||
<button class="tab new" data-type="sankey">
|
||||
<span class="eyebrow">34</span>Sankey
|
||||
</button>
|
||||
<button class="tab new" data-type="fishbone">
|
||||
<span class="eyebrow">35</span>Fishbone
|
||||
</button>
|
||||
<button class="tab new" data-type="wardley">
|
||||
<span class="eyebrow">36</span>Wardley map
|
||||
</button>
|
||||
<button class="tab new" data-type="kanban">
|
||||
<span class="eyebrow">37</span>Kanban
|
||||
</button>
|
||||
<button class="tab new" data-type="journey">
|
||||
<span class="eyebrow">38</span>User journey
|
||||
</button>
|
||||
<button class="tab new" data-type="deployment">
|
||||
<span class="eyebrow">39</span>Deployment
|
||||
</button>
|
||||
<button class="tab new" data-type="dependency">
|
||||
<span class="eyebrow">40</span>Dependency graph
|
||||
</button>
|
||||
<button class="tab new" data-type="uml-class">
|
||||
<span class="eyebrow">41</span>UML class
|
||||
</button>
|
||||
<button class="tab new" data-type="story-map">
|
||||
<span class="eyebrow">42</span>Story map
|
||||
</button>
|
||||
<button class="tab new" data-type="db-schema">
|
||||
<span class="eyebrow">43</span>Database schema
|
||||
</button>
|
||||
<button class="tab new" data-type="sequence-oauth">
|
||||
<span class="eyebrow">34</span>Sequence · OAuth
|
||||
<span class="eyebrow">44</span>Sequence · OAuth
|
||||
</button>
|
||||
<button class="tab new" data-type="import-drawio" data-single>
|
||||
<span class="eyebrow">35</span>Import · draw.io
|
||||
<span class="eyebrow">45</span>Import · draw.io
|
||||
</button>
|
||||
<button class="tab new" data-type="import-mermaid" data-single>
|
||||
<span class="eyebrow">36</span>Import · Mermaid
|
||||
<span class="eyebrow">46</span>Import · Mermaid
|
||||
</button>
|
||||
<button class="tab new" data-type="quadrant-consultant" data-single>
|
||||
<span class="eyebrow">37</span>Quadrant · consultant
|
||||
<span class="eyebrow">47</span>Quadrant · consultant
|
||||
</button>
|
||||
<button class="tab new" data-type="loop-terminal" data-single>
|
||||
<span class="eyebrow">38</span>Loop · terminal
|
||||
<span class="eyebrow">48</span>Loop · terminal
|
||||
</button>
|
||||
<button class="tab new" data-type="policy-trace-animated" data-single>
|
||||
<span class="eyebrow">39</span>Policy trace · animated
|
||||
<span class="eyebrow">49</span>Policy trace · animated
|
||||
</button>
|
||||
<button class="tab new" data-type="queue-animated" data-single>
|
||||
<span class="eyebrow">40</span>Fan-in queue · animated
|
||||
<span class="eyebrow">50</span>Fan-in queue · animated
|
||||
</button>
|
||||
<button class="tab new" data-type="paved-road-animated" data-single>
|
||||
<span class="eyebrow">41</span>Secure paved road · animated
|
||||
<span class="eyebrow">51</span>Secure paved road · animated
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ Write the new tokens to `style-guide.md`. Suggest running the `/regenerate-examp
|
||||
|
||||
After onboarding, the user should:
|
||||
|
||||
1. Open `assets/index.html` (gallery) and confirm the new palette feels coherent across all 28 types.
|
||||
1. Open `assets/index.html` (gallery) and confirm the new palette feels coherent across all 38 types.
|
||||
2. If any type looks off, they usually need to tune `muted` (often too dark or too light against the new `paper`).
|
||||
|
||||
---
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Semantic patterns
|
||||
|
||||
Semantic patterns describe **what a system does**; the 28 visual types describe **how information is arranged**. Choose a pattern first when behavior, state, enforcement, or risk is load-bearing, then use its nearest visual type as the layout grammar. If no pattern matches, choose a visual type directly.
|
||||
Semantic patterns describe **what a system does**; the 38 visual types describe **how information is arranged**. Choose a pattern first when behavior, state, enforcement, or risk is load-bearing, then use its nearest visual type as the layout grammar. If no pattern matches, choose a visual type directly.
|
||||
|
||||
Use one primary pattern per figure. A second pattern may supply at most one supporting primitive; if both need full treatment, split overview and detail. Labels and outcomes must remain complete in a static frame.
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Database Schema
|
||||
|
||||
**Best for:** the *physical* schema — real tables, real SQL types, real constraints, real indexes, and foreign keys that connect one column to another column. It's the DDL made legible: migrations, review of a real database, and anything where the column type or the `ON DELETE` behavior is the point.
|
||||
|
||||
**Not for domain modeling.** [ER / data model](type-er.md) is entity-level: relationship lines join *boxes* and carry cardinality, fields are a plain list, and it's the right tool for a conceptual or domain conversation. Database schema is column-level: the FK connector anchors to the specific column row on both ends — the one capability ER doesn't have, and the reason this type exists. If you're discussing what an Order *is*, use ER. If you're discussing what happens when a row is deleted, use this.
|
||||
|
||||
## Layout conventions
|
||||
|
||||
### Table box
|
||||
|
||||
- **Header band** — `schema.table` (e.g. `public.orders`) in Geist sans 12px weight 600, with a rectangular type tag (`rx=2`, NOT a pill) reading `TABLE`. A hairline separates it from the body.
|
||||
- **Column rows** — fixed 24px row height so connectors can anchor predictably. Each row: column name in Geist sans 12px anchored left, SQL type in Geist Mono 9px `muted` anchored right (`uuid`, `text`, `numeric(12,2)`, `timestamptz`), and constraint chips between them as small `rx=2` tags in Geist Mono 8px: `PK`, `FK`, `UQ`, `NN`. Alternate row background `ink @ 0.02` on even rows for scannability.
|
||||
- **Overflow row** — when a table has more columns than the budget allows, the last row is a Geist Mono 9px `muted` line: `+ N more columns`. Never silently truncate a table without saying so.
|
||||
- **Index compartment** — an optional final compartment separated by a hairline, labelled with a Geist Mono 8px uppercase `INDEXES` eyebrow, listing index names in Geist Mono 9px (`idx_orders_customer_id`, `uq_products_sku`). List only the indexes that matter to the story, not every index on the table.
|
||||
|
||||
### Foreign-key connectors — the defining rule
|
||||
|
||||
Each FK edge starts at the **vertical centre of its source column row** and ends at the **vertical centre of the referenced column row**, routed with orthogonal rounded elbows (see SKILL.md §6 and [type-architecture.md](type-architecture.md) for the elbow formula and bridge/hop primitive). Label each edge in Geist Mono 8px with its referential action — `ON DELETE CASCADE`, `ON DELETE RESTRICT`, `ON DELETE SET NULL` — masked with the standard 6–10px gap. Fixed 24px row height guarantees ≥12px separation only when two FKs attach to *different* rows on the same table edge — the row spacing itself is the fan. When two or more FKs attach to the *same* row on the same edge (e.g. two child tables both referencing the same parent's primary key), anchoring all of them at the exact row centre would collide at a single point, which SKILL.md §6 rule 4 forbids. Offset each attach point symmetrically around the row's vertical centre instead — ±8px for two edges, keeping every point inside the row's 24px band and ≥12px from its neighbor — so each connector still reads as attaching to that row while remaining independently traceable.
|
||||
|
||||
### Schema grouping
|
||||
|
||||
Tables in a non-default schema sit inside a containment rect (`rx=8`, `ink @ 0.02` fill, `ink @ 0.20` stroke dashed `4,4`) with a Geist Mono 8px uppercase tracked schema label in its top-left corner. Draw the group rect first so tables paint over it.
|
||||
|
||||
### Focal rule
|
||||
|
||||
The 2 accent elements are: (1) the one destructive FK (`ON DELETE CASCADE`) — the edge and its label count together, since a labelled edge is one thing; and (2) the table that FK cascades into, carrying `accent-tint` on its **header band only**, never on the whole box. Nothing else on the diagram is `accent`.
|
||||
|
||||
If a schema has no destructive FK, it has no focal element. Leave it unaccented rather than promoting an arbitrary table.
|
||||
|
||||
## Complexity budget
|
||||
|
||||
Max 5 tables, max 8 column rows shown per table, max 6 FK edges, max 2 accent elements. Over budget → show the subsystem, not the database, and say so in a caption.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Drawing every column of every table — a schema diagram is an argument about a subsystem, not a `\d+` dump.
|
||||
- FK lines that connect box to box instead of column to column — that's ER, use ER instead.
|
||||
- Missing SQL types — the type is half the content.
|
||||
- Unlabelled FK edges — the `ON DELETE` behavior is what a reviewer is looking for.
|
||||
- Constraint chips on every row until the chips are the noise.
|
||||
- Index compartments listing every index rather than the ones that matter to the story.
|
||||
- Mixing conceptual entity names with physical table names in one diagram.
|
||||
|
||||
## Examples
|
||||
|
||||
- `assets/example-db-schema.html` — minimal light
|
||||
- `assets/example-db-schema-dark.html` — minimal dark
|
||||
- `assets/example-db-schema-full.html` — full editorial
|
||||
@@ -0,0 +1,44 @@
|
||||
# Dependency Graph
|
||||
|
||||
**Best for:** what depends on what, across packages, modules, or services — specifically to show two things a **tree** cannot structurally express: (a) a node with **more than one parent** (a shared dependency that several things converge on), and (b) a **cycle**. If neither appears in the data — every node has exactly one parent and nothing points backward — use [Tree](type-tree.md) instead; say so explicitly rather than forcing a graph layout on tree-shaped data.
|
||||
|
||||
## Layout conventions
|
||||
|
||||
- **Ranked layers.** Nodes sit in horizontal rank rows by dependency depth: rank 0 (entry points nothing depends on) at the top, deeper ranks below. Forward edges point downward across ranks, or run horizontal within a single rank row when a dependency compresses to the same depth as its dependent (e.g. a shallow sibling dependency) — never upward outside the one marked cycle. Rank rows are 120px apart.
|
||||
- Nodes are the standard node-box pattern (§6): `rx=6`, 160px wide, 56px tall.
|
||||
- **Fan-in badge.** Every node carries a Geist Mono 8px badge in its top-right corner, inside a small `rx=2` box, showing how many nodes depend on it (`4 in`). A mask fully inside a node is a badge chip, not a label — legitimate under §6 rule 6. The node with the highest fan-in is the diagram's structural story; size nothing else to compete with it.
|
||||
- **Node treatments** (§5 Node type → treatment):
|
||||
- Internal package/service → white fill + `ink` stroke.
|
||||
- External / third-party → `ink @ 0.03` fill + `ink @ 0.30` stroke (the External/Cloud treatment).
|
||||
- Leaf with no outgoing edges → `ink @ 0.05` fill + `muted` stroke.
|
||||
- **The cycle.** At most one back-edge points upward against rank order. It is the editorial point of the diagram: `accent` stroke, dashed `5,4`, `marker-end="url(#arrow-accent)"`, routed **around the outside** of the node stack — never straight through the middle, and never behind a node it doesn't connect to — with a masked Geist Mono 8px `CYCLE` label at its visible end. The two nodes the cycle touches stay in their normal node treatment (§5) — no accent stroke or fill on the nodes themselves, or the 2-accent budget is blown on the wrong elements.
|
||||
- **Focal rule:** the 2 accent elements permitted per diagram are the back-edge and its `CYCLE` label. Nothing else in a dependency graph is accent.
|
||||
- All six §6 Mandatory connector rules apply in full, no exemptions: rounded right-angle elbows (`r=8`) between off-axis nodes, 6–10px label-margin, no overlapping connectors (bridge/hop at crossings), fanned attach points (≥12px apart) where multiple edges share a box edge, no connector passing behind a non-endpoint box, no label mask clipped by a later-painted node.
|
||||
|
||||
## Complexity budget
|
||||
|
||||
| Limit | Rule |
|
||||
|---|---|
|
||||
| Max nodes | 9 |
|
||||
| Max edges | 14 |
|
||||
| Max rank layers | 4 |
|
||||
| Max highlighted cycles | 1 |
|
||||
| Max accent elements | 2 |
|
||||
|
||||
Over budget: collapse a leaf cluster into one aggregate node labelled with its count (e.g. `+6 leaves`), and say so in a caption — don't silently drop nodes.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Drawing a dependency graph when the data is actually a tree (single parent everywhere, no cycles) — use Tree instead.
|
||||
- Forward edges that point upward without being the one marked cycle.
|
||||
- A hairball layout with no rank ordering — rank first, always; ranking is what makes the graph readable, not an optional polish pass.
|
||||
- One node per file instead of per package/module/service — the graph is about dependency structure, not the filesystem.
|
||||
- Unlabelled external dependencies — version or registry belongs in the Geist Mono sublabel (`v3.23 · npm`), not left implicit.
|
||||
- Highlighting more than one cycle in a single diagram — pick the one that matters editorially; a second cycle competes with the first and neither reads.
|
||||
- Omitting the fan-in badges — without them the reader can't see at a glance where dependencies concentrate, which is the entire reason this type exists over a plain tree.
|
||||
|
||||
## Examples
|
||||
|
||||
- `assets/example-dependency.html` — minimal light
|
||||
- `assets/example-dependency-dark.html` — minimal dark
|
||||
- `assets/example-dependency-full.html` — full editorial
|
||||
@@ -0,0 +1,45 @@
|
||||
# Deployment
|
||||
|
||||
**Best for:** where the software actually *runs* — hosts, VMs, pods, and managed services placed inside environment or network-boundary zones, with replica counts and versioned artifacts called out. Architecture answers "what talks to what"; deployment answers "what is installed on which host, in which environment, behind which network boundary, at how many replicas." If the diagram has no physical-placement decision to show — no zone boundary, no replica count, no version that matters — use `type-architecture.md` instead; redrawing the logical architecture with hostnames bolted on is the most common deployment anti-pattern.
|
||||
|
||||
## Layout conventions
|
||||
|
||||
Three nesting levels, outermost to innermost — reuses the containment grammar from `type-nested.md`, specialized for infrastructure:
|
||||
|
||||
1. **Zone** — an environment or network boundary (`edge`, `prod / eu-west-1`, `data`). A large rect, `rx=8`, fill `ink @ 0.02`, stroke `ink @ 0.20` dashed `4,4`. A Geist Mono 8px (or 7px for tight layouts) uppercase tracked eyebrow label sits inside the top-left corner, on a paper-colored mask over the border. Zones are drawn **first** — before arrows and nodes — so labels and boxes paint over them (z-order: bg → zones → arrows → labels → nodes).
|
||||
2. **Infrastructure node** — a host, VM, pod, or managed service inside a zone. The §6 node-box pattern (SKILL.md) at `rx=6`, with a rectangular type tag (`rx=2`, **not** a pill) carrying `POD` / `VM` / `MANAGED` / `CDN` in the top-left corner.
|
||||
3. **Artifact chip** — what is deployed onto that node. A small 24px-tall rect `rx=4`, fill `ink @ 0.05`, stroke `muted`, holding the image or service name in Geist sans 12px (left-aligned) plus a Geist Mono 9px version tag (right-aligned, e.g. `v2.4.1`). A node can carry more than one chip (stacked with an 8px gap) when more than one artifact is co-located (a sidecar, for example).
|
||||
|
||||
**Replica badge.** A node running N copies carries a right-aligned Geist Mono 8px badge (`x3`) inside its own small `rx=2` box in the node's top-right corner. A mask fully inside a node is a badge chip and is a legitimate exception to the "mask must not overlap a later node" rule (SKILL.md §6 rule 6) — it's part of that same node.
|
||||
|
||||
**Network paths.** Orthogonal elbows between nodes (SKILL.md §6, elbow formula and port-selection rules in `type-architecture.md`), labelled with protocol and port in Geist Mono 8px (`HTTPS:443`, `TLS:5432`). A path that crosses a zone boundary is `link`-blue; a path that stays inside one zone is `muted`. Async or replication paths are dashed `5,4`.
|
||||
|
||||
**Focal rule.** The 2 accent elements are the single-point-of-failure or the newly-introduced piece — never more than 2. In the canonical example: the RDS primary (accent-tint fill, accent stroke) and the replication path to its standby (accent, dashed).
|
||||
|
||||
## Complexity budget
|
||||
|
||||
| Limit | Rule |
|
||||
|---|---|
|
||||
| Max zones | 3 |
|
||||
| Max infrastructure nodes | 6 |
|
||||
| Max artifact chips | 9 |
|
||||
| Max network paths | 8 |
|
||||
| Max accent elements | 2 |
|
||||
|
||||
Over budget → split into one deployment diagram per environment.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Redrawing the logical architecture with hostnames bolted on. If no placement decision is visible (which host, which zone, how many replicas), use `type-architecture.md` instead.
|
||||
- Zones that are just visual grouping with no boundary meaning — a zone must be a real environment or network boundary, not a layout convenience.
|
||||
- Artifact chips without a version tag. The version is why the diagram exists; an unversioned chip is a wasted box.
|
||||
- One node drawn per replica instead of a single node with a replica badge.
|
||||
- Cloud-vendor icon soup standing in for named nodes — name the host or service, don't decorate it.
|
||||
- Unlabelled network paths. Protocol and port are the content of a deployment diagram, not decoration.
|
||||
- Mixing two environments in one diagram (e.g. staging and production nodes side by side without a zone boundary that actually separates them).
|
||||
|
||||
## Examples
|
||||
|
||||
- `assets/example-deployment.html` — minimal light
|
||||
- `assets/example-deployment-dark.html` — minimal dark
|
||||
- `assets/example-deployment-full.html` — full editorial
|
||||
@@ -1,6 +1,8 @@
|
||||
# ER / Data Model
|
||||
|
||||
**Best for:** database schemas, API resource relationships, domain models.
|
||||
**Best for:** conceptual and logical data models, API resource relationships, domain models — anything where the story is *entities and cardinality*.
|
||||
|
||||
**Not for the physical schema.** ER is entity-level: relationship lines join *boxes* and carry cardinality at each end. When the point is real tables with SQL types, constraint chips, indexes, and foreign keys that anchor **column to column**, use [`type-db-schema.md`](type-db-schema.md) instead.
|
||||
|
||||
## Layout conventions
|
||||
- Each entity is a two-section box:
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
# Fishbone / Ishikawa (root-cause)
|
||||
|
||||
**Best for:** structured root-cause analysis. One observed effect, causes grouped by category, sub-causes hanging off each category. The standard artefact of an incident post-mortem — use it when a reader needs to see *what was investigated*, not just the conclusion.
|
||||
|
||||
## Layout conventions
|
||||
|
||||
- **Spine.** A horizontal line (`ink`, 1.2px) runs left→right at the vertical centre `CY`, terminating with an arrowhead into an **effect box** at the right head — the node-box pattern from SKILL.md §6, the observed-effect statement inside.
|
||||
- **Bones.** Category lines are straight diagonals at a fixed **60°** to the spine, alternating above and below, evenly spaced along it. Each bone carries its category name at the outer end in a small tag-style box (`rx=4`, not a pill) — Geist sans 12px weight 600.
|
||||
- **Sub-causes.** Short horizontal ticks (32px, `soft`) branch off each bone at fixed points along its length, each with a sub-cause label in Geist Mono 9px sitting past the open end of the tick.
|
||||
- **Diagonal exemption.** SKILL.md §6 rule 1 (mandatory rounded right-angle elbows) does not apply to the 60° bones — they are this type's defining grammar. The exemption covers **bones and sub-cause ticks only**; any other connector in a fishbone diagram (e.g. a callout leader, a cross-reference arrow) still uses rounded right-angle elbows.
|
||||
- **Focal rule.** Exactly one bone is the confirmed root cause: its line is `accent`, and its category tag uses `accent-tint` fill + `accent` stroke. The effect box is styled the same way (`accent-tint` fill, `accent` stroke) since it's the diagram's headline. That pair — root-cause bone and effect box — is the full 2-accent budget; every other bone, tag, and tick stays `ink` / `muted` / `soft`.
|
||||
- **Drawing order:** background → spine → bones → sub-cause ticks → category tag boxes → effect box → legend. Lines before boxes, so box fills cap the line ends cleanly.
|
||||
|
||||
## Math
|
||||
|
||||
For a spine at `y = CY` with the effect box's left edge at `x = HEAD`, bone `k` (1-indexed, k = 1..6) attaches to the spine at:
|
||||
|
||||
```
|
||||
attach_x(k) = HEAD - 160 - k * 160
|
||||
```
|
||||
|
||||
Bones alternate above (`k` odd) and below (`k` even) the spine. A bone's far endpoint — where the category tag sits — is the integer-rounded 60° offset from its attach point:
|
||||
|
||||
```
|
||||
dx = -96
|
||||
dy = ∓168 (minus = above, plus = below)
|
||||
far_x(k) = attach_x(k) + dx
|
||||
far_y(k) = CY ∓ 168
|
||||
```
|
||||
|
||||
### Pre-computed reference (5-bone layout, HEAD=1200, CY=320)
|
||||
|
||||
| Bone `k` | Category slot | Side | `attach_x` | `far_x, far_y` |
|
||||
|---|---|---|---|---|
|
||||
| 1 | first | above | 880 | 784, 152 |
|
||||
| 2 | second | below | 720 | 624, 488 |
|
||||
| 3 | third | above | 560 | 464, 152 |
|
||||
| 4 | fourth | below | 400 | 304, 488 |
|
||||
| 5 | fifth | above | 240 | 144, 152 |
|
||||
|
||||
A 6th bone (below) would attach at `x=80`, far endpoint `(-16, 488)`, and its category tag would run from `x=-76` to `x=44` against a viewBox that starts at `x=-40`. It clips. **Five is the ceiling at `HEAD=1200`**: to draw a 6th, widen `HEAD` *and* the viewBox width by at least 160 each, keeping the viewBox origin at `-40`. Both have to move together — widening `HEAD` alone pushes the 200px effect box to `1360..1560` past the `1440` right edge, trading a clipped tag on the left for a clipped effect on the right. Or drop a category.
|
||||
|
||||
**Sub-cause ticks** sit at fractions `m/6` along the bone (`m = 2, 4` for two ticks; `m = 3` for one), so their coordinates stay on the 4px grid:
|
||||
|
||||
```
|
||||
tick_x(k, m) = attach_x(k) - 16 * m
|
||||
tick_y(k, m) = CY ∓ 28 * m
|
||||
```
|
||||
|
||||
The tick itself is a 32px horizontal line from `(tick_x, tick_y)` to `(tick_x - 32, tick_y)`; the label sits past its open end, `text-anchor="end"` at `x = tick_x - 36`, `y = tick_y - 4` (above-bones) or `tick_y + 12` (below-bones).
|
||||
|
||||
## Complexity budget
|
||||
|
||||
| Limit | Rule |
|
||||
|---|---|
|
||||
| Max categories (bones) | 5 at `HEAD=1200`. A 6th requires a widened canvas — see Geometry |
|
||||
| Max sub-causes per bone | 3 |
|
||||
| Max sub-causes total | 18 |
|
||||
| Max accent elements | 2 (root-cause bone+tag, effect box) |
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- **More than 5 bones on the default canvas.** The 6th tag clips the viewBox. Widen `HEAD` and the viewBox width together and deliberately, or split by subsystem into two fishbones.
|
||||
- **Sub-causes that restate the category.** A "Deploy" bone with a sub-cause labeled "deployment issue" adds a node without adding information.
|
||||
- **A bone with zero sub-causes.** An empty category is a placeholder, not a finding — delete the bone or don't draw it until there's something under it.
|
||||
- **Accenting more than one root cause.** If two categories are both confirmed, that's two diagrams (or a merged cause) — not two accent bones. The 2-accent budget is load-bearing for the "this is the answer" signal.
|
||||
- **Using fishbone for a sequence of failures.** A chain of things that happened in order is a timeline or sequence diagram — fishbone is for causes of *one* effect, not a chronology.
|
||||
- **Pasting in the classic 6M template (Man/Machine/Material/Method/Measurement/Environment) with empty categories.** Name categories from the actual investigation, not a generic checklist waiting to be filled in later.
|
||||
- **Effect box phrased as a solution.** "Add a p99 alert" is a fix, not an observed effect. The box states what was seen — a symptom, a measurement, an incident — never the remedy.
|
||||
|
||||
## Examples
|
||||
|
||||
- `assets/example-fishbone.html` — minimal light. Checkout p99 latency incident, 5 categories, Data confirmed as root cause.
|
||||
- `assets/example-fishbone-dark.html` — minimal dark, same data.
|
||||
- `assets/example-fishbone-full.html` — full editorial: container framing + 3 summary cards of varied widths + footer.
|
||||
@@ -0,0 +1,53 @@
|
||||
# User Journey Map
|
||||
|
||||
**Best for:** what a person does across the stages of an experience and how it *feels* at each one. The sentiment curve is the load-bearing element — without it this is just a process diagram with extra rows, so if you can't name a sentiment for every stage, use **Process** or **Timeline** instead.
|
||||
|
||||
## Layout conventions
|
||||
|
||||
Vertical stack, top to bottom, for one persona:
|
||||
|
||||
- **Stage headers** — 5 equal columns, 200px wide, 24px gutters (max 6 stages). Each column: a Geist Mono 8px uppercase tracked eyebrow (`STAGE 1` … `STAGE 5`) over the stage name in Geist sans 12px weight 600, both centered on the column.
|
||||
- **Sentiment band (the differentiator)** — a 160px-tall plot area directly under the headers. 3 horizontal reference hairlines at `rule` 0.10 opacity mark the levels `HIGH` / `NEUTRAL` / `LOW` — Geist Mono 8px `muted`, anchored `end` in a 64px left margin. **Never emoji, never `writing-mode` vertical text for these labels.** A smooth `muted` 1.5px polyline runs through one `r=5` dot per stage, each value snapped to one of five ordinal levels (`HIGH`, `MED-HIGH`, `NEUTRAL`, `MED-LOW`, `LOW`) — only the three that are actually used need a labeled hairline. The trough stage's dot and its incoming segment are `accent`; everything else on the curve is `muted`.
|
||||
- **Content rows** — up to 3 labelled bands below the sentiment band, each with a Geist Mono 8px uppercase row label sitting in the left margin (same column as the sentiment level labels, never rotated):
|
||||
- `ACTIONS` — what the user does, Geist sans 12px, one short line per stage.
|
||||
- `TOUCHPOINTS` — the surface the action happens on (email, app, docs…), Geist Mono 9px `muted`.
|
||||
- An optional third row for a metric or owner, same treatment as touchpoints.
|
||||
Rows are separated by hairlines that span the full plot width (from the left margin edge to the last stage's right edge).
|
||||
- **Pain markers** — on stages where sentiment drops, a small dashed-stroke tag box (`rx=2`, `accent @ 0.50` stroke dashed `3,3`, no fill or a faint accent tint) sits under that stage's actions cell, with a Geist Mono 8px label naming the friction. Max 2 per diagram, and only on the trough — tagging every stage erases the signal.
|
||||
- **Legend** — horizontal strip at the bottom per the global rule (hairline separator above, 160–180px between entries): three keys, in order — the sentiment line, the trough-stage highlight, and the pain-marker tag. The trough key is not optional: the dip is the finding, so the reader needs the highlight named.
|
||||
|
||||
## Connector note
|
||||
|
||||
The sentiment polyline is a **data curve**, not a connector between nodes — §6 rule 1 (mandatory orthogonal elbows) does not apply to it. This exemption covers the sentiment curve **only**; any other connector in a journey map (there normally are none) still follows the standard connector rules.
|
||||
|
||||
## Geometry
|
||||
|
||||
- Stage grid: left margin 64px, then 5 columns of 200px with 24px gutters (`col_left = 64 + i·224`).
|
||||
- Sentiment band: 160px tall. Hairlines at the top (`HIGH`), middle (`NEUTRAL`), and bottom (`LOW`) of the band, 80px apart.
|
||||
- Dot x = the horizontal center of its stage column. Dot y = the snapped level's hairline y (or the interpolated position for an unlabeled level).
|
||||
- Row height: enough for one line of text plus, in the `ACTIONS` row only, up to two stacked 16px-tall pain-marker boxes.
|
||||
|
||||
## Focal rule
|
||||
|
||||
Exactly 2 accent elements: the trough dot + its incoming curve segment (counts as one), and its pain marker(s) (counts as the other). Nothing else on the map is `accent`.
|
||||
|
||||
## Complexity budget
|
||||
|
||||
Max 6 stages · max 3 content rows · 5 sentiment levels (ordinal, named — never numeric) · max 2 pain markers · max 2 accent elements.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- **No sentiment curve.** If every stage feels the same, or you can't name a feeling, you're drawing a process — use **Process** or **Timeline**.
|
||||
- **More than 6 stages.** Split into two journeys (e.g. acquisition and retention) rather than cramming a wide funnel into one map.
|
||||
- **Numeric sentiment axis** (a 0–100 score). Sentiment here is ordinal — five named levels, not a chart of a continuous metric.
|
||||
- **Emoji as sentiment markers.** Use the named levels and hairlines; emoji don't localize, print, or hold up at small sizes.
|
||||
- **Multiple personas on one map.** Overlaying two sentiment curves erases both. One map per persona.
|
||||
- **Pain markers on every stage.** If nothing is un-marked, nothing is focal.
|
||||
- **`writing-mode` vertical row labels.** Horizontal only, in the left margin — same rule as every other type in this skill.
|
||||
- **Using it for an internal system flow with no human in it.** No person, no sentiment, no journey map — that's an architecture or data-flow diagram.
|
||||
|
||||
## Examples
|
||||
|
||||
- `assets/example-journey.html` — minimal light. *Trial to paid: the first week*, 5 stages, trough at "Hit the limit".
|
||||
- `assets/example-journey-dark.html` — minimal dark, same data.
|
||||
- `assets/example-journey-full.html` — full editorial: container framing + 3 varied-width summary cards + footer.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Kanban Board
|
||||
|
||||
**Best for:** a snapshot of work-in-progress by state — what is queued, what is moving, what is stuck, and where the WIP limit is being breached. This is a *state census*, not a flow: a kanban board has **no connectors at all**. That is exactly what separates it from swimlane (lanes plus a flow crossing them, with handoffs as the load-bearing edges) and from process (ordered steps with directional handoffs). If the diagram needs an arrow anywhere, it isn't a kanban board anymore — reach for swimlane or process instead.
|
||||
|
||||
## Layout conventions
|
||||
|
||||
- **Up to 5 vertical columns of equal width (240px), 32px gutters.** Column background is `ink @ 0.02`; no column border.
|
||||
- **Header band per column:** column name in Geist sans 12px weight 600, anchored left. A right-anchored **WIP chip** — a rectangular tag (`rx=2`, **never** a pill) holding `n/limit` in Geist Mono 8px — or bare `n` on a queue or terminal column, which carries no limit. Every in-process column states one. A hairline `rule` separator sits directly under the header band, full column width.
|
||||
- **Cards:** the §6 node-box pattern at `rx=6`, column width minus 16px padding on each side, 56px tall, 12px vertical gap between cards. Content: title in Geist sans 12px weight 600, and a Geist Mono 9px `muted` sublabel line for `TICKET-ID · owner` (e.g. `AVA-214 · nadia`).
|
||||
- **Drawing order:** background → column fills → header text + WIP chips → header rules → cards (each: box, then left accent bar if blocked, then title, then sublabel) → legend.
|
||||
|
||||
## Card states — the type's semantic vocabulary
|
||||
|
||||
Every card renders in exactly one of four states. Document and use all four when the board has enough cards to show them:
|
||||
|
||||
| State | Fill | Stroke | Extra |
|
||||
|---|---|---|---|
|
||||
| `default` | white | `ink` | — |
|
||||
| `blocked` | `accent @ 0.05` | `accent` (full opacity) dashed `4,4` | 4px `accent` bar on the card's left edge |
|
||||
| `waiting / external` | `ink @ 0.02` | `ink @ 0.20` dashed `4,3` | — |
|
||||
| `done` | `ink @ 0.05` | `muted` | — |
|
||||
|
||||
These map directly onto the SKILL.md §5 node-treatment table (`security` → blocked, `optional` → waiting/external, `store` → done) — the board reuses the system's existing semantic fills rather than inventing new ones.
|
||||
|
||||
## Over-limit column
|
||||
|
||||
When a column's card count `n` exceeds its stated `limit`, the WIP chip's stroke and text go `accent`. That plus one blocked card is the full 2-accent budget for the diagram — don't spend the budget anywhere else.
|
||||
|
||||
## Complexity budget
|
||||
|
||||
- Max 5 columns, max 4 cards per column, max 12 cards total.
|
||||
- Max 2 accent elements (the over-limit chip counts as one; a blocked card counts as one).
|
||||
- Over budget on a single column → aggregate the backlog into one count card (`+14 more`) rather than listing every item.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- **Drawing arrows between cards.** A board shows state, not flow — if the flow itself needs to be visible, use swimlane or process instead.
|
||||
- **More than 4 cards in a column.** Aggregate into a count card; don't let a column scroll off the canvas.
|
||||
- **One card per person.** That's an org chart wearing a kanban skin.
|
||||
- **In-process columns without WIP limits.** The limit is half the point of the type — a column work flows *through* with no stated limit can't show that it's over budget. The entry queue and the terminal column are the exception: nothing is constrained by holding more done work, so they show a bare count.
|
||||
- **Accenting every blocked card.** One or two blocked cards read as signal; a column full of accent reads as noise and erases the focal rule.
|
||||
- **A "Done" column that grows without bound.** Cap it and note the archive point in the sublabel or a footnote — an unbounded Done column just becomes a second Backlog.
|
||||
- **Pill-shaped WIP tags.** The chip is a rectangle at `rx=2`, matching the type-tag primitive in §6 — pills read as status badges from an unrelated design language.
|
||||
|
||||
## Examples
|
||||
|
||||
- `assets/example-kanban.html` — minimal light. Platform-team board: Backlog (3), In progress (4/3, over limit), Review (2/3), Done (2), one blocked card.
|
||||
- `assets/example-kanban-dark.html` — minimal dark, same data.
|
||||
- `assets/example-kanban-full.html` — full editorial: container framing + 3 summary cards of varied widths + footer.
|
||||
@@ -0,0 +1,72 @@
|
||||
# Sankey / Flow-Quantity
|
||||
|
||||
**Best for:** showing where a *quantity* goes as it splits and merges across a small number of stages — CI compute budgets, funnel-adjacent volume flows, cost or headcount allocation. This is the one type where band **thickness carries data**; if the reader doesn't need to compare magnitudes, use process or pyramid instead.
|
||||
|
||||
## Layout conventions
|
||||
|
||||
- **Exactly 3 stage columns**, left → right. No more, no less — above 3 stages, split into two linked diagrams.
|
||||
- **Nodes are vertical bars**, `width=12`, solid `ink` fill, no stroke. Height is proportional to the quantity passing through, rounded to the nearest 4px so it stays on-grid — the rounding is a rendering step, not a data change; the true value still prints in the quantity sublabel.
|
||||
- **Flows are filled ribbons**, not stroked lines: a single closed `<path>` per flow. Top edge is a cubic Bézier from the source node's top-offset to the target node's top-offset; bottom edge is the same curve run in reverse from target-bottom back to source-bottom. **Both** control points sit at the horizontal midpoint between the two columns, each at the y of the end it belongs to: `C midX,y0 midX,y1 targetX,y1`. That is what makes the band leave and arrive **horizontally**, so it plugs into each bar square-on. Putting the second control on the target's x instead (`C midX,y0 targetX,y1 targetX,y1`) collapses it onto the endpoint, the arrival tangent degenerates, and every ribbon meets its bar at a visible slant — it looks like the band is not attached.
|
||||
- **No arrowheads, ever.** Direction is implied by the left-to-right column order. This is a deliberate, explicit exemption from SKILL.md §6's orthogonal-elbow-and-arrowhead rule: ribbons are area encodings, not connectors, and a marker on a filled band reads as clutter, not information.
|
||||
- **Ribbon fill:** `muted` at `0.18` opacity for ordinary flows. The one editorial focal path (see below) uses `accent` at `0.28` opacity. A focal path may span more than one ribbon segment (e.g. two flows that both feed the same downstream node as halves of one story) — accenting every ribbon that belongs to that single path still counts as **one** focal element, not one per ribbon. Never per-flow rainbow coloring — color is reserved for the one path that deserves attention.
|
||||
- **Node ordering minimizes crossings.** Order nodes within each column so that flows converging on (or diverging from) the same node stay in a consistent top-to-bottom sequence across columns. If two ribbons still cross more than once, reorder the nodes — don't let them tangle.
|
||||
- **Labels:** node name in Geist sans 12px 600, quantity directly under it in Geist Mono 9px `muted`. Placement is column-specific: column 1 sits outside the bar, `text-anchor="end"`; column 2 sits in the gutter above its bar, `text-anchor="middle"`, **vertically centred in that gutter** rather than flush to the top, or the cap line grazes the bar above; column 3 sits outside the bar, `text-anchor="start"`.
|
||||
|
||||
Centring the column-2 label in its gutter is what makes it legible: the gutter is the gap the bands themselves leave between two nodes, so a centred label sits on clean paper and needs no backing at all. Don't reach for a mask rect here — over a filled band it reads as a hole punched in the diagram, and if the label already sits on paper it is solving nothing. If a label genuinely has nowhere clear to sit, the column is over budget: drop a node or split the diagram.
|
||||
- **Column headers:** Geist Mono 8px, uppercase, tracked, centered above each column — "CI COMPUTE BUDGET / TEST STAGE / OUTCOME" style eyebrows, not full sentences.
|
||||
- **Drawing order:** background → column headers → ordinary ribbons → the accent path's ribbon(s) (painted last among ribbons so they read on top) → node bars → node labels → legend.
|
||||
- **Legend:** horizontal strip at the bottom per the global rule, viewBox extended ~60–100px to fit it (a Sankey's ribbons need more bottom clearance than a typical diagram because node stacks run tall). Swatches are 16×8 rectangles matching each ribbon treatment, plus an optional italic aside stating the editorial read.
|
||||
|
||||
## Scale rule
|
||||
|
||||
Pick **one px-per-unit constant `k`** for the whole diagram and apply it to every node and ribbon thickness — never a different scale per column. Round each resulting thickness to the nearest 4px (grid compliance) and reconcile the rounding at the node level, not the flow level, so a node's outgoing ribbons still sum exactly to its (rounded) height.
|
||||
|
||||
**When you control the numbers, choose ones that land on the grid.** At `k`, a 4px step is `4/k` units; pick quantities that are whole multiples of it and the drawn area is then exactly the printed number. Rounding is the fallback for real data you cannot choose, and it is a real cost: at `k = 0.02` a 4px step is 200 minutes, so a rounded bar can misstate its own label by up to 100. In a chart whose entire premise is that area encodes quantity, a bar that disagrees with the number printed beside it spends credibility to buy grid compliance. Never let the rounding exceed one 4px step, and never round a flow so far that a node's ribbons no longer sum to its bar.
|
||||
|
||||
**Minimum rendered ribbon thickness: 4px.** Anything that would round to less gets folded into an "other" band rather than drawn as an invisible hairline — a Sankey ribbon the reader can't see isn't communicating anything.
|
||||
|
||||
### Worked reference (k = 0.02 px/unit, budget = 12,000 CI minutes)
|
||||
|
||||
| Node | Quantity | Height (px) |
|
||||
|---|---|---|
|
||||
| CI minutes (col 1) | 12,000 | 240 |
|
||||
| Unit tests | 5,200 | 104 |
|
||||
| E2E | 4,000 | 80 |
|
||||
| Build | 2,000 | 40 |
|
||||
| Lint | 800 | 16 |
|
||||
| Passed | 9,400 | 188 |
|
||||
| Failed | 1,600 | 32 |
|
||||
| Flaked | 1,000 | 20 |
|
||||
|
||||
Each column's node heights sum to the same 240px total — that invariant (total-in equals total-out) is what makes the diagram trustworthy at a glance. If your columns don't sum to the same total, the data has a leak or the layout has a bug.
|
||||
|
||||
## Complexity budget
|
||||
|
||||
| Limit | Rule |
|
||||
|---|---|
|
||||
| Max stage columns | 3 |
|
||||
| Max nodes | 8 |
|
||||
| Max flows (ribbons) | 12 |
|
||||
| Max accent elements | 2 (a focal path's ribbons count as one, regardless of how many segments it spans) |
|
||||
|
||||
Over budget → split into two linked Sankeys (e.g. an overview stage-1→stage-2 diagram plus a detail stage-2→stage-3 diagram) rather than cramming a fourth column or a ninth node into one canvas.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- **Ribbons that meet the bar at a slant.** The single most common way to get this type wrong, and it is a control-point mistake, not a layout one: both controls belong on the midpoint x. A band arriving at an angle reads as detached from its node, which undermines the one thing the diagram is asserting — that this quantity flows into that node.
|
||||
- **An opaque mask rect behind a column-2 label.** Over a filled band it reads as a hole punched in the diagram, and it erases any bar it overlaps. Centre the label in the gutter instead, where it sits on clean paper and needs nothing behind it.
|
||||
- **Column-2 labels flush to the top of their gutter.** Centre them, or the cap line touches the bar above and the label looks like it belongs to the wrong node.
|
||||
- **A bar whose height disagrees with the number printed beside it.** See the scale rule: choose grid-friendly quantities when the data is yours to choose.
|
||||
- **Ribbons crossing more than once.** Reorder the nodes in the offending column; a Sankey with a tangle in the middle is unreadable.
|
||||
- **Ribbons thinner than 4px.** Merge them into an "other" band instead of drawing a sliver no one can see.
|
||||
- **Per-flow rainbow coloring.** One `muted` treatment for ordinary flows, one `accent` treatment for the flow that carries the editorial point — never a distinct hue per ribbon.
|
||||
- **Using Sankey for a simple funnel.** A single narrowing quantity with no splits or merges is a pyramid/funnel, not a Sankey.
|
||||
- **Using Sankey for a plain step sequence.** If nothing splits, merges, or varies in thickness, it's a process diagram — the whole point of Sankey is quantity that branches.
|
||||
- **Stacking two flows at the same node-edge offset.** Every flow gets its own offset range within the node's height, in a consistent top-to-bottom order — never two ribbons overlapping at the same attach point.
|
||||
- **Percentages or quantities that don't sum to the source total.** Every node's outgoing (or incoming) flows must sum to that node's own height. A Sankey that doesn't balance reads as an error, not a design choice.
|
||||
|
||||
## Examples
|
||||
|
||||
- `assets/example-sankey.html` — minimal light. A month of CI compute (12,000 minutes) splitting into test/build/lint stages, merging into passed/failed/flaked outcomes; the flaky-rerun path is the one accent flow.
|
||||
- `assets/example-sankey-dark.html` — minimal dark, same data.
|
||||
- `assets/example-sankey-full.html` — full editorial: container framing + 3 summary cards of varied widths + footer.
|
||||
@@ -0,0 +1,46 @@
|
||||
# User Story Map
|
||||
|
||||
**Best for:** the Jeff Patton user story map — it answers "what is the whole story, and where do we cut the first release?" Narrative order runs left to right; priority runs top to bottom. The **release cut** is the editorial point of the diagram: a map without one is just a backlog in a grid.
|
||||
|
||||
This is neither **Kanban** nor **User journey**, and the distinction is load-bearing:
|
||||
|
||||
- **Kanban** shows *state* — columns are Todo/Doing/Done, there is no narrative order and nothing to slice.
|
||||
- **User journey** shows *one persona's feelings* across stages via a sentiment curve.
|
||||
- A **story map** has neither state nor sentiment. Its columns are **narrative order** and its rows are **release slices**. If you aren't slicing releases, use one of those two instead.
|
||||
|
||||
## Layout conventions
|
||||
|
||||
Vertical stack, top to bottom:
|
||||
|
||||
1. **Backbone** — up to 5 activities in left-to-right narrative order, as wide header cards (200px, `rx=6`, `ink @ 0.05` fill, `muted` stroke). Each card: the activity name in Geist sans 12px weight 600, above a Geist Mono 8px uppercase tracked eyebrow (`ACTIVITY 1` … `ACTIVITY 5`). 24px gutters between cards.
|
||||
2. **Walking skeleton row** — directly under each activity, its user *steps* as smaller cards (`rx=4`, 32px tall, white fill, `ink` stroke), Geist sans 12px. One or two per activity; reserve room for two so every activity's row bottom aligns even when it only has one step.
|
||||
3. **Release slices** — horizontal bands below the skeleton row, each a full-width lane with a Geist Mono 8px uppercase tracked label (`MVP`, `RELEASE 2`, `LATER`) sitting in a 96px left margin. Band background alternates `ink @ 0.02` / none. Inside each band, story cards (`rx=4`, 48px tall) sit in their activity's column: story title in Geist sans 12px weight 600, plus a Geist Mono 9px estimate/ticket sublabel (e.g. `RPT-114 · 3pt`). Whenever any slice has a gap (an activity with no card in that row), add `rule` hairline column separators (0.8px, dashed `4,4`, 0.10 opacity) running from just under the backbone down to the bottom of the last slice, drawn before the cards — without them, a gappy grid reads as scattered rather than mapped to its activity.
|
||||
4. **The release cut line** — a full-width horizontal `accent` rule (1.5px) immediately under the MVP band, with a masked Geist Mono 8px label `RELEASE CUT` at its right end. This is accent element one.
|
||||
5. **Legend** — horizontal strip at the bottom per the global rule (hairline separator above): one swatch for the default story card, one for the highest-risk treatment, one for the release-cut line.
|
||||
|
||||
## Focal rule
|
||||
|
||||
Exactly 2 accent elements: the release cut line with its `RELEASE CUT` label (counts as one), and the single riskiest story card — drawn with `accent @ 0.05` fill, `accent` dashed `4,4` stroke, and a small rectangular `RISK` tag (`rx=2`, matching the type-tag primitive) — counts as the other. Nothing else on the map is `accent`.
|
||||
|
||||
## Complexity budget
|
||||
|
||||
- Max 5 activities, max 3 release slices, max 12 story cards total, max 4 cards per slice.
|
||||
- Max 2 accent elements (the release cut counts as one; the riskiest card counts as the other).
|
||||
- Over budget on a single activity or slice → split into one map per activity, or collapse a slice into a count card rather than listing every item.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- **No release cut.** A map with no cut line is a backlog in a grid, not a map — the cut is the whole point.
|
||||
- **Columns ordered by priority instead of narrative sequence.** The backbone is a story, left to right, in the order the user experiences it — not a ranked list.
|
||||
- **Story cards that are features, not user-visible outcomes.** "Add a chart" is a story; "Refactor the charting service" is not — it belongs in a backlog ticket, not the map.
|
||||
- **Slices named by date instead of by outcome.** `MVP` / `RELEASE 2` / `LATER` describe scope; `Q3` / `Q4` describe a calendar and drift the moment the schedule slips.
|
||||
- **More than 3 slices.** Beyond MVP, next, and later, nobody believes the ordering — collapse the tail into "Later."
|
||||
- **Mixing two personas in one map.** One map per persona, same as user journey.
|
||||
- **Adding state columns.** That turns it into a kanban board — this type has no state.
|
||||
- **Adding a sentiment curve.** That turns it into a user journey — this type has no feelings axis.
|
||||
|
||||
## Examples
|
||||
|
||||
- `assets/example-story-map.html` — minimal light. *Reporting, first release*: four activities (`Find the data`, `Build the report`, `Share it`, `Trust it`), MVP/Release 2/Later slices, release cut under MVP, `Row-level permissions` flagged as the riskiest story.
|
||||
- `assets/example-story-map-dark.html` — minimal dark, same data.
|
||||
- `assets/example-story-map-full.html` — full editorial: container framing + 3 summary cards of varied widths + footer.
|
||||
@@ -0,0 +1,65 @@
|
||||
# UML Class Diagram
|
||||
|
||||
**Best for:** the static structure of an object model — classes, what they own, what they inherit, and what they merely depend on. The distinguishing content is the **operations compartment** and the **typed relationship vocabulary** (the arrowheads carry meaning). ER cannot express either — use this type only when operations or the inheritance/composition vocabulary are the point of the diagram, and `type-er.md` when the story is entities and cardinality.
|
||||
|
||||
**Other UML diagrams — route elsewhere.** UML is a family; only the class diagram gets its own grammar here:
|
||||
|
||||
| UML diagram | Use instead |
|
||||
|---|---|
|
||||
| Sequence | [type-sequence.md](type-sequence.md) |
|
||||
| State machine | [type-state.md](type-state.md) |
|
||||
| Component | [type-architecture.md](type-architecture.md) |
|
||||
| Deployment | [type-architecture.md](type-architecture.md) |
|
||||
| Activity | [type-swimlane.md](type-swimlane.md) or [type-flowchart.md](type-flowchart.md) |
|
||||
| Conceptual / domain ER | [type-er.md](type-er.md) |
|
||||
|
||||
## Layout conventions
|
||||
|
||||
- Each class is a **single box** (`rx=6`), divided by full-width hairlines into up to three compartments. Compartment heights follow content — never pad to a uniform height.
|
||||
1. **Name** — class name in Geist sans, 12px, weight 600, **centered**. An interface carries a Geist Mono 8px `«interface»` stereotype line above the name. An abstract class sets the name in *italic*.
|
||||
2. **Attributes** — one line each, Geist Mono 9px, left-aligned: `+ name: Type`. Visibility markers: `+` public, `-` private, `#` protected.
|
||||
3. **Operations** — one line each, Geist Mono 9px, left-aligned: `+ method(arg): Return`.
|
||||
Omit a compartment entirely when a class has no members in it (an interface with no attributes skips that compartment).
|
||||
- Attribute/operation lines are a single combined string, not the two-column field/type layout ER uses — that visual distinction keeps the two types from reading the same.
|
||||
- Coral (accent) is reserved for the class being implemented or extended (the focal type) — accent-tint fill, accent stroke. Its inbound inheritance/realization edges count as **one** additional accent element (treated as a group), for 2 accent elements total per diagram.
|
||||
|
||||
## Relationship vocabulary
|
||||
|
||||
Define every marker used in `<defs>` and show all six in the legend, even the ones not used in the diagram body — the legend is this type's complete grammar reference.
|
||||
|
||||
| Relationship | Line | Ending (at the target/owner end) |
|
||||
|---|---|---|
|
||||
| Inheritance (`extends`) | solid | large **hollow triangle** — `paper` fill, `ink` stroke |
|
||||
| Realization (`implements`) | dashed `5,4` | same hollow triangle |
|
||||
| Composition (owns, cascades) | solid | **filled diamond** at the OWNER end, `ink` fill |
|
||||
| Aggregation (has, independent) | solid | **hollow diamond** at the OWNER end |
|
||||
| Association | solid | plain open arrowhead, multiplicity at BOTH ends |
|
||||
| Dependency (uses) | dashed `4,3` | plain open arrowhead |
|
||||
|
||||
Multiplicities (`1`, `0..*`, `1..*`) sit in Geist Mono 8px, 10–12px off the box edge, on an opaque mask over the line — same convention as ER cardinality labels.
|
||||
|
||||
## Connector rules
|
||||
|
||||
All six SKILL.md §6 connector rules apply in full — orthogonal rounded elbows (`r=8`), no diagonals, bridge/hop for unavoidable crossings, fanned attach points ≥12px apart when several relationships share an edge, masked labels with the 6–10px gap, connectors drawn before boxes. Prefer laying classes out so relationships resolve to straight lines or single-elbow routes; a class diagram with every edge bridging is over budget — split by package instead.
|
||||
|
||||
## Complexity budget
|
||||
|
||||
Max 7 classes, max 8 relationships, max 5 members per compartment (overflow becomes a Geist Mono `…` line), max 2 accent elements. Over budget → split by package.
|
||||
|
||||
Seven is the ceiling rather than the target. Three compartments per box makes a class diagram dense fast, so treat 4–5 classes as the normal size; the shipped example uses all seven only because it doubles as the legend for the full relationship vocabulary.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Getters and setters listed as operations — they're noise; show behavior that matters.
|
||||
- Every attribute and method dumped in — a class diagram is an argument, not a header file.
|
||||
- Composition and aggregation used interchangeably — the filled diamond means the part dies with the whole. Say so, or use the hollow diamond.
|
||||
- Association arrows with no multiplicity.
|
||||
- Drawing a class diagram when there's no inheritance and no operations — that's ER.
|
||||
- Stereotype guillemets on everything, not just the interfaces/abstracts that need them.
|
||||
- Boxes padded to equal height.
|
||||
|
||||
## Examples
|
||||
|
||||
- `assets/example-uml-class.html` — minimal light
|
||||
- `assets/example-uml-class-dark.html` — minimal dark
|
||||
- `assets/example-uml-class-full.html` — full editorial
|
||||
@@ -0,0 +1,39 @@
|
||||
# Wardley Map
|
||||
|
||||
**Best for:** positioning the components of a value chain against how evolved (commoditised) each one is, so a reader can see what to build, what to buy, and what is about to move. A strategy artefact, not an architecture diagram — it says nothing about how components connect at runtime, only where each one sits on the genesis-to-commodity curve.
|
||||
|
||||
## Layout conventions
|
||||
|
||||
- **Y axis — value chain.** `Visible to the user` at the top, down to `Invisible` at the bottom. Label the axis as stacked separate `<text>` lines flanking the axis endpoints — **never `writing-mode`** to rotate a label vertically. Axis line `rule-solid` at `0.20` opacity.
|
||||
- **X axis — evolution.** Four bands separated by three vertical hairlines: `Genesis | Custom-built | Product | Commodity`. Band separators are `rule` at `0.10` opacity, dashed `4,4`. Band labels sit along the bottom axis: Geist Mono 8px, uppercase, tracked, `muted`. Axis line `rule-solid` at `0.20` opacity.
|
||||
- **Components** are a small circle `r=6`, filled `paper` with an `ink` 1px stroke. Label in Geist sans 12px weight 600, placed 12px above the dot, `text-anchor="middle"`. An optional Geist Mono 9px `muted` sublabel can sit under the dot for a technical qualifier — use sparingly.
|
||||
- **Dependency links** are thin `muted` 0.8px straight lines from a component down to the component it depends on. **These are the one documented exemption from §6 rule 1 (mandatory orthogonal elbows) — dependency links only.** A component's y/x position on this map *is* the data; forcing it onto an axis to make an elbow route work would misplace it. No arrowhead — the line itself states the dependency.
|
||||
- **Movement** — a component that is evolving carries a short right-pointing arrow: `accent`, dashed `5,4`, `marker-end="url(#arrow-accent)"`, running from the dot's edge toward the next band. The arrow needs no on-arrow text label: the legend's "Evolving" swatch (accent dot + dashed accent arrow) already names what accent + dashes mean, and since arrows on this map can only ever point right (see Anti-patterns — a left-pointing movement arrow is a contradiction, not a design choice), the direction itself already says "toward commodity." A bare word like `COMMODITISING` restates what shape and color already signal — apply §1's remove test: can I remove this label, does color or shape already carry it? Here it does, so the label is the thing that goes, not its position.
|
||||
An on-arrow label is legitimate only when it qualifies something the arrow *can't* express on its own — a timeframe (`BY Q3`), a driver (`VENDOR LOCK-IN`) — never a restatement of direction. If you add one, mask it Geist Mono 8px, 6–10px clear of the stroke, same masking rule as any arrow label (§6 rule 2), and confirm there's runway for the mask first: the space directly around a component's dot is usually the most congested spot on the map, because that's also where its own dependency links fan out. Worked example — the moving component in `example-wardley.html` sits at `(420,156)` with links fanning to `(620,220)` and `(680,252)`; at the mandatory 6–10px gap band below the arrow (`y=164–176`), those two lines cross at `x=441.7–482.5` and `x=445–482.5` respectively — almost the full width of the arrow's own span (`x=428–504`), leaving no clear pocket wide enough for a label. Above the dot is no better: the component's own name label already occupies that band per the 12px-above-dot convention. Check both bands for interference before adding a movement label; if neither is clear, don't add one — the legend already covers it.
|
||||
- **Focal rule:** the 2 accent elements on this type are the moving component's dot (`accent-tint` fill, `accent` stroke) and its movement arrow — or two moving components sharing those 2 accent slots with no other accent anywhere else on the map.
|
||||
- **Legend:** horizontal strip at the bottom, hairline separator above it, viewBox extended ~60px — same as every other type.
|
||||
|
||||
## Complexity budget
|
||||
|
||||
| Limit | Rule |
|
||||
|---|---|
|
||||
| Max components | 9 |
|
||||
| Max dependency links | 12 |
|
||||
| Max movement arrows | 2 |
|
||||
| Max accent elements | 2 |
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- **Treating the x axis as a maturity score.** Evolution is four qualitative bands (genesis / custom-built / product / commodity), not a 0–10 slider. Don't plot a component at "6.5" — place it in a band.
|
||||
- **A component with no dependency link.** If nothing in the value chain depends on it and it depends on nothing, it isn't part of the chain — delete it or wire it in.
|
||||
- **Arrows drawn right-to-left.** Evolution only moves toward commodity. A left-pointing movement arrow is a contradiction, not a design choice.
|
||||
- **Using the map as an architecture diagram.** No request/response direction, no protocols, no runtime topology — that's the architecture type. This type answers "what's worth building vs. buying," not "how does traffic flow."
|
||||
- **Numbering the y axis.** Value-chain visibility is ordinal (more visible to the user, less visible), not a quantity. Numeric ticks imply a measurement that doesn't exist.
|
||||
- **More than 2 movement arrows.** Beyond that the map stops making a single point and turns into a forecast nobody can act on.
|
||||
- **`writing-mode` vertical axis text.** Stack the words as separate horizontal `<text>` lines instead — see Layout conventions above.
|
||||
|
||||
## Examples
|
||||
|
||||
- `assets/example-wardley.html` — minimal light. AI assistant product, agent orchestration commoditising toward Product.
|
||||
- `assets/example-wardley-dark.html` — minimal dark, same map.
|
||||
- `assets/example-wardley-full.html` — full editorial: framed container + 3 varied-width cards (the moving component, a genesis-stage build, and the commodity base).
|
||||
Reference in New Issue
Block a user