[verified] add semantic patterns and accessible motion
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
---
|
||||
name: diagram-design
|
||||
description: Create technical and product diagrams — 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, bar chart, line chart, Gantt, scatter plot, high-level, process, medallion, data flow, DP integration, DP security matrix — as standalone HTML files with inline SVG. Also imports existing draw.io / diagrams.net files (.drawio, .drawio.png, .drawio.svg) and Mermaid source (.mmd, .mermaid, or Markdown fences), redrawing them at a chosen output format (HTML / SVG / PNG), canvas size (slide, social card, doc, print), and level of detail (faithful reproduction or simplified for the audience's technical level). Ships with a neutral editorial skin and a first-run gate that prompts users to customize the style guide (colors, fonts) from their own website before generating. Includes annotation-callout primitive and optional sketchy variant.
|
||||
description: Create branded technical and product diagrams in 27 visual types, with semantic patterns, optional accessible motion, and .drawio/Mermaid import.
|
||||
license: MIT
|
||||
metadata:
|
||||
version: "2.2"
|
||||
version: "2.3"
|
||||
---
|
||||
|
||||
# Diagram Design
|
||||
|
||||
Create visual diagrams as self-contained HTML files with inline SVG and CSS, following an opinionated editorial design system.
|
||||
|
||||
Twenty-seven diagram types. One shared design system, complexity budget, and taste gate. Type-specific conventions live in `references/` and are loaded only when you pick a type.
|
||||
Twenty-seven visual types. Semantic patterns describe behavior independently; type references describe layout. Details load from `references/` only when selected.
|
||||
|
||||
---
|
||||
|
||||
@@ -32,16 +32,12 @@ Then branch:
|
||||
|
||||
**Once the style guide has been customized** (or the user explicitly opted for default), skip this gate on subsequent runs. A simple way to detect customization: if the `accent` value in `style-guide.md` differs from `#b5523a`, assume custom.
|
||||
|
||||
Don't silently ship default-skinned diagrams into a branded project — that's the failure mode this gate exists to prevent.
|
||||
|
||||
---
|
||||
|
||||
## 1. Philosophy
|
||||
|
||||
**The highest-quality move is usually deletion.**
|
||||
|
||||
From `.impeccable.md`: *"Confident restraint. Earn every element. One color accent, two families, a small spacing vocabulary. If removing it wouldn't hurt the page, remove it."*
|
||||
|
||||
Applied to schematics:
|
||||
|
||||
- Every node represents a distinct idea. Two nodes that always travel together are one node.
|
||||
@@ -68,9 +64,23 @@ Before drawing, ask: *Would the reader learn more from this than from a well-wri
|
||||
|
||||
---
|
||||
|
||||
## 3. Diagram Types
|
||||
## 3. Selection: semantic pattern, then visual type
|
||||
|
||||
### Selection guide
|
||||
When behavior, state, enforcement, or risk carries the meaning, first load [`references/semantic-patterns.md`](references/semantic-patterns.md) and choose one primary pattern. Then choose the nearest visual type for layout. If no pattern matches, choose the type directly.
|
||||
|
||||
| Behavioral trigger | Semantic pattern → nearest type |
|
||||
|---|---|
|
||||
| Fan-in, queue depth, finite capacity, bottleneck | **Fan-in queue / bottleneck** → Data flow |
|
||||
| Repeated Question / Input / Governance / Output slots across stages | **Stage framework with semantic slots** → Process |
|
||||
| Conversation or loose input becomes a structured durable artifact | **Unstructured input → structured artifact** → Data flow |
|
||||
| Two rule traces need pass/fail/skipped/not-reached and first divergence | **Paired policy-evaluation traces** → Flowchart |
|
||||
| Trust boundaries plus permitted/forbidden ingress or deploy paths | **Secure paved road** → Architecture |
|
||||
| Controls grouped by where they are enforced | **Governance / control catalog** → Layer stack |
|
||||
| Defenses compensate for prior gaps and residual risk propagates | **Compensating security layers** → Layer stack |
|
||||
|
||||
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 (27)
|
||||
|
||||
| If you're showing… | Use | Reference |
|
||||
|---|---|---|
|
||||
@@ -105,10 +115,10 @@ Before drawing, ask: *Would the reader learn more from this than from a well-wri
|
||||
Rules of thumb:
|
||||
|
||||
- If a 3-column table communicates the same thing, pick the table.
|
||||
- If you're combining two types, pick the dominant axis — don't hybridize grammars.
|
||||
- If two types seem useful, pick the dominant axis; a semantic pattern may add behavior-specific primitives, not a second layout grammar.
|
||||
- If you're past the complexity budget (§7), split into an overview + detail.
|
||||
|
||||
**Always load the relevant `references/type-*.md` before drawing** — it contains layout conventions, anti-patterns, and example files for that type.
|
||||
**Always load the chosen `references/type-*.md` before drawing.** When routed above, also load `semantic-patterns.md`; when animation is chosen, load `animation.md`.
|
||||
|
||||
---
|
||||
|
||||
@@ -195,6 +205,7 @@ Universal building blocks. Type-specialized primitives (lifeline, activation bar
|
||||
- Hand-drawn variant → [primitive-sketchy.md](references/primitive-sketchy.md)
|
||||
- Icon set (laptop, server, DB, K8s, Docker, AWS, …) → [primitive-icons.md](references/primitive-icons.md). Browse the gallery at [`assets/icons.html`](assets/icons.html).
|
||||
- Terminal / CLI-window variant → [primitive-terminal.md](references/primitive-terminal.md)
|
||||
- Optional explanatory motion → [animation.md](references/animation.md)
|
||||
|
||||
### Background
|
||||
|
||||
@@ -366,6 +377,7 @@ Quick check: if a coordinate ends in 1, 2, 3, 5, 6, 7, 9 — fix it.
|
||||
| Max tasks (Gantt) | 12 |
|
||||
| Max points (scatter plot) | 30 |
|
||||
| Max annotation callouts | 2 |
|
||||
| Max motion (optional) | 8 steps, 12 marked items, 2 simultaneous items — see [animation.md](references/animation.md) |
|
||||
|
||||
If you exceed, split into two diagrams (overview + detail).
|
||||
|
||||
@@ -409,7 +421,8 @@ Run before producing any diagram.
|
||||
|
||||
**Type fit:**
|
||||
|
||||
- [ ] Right type for what I'm showing? (§3 selection guide)
|
||||
- [ ] If behavior matters, did I choose one semantic pattern before the visual type and load `semantic-patterns.md`?
|
||||
- [ ] Right visual type for the layout? (§3 visual-type guide)
|
||||
- [ ] Would a table / paragraph do the same job? (If yes — don't draw.)
|
||||
- [ ] Loaded the matching `references/type-*.md`?
|
||||
- [ ] If this is an import — format, size, detail level, and audience set? `viewBox` and type ramp match the size preset? (§11, [output-spec.md §6](references/output-spec.md))
|
||||
@@ -444,9 +457,11 @@ Run before producing any diagram.
|
||||
- [ ] No vertical `writing-mode` text?
|
||||
- [ ] `viewBox` expanded for the legend strip (~60px)?
|
||||
- [ ] Every font size, coord, width, height, gap divisible by 4?
|
||||
- [ ] If animated, does the complete static/no-JS frame work, does reduced motion hide/disable playback, is the controller copied verbatim from `template-motion.html`, and did I run `verify-motion.py --shipped` plus the skin linter?
|
||||
|
||||
**Typography:**
|
||||
|
||||
- [ ] Brand match uses exact public families/weights, verified via `getComputedStyle`; fallbacks disclosed?
|
||||
- [ ] Human-readable names in Geist sans, not Geist Mono?
|
||||
- [ ] Technical sublabels (ports, commands, URLs) in Geist Mono?
|
||||
- [ ] Page title in Instrument Serif?
|
||||
@@ -470,12 +485,15 @@ Every diagram ships in three variants (see `assets/`):
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
### To create a new diagram
|
||||
|
||||
1. Copy the variant closest to what you want (`template.html` for minimal, `template-full.html` for cards).
|
||||
2. Load the matching `references/type-<name>.md` for layout conventions.
|
||||
3. Replace the eyebrow, h1, and SVG body. Replace `[diagram-slug]` with the file's diagram/variant slug, fill the copied `<title>` / `<desc>` placeholders, and do not delete them.
|
||||
4. Run the §9 taste gate.
|
||||
1. Copy the variant closest to what you want (`template.html` for minimal, `template-full.html` for cards, `template-motion.html` only when motion is requested).
|
||||
2. If behavior is load-bearing, choose a semantic pattern; then load the matching `references/type-<name>.md`.
|
||||
3. Replace the eyebrow, h1, and SVG body. Replace `[diagram-slug]` with the file slug and fill `<title>` / `<desc>`.
|
||||
4. If motion is requested, load `animation.md`; otherwise keep mode `none` and no script.
|
||||
5. Run the §9 taste gate.
|
||||
|
||||
---
|
||||
|
||||
@@ -516,9 +534,9 @@ Always produce a single self-contained `.html` file:
|
||||
|
||||
- Embedded CSS (no external except Google Fonts)
|
||||
- Inline SVG (no external images)
|
||||
- No JavaScript required
|
||||
- Static by default; minimal inline JavaScript only for explicit animation controls/state
|
||||
|
||||
Renders correctly in any modern browser.
|
||||
Renders correctly in any modern browser. Motion-enabled output must render its complete meaning without JavaScript; under `prefers-reduced-motion: reduce` it shows the complete static frame and hides/disables playback controls.
|
||||
|
||||
### Accessible SVG contract
|
||||
|
||||
|
||||
@@ -0,0 +1,482 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Where policy traces diverge</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; }
|
||||
:root {
|
||||
--paper: #f5f5f5;
|
||||
--paper-2: #ececec;
|
||||
--ink: #2d3142;
|
||||
--muted: #4f5d75;
|
||||
--soft: #7a8399;
|
||||
--rule: rgba(45,49,66,0.12);
|
||||
--rule-strong: rgba(45,49,66,0.30);
|
||||
--accent: #eb6c36;
|
||||
--accent-tint: rgba(235,108,54,0.08);
|
||||
--sans: 'Geist', system-ui, sans-serif;
|
||||
--serif: 'Instrument Serif', serif;
|
||||
--mono: 'Geist Mono', ui-monospace, monospace;
|
||||
--motion-fast: 160ms;
|
||||
--motion-step: 480ms;
|
||||
--motion-hold: 720ms;
|
||||
--motion-ease: cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
padding: 32px;
|
||||
color: var(--ink);
|
||||
background: var(--paper);
|
||||
font-family: var(--sans);
|
||||
}
|
||||
[data-motion-root] {
|
||||
width: min(100%, 1280px);
|
||||
margin: 0 auto;
|
||||
}
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
gap: 32px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.eyebrow {
|
||||
margin: 0 0 8px;
|
||||
color: var(--muted);
|
||||
font: 500 8px/1.4 var(--mono);
|
||||
letter-spacing: .18em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
font: 400 clamp(28px, 3vw, 40px)/1.08 var(--serif);
|
||||
letter-spacing: -.02em;
|
||||
}
|
||||
.lede {
|
||||
max-width: 480px;
|
||||
margin: 0 0 4px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.diagram-wrap {
|
||||
overflow-x: auto;
|
||||
border-top: 1px solid var(--rule);
|
||||
border-bottom: 1px solid var(--rule);
|
||||
}
|
||||
svg { display: block; width: 100%; min-width: 880px; }
|
||||
|
||||
[data-motion-controls] {
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 60px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.motion-ready [data-motion-controls] { display: flex; }
|
||||
[data-motion-controls][hidden] { display: none !important; }
|
||||
[data-motion-controls] button {
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--muted);
|
||||
border-radius: 4px;
|
||||
color: var(--ink);
|
||||
background: var(--paper);
|
||||
font: 600 12px/1 var(--sans);
|
||||
cursor: pointer;
|
||||
}
|
||||
[data-motion-controls] button:hover { border-color: var(--accent); }
|
||||
[data-motion-controls] button:focus-visible,
|
||||
[data-motion-root]:focus-visible {
|
||||
outline: 3px solid var(--accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
[data-motion-controls] button:disabled { cursor: not-allowed; opacity: .45; }
|
||||
.keyboard-help {
|
||||
margin-left: 8px;
|
||||
color: var(--soft);
|
||||
font: 500 9px/1.4 var(--mono);
|
||||
}
|
||||
[data-motion-status] {
|
||||
margin-left: auto;
|
||||
min-width: 272px;
|
||||
padding-left: 16px;
|
||||
border-left: 1px solid var(--rule);
|
||||
color: var(--muted);
|
||||
font: 500 10px/1.4 var(--mono);
|
||||
text-align: right;
|
||||
}
|
||||
noscript p {
|
||||
margin: 12px 0 0;
|
||||
color: var(--muted);
|
||||
font: 500 10px/1.4 var(--mono);
|
||||
}
|
||||
|
||||
/* The source frame is complete. Hiding begins only after user activation. */
|
||||
.motion-ready [data-motion-item] {
|
||||
opacity: .12;
|
||||
transform: translateY(8px);
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
transition: opacity var(--motion-step) var(--motion-ease),
|
||||
transform var(--motion-step) var(--motion-ease);
|
||||
}
|
||||
.motion-ready [data-motion-item].is-visible,
|
||||
.motion-ready[data-frame="end"] [data-motion-item],
|
||||
.motion-ready[data-frame="static"] [data-motion-item] {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
.row-focus { fill: transparent; stroke: transparent; stroke-width: 1.2; }
|
||||
.motion-ready [data-motion-item].is-current .row-focus {
|
||||
stroke: var(--muted);
|
||||
stroke-dasharray: 4 4;
|
||||
}
|
||||
html[data-motion="static"] [data-motion-controls] { display: none !important; }
|
||||
html[data-motion="static"] [data-motion-item] {
|
||||
opacity: 1 !important;
|
||||
transform: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: .001ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
scroll-behavior: auto !important;
|
||||
transition-duration: .001ms !important;
|
||||
}
|
||||
[data-motion-item] { opacity: 1 !important; transform: none !important; }
|
||||
[data-motion-decorative] { display: none !important; }
|
||||
[data-motion-controls] { display: none !important; }
|
||||
}
|
||||
@media print {
|
||||
body { padding: 0; }
|
||||
[data-motion-controls], [data-motion-decorative] { display: none !important; }
|
||||
[data-motion-item] { opacity: 1 !important; transform: none !important; }
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
body { padding: 20px; }
|
||||
.page-header { display: block; }
|
||||
.lede { margin-top: 12px; }
|
||||
[data-motion-status] { min-width: 200px; }
|
||||
.keyboard-help { display: none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Use ?motion=static for deterministic screenshots and export. -->
|
||||
<main data-motion-root data-motion-mode="step" data-step-count="5" data-step-current="5"
|
||||
data-frame="static" data-static-frame="complete" tabindex="-1">
|
||||
<header class="page-header">
|
||||
<div>
|
||||
<p class="eyebrow">Semantic pattern · Paired policy-evaluation traces</p>
|
||||
<h1>Where policy traces diverge</h1>
|
||||
</div>
|
||||
<p class="lede">Two requests traverse the same ordered rules. Explicit states distinguish a rule that does not apply from one the evaluator never reaches.</p>
|
||||
</header>
|
||||
|
||||
<div class="diagram-wrap">
|
||||
<svg viewBox="0 0 1200 660" xmlns="http://www.w3.org/2000/svg" role="img"
|
||||
aria-labelledby="policy-trace-animated-title policy-trace-animated-desc"
|
||||
data-motion-primitive="policy-evaluation">
|
||||
<title id="policy-trace-animated-title">Where two policy evaluations first diverge</title>
|
||||
<desc id="policy-trace-animated-desc">Two requests pass identity and audience checks. At the data-class rule, the read-only request passes while the sensitive-write request fails; later rules are skipped or not reached, producing permitted and denied outcomes.</desc>
|
||||
<rect width="1200" height="660" fill="#f5f5f5"/>
|
||||
|
||||
<!-- Persistent trace paths are meaningful in every frame. -->
|
||||
<line x1="620" y1="104" x2="620" y2="576" stroke="rgba(45,49,66,0.30)" stroke-width="1"/>
|
||||
<line data-trace-connector="trace-b" x1="936" y1="104" x2="936" y2="336" stroke="rgba(45,49,66,0.30)" stroke-width="1"/>
|
||||
|
||||
<!-- Shared rule column and trace headers. -->
|
||||
<text x="72" y="76" fill="#4f5d75" font-family="'Geist Mono', monospace" font-size="12" letter-spacing="1.6">ORDERED POLICY</text>
|
||||
<rect x="500" y="44" width="240" height="60" rx="6" fill="#ffffff" stroke="#2d3142"/>
|
||||
<text x="620" y="72" text-anchor="middle" fill="#2d3142" font-family="'Geist', sans-serif" font-size="12" font-weight="600">Trace A · read-only</text>
|
||||
<text x="620" y="92" text-anchor="middle" fill="#4f5d75" font-family="'Geist Mono', monospace" font-size="12">internal metrics request</text>
|
||||
<rect x="816" y="44" width="240" height="60" rx="6" fill="#ffffff" stroke="#2d3142"/>
|
||||
<text x="936" y="72" text-anchor="middle" fill="#2d3142" font-family="'Geist', sans-serif" font-size="12" font-weight="600">Trace B · sensitive write</text>
|
||||
<text x="936" y="92" text-anchor="middle" fill="#4f5d75" font-family="'Geist Mono', monospace" font-size="12">restricted records request</text>
|
||||
|
||||
<g data-motion-item data-step="1" aria-label="Rule 1: Identity bound passes for both traces">
|
||||
<rect class="row-focus" x="48" y="124" width="1080" height="68" rx="4"/>
|
||||
<text x="72" y="152" fill="#2d3142" font-family="'Geist', sans-serif" font-size="12" font-weight="600">1 · Identity bound</text>
|
||||
<text x="72" y="172" fill="#4f5d75" font-family="'Geist Mono', monospace" font-size="12">named principal required</text>
|
||||
<rect x="532" y="132" width="176" height="52" rx="4" fill="rgba(45,49,66,0.05)" stroke="#4f5d75"/>
|
||||
<text x="620" y="164" text-anchor="middle" fill="#2d3142" font-family="'Geist Mono', monospace" font-size="10" font-weight="600">✓ PASS</text>
|
||||
<rect x="848" y="132" width="176" height="52" rx="4" fill="rgba(45,49,66,0.05)" stroke="#4f5d75"/>
|
||||
<text x="936" y="164" text-anchor="middle" fill="#2d3142" font-family="'Geist Mono', monospace" font-size="10" font-weight="600">✓ PASS</text>
|
||||
</g>
|
||||
|
||||
<g data-motion-item data-step="2" aria-label="Rule 2: Internal audience passes for both traces">
|
||||
<rect class="row-focus" x="48" y="200" width="1080" height="68" rx="4"/>
|
||||
<text x="72" y="228" fill="#2d3142" font-family="'Geist', sans-serif" font-size="12" font-weight="600">2 · Internal audience</text>
|
||||
<text x="72" y="248" fill="#4f5d75" font-family="'Geist Mono', monospace" font-size="12">employees only</text>
|
||||
<rect x="532" y="208" width="176" height="52" rx="4" fill="rgba(45,49,66,0.05)" stroke="#4f5d75"/>
|
||||
<text x="620" y="240" text-anchor="middle" fill="#2d3142" font-family="'Geist Mono', monospace" font-size="10" font-weight="600">✓ PASS</text>
|
||||
<rect x="848" y="208" width="176" height="52" rx="4" fill="rgba(45,49,66,0.05)" stroke="#4f5d75"/>
|
||||
<text x="936" y="240" text-anchor="middle" fill="#2d3142" font-family="'Geist Mono', monospace" font-size="10" font-weight="600">✓ PASS</text>
|
||||
</g>
|
||||
|
||||
<g data-motion-item data-step="3" aria-label="Rule 3 is the first divergence: Trace A passes and Trace B fails">
|
||||
<rect class="row-focus" x="48" y="276" width="1080" height="80" rx="4"/>
|
||||
<text x="72" y="304" fill="#2d3142" font-family="'Geist', sans-serif" font-size="12" font-weight="600">3 · Data class</text>
|
||||
<text x="72" y="324" fill="#4f5d75" font-family="'Geist Mono', monospace" font-size="12">restricted writes denied</text>
|
||||
<rect x="532" y="284" width="176" height="52" rx="4" fill="rgba(45,49,66,0.05)" stroke="#4f5d75"/>
|
||||
<text x="620" y="316" text-anchor="middle" fill="#2d3142" font-family="'Geist Mono', monospace" font-size="10" font-weight="600">✓ PASS</text>
|
||||
<rect data-trace-b-terminal="fail" x="848" y="284" width="176" height="52" rx="4" fill="rgba(235,108,54,0.08)" stroke="#eb6c36" stroke-width="1.2"/>
|
||||
<text x="936" y="316" text-anchor="middle" fill="#eb6c36" font-family="'Geist Mono', monospace" font-size="10" font-weight="600">× FAIL</text>
|
||||
<path d="M716 344 V352 H840 V344" fill="none" stroke="#eb6c36" stroke-width="1.2"/>
|
||||
<rect x="712" y="336" width="132" height="20" rx="2" fill="#f5f5f5"/>
|
||||
<text x="778" y="351" text-anchor="middle" fill="#eb6c36" font-family="'Geist Mono', monospace" font-size="12" font-weight="600" letter-spacing=".8">FIRST DIVERGENCE</text>
|
||||
</g>
|
||||
|
||||
<g data-motion-item data-step="4" aria-label="Rule 4 is skipped for Trace A and not reached for Trace B">
|
||||
<rect class="row-focus" x="48" y="364" width="1080" height="68" rx="4"/>
|
||||
<text x="72" y="392" fill="#2d3142" font-family="'Geist', sans-serif" font-size="12" font-weight="600">4 · Human approval</text>
|
||||
<text x="72" y="412" fill="#4f5d75" font-family="'Geist Mono', monospace" font-size="12">required only for writes</text>
|
||||
<rect x="532" y="372" width="176" height="52" rx="4" fill="#f5f5f5" stroke="#7a8399" stroke-dasharray="4 4"/>
|
||||
<text x="620" y="404" text-anchor="middle" fill="#4f5d75" font-family="'Geist Mono', monospace" font-size="10" font-weight="600">— SKIPPED</text>
|
||||
<rect data-trace-b-state="not-reached" x="848" y="372" width="176" height="52" rx="4" fill="rgba(45,49,66,0.02)" stroke="rgba(45,49,66,0.30)" stroke-dasharray="4 4"/>
|
||||
<text x="936" y="404" text-anchor="middle" fill="#7a8399" font-family="'Geist Mono', monospace" font-size="10" font-weight="600">○ NOT REACHED</text>
|
||||
</g>
|
||||
|
||||
<g data-motion-item data-step="5" aria-label="Rule 5 passes for Trace A and is not reached for Trace B; outcomes are permitted and denied">
|
||||
<rect class="row-focus" x="48" y="440" width="1080" height="152" rx="4"/>
|
||||
<text x="72" y="468" fill="#2d3142" font-family="'Geist', sans-serif" font-size="12" font-weight="600">5 · Deploy gate</text>
|
||||
<text x="72" y="488" fill="#4f5d75" font-family="'Geist Mono', monospace" font-size="12">all reached rules must pass</text>
|
||||
<rect x="532" y="448" width="176" height="52" rx="4" fill="rgba(45,49,66,0.05)" stroke="#4f5d75"/>
|
||||
<text x="620" y="480" text-anchor="middle" fill="#2d3142" font-family="'Geist Mono', monospace" font-size="10" font-weight="600">✓ PASS</text>
|
||||
<rect data-trace-b-state="not-reached" x="848" y="448" width="176" height="52" rx="4" fill="rgba(45,49,66,0.02)" stroke="rgba(45,49,66,0.30)" stroke-dasharray="4 4"/>
|
||||
<text x="936" y="480" text-anchor="middle" fill="#7a8399" font-family="'Geist Mono', monospace" font-size="10" font-weight="600">○ NOT REACHED</text>
|
||||
|
||||
<rect x="500" y="528" width="240" height="56" rx="6" fill="#2d3142" stroke="#2d3142"/>
|
||||
<text x="620" y="552" text-anchor="middle" fill="#f5f5f5" font-family="'Geist Mono', monospace" font-size="12" letter-spacing="1.2">OUTCOME</text>
|
||||
<text x="620" y="572" text-anchor="middle" fill="#f5f5f5" font-family="'Geist', sans-serif" font-size="12" font-weight="600">Permitted</text>
|
||||
<rect data-trace-b-outcome="denied" x="816" y="528" width="240" height="56" rx="6" fill="#f5f5f5"/>
|
||||
<rect x="816" y="528" width="240" height="56" rx="6" fill="rgba(235,108,54,0.08)" stroke="#eb6c36" stroke-width="1.2"/>
|
||||
<text x="936" y="552" text-anchor="middle" fill="#eb6c36" font-family="'Geist Mono', monospace" font-size="12" letter-spacing="1.2">OUTCOME</text>
|
||||
<text x="936" y="572" text-anchor="middle" fill="#2d3142" font-family="'Geist', sans-serif" font-size="12" font-weight="600">Denied · review</text>
|
||||
</g>
|
||||
|
||||
<line x1="72" y1="620" x2="1128" y2="620" stroke="rgba(45,49,66,0.12)"/>
|
||||
<text x="72" y="640" fill="#4f5d75" font-family="'Geist Mono', monospace" font-size="12">✓ PASS</text>
|
||||
<text x="176" y="640" fill="#eb6c36" font-family="'Geist Mono', monospace" font-size="12">× FAIL</text>
|
||||
<text x="272" y="640" fill="#4f5d75" font-family="'Geist Mono', monospace" font-size="12">— SKIPPED · rule intentionally bypassed</text>
|
||||
<text x="564" y="640" fill="#7a8399" font-family="'Geist Mono', monospace" font-size="12">○ NOT REACHED · earlier terminal result</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div data-motion-controls aria-label="Policy trace playback controls">
|
||||
<button type="button" data-motion-action="prev">Previous</button>
|
||||
<button type="button" data-motion-action="play" aria-pressed="false">Play</button>
|
||||
<button type="button" data-motion-action="pause" aria-pressed="true">Pause</button>
|
||||
<button type="button" data-motion-action="next">Next</button>
|
||||
<button type="button" data-motion-action="replay">Replay</button>
|
||||
<span class="keyboard-help">←/→ step · Space play/pause · R replay · Home reset</span>
|
||||
<span data-motion-status role="status" aria-live="polite" aria-atomic="true">Static frame · all 5 rules visible</span>
|
||||
</div>
|
||||
<noscript><p>Animation controls require JavaScript. The complete final policy trace is shown above.</p></noscript>
|
||||
</main>
|
||||
|
||||
<script data-diagram-controls>
|
||||
(() => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const staticOverride = params.get('motion') === 'static' || document.documentElement.dataset.motion === 'static';
|
||||
const requestedStep = params.get('step');
|
||||
const reduce = matchMedia('(prefers-reduced-motion: reduce)');
|
||||
document.fonts.ready.then(() => { document.documentElement.dataset.fontsReady = 'true'; });
|
||||
|
||||
document.querySelectorAll('[data-motion-root]').forEach((root) => {
|
||||
const count = Number(root.dataset.stepCount);
|
||||
const parsedStep = requestedStep !== null && /^\d+$/.test(requestedStep) ? Number(requestedStep) : null;
|
||||
const exactStep = params.get('motion') === 'step' && Number.isSafeInteger(parsedStep) && parsedStep >= 0 && parsedStep <= count ? parsedStep : null;
|
||||
const items = [...root.querySelectorAll('[data-motion-item]')];
|
||||
const labels = Array.from({ length: count + 1 }, (_, index) => index === 0
|
||||
? 'Ready'
|
||||
: items.filter((item) => !item.hasAttribute('data-motion-decorative') && Number(item.dataset.step) === index)
|
||||
.map((item) => item.getAttribute('aria-label')).join('; '));
|
||||
const controls = root.querySelector('[data-motion-controls]');
|
||||
const status = root.querySelector('[data-motion-status]');
|
||||
const visibleStep = root.querySelector('[data-motion-step-label]');
|
||||
let step = count;
|
||||
let timer = 0;
|
||||
let playing = false;
|
||||
let stepBeforeReduce = 0;
|
||||
let resumeAfterReduce = root.dataset.motionMode === 'reveal';
|
||||
|
||||
const buttons = (action) => controls.querySelector(`[data-motion-action="${action}"]`);
|
||||
const clearClock = () => { window.clearTimeout(timer); timer = 0; };
|
||||
const announce = () => { status.textContent = `Step ${step} of ${count}: ${labels[step] || 'Complete'}`; };
|
||||
const setControlsAvailable = (available) => {
|
||||
controls.hidden = !available;
|
||||
controls.setAttribute('aria-hidden', String(!available));
|
||||
controls.dataset.playbackDisabled = String(!available);
|
||||
controls.querySelectorAll('button').forEach((control) => { control.disabled = !available; });
|
||||
};
|
||||
|
||||
function render(next, userInitiated = false) {
|
||||
step = Math.max(0, Math.min(count, next));
|
||||
root.dataset.stepCurrent = String(step);
|
||||
root.dataset.frame = step === count ? 'end' : step === 0 ? 'start' : 'step';
|
||||
items.forEach((item) => {
|
||||
const itemStep = Number(item.dataset.step);
|
||||
item.classList.toggle('is-visible', itemStep <= step);
|
||||
item.classList.toggle('is-current', itemStep === step);
|
||||
});
|
||||
if (visibleStep) visibleStep.textContent = String(step);
|
||||
const unavailable = controls.dataset.playbackDisabled === 'true';
|
||||
buttons('prev').disabled = unavailable || step === 0;
|
||||
buttons('next').disabled = unavailable || step === count;
|
||||
if (userInitiated) announce();
|
||||
}
|
||||
|
||||
function pause(userInitiated = false) {
|
||||
clearClock();
|
||||
playing = false;
|
||||
root.classList.remove('is-playing');
|
||||
buttons('play').setAttribute('aria-pressed', 'false');
|
||||
buttons('pause').setAttribute('aria-pressed', 'true');
|
||||
if (userInitiated) status.textContent = `Paused at step ${step} of ${count}`;
|
||||
}
|
||||
|
||||
function finish() {
|
||||
pause(false);
|
||||
root.dataset.frame = 'end';
|
||||
status.textContent = `Complete · step ${count} of ${count}`;
|
||||
}
|
||||
|
||||
function tick() {
|
||||
if (!playing) return;
|
||||
if (step >= count) { finish(); return; }
|
||||
render(step + 1, false);
|
||||
if (step >= count) { finish(); return; }
|
||||
timer = window.setTimeout(tick, 720);
|
||||
}
|
||||
|
||||
function play(fromStart = false, userInitiated = false) {
|
||||
clearClock();
|
||||
if (fromStart || step >= count) render(0, userInitiated);
|
||||
playing = true;
|
||||
root.classList.add('is-playing');
|
||||
buttons('play').setAttribute('aria-pressed', 'true');
|
||||
buttons('pause').setAttribute('aria-pressed', 'false');
|
||||
timer = window.setTimeout(tick, 720);
|
||||
}
|
||||
|
||||
controls.addEventListener('click', (event) => {
|
||||
const button = event.target.closest('[data-motion-action]');
|
||||
if (!button) return;
|
||||
const action = button.dataset.motionAction;
|
||||
if (action === 'play') play(false, true);
|
||||
if (action === 'pause') pause(true);
|
||||
if (action === 'replay') play(true, true);
|
||||
if (action === 'prev') { pause(false); render(step - 1, true); }
|
||||
if (action === 'next') { pause(false); render(step + 1, true); }
|
||||
});
|
||||
|
||||
root.addEventListener('keydown', (event) => {
|
||||
if (event.target.matches('input, textarea, select, a[href]')) return;
|
||||
const keys = { ArrowLeft: step - 1, ArrowRight: step + 1, Home: 0, End: count };
|
||||
if (event.code === 'Space' && !event.target.closest('button')) {
|
||||
event.preventDefault();
|
||||
playing ? pause(true) : play(false, true);
|
||||
return;
|
||||
}
|
||||
if (event.key.toLowerCase() === 'r') {
|
||||
event.preventDefault();
|
||||
play(true, true);
|
||||
return;
|
||||
}
|
||||
if (!(event.key in keys)) return;
|
||||
event.preventDefault();
|
||||
pause(false);
|
||||
render(keys[event.key], true);
|
||||
});
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'hidden' && playing) pause(false);
|
||||
});
|
||||
reduce.addEventListener('change', (event) => {
|
||||
if (event.matches && root.dataset.motionState !== 'reduced') {
|
||||
stepBeforeReduce = step;
|
||||
resumeAfterReduce = playing;
|
||||
}
|
||||
pause(false);
|
||||
if (event.matches) {
|
||||
setControlsAvailable(false);
|
||||
render(count, false);
|
||||
root.dataset.frame = 'static';
|
||||
root.dataset.motionState = 'reduced';
|
||||
status.textContent = 'Reduced motion · complete static frame · playback controls unavailable';
|
||||
} else if (staticOverride || root.dataset.motionMode === 'none') {
|
||||
setControlsAvailable(false);
|
||||
render(count, false);
|
||||
root.dataset.frame = 'static';
|
||||
root.dataset.motionState = 'static';
|
||||
status.textContent = 'Static frame · complete diagram · playback controls unavailable';
|
||||
} else {
|
||||
delete root.dataset.motionState;
|
||||
if (exactStep !== null) {
|
||||
setControlsAvailable(false);
|
||||
render(exactStep, false);
|
||||
root.dataset.frame = 'step';
|
||||
root.dataset.motionState = 'test';
|
||||
status.textContent = `Test frame · step ${step} of ${count} · playback controls unavailable`;
|
||||
} else {
|
||||
setControlsAvailable(true);
|
||||
render(stepBeforeReduce, false);
|
||||
status.textContent = `Ready · step ${step} of ${count}`;
|
||||
if (resumeAfterReduce && root.dataset.motionMode === 'reveal') {
|
||||
play(false, false);
|
||||
}
|
||||
}
|
||||
resumeAfterReduce = false;
|
||||
}
|
||||
});
|
||||
|
||||
root.classList.add('motion-ready');
|
||||
if (staticOverride) {
|
||||
document.documentElement.dataset.motion = 'static';
|
||||
pause(false);
|
||||
setControlsAvailable(false);
|
||||
render(count, false);
|
||||
root.dataset.frame = 'static';
|
||||
root.dataset.motionState = 'static';
|
||||
status.textContent = 'Static frame · complete diagram · playback controls unavailable';
|
||||
} else if (reduce.matches || root.dataset.motionMode === 'none') {
|
||||
pause(false);
|
||||
setControlsAvailable(false);
|
||||
render(count, false);
|
||||
root.dataset.frame = 'static';
|
||||
root.dataset.motionState = reduce.matches ? 'reduced' : 'static';
|
||||
status.textContent = reduce.matches
|
||||
? 'Reduced motion · complete static frame · playback controls unavailable'
|
||||
: 'Static frame · complete diagram · playback controls unavailable';
|
||||
} else if (exactStep !== null) {
|
||||
document.documentElement.dataset.motion = 'step';
|
||||
pause(false);
|
||||
setControlsAvailable(false);
|
||||
render(exactStep, false);
|
||||
root.dataset.frame = 'step';
|
||||
root.dataset.motionState = 'test';
|
||||
status.textContent = `Test frame · step ${step} of ${count} · playback controls unavailable`;
|
||||
} else if (root.dataset.motionMode === 'reveal') {
|
||||
setControlsAvailable(true);
|
||||
render(0, false);
|
||||
play(false, false);
|
||||
} else {
|
||||
pause(false);
|
||||
setControlsAvailable(true);
|
||||
render(0, false);
|
||||
status.textContent = `Ready · step 0 of ${count}`;
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,427 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Motion diagram template</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; }
|
||||
:root {
|
||||
--color-paper: #f5f5f5;
|
||||
--color-ink: #2d3142;
|
||||
--color-muted: #4f5d75;
|
||||
--color-soft: #7a8399;
|
||||
--color-accent: #eb6c36;
|
||||
--color-rule: rgba(45,49,66,0.12);
|
||||
--font-sans: 'Geist', system-ui, sans-serif;
|
||||
--font-serif: 'Instrument Serif', serif;
|
||||
--font-mono: 'Geist Mono', ui-monospace, monospace;
|
||||
--motion-fast: 160ms;
|
||||
--motion-step: 480ms;
|
||||
--motion-hold: 720ms;
|
||||
--motion-total: 4320ms;
|
||||
--motion-ease: cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 48px 32px;
|
||||
color: var(--color-ink);
|
||||
background: var(--color-paper);
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
[data-motion-root] { width: min(100%, 1000px); }
|
||||
.eyebrow {
|
||||
margin: 0 0 8px;
|
||||
color: var(--color-muted);
|
||||
font: 500 12px/1.4 var(--font-mono);
|
||||
letter-spacing: .18em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
h1 { margin: 0 0 24px; font: 400 32px/1.15 var(--font-serif); }
|
||||
svg { display: block; width: 100%; min-width: 720px; }
|
||||
|
||||
/* Static source is complete. Only an initialized enhancement hides items. */
|
||||
.motion-ready [data-motion-item] {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
transform-box: fill-box;
|
||||
transform-origin: center;
|
||||
transition: opacity var(--motion-step) var(--motion-ease),
|
||||
transform var(--motion-step) var(--motion-ease);
|
||||
}
|
||||
.motion-ready [data-motion-item].is-visible,
|
||||
.motion-ready[data-frame="end"] [data-motion-item],
|
||||
.motion-ready[data-frame="static"] [data-motion-item] {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
.draw-path {
|
||||
fill: none;
|
||||
stroke: var(--color-accent);
|
||||
stroke-width: 3;
|
||||
stroke-dasharray: 1;
|
||||
stroke-dashoffset: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
.motion-ready .draw-path.is-visible {
|
||||
animation: draw-path var(--motion-step) linear forwards;
|
||||
}
|
||||
.flow-token { opacity: 0; pointer-events: none; }
|
||||
.motion-ready.is-playing .flow-token.is-visible {
|
||||
opacity: 1;
|
||||
animation: token-flow 3.2s linear 1 forwards;
|
||||
}
|
||||
.motion-ready[data-motion-mode="loop"].is-playing .flow-token.is-visible {
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
@keyframes draw-path { to { stroke-dashoffset: 0; } }
|
||||
@keyframes token-flow {
|
||||
0% { transform: translateX(0); }
|
||||
100% { transform: translateX(240px); }
|
||||
}
|
||||
|
||||
[data-motion-controls] {
|
||||
display: none;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--color-rule);
|
||||
}
|
||||
.motion-ready [data-motion-controls] { display: flex; }
|
||||
[data-motion-controls][hidden] { display: none !important; }
|
||||
[data-motion-controls] button {
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--color-muted);
|
||||
border-radius: 4px;
|
||||
color: var(--color-ink);
|
||||
background: var(--color-paper);
|
||||
font: 600 12px/1 var(--font-sans);
|
||||
cursor: pointer;
|
||||
}
|
||||
[data-motion-controls] button:hover { border-color: var(--color-accent); }
|
||||
[data-motion-controls] button:focus-visible {
|
||||
outline: 3px solid var(--color-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
[data-motion-controls] button:disabled { cursor: not-allowed; opacity: .45; }
|
||||
[data-motion-status-visible] {
|
||||
margin-left: auto;
|
||||
color: var(--color-muted);
|
||||
font: 500 12px/1.4 var(--font-mono);
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0,0,0,0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
html[data-motion="static"] [data-motion-controls],
|
||||
html[data-motion="static"] [data-motion-decorative] { display: none !important; }
|
||||
html[data-motion="static"] [data-motion-item] {
|
||||
opacity: 1 !important;
|
||||
transform: none !important;
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
html[data-motion="step"] [data-motion-controls],
|
||||
html[data-motion="step"] [data-motion-decorative] { display: none !important; }
|
||||
html[data-motion="step"] [data-motion-item] {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: .001ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
scroll-behavior: auto !important;
|
||||
transition-duration: .001ms !important;
|
||||
}
|
||||
[data-motion-item] { opacity: 1 !important; transform: none !important; }
|
||||
[data-motion-decorative] { display: none !important; }
|
||||
[data-motion-controls] { display: none !important; }
|
||||
}
|
||||
@media print {
|
||||
[data-motion-controls], [data-motion-decorative] { display: none !important; }
|
||||
[data-motion-item] {
|
||||
opacity: 1 !important;
|
||||
transform: none !important;
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Replace template-motion in IDs and prose when copying this file. -->
|
||||
<main data-motion-root data-motion-mode="step" data-step-count="5" data-step-current="5" data-frame="static" data-static-frame="complete">
|
||||
<p class="eyebrow">Process · Optional motion</p>
|
||||
<h1>Request evaluation</h1>
|
||||
|
||||
<svg viewBox="0 0 960 480" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="template-motion-title template-motion-desc">
|
||||
<title id="template-motion-title">Request evaluation</title>
|
||||
<desc id="template-motion-desc">A request is validated, evaluated against a policy, queued, and appended to an audit log.</desc>
|
||||
<defs>
|
||||
<marker id="template-motion-arrow" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#4f5d75"/>
|
||||
</marker>
|
||||
</defs>
|
||||
<rect width="960" height="480" fill="#f5f5f5"/>
|
||||
|
||||
<!-- Persistent connectors carry the static meaning. -->
|
||||
<path d="M248 180 H356" fill="none" stroke="#4f5d75" marker-end="url(#template-motion-arrow)"/>
|
||||
<path d="M556 180 H664" fill="none" stroke="#4f5d75" marker-end="url(#template-motion-arrow)"/>
|
||||
<path d="M764 232 V328" fill="none" stroke="#4f5d75" marker-end="url(#template-motion-arrow)"/>
|
||||
|
||||
<g data-motion-item data-step="1" aria-label="Step 1: Request received">
|
||||
<rect x="48" y="128" width="200" height="104" rx="8" fill="#f5f5f5" stroke="#2d3142"/>
|
||||
<text x="148" y="172" text-anchor="middle" fill="#2d3142" font-family="Geist, sans-serif" font-size="16" font-weight="600">Request</text>
|
||||
<text x="148" y="196" text-anchor="middle" fill="#4f5d75" font-family="Geist Mono, monospace" font-size="12">fields complete</text>
|
||||
</g>
|
||||
<g data-motion-item data-step="2" aria-label="Step 2: Policy passed">
|
||||
<rect x="356" y="128" width="200" height="104" rx="8" fill="rgba(235,108,54,0.08)" stroke="#eb6c36"/>
|
||||
<text x="456" y="168" text-anchor="middle" fill="#2d3142" font-family="Geist, sans-serif" font-size="16" font-weight="600">Policy</text>
|
||||
<text x="456" y="196" text-anchor="middle" fill="#4f5d75" font-family="Geist Mono, monospace" font-size="12">✓ PASS · rule 2</text>
|
||||
</g>
|
||||
<g data-motion-item data-step="3" aria-label="Step 3: Three items queued">
|
||||
<rect x="664" y="128" width="200" height="104" rx="8" fill="#f5f5f5" stroke="#2d3142"/>
|
||||
<text x="764" y="164" text-anchor="middle" fill="#2d3142" font-family="Geist, sans-serif" font-size="16" font-weight="600">Queue</text>
|
||||
<rect x="700" y="184" width="16" height="16" fill="#4f5d75"/><rect x="724" y="184" width="16" height="16" fill="#4f5d75"/><rect x="748" y="184" width="16" height="16" fill="#4f5d75"/>
|
||||
<text x="796" y="196" fill="#4f5d75" font-family="Geist Mono, monospace" font-size="12">3 queued</text>
|
||||
</g>
|
||||
<g data-motion-item data-step="4" aria-label="Step 4: Audit entry appended">
|
||||
<rect x="664" y="328" width="200" height="88" rx="8" fill="#f5f5f5" stroke="#2d3142"/>
|
||||
<text x="764" y="364" text-anchor="middle" fill="#2d3142" font-family="Geist, sans-serif" font-size="16" font-weight="600">Audit log</text>
|
||||
<text x="764" y="388" text-anchor="middle" fill="#4f5d75" font-family="Geist Mono, monospace" font-size="12">0042 · appended</text>
|
||||
</g>
|
||||
<g data-motion-item data-step="5" aria-label="Step 5: Evaluation complete">
|
||||
<text x="48" y="376" fill="#2d3142" font-family="Geist, sans-serif" font-size="16" font-weight="600">Outcome: accepted and recorded</text>
|
||||
</g>
|
||||
|
||||
<!-- Decorative duplicate path and token never carry unique meaning. -->
|
||||
<path data-motion-item data-step="2" data-motion-decorative aria-hidden="true" focusable="false" pathLength="1" class="draw-path" d="M248 180 H356"/>
|
||||
<circle data-motion-item data-step="3" data-motion-decorative aria-hidden="true" focusable="false" class="flow-token" cx="556" cy="180" r="6" fill="#eb6c36"/>
|
||||
</svg>
|
||||
|
||||
<div data-motion-controls aria-label="Diagram playback controls">
|
||||
<button type="button" data-motion-action="prev">Previous</button>
|
||||
<button type="button" data-motion-action="play" aria-pressed="false">Play</button>
|
||||
<button type="button" data-motion-action="pause" aria-pressed="true">Pause</button>
|
||||
<button type="button" data-motion-action="next">Next</button>
|
||||
<button type="button" data-motion-action="replay">Replay</button>
|
||||
<span class="keyboard-help">Arrow keys step · Home/End jump</span>
|
||||
<span data-motion-status-visible aria-hidden="true">Step <span data-motion-step-label>5</span> of 5</span>
|
||||
</div>
|
||||
<p class="sr-only" data-motion-status role="status" aria-live="polite" aria-atomic="true"></p>
|
||||
</main>
|
||||
|
||||
<script data-diagram-controls>
|
||||
(() => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const staticOverride = params.get('motion') === 'static' || document.documentElement.dataset.motion === 'static';
|
||||
const requestedStep = params.get('step');
|
||||
const reduce = matchMedia('(prefers-reduced-motion: reduce)');
|
||||
document.fonts.ready.then(() => { document.documentElement.dataset.fontsReady = 'true'; });
|
||||
|
||||
document.querySelectorAll('[data-motion-root]').forEach((root) => {
|
||||
const count = Number(root.dataset.stepCount);
|
||||
const parsedStep = requestedStep !== null && /^\d+$/.test(requestedStep) ? Number(requestedStep) : null;
|
||||
const exactStep = params.get('motion') === 'step' && Number.isSafeInteger(parsedStep) && parsedStep >= 0 && parsedStep <= count ? parsedStep : null;
|
||||
const items = [...root.querySelectorAll('[data-motion-item]')];
|
||||
const labels = Array.from({ length: count + 1 }, (_, index) => index === 0
|
||||
? 'Ready'
|
||||
: items.filter((item) => !item.hasAttribute('data-motion-decorative') && Number(item.dataset.step) === index)
|
||||
.map((item) => item.getAttribute('aria-label')).join('; '));
|
||||
const controls = root.querySelector('[data-motion-controls]');
|
||||
const status = root.querySelector('[data-motion-status]');
|
||||
const visibleStep = root.querySelector('[data-motion-step-label]');
|
||||
let step = count;
|
||||
let timer = 0;
|
||||
let playing = false;
|
||||
let stepBeforeReduce = 0;
|
||||
let resumeAfterReduce = root.dataset.motionMode === 'reveal';
|
||||
|
||||
const buttons = (action) => controls.querySelector(`[data-motion-action="${action}"]`);
|
||||
const clearClock = () => { window.clearTimeout(timer); timer = 0; };
|
||||
const announce = () => { status.textContent = `Step ${step} of ${count}: ${labels[step] || 'Complete'}`; };
|
||||
const setControlsAvailable = (available) => {
|
||||
controls.hidden = !available;
|
||||
controls.setAttribute('aria-hidden', String(!available));
|
||||
controls.dataset.playbackDisabled = String(!available);
|
||||
controls.querySelectorAll('button').forEach((control) => { control.disabled = !available; });
|
||||
};
|
||||
|
||||
function render(next, userInitiated = false) {
|
||||
step = Math.max(0, Math.min(count, next));
|
||||
root.dataset.stepCurrent = String(step);
|
||||
root.dataset.frame = step === count ? 'end' : step === 0 ? 'start' : 'step';
|
||||
items.forEach((item) => {
|
||||
const itemStep = Number(item.dataset.step);
|
||||
item.classList.toggle('is-visible', itemStep <= step);
|
||||
item.classList.toggle('is-current', itemStep === step);
|
||||
});
|
||||
if (visibleStep) visibleStep.textContent = String(step);
|
||||
const unavailable = controls.dataset.playbackDisabled === 'true';
|
||||
buttons('prev').disabled = unavailable || step === 0;
|
||||
buttons('next').disabled = unavailable || step === count;
|
||||
if (userInitiated) announce();
|
||||
}
|
||||
|
||||
function pause(userInitiated = false) {
|
||||
clearClock();
|
||||
playing = false;
|
||||
root.classList.remove('is-playing');
|
||||
buttons('play').setAttribute('aria-pressed', 'false');
|
||||
buttons('pause').setAttribute('aria-pressed', 'true');
|
||||
if (userInitiated) status.textContent = `Paused at step ${step} of ${count}`;
|
||||
}
|
||||
|
||||
function finish() {
|
||||
pause(false);
|
||||
root.dataset.frame = 'end';
|
||||
status.textContent = `Complete · step ${count} of ${count}`;
|
||||
}
|
||||
|
||||
function tick() {
|
||||
if (!playing) return;
|
||||
if (step >= count) { finish(); return; }
|
||||
render(step + 1, false);
|
||||
if (step >= count) { finish(); return; }
|
||||
timer = window.setTimeout(tick, 720);
|
||||
}
|
||||
|
||||
function play(fromStart = false, userInitiated = false) {
|
||||
clearClock();
|
||||
if (fromStart || step >= count) render(0, userInitiated);
|
||||
playing = true;
|
||||
root.classList.add('is-playing');
|
||||
buttons('play').setAttribute('aria-pressed', 'true');
|
||||
buttons('pause').setAttribute('aria-pressed', 'false');
|
||||
timer = window.setTimeout(tick, 720);
|
||||
}
|
||||
|
||||
controls.addEventListener('click', (event) => {
|
||||
const button = event.target.closest('[data-motion-action]');
|
||||
if (!button) return;
|
||||
const action = button.dataset.motionAction;
|
||||
if (action === 'play') play(false, true);
|
||||
if (action === 'pause') pause(true);
|
||||
if (action === 'replay') play(true, true);
|
||||
if (action === 'prev') { pause(false); render(step - 1, true); }
|
||||
if (action === 'next') { pause(false); render(step + 1, true); }
|
||||
});
|
||||
|
||||
root.addEventListener('keydown', (event) => {
|
||||
if (event.target.matches('input, textarea, select, a[href]')) return;
|
||||
const keys = { ArrowLeft: step - 1, ArrowRight: step + 1, Home: 0, End: count };
|
||||
if (event.code === 'Space' && !event.target.closest('button')) {
|
||||
event.preventDefault();
|
||||
playing ? pause(true) : play(false, true);
|
||||
return;
|
||||
}
|
||||
if (event.key.toLowerCase() === 'r') {
|
||||
event.preventDefault();
|
||||
play(true, true);
|
||||
return;
|
||||
}
|
||||
if (!(event.key in keys)) return;
|
||||
event.preventDefault();
|
||||
pause(false);
|
||||
render(keys[event.key], true);
|
||||
});
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'hidden' && playing) pause(false);
|
||||
});
|
||||
reduce.addEventListener('change', (event) => {
|
||||
if (event.matches && root.dataset.motionState !== 'reduced') {
|
||||
stepBeforeReduce = step;
|
||||
resumeAfterReduce = playing;
|
||||
}
|
||||
pause(false);
|
||||
if (event.matches) {
|
||||
setControlsAvailable(false);
|
||||
render(count, false);
|
||||
root.dataset.frame = 'static';
|
||||
root.dataset.motionState = 'reduced';
|
||||
status.textContent = 'Reduced motion · complete static frame · playback controls unavailable';
|
||||
} else if (staticOverride || root.dataset.motionMode === 'none') {
|
||||
setControlsAvailable(false);
|
||||
render(count, false);
|
||||
root.dataset.frame = 'static';
|
||||
root.dataset.motionState = 'static';
|
||||
status.textContent = 'Static frame · complete diagram · playback controls unavailable';
|
||||
} else {
|
||||
delete root.dataset.motionState;
|
||||
if (exactStep !== null) {
|
||||
setControlsAvailable(false);
|
||||
render(exactStep, false);
|
||||
root.dataset.frame = 'step';
|
||||
root.dataset.motionState = 'test';
|
||||
status.textContent = `Test frame · step ${step} of ${count} · playback controls unavailable`;
|
||||
} else {
|
||||
setControlsAvailable(true);
|
||||
render(stepBeforeReduce, false);
|
||||
status.textContent = `Ready · step ${step} of ${count}`;
|
||||
if (resumeAfterReduce && root.dataset.motionMode === 'reveal') {
|
||||
play(false, false);
|
||||
}
|
||||
}
|
||||
resumeAfterReduce = false;
|
||||
}
|
||||
});
|
||||
|
||||
root.classList.add('motion-ready');
|
||||
if (staticOverride) {
|
||||
document.documentElement.dataset.motion = 'static';
|
||||
pause(false);
|
||||
setControlsAvailable(false);
|
||||
render(count, false);
|
||||
root.dataset.frame = 'static';
|
||||
root.dataset.motionState = 'static';
|
||||
status.textContent = 'Static frame · complete diagram · playback controls unavailable';
|
||||
} else if (reduce.matches || root.dataset.motionMode === 'none') {
|
||||
pause(false);
|
||||
setControlsAvailable(false);
|
||||
render(count, false);
|
||||
root.dataset.frame = 'static';
|
||||
root.dataset.motionState = reduce.matches ? 'reduced' : 'static';
|
||||
status.textContent = reduce.matches
|
||||
? 'Reduced motion · complete static frame · playback controls unavailable'
|
||||
: 'Static frame · complete diagram · playback controls unavailable';
|
||||
} else if (exactStep !== null) {
|
||||
document.documentElement.dataset.motion = 'step';
|
||||
pause(false);
|
||||
setControlsAvailable(false);
|
||||
render(exactStep, false);
|
||||
root.dataset.frame = 'step';
|
||||
root.dataset.motionState = 'test';
|
||||
status.textContent = `Test frame · step ${step} of ${count} · playback controls unavailable`;
|
||||
} else if (root.dataset.motionMode === 'reveal') {
|
||||
setControlsAvailable(true);
|
||||
render(0, false);
|
||||
play(false, false);
|
||||
} else {
|
||||
pause(false);
|
||||
setControlsAvailable(true);
|
||||
render(0, false);
|
||||
status.textContent = `Ready · step 0 of ${count}`;
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,138 @@
|
||||
# Optional animation
|
||||
|
||||
Animation explains a complete static diagram; it never supplies missing meaning. Load this reference only when motion is explicitly requested or materially clarifies order, accumulation, evaluation, containment, or propagation. Otherwise use mode `none` and ship static HTML.
|
||||
|
||||
## Modes
|
||||
|
||||
Choose one mode per figure with `data-motion-mode="none|reveal|step|loop"`.
|
||||
|
||||
| Mode | Behavior | Controls / implementation | Use |
|
||||
|---|---|---|---|
|
||||
| `none` | Complete stable figure | No JavaScript | Default, print, screenshot, export, reduced-motion fallback with playback controls unavailable |
|
||||
| `reveal` | One deterministic run ending complete | CSS-only for ≤5s; otherwise use the scoped controller | Short ordered explanation; never auto-replay |
|
||||
| `step` | Paused semantic states | Minimal inline JS for Play, Pause, Replay, Previous, Next | Teaching, comparison, policy traces |
|
||||
| `loop` | One decorative token repeats without changing meaning | CSS-only default | Quiet flow hint; ≥3s cycle |
|
||||
|
||||
Only `loop` repeats. Queue state, typing, field values, policy outcomes, containment, and audit entries use `reveal` or `step` and finish complete.
|
||||
|
||||
## Static-first enhancement contract
|
||||
|
||||
1. **Source is complete.** Every semantic node, label, connector, status, and outcome is visible in the HTML/SVG before enhancement. Only selectors below `.motion-ready` may hide or transform them.
|
||||
2. **Stable capture.** Initial `data-frame="static"`, `?motion=static`, print, no-JS, and standalone SVG export expose the complete frame and hide controls/decorative tokens. Do not capture after an arbitrary delay.
|
||||
3. **CSS owns presentation.** Use CSS transitions/keyframes for appearance and travel. Minimal inline JavaScript is allowed only to bind explicit controls, update step/state attributes, schedule deterministic steps, and update the dedicated live-status region. No fetches, markup injection, path measurement, or mutation of semantic diagram labels or values.
|
||||
4. **One clock.** Use `--motion-fast: 160ms`, `--motion-step: 480ms`, `--motion-hold: 720ms`, and `--motion-total` ≤ `8000ms`; derive delays from integer steps. No randomness, springs, or transition-event timing.
|
||||
5. **Explicit order.** Mark items `data-motion-item data-step="N"` for integer steps 1–8. DOM order follows narrative order. At most two items enter per step.
|
||||
6. **Stable end.** Completion exposes all items and sets `data-frame="end"`. Replay resets to step 0 first. Pause clears the pending timer and resume continues from the same step.
|
||||
7. **Scoped state.** Controls operate on their nearest `[data-motion-root]`; IDs, timers, live regions, and step state never cross figure boundaries.
|
||||
8. **Failure-safe startup.** JavaScript adds `.motion-ready` only after controls are bound. A script error before that point leaves the complete source visible.
|
||||
|
||||
## Semantic primitives
|
||||
|
||||
Every primitive has text, count, symbol, pattern, or outline in addition to color.
|
||||
|
||||
| Primitive | Mechanism | Static / reduced-motion result | Limit |
|
||||
|---|---|---|---|
|
||||
| **Path draw** | Decorative duplicate path with `pathLength="1"` and animated dash offset | Base labeled connector remains visible | ≤2 paths; one active |
|
||||
| **Staggered reveal** (stage reveal) | `data-motion-item` + opacity/translate ≤8px | All stages visible | ≤8 steps, 12 items |
|
||||
| **Queue counter** (queue accumulation) | Stable slots; item reveal plus visible numeric count | Final queue and count visible | ≤5 items; no reorder |
|
||||
| **Typing / field population** | Full accessible string; clipped decorative overlay or labeled row reveal | Complete text/fields visible once | ≤32 typed chars or 6 fields |
|
||||
| **Policy evaluation** (rule evaluation) | Ordered rule rows with text statuses and a current-row outline | Every state and outcome visible | 3–6 rules; 2 traces |
|
||||
| **Flow token** | `aria-hidden` token on a fixed path | Token hidden; connector remains | One token; loop ≥3s |
|
||||
| **Containment** | Reveal children, then persistent labeled boundary | Children and boundary visible | One boundary transition |
|
||||
| **Audit append** | Chronological rows revealed; stable timestamp/sequence | Complete ordered log visible | ≤5 appended rows |
|
||||
|
||||
Do not animate layout coordinates, connector routes, `viewBox`, node dimensions, or semantic text. Avoid zoom, parallax, bounce, shake, glow, particles, and indefinite blinking.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--motion-fast: 160ms;
|
||||
--motion-step: 480ms;
|
||||
--motion-hold: 720ms;
|
||||
--motion-ease: cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
.motion-ready [data-motion-item] {
|
||||
opacity: 0;
|
||||
transform: translateY(8px);
|
||||
transition: opacity var(--motion-step) var(--motion-ease),
|
||||
transform var(--motion-step) var(--motion-ease);
|
||||
}
|
||||
.motion-ready [data-motion-item].is-visible,
|
||||
.motion-ready[data-frame="end"] [data-motion-item] {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
[data-motion-controls][hidden] { display: none !important; }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.001ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.001ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
[data-motion-item] { opacity: 1 !important; transform: none !important; }
|
||||
[data-motion-decorative] { display: none !important; }
|
||||
[data-motion-controls] { display: none !important; }
|
||||
}
|
||||
@media print {
|
||||
[data-motion-controls], [data-motion-decorative] { display: none !important; }
|
||||
[data-motion-item] { opacity: 1 !important; transform: none !important; }
|
||||
}
|
||||
```
|
||||
|
||||
## Interactive controls and keyboard
|
||||
|
||||
Every interactive `step` figure provides native buttons for **Play, Pause, Replay, Previous, and Next**, outside the SVG. Use `data-motion-action="play|pause|replay|prev|next"`, ≥44×44px targets, visible focus, disabled state for unavailable actions, and `aria-pressed` for play/pause state.
|
||||
|
||||
When focus is within the motion root: `ArrowRight` advances, `ArrowLeft` goes back, `Home` resets, `End` completes, and `Space` toggles play/pause when focus is not already on a native control. Do not capture keys from inputs, links, or unrelated regions. Never move focus as the frame changes.
|
||||
|
||||
Provide visible instructions and a scoped `role="status" aria-live="polite" aria-atomic="true"`. Announce user actions such as “Step 3 of 5: first divergence”; do not announce every autoplay frame. Controls operate only on their nearest `[data-motion-root]`.
|
||||
|
||||
Use [`assets/template-motion.html`](../assets/template-motion.html) rather than inventing another controller. Its inline controller is the executable implementation contract: copy that script body verbatim. The skin linter rejects modified or additional controllers, even when they carry `data-diagram-controls`. Replace diagram content and slug-prefixed IDs, but preserve the controller and its state/control attributes.
|
||||
|
||||
## Reduced motion, color, and accessibility
|
||||
|
||||
- `prefers-reduced-motion: reduce` initializes at the complete static frame, disables and hides every playback control, hides decorative movement, and exposes `data-motion-state="reduced"` plus status text that playback is unavailable. It never presents partial-step announcements beside a complete frame.
|
||||
- The SVG's `<title>` and `<desc>` describe the complete meaning, not the animation. Interaction instructions remain visible HTML text.
|
||||
- Decorative overlays carry `aria-hidden="true" focusable="false"`. Semantic text exists once in the accessibility tree.
|
||||
- State is never color-only: policy uses symbol + `PASS/FAIL/SKIPPED/NOT REACHED`; queues show counts; active stages use number/label/outline.
|
||||
- Nothing flashes or changes luminance more than three times per second.
|
||||
|
||||
## Complexity and deterministic timing
|
||||
|
||||
Motion does not raise the static diagram budget: ≤8 semantic steps (target 3–6), ≤12 marked items, ≤2 simultaneous reveals, ≤2 drawn paths, one flow-token loop, 160–600ms transitions, 400–1200ms holds, ≤24px translation, and 3–8s total autoplay.
|
||||
|
||||
Declare `data-step-count`; do not infer steps from transition events. Use one `setTimeout` chain per root, clear it on Pause/Replay/page hide and immediately after rendering the final step, and never use `setInterval` for semantic playback. Pause when `document.visibilityState` becomes hidden and do not catch up later. `?motion=step&step=N` may expose an exact zero-duration frame for visual regression only when `N` is a non-negative base-10 integer from 0 through `data-step-count`; missing, fractional, negative, and over-budget values leave normal playback in place.
|
||||
|
||||
The final-state capture contract is synchronous: `?motion=static`, `<html data-motion="static">`, or mode `none` exposes every semantic item, hides controls and decorative overlays, and sets `data-frame="static"`. Wait for `document.fonts.ready` before capture. Two captures from the same URL, viewport, fonts, and device scale must be pixel-identical; random delays, generated IDs, clocks, and runtime path measurement are forbidden.
|
||||
|
||||
## Export and verification
|
||||
|
||||
PNG and SVG exports are static final-state artifacts unless the user explicitly requests a named step. Before capture, open `?motion=static`, await `document.fonts.ready`, and assert `data-frame="static"`. SVG extraction omits HTML controls and scripts; source-visible semantic markup keeps the result complete.
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python3 scripts/verify-motion.py path/to/animated-diagram.html
|
||||
python3 scripts/test-verify-motion.py
|
||||
python3 scripts/lint-skin.py path/to/animated-diagram.html
|
||||
```
|
||||
|
||||
The verifier checks mode/state declarations, contiguous steps, motion budgets, complete SVG naming, no-JS source visibility, decorative accessibility, the full control set, live status, reduced-motion/print CSS, keyboard handling, page-hide pause, bounded static/test overrides, immediate final-step stop, and exact canonical-controller identity. Its adversarial tests mutate the canonical template to prove each failure is rejected.
|
||||
|
||||
Then verify in a browser:
|
||||
|
||||
1. Disable JavaScript: the complete diagram remains visible and meaningful.
|
||||
2. Emulate `prefers-reduced-motion: reduce`: the final state is complete, playback controls are hidden and disabled, and the DOM status says playback is unavailable.
|
||||
3. Use keyboard only: Tab reaches each native control; Enter/Space operate it; Left/Right/Home/End step without moving focus.
|
||||
4. Pause, resume, and replay twice: ordering and final state are identical.
|
||||
5. Capture `?motion=static` twice after `document.fonts.ready`: pixels are stable.
|
||||
6. Print preview plus PNG/SVG export: controls and decorative tokens are absent, while all semantic labels and relationships remain.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Autoplay on load, viewport re-entry, or an endless semantic loop.
|
||||
- A blank/partial no-JS or reduced-motion frame.
|
||||
- Motion that rescues an over-dense or unlabeled static diagram.
|
||||
- Pass/fail, queue fullness, or outcome encoded only by hue.
|
||||
- Remote scripts, general application logic, runtime geometry, or duplicated semantic text.
|
||||
- Capturing at wall-clock delay instead of the explicit static override.
|
||||
@@ -48,7 +48,7 @@ Tools that don't fetch remote fonts at import time (offline Illustrator, some Fi
|
||||
|
||||
## PNG export procedure
|
||||
|
||||
Render **the original HTML** (not the extracted SVG) and screenshot only the `<svg>` element's bounding box. This keeps font loading reliable (already wired in the source HTML) while satisfying the "diagram only" rule. The PNG always has a **transparent background** (`omit_background=True`) so it can be placed on any slide or doc colour without a white halo.
|
||||
Render **the original HTML** (not the extracted SVG) and screenshot only the `<svg>` element's bounding box. This keeps font loading reliable (already wired in the source HTML) while satisfying the "diagram only" rule. The PNG always has a **transparent background** (`omit_background=True`) so it can be placed on any slide or doc colour without a white halo. For motion-enabled HTML, append `?motion=static`, await `document.fonts.ready`, and assert the motion root has `data-frame="static"` before capture; never export at an arbitrary wall-clock delay.
|
||||
|
||||
### Detection
|
||||
|
||||
@@ -131,6 +131,6 @@ If the target aspect ratio doesn't match the `viewBox` aspect ratio, say so and
|
||||
## What this command never does
|
||||
|
||||
- Modifies the source HTML.
|
||||
- Adds export buttons or `<script>` tags to generated diagrams (the "no JS in deliverables" rule in §11 still stands).
|
||||
- Adds export buttons or `<script>` tags. Static diagrams remain script-free; an already motion-enabled source may retain the scoped controller from [`animation.md`](animation.md), but export never injects another controller.
|
||||
- Auto-emits `.svg` or `.png` alongside HTML generation. Manual on every call.
|
||||
- Embeds an HTML wrapper (cards, headers) into the SVG via `foreignObject`. Too fragile across renderers.
|
||||
|
||||
@@ -77,6 +77,18 @@ Read the rendered `font-family` stack of:
|
||||
|
||||
If the site has only one family, keep the schematic defaults for the missing roles (Instrument Serif for title, Geist Mono for mono). Don't force-pick a mono font that isn't on the site.
|
||||
|
||||
### Exact-font gate for brand-matched output
|
||||
|
||||
Do not replace a detected brand family with `serif`, `system-ui`, or `ui-monospace` merely to make the file dependency-free. A public font is part of the visual system.
|
||||
|
||||
1. Record the computed family and weight used by the sampled heading, body, and technical-label elements.
|
||||
2. Trace each family to its source: an existing Google Fonts stylesheet, an installed/system stack, or a custom-hosted `@font-face`.
|
||||
3. If it is available through Google Fonts, carry the exact family name, weights, and approved stylesheet into the style guide and generated HTML. The single-file allowlist accepts only a parsed HTTPS URL whose hostname is exactly `fonts.googleapis.com` and whose path is exactly `/css2`; prefix/lookalike hosts and other paths fail. Preserve an intentional system stack in order and verify the resolved family on the target machine.
|
||||
4. A custom-hosted or paid font is not compatible with the default single-file allowlist. Label that role `fallback` unless the user separately approves and packages the font; never silently add a remote font URL or claim an exact match.
|
||||
5. Verify the rendered output with `getComputedStyle`; a declared family that failed to load does not pass.
|
||||
|
||||
For a page containing bespoke diagrams or editorial figures, inspect their rendered font roles as well as the surrounding article. A figure-specific stylesheet may intentionally differ from the site's global heading/body stack.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — map to semantic roles
|
||||
@@ -120,6 +132,16 @@ Show the user what will change in `style-guide.md`. Only the tokens table — ev
|
||||
|
||||
Also regenerate the dark variant via the inversion rule (`rgba(11,13,11, X)` → `rgba(ink-rgb, X)`).
|
||||
|
||||
Include a compact **brand fidelity receipt** with the preview:
|
||||
|
||||
- sampled URLs;
|
||||
- detected paper, ink, muted, accent, surface, and rule values;
|
||||
- title, body, and technical-label families with weights and source URLs;
|
||||
- `exact` or `fallback` for each font role;
|
||||
- any page-specific figure styling that should override the global site skin.
|
||||
|
||||
The receipt is required when the user says “match this site,” “use their branding,” or provides a page as the visual reference.
|
||||
|
||||
---
|
||||
|
||||
## Step 5 — apply
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
# Semantic patterns
|
||||
|
||||
Semantic patterns describe **what a system does**; the 27 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.
|
||||
|
||||
## Routing table
|
||||
|
||||
| The reader must understand… | Semantic pattern | Nearest visual type |
|
||||
|---|---|---|
|
||||
| Many arrivals competing for finite service capacity | **Fan-in queue / bottleneck** | Data flow |
|
||||
| Repeated questions, inputs, controls, and outputs across stages | **Stage framework with semantic slots** | Process |
|
||||
| A loose conversation becoming a durable structured record | **Unstructured input → structured artifact** | Data flow |
|
||||
| Why two policy decisions differ and where they first diverge | **Paired policy-evaluation traces** | Flowchart |
|
||||
| Which routes cross a trust boundary and which routes are blocked | **Secure paved road** | Architecture |
|
||||
| Which controls apply at each enforcement surface | **Governance / control catalog** | Layer stack |
|
||||
| How defenses reduce risk and what risk remains | **Compensating security layers** | Layer stack |
|
||||
|
||||
## 1. Fan-in queue / bottleneck
|
||||
|
||||
**Selection triggers:** Several producers converge on one reviewer, service, gate, or constrained resource; the story depends on arrival rate, queue depth, wait, capacity, or backpressure.
|
||||
|
||||
**Required primitives:** Distinct sources; fanned ingress; an ordered queue with visible slots and count; a capacity/service-rate label; one constrained service point; admitted and deferred/rejected outcomes. Label units (`8/hour`, `3 slots`), not just “high.”
|
||||
|
||||
**Complexity budget:** ≤5 sources, ≤5 queue slots, one bottleneck, two outcomes, and ≤9 primary nodes. Aggregate excess sources as a named cohort.
|
||||
|
||||
**Anti-patterns:** Equal-width pipeline that hides contention; arrows merged before they can be traced; capacity implied only by box size; decorative pile-up; animation that changes item order; red alone meaning overloaded.
|
||||
|
||||
**Static fallback:** Show the representative final queue, numeric count/capacity, bottleneck label, and both outcome paths. A still must reveal why work waits.
|
||||
|
||||
**Nearest visual type:** **Data flow** by default; use **Process** when service stages, rather than sources, dominate.
|
||||
|
||||
## 2. Stage framework with semantic slots
|
||||
|
||||
**Selection triggers:** A lifecycle or operating model repeats the same semantic questions across stages, commonly Question, Input, Governance, and Output. Cross-stage comparability matters more than message timing.
|
||||
|
||||
**Required primitives:** Ordered stage headers; a consistent slot grid; explicit empty/not-applicable slots; stage-to-stage handoff; stable slot labels; one primary output per stage. Preserve slot order in every stage.
|
||||
|
||||
**Complexity budget:** 3–6 stages, 3–4 slot kinds, ≤20 populated cells, ≤2 lines per cell. Split detail when a cell needs prose.
|
||||
|
||||
**Anti-patterns:** Each stage invents a different internal layout; slot meaning encoded by position with no labels; fake precision from dozens of cells; confusing stage order with ownership lanes; shrinking text to keep one canvas.
|
||||
|
||||
**Static fallback:** Render the full stage × slot matrix with handoffs and explicit `—` or `Not applicable` entries. Do not depend on staged reveal to teach the schema.
|
||||
|
||||
**Nearest visual type:** **Process**; use **Swimlane** only when the repeated rows represent owners rather than semantic slots.
|
||||
|
||||
## 3. Unstructured input → structured artifact
|
||||
|
||||
**Selection triggers:** Dialogue, notes, prompts, or a rambling request are elicited, normalized, and written into a durable brief, ticket, record, schema, or other structured artifact.
|
||||
|
||||
**Required primitives:** Source utterance(s); clarifying questions; extracted field/value pairs; a named transformation; the durable artifact boundary; provenance links from representative statements to fields; missing/unknown state.
|
||||
|
||||
**Complexity budget:** ≤4 exchanges, ≤6 artifact fields, one transformation, and ≤3 provenance links. Show representative content, not a transcript.
|
||||
|
||||
**Anti-patterns:** “AI magic” sparkle between two boxes; artifact shown as another chat bubble; fields appearing without sources; inventing certainty for missing facts; typing animation as the only readable copy.
|
||||
|
||||
**Static fallback:** Show a short source excerpt beside the completed labeled artifact, with at least one provenance mapping and any unknown fields visible.
|
||||
|
||||
**Nearest visual type:** **Data flow**; use **Process** when elicitation has several ordered gates.
|
||||
|
||||
## 4. Paired policy-evaluation traces
|
||||
|
||||
**Selection triggers:** Two otherwise similar requests reach different outcomes; the reader needs rule-by-rule `PASS`, `FAIL`, `SKIPPED`, or `NOT REACHED` state and the first divergence.
|
||||
|
||||
**Required primitives:** The same ordered rules on both traces; explicit status text plus symbol/shape; inputs that differ; final outcomes; a labeled first-divergence marker; a distinction between `SKIPPED` (applicable flow intentionally bypassed) and `NOT REACHED` (evaluation stopped earlier).
|
||||
|
||||
**Complexity budget:** Exactly 2 traces, 3–6 rules, one first divergence, ≤12 status cells, and one outcome per trace. Move rule prose to notes if labels exceed one line.
|
||||
|
||||
**Anti-patterns:** Comparing two independently ordered flows; green/red dots without words; treating skipped and not-reached as synonyms; highlighting every difference; continuing a denied trace as if downstream rules ran.
|
||||
|
||||
**Static fallback:** Show all rule states and both outcomes at once; use a persistent bracket/line and label for the first divergence.
|
||||
|
||||
**Nearest visual type:** **Flowchart** for ordered decision logic; use **Sequence** only when messages between actors and time are also load-bearing.
|
||||
|
||||
## 5. Secure paved road
|
||||
|
||||
**Selection triggers:** A supported architecture creates a bounded route from intake/build to deployment; trust boundaries, privileged moments, permitted ingress, forbidden ingress, and approved versus blocked deploy paths are the point.
|
||||
|
||||
**Required primitives:** Labeled trust boundaries; actors and identities; permitted ingress with a positive text label; forbidden ingress terminating at the boundary; approved deployment path; blocked bypass path; privileged gate; isolated runtime; audit destination. Use different line styles and stop symbols in addition to color.
|
||||
|
||||
**Complexity budget:** ≤3 trust zones, ≤8 components, ≤10 paths, ≤2 forbidden paths, and one privileged gate. Split control detail into a catalog figure.
|
||||
|
||||
**Anti-patterns:** Dashed box called “security” with no route semantics; forbidden arrow crossing into the protected zone; secrets or identity implied but unlabeled; every component styled as trusted; a bypass path that visually rejoins the approved route.
|
||||
|
||||
**Static fallback:** Render every boundary and both permitted/forbidden routes. Blocked paths must visibly stop before entry or deployment.
|
||||
|
||||
**Nearest visual type:** **Architecture**.
|
||||
|
||||
## 6. Governance / control catalog
|
||||
|
||||
**Selection triggers:** A control inventory must be understood by where it is enforced: authoring, workspace, merge/CI, deploy/runtime, or another named surface. A single checklist would hide those enforcement points.
|
||||
|
||||
**Required primitives:** Enforcement-surface groups; named controls; enforcement actor (`code`, `platform`, `human`); timing (`write`, `merge`, `deploy`, `run`); bypassability or exception route; coverage/gap notation.
|
||||
|
||||
**Complexity budget:** 3–5 surfaces, 3–7 controls per surface, ≤24 controls total, and ≤3 attributes per control. Summarize counts only when the item list exists elsewhere.
|
||||
|
||||
**Anti-patterns:** 35 tiny pills; grouping by vague themes instead of enforcement point; mixing aspirations with enforced controls; icons without control names; claiming defense-in-depth without showing surface coverage.
|
||||
|
||||
**Static fallback:** Show the complete grouped catalog with surface headers and text labels for actor and enforcement timing; preserve gaps and exceptions.
|
||||
|
||||
**Nearest visual type:** **Layer stack**; use **DP security matrix** when role permissions, not enforcement surfaces, are the dominant comparison.
|
||||
|
||||
## 7. Compensating security layers
|
||||
|
||||
**Selection triggers:** No layer is perfect; each defense covers a failure left by the previous layer, and residual risk must visibly narrow, transfer, or remain through the stack.
|
||||
|
||||
**Required primitives:** Ordered threat/risk input; named defensive layers; each layer's mitigation; explicit limitation or escape; residual-risk carrier between layers; final residual risk and consequence/response. Use labels or decreasing measures, never area alone.
|
||||
|
||||
**Complexity budget:** 3–5 layers, one primary risk thread, ≤2 mitigations per layer, and one final residual-risk statement. Split multiple unrelated threats into separate figures.
|
||||
|
||||
**Anti-patterns:** Implying the final layer makes risk zero; equal opaque slabs with no propagation; treating audit as prevention; shrinking shapes without numeric or verbal meaning; reversing prevention/detection/recovery order without explanation.
|
||||
|
||||
**Static fallback:** Show the complete propagation chain: initial risk → mitigation → escaped risk at every layer → final residual risk and response.
|
||||
|
||||
**Nearest visual type:** **Layer stack**; use **Nested** when containment boundaries, rather than ordered compensation, carry the meaning.
|
||||
|
||||
## Composition rules
|
||||
|
||||
- The semantic pattern may specialize status, boundary, queue, or propagation primitives; the selected type still owns page axis, connector grammar, spacing, and type-specific limits.
|
||||
- Apply the stricter of the pattern budget and visual-type budget. Semantic cells/statuses are not permission to exceed the nine-node overview target.
|
||||
- Use stable text for states and outcomes. Color, motion, and position reinforce meaning but never carry it alone.
|
||||
- Optional animation is a presentation layer, not another pattern. Load [`animation.md`](animation.md) only when motion is requested or materially clarifies ordered change.
|
||||
Reference in New Issue
Block a user