diff --git a/README.md b/README.md index 242506e..7564244 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,21 @@ The skill ships with five prompt templates: | `/project-recap` | Mental model snapshot for context-switching back to a project | | `/fact-check` | Verify accuracy of a review page or plan doc against actual code | +### `--quick` mode (preserves default behavior) + +Default behavior is unchanged: full one-shot HTML generation. + +When you add `--quick` to `/generate-web-diagram`, the skill uses a static renderer pipeline: +1. Model emits compact JSON spec +2. `quick/render-architecture.mjs` assembles HTML +3. If quick render fails, it falls back to the original full-generation path + +Example: + +``` +/generate-web-diagram --quick architecture overview of our rpc layer +``` + `/diff-review` is probably the most useful. Run it with no arguments to diff against `main`, or pass any git ref: ``` diff --git a/SKILL.md b/SKILL.md index 525c110..c441f08 100644 --- a/SKILL.md +++ b/SKILL.md @@ -14,6 +14,17 @@ Generate self-contained HTML files for technical diagrams, visualizations, and d **Proactive table rendering.** When you're about to present tabular data as an ASCII box-drawing table in the terminal (comparisons, audits, feature matrices, status reports, any structured rows/columns), generate an HTML page instead. The threshold: if the table has 4+ rows or 3+ columns, it belongs in the browser. Don't wait for the user to ask — render it as HTML automatically and tell them the file path. You can still include a brief text summary in the chat, but the table itself should be the HTML page. +## Quick Mode (`--quick`) + +Default behavior remains the original full-HTML mechanism. Only use quick mode when the user explicitly includes `--quick`. + +Quick mode uses the static renderer at `./quick/render-architecture.mjs`: +- Model outputs a compact JSON spec (see `./quick/README.md`) +- Renderer assembles final HTML with reusable CSS/components +- Faster and cheaper than regenerating full CSS/HTML every run + +**Fallback rule (mandatory):** if quick rendering fails, or if the request is not a good fit for the architecture schema, immediately fall back to the original full-HTML workflow in this skill. + ## Workflow ### 1. Think (5 seconds, not 5 minutes) diff --git a/prompts/diff-review.md b/prompts/diff-review.md index ffa11a5..73df86c 100644 --- a/prompts/diff-review.md +++ b/prompts/diff-review.md @@ -3,6 +3,26 @@ description: Generate a visual HTML diff review — before/after architecture co --- Load the visual-explainer skill, then generate a comprehensive visual diff review as a self-contained HTML page. +## Quick Mode (`--quick`) + +Preserve original behavior by default. Only use quick mode if the user includes `--quick`. + +If `--quick` is present: +1. Remove `--quick` from args. +2. Read `./quick/README.md`. +3. Build a compact **JSON spec** for the quick architecture renderer that summarizes the diff: + - title/subtitle describe the diff scope + - gateway cards = changed modules / entry points + - pipeline steps = detection, analysis, verification, output + - database cards = files/state touched + - outputs = good/bad/risks and follow-ups + - include KPIs and legend when possible +4. Save spec to `/tmp/ve-quick-diff-.json`. +5. Render with: + - `node $HOME/.pi/agent/skills/visual-explainer/quick/render-architecture.mjs /tmp/ve-quick-diff-.json ~/.agent/diagrams/-diff-quick.html` +6. Open the resulting HTML and report path. +7. If quick render fails, or if the request needs full rich diff semantics that don’t fit quick schema, fall back immediately to the full workflow below. + Follow the visual-explainer skill workflow. Read the reference template, CSS patterns, and mermaid theming references before generating. Use a GitHub-diff-inspired aesthetic with red/green before/after panels, but vary fonts and palette from previous diagrams. **Scope detection** — determine what to diff based on `$1`: diff --git a/prompts/generate-web-diagram.md b/prompts/generate-web-diagram.md index 64e5e1d..a8b5bb0 100644 --- a/prompts/generate-web-diagram.md +++ b/prompts/generate-web-diagram.md @@ -3,6 +3,21 @@ description: Generate a beautiful standalone HTML diagram and open it in the bro --- Load the visual-explainer skill, then generate an HTML diagram for: $@ +## Quick Mode (`--quick`) + +Preserve the original mechanism by default. Only enter quick mode when the user includes the literal `--quick` flag in `$@`. + +If `--quick` is present, do this first: +1. Remove `--quick` from the request text. +2. Read `./quick/README.md` for the JSON schema and optional rich fields. +3. Generate a **JSON spec** (not HTML) for the architecture renderer and save it to `/tmp/ve-quick-.json`. +4. Render HTML using: + - `node $HOME/.pi/agent/skills/visual-explainer/quick/render-architecture.mjs /tmp/ve-quick-.json ~/.agent/diagrams/-quick.html` +5. If render succeeds, open the generated HTML in the browser and report the path. +6. If render fails OR the request is not a good fit for the architecture schema, automatically fall back to the original full HTML generation workflow below. + +Without `--quick` (or after fallback), run the original mechanism unchanged. + Follow the visual-explainer skill workflow. Read the reference template and CSS patterns before generating. Pick a distinctive aesthetic that fits the content — vary fonts, palette, and layout style from previous diagrams. If `surf` CLI is available (`which surf`), consider generating an AI illustration via `surf gemini --generate-image` when an image would genuinely enhance the page — a hero banner, conceptual illustration, or educational diagram that Mermaid can't express. Match the image style to the page's palette. Embed as base64 data URI. See css-patterns.md "Generated Images" for container styles. Skip images when the topic is purely structural or data-driven. diff --git a/prompts/plan-review.md b/prompts/plan-review.md index f42c771..0bc3e41 100644 --- a/prompts/plan-review.md +++ b/prompts/plan-review.md @@ -3,6 +3,26 @@ description: Generate a visual HTML plan review — current codebase state vs. p --- Load the visual-explainer skill, then generate a comprehensive visual plan review as a self-contained HTML page, comparing the current codebase against a proposed implementation plan. +## Quick Mode (`--quick`) + +Preserve original behavior by default. Only use quick mode if the user includes `--quick`. + +If `--quick` is present: +1. Remove `--quick` from args. +2. Read `./quick/README.md`. +3. Build a compact **JSON spec** for the quick architecture renderer that summarizes plan-vs-code status: + - title/subtitle summarize plan scope and confidence + - gateway cards = current-state modules and planned touchpoints + - pipeline steps = plan parse, code cross-check, gap detection, risk synthesis + - database cards = files/config/tests/docs impacted + - outputs = matches, gaps, risks, and recommendations + - include KPIs and legend +4. Save spec to `/tmp/ve-quick-plan-.json`. +5. Render with: + - `node $HOME/.pi/agent/skills/visual-explainer/quick/render-architecture.mjs /tmp/ve-quick-plan-.json ~/.agent/diagrams/-plan-quick.html` +6. Open the resulting HTML and report path. +7. If quick render fails, or if the request requires full plan-review depth beyond quick schema, fall back immediately to the full workflow below. + Follow the visual-explainer skill workflow. Read the reference template, CSS patterns, and mermaid theming references before generating. Use a blueprint/editorial aesthetic with current-state vs. planned-state panels, but vary fonts and palette from previous diagrams. **Inputs:** diff --git a/prompts/project-recap.md b/prompts/project-recap.md index 71adf07..1f72f53 100644 --- a/prompts/project-recap.md +++ b/prompts/project-recap.md @@ -3,6 +3,26 @@ description: Generate a visual HTML project recap — rebuild mental model of a --- Load the visual-explainer skill, then generate a comprehensive visual project recap as a self-contained HTML page. +## Quick Mode (`--quick`) + +Preserve original behavior by default. Only use quick mode if the user includes `--quick`. + +If `--quick` is present: +1. Remove `--quick` from args. +2. Read `./quick/README.md`. +3. Build a compact **JSON spec** for the quick architecture renderer that summarizes current project state: + - title/subtitle = project and time window + - gateway cards = major modules/entry points + - pipeline steps = recent activity flow and current execution path + - database cards = durable state, logs, and knowledge stores + - outputs = what is working, in progress, risky, and next steps + - include KPIs and legend +4. Save spec to `/tmp/ve-quick-recap-.json`. +5. Render with: + - `node $HOME/.pi/agent/skills/visual-explainer/quick/render-architecture.mjs /tmp/ve-quick-recap-.json ~/.agent/diagrams/-recap-quick.html` +6. Open the resulting HTML and report path. +7. If quick render fails, or if the request needs full recap depth beyond quick schema, fall back immediately to the full workflow below. + Follow the visual-explainer skill workflow. Read the reference template, CSS patterns, and mermaid theming references before generating. Use a warm editorial or paper/ink aesthetic with muted blues and greens, but vary fonts and palette from previous diagrams. **Time window** — determine the recency window from `$1`: diff --git a/quick/README.md b/quick/README.md new file mode 100644 index 0000000..cfb78bb --- /dev/null +++ b/quick/README.md @@ -0,0 +1,112 @@ +# static-assembler prototype + +A lightweight rendering pipeline for visual-explainer architecture pages. + +## Goal + +Move repetitive CSS/HTML scaffolding out of the LLM output. The LLM emits a compact JSON spec; this renderer produces the final self-contained HTML. + +## Usage + +```bash +node render-architecture.mjs spec.json out.html +``` + +## Spec shape + +All new fields are **optional** — existing specs work unchanged. + +```json +{ + "title": "Rho System Architecture", + "subtitle": "worker orchestration and memory pipelines", + "theme": "terracotta", + + "kpis": [ + { "value": "41s", "label": "Avg render time", "tone": "green", "delta": "↓75% vs v1" }, + { "value": "880", "label": "LLM tokens out", "tone": "teal", "delta": "↓89% vs v1" }, + { "value": "5", "label": "Benchmark runs", "tone": "accent" } + ], + + "legend": [ + { "label": "no-llm", "tone": "teal" }, + { "label": "llm", "tone": "green" }, + { "label": "db", "tone": "accent" } + ], + + "inputLabel": "Input Sources", + "inputSources": [ + { "icon": "🖥️", "label": "Terminal" }, + { "icon": "📨", "label": "Email" } + ], + + "gateway": { + "label": "Gateway Layer", + "cards": [ + { + "title": "CLI Router", + "desc": "Dispatches user tasks to skills and tools.", + "tags": ["fish shell", "tmux"] + } + ] + }, + + "pipeline": { + "label": "Processing Pipeline", + "steps": [ + { "name": "Parse", "detail": "Intent extraction", "kind": "no-llm" }, + { "name": "Reason", "detail": "LLM planning", "kind": "llm" }, + { "name": "Persist", "detail": "Write `brain.jsonl`", "kind": "db" } + ] + }, + + "database": { + "label": "Storage Layer", + "cards": [ + { + "title": "brain.jsonl", + "desc": "Durable memories and tasks", + "tags": ["JSONL", "append-only"] + } + ] + }, + + "outputs": [ + { + "label": "User Surfaces", + "blurb": "Everything the user sees or hears directly.", + "items": ["CLI", "HTML diagrams", "notifications"] + }, + { "label": "Automation", "items": ["heartbeats", "reminders", "subagents"] }, + { "label": "Observability","items": ["logs", "metrics", "artifacts"] } + ], + + "calloutTitle": "Invariant", + "callout": "All automation should remain deterministic and verifiable.", + "generatedAt": "2026-02-26T00:00:00Z" +} +``` + +### Optional rich fields + +| Field | Type | Where | Description | +|---|---|---|---| +| `kpis` | `[{value, label, tone?, delta?}]` | top-level | KPI strip rendered under subtitle | +| `legend` | `[{label, tone}]` | top-level | Compact badge row (e.g. pipeline kind key) | +| `gateway.cards[].tags` | `string[]` | gateway/database cards | Pill badges shown below desc | +| `database.cards[].tags` | `string[]` | gateway/database cards | Pill badges shown below desc | +| `outputs[].blurb` | `string` | output sections | Short paragraph under section label | + +### Inline code formatting + +Wrap text in backticks in any textual field (`desc`, `detail`, `blurb`, `callout`, `items[]`) to produce styled `` spans. HTML is safely escaped before conversion — no XSS risk. + +Example: `"detail": "Writes to \`brain.jsonl\`"` → renders as `brain.jsonl`. + +## Themes + +`theme` supports: +- `terracotta` +- `teal` +- `rose` +- `blueprint` diff --git a/quick/base.css b/quick/base.css new file mode 100644 index 0000000..bea4781 --- /dev/null +++ b/quick/base.css @@ -0,0 +1,472 @@ +/* Static Visual Explainer Framework (architecture profile) + LLM picks theme + fills JSON spec; renderer owns layout/CSS/interaction shell. */ + +:root { + --font-body: 'IBM Plex Sans', system-ui, sans-serif; + --font-mono: 'IBM Plex Mono', 'SF Mono', Consolas, monospace; + + --bg: #faf7f5; + --surface: #ffffff; + --surface2: #f5f0ec; + --surface-elevated: #fff9f5; + --border: rgba(0, 0, 0, 0.07); + --border-bright: rgba(0, 0, 0, 0.14); + --text: #292017; + --text-dim: #8a7e72; + --accent: #c2410c; + --accent-dim: rgba(194, 65, 12, 0.08); + --green: #4d7c0f; + --green-dim: rgba(77, 124, 15, 0.08); + --orange: #b45309; + --orange-dim: rgba(180, 83, 9, 0.08); + --teal: #0f766e; + --teal-dim: rgba(15, 118, 110, 0.08); + --plum: #9f1239; + --plum-dim: rgba(159, 18, 57, 0.08); +} + +/* Theme presets; LLM only selects the preset key. */ +body[data-theme='terracotta'] { + --font-body: 'IBM Plex Sans', system-ui, sans-serif; + --font-mono: 'IBM Plex Mono', 'SF Mono', Consolas, monospace; + --bg: #faf7f5; + --surface: #ffffff; + --surface2: #f5f0ec; + --surface-elevated: #fff9f5; + --text: #292017; + --text-dim: #8a7e72; + --accent: #c2410c; + --green: #4d7c0f; + --orange: #b45309; + --teal: #0f766e; + --plum: #9f1239; +} + +body[data-theme='teal'] { + --font-body: 'Bricolage Grotesque', system-ui, sans-serif; + --font-mono: 'Fragment Mono', 'SF Mono', Consolas, monospace; + --bg: #f0fdfa; + --surface: #ffffff; + --surface2: #e6f7f3; + --surface-elevated: #ecfffb; + --text: #134e4a; + --text-dim: #5f8a85; + --accent: #0d9488; + --green: #0284c7; + --orange: #d97706; + --teal: #0f766e; + --plum: #7c3aed; +} + +body[data-theme='rose'] { + --font-body: 'Space Grotesk', system-ui, sans-serif; + --font-mono: 'JetBrains Mono', 'SF Mono', Consolas, monospace; + --bg: #fff5f7; + --surface: #ffffff; + --surface2: #fef0f4; + --surface-elevated: #fff8fa; + --text: #3a0f23; + --text-dim: #8b5a6e; + --accent: #be185d; + --green: #16a34a; + --orange: #d97706; + --teal: #0369a1; + --plum: #9333ea; +} + +body[data-theme='blueprint'] { + --font-body: 'Sora', system-ui, sans-serif; + --font-mono: 'IBM Plex Mono', 'SF Mono', Consolas, monospace; + --bg: #f4f9ff; + --surface: #ffffff; + --surface2: #edf4ff; + --surface-elevated: #f8fbff; + --text: #12243f; + --text-dim: #5f7290; + --accent: #2563eb; + --green: #0891b2; + --orange: #d97706; + --teal: #0f766e; + --plum: #7c3aed; +} + +@media (prefers-color-scheme: dark) { + :root, + body[data-theme] { + --bg: #181312; + --surface: #231d1b; + --surface2: #2b2421; + --surface-elevated: #332a26; + --border: rgba(255, 255, 255, 0.08); + --border-bright: rgba(255, 255, 255, 0.15); + --text: #f1e8de; + --text-dim: #baa997; + --accent-dim: color-mix(in srgb, var(--accent) 25%, transparent); + --green-dim: color-mix(in srgb, var(--green) 25%, transparent); + --orange-dim: color-mix(in srgb, var(--orange) 25%, transparent); + --teal-dim: color-mix(in srgb, var(--teal) 25%, transparent); + --plum-dim: color-mix(in srgb, var(--plum) 25%, transparent); + } +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + background: var(--bg); + background-image: + radial-gradient(ellipse at 22% 0%, color-mix(in srgb, var(--accent) 12%, transparent) 0%, transparent 55%), + radial-gradient(ellipse at 78% 100%, color-mix(in srgb, var(--teal) 10%, transparent) 0%, transparent 45%); + color: var(--text); + font-family: var(--font-body); + min-height: 100vh; + padding: 36px; +} + +@keyframes fadeUp { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +.animate { + animation: fadeUp 0.35s ease-out both; + animation-delay: calc(var(--i, 0) * 0.05s); +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-delay: 0ms !important; + transition-duration: 0.01ms !important; + } +} + +.container { + margin: 0 auto; + max-width: 1080px; + min-width: 0; +} + +h1 { + font-size: clamp(30px, 5vw, 40px); + font-weight: 700; + letter-spacing: -0.8px; + text-wrap: balance; + /* gradient title: text colour → accent tint */ + background: linear-gradient(135deg, var(--text) 40%, color-mix(in srgb, var(--accent) 75%, var(--text))); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.subtitle { + color: var(--text-dim); + font-family: var(--font-mono); + font-size: 12px; + margin-top: 8px; + margin-bottom: 30px; +} + +.diagram { + display: grid; + gap: 20px; + min-width: 0; +} + +.section { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 12px; + padding: 20px 24px; + min-width: 0; +} + +.section--hero { + background: var(--surface-elevated); + border-color: color-mix(in srgb, var(--border) 50%, var(--accent) 50%); + /* accent glow: subtle outer ring + inset highlight */ + box-shadow: + 0 4px 20px rgba(0, 0, 0, 0.07), + 0 0 0 1px color-mix(in srgb, var(--accent) 12%, transparent), + inset 0 1px 0 color-mix(in srgb, var(--accent) 20%, transparent); + padding: 26px 30px; +} + +.section--accent { border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); } +.section--green { border-color: color-mix(in srgb, var(--green) 40%, var(--border)); } +.section--orange { border-color: color-mix(in srgb, var(--orange) 40%, var(--border)); } +.section--teal { border-color: color-mix(in srgb, var(--teal) 40%, var(--border)); } +.section--plum { border-color: color-mix(in srgb, var(--plum) 40%, var(--border)); } + +.section-label { + align-items: center; + color: var(--text-dim); + display: flex; + font-family: var(--font-mono); + font-size: 11px; + font-weight: 700; + gap: 8px; + letter-spacing: 1.5px; + margin-bottom: 14px; + text-transform: uppercase; +} + +.section-label .dot { + border-radius: 50%; + height: 8px; + width: 8px; +} + +.section--accent .section-label { color: var(--accent); } +.section--green .section-label { color: var(--green); } +.section--orange .section-label { color: var(--orange); } +.section--teal .section-label { color: var(--teal); } +.section--plum .section-label { color: var(--plum); } + +.section--accent .dot { background: var(--accent); } +.section--green .dot { background: var(--green); } +.section--orange .dot { background: var(--orange); } +.section--teal .dot { background: var(--teal); } +.section--plum .dot { background: var(--plum); } + +.sources { + display: flex; + flex-wrap: wrap; + gap: 10px; + justify-content: center; +} + +.source-pill { + align-items: center; + background: var(--surface2); + border: 1px solid var(--border); + border-radius: 9px; + display: inline-flex; + font-family: var(--font-mono); + font-size: 13px; + font-weight: 500; + gap: 8px; + max-width: 100%; + overflow-wrap: anywhere; + padding: 8px 14px; + word-break: break-word; +} + +.flow-arrow { + align-items: center; + color: var(--text-dim); + display: flex; + font-family: var(--font-mono); + font-size: 12px; + gap: 8px; + justify-content: center; +} + +.flow-arrow svg { + fill: none; + height: 20px; + stroke: var(--border-bright); + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2; + width: 20px; +} + +.inner-grid { + display: grid; + gap: 12px; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); +} + +.inner-card { + background: var(--surface2); + border: 1px solid var(--border); + border-radius: 8px; + min-width: 0; + overflow-wrap: break-word; + padding: 12px 14px; + transition: transform 0.15s ease, box-shadow 0.15s ease; +} + +.inner-card:hover { + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1); + transform: translateY(-2px); +} + +.inner-card .title { + font-size: 13px; + font-weight: 700; + margin-bottom: 5px; +} + +.inner-card .desc { + color: var(--text-dim); + font-size: 12px; + line-height: 1.5; +} + +.pipeline { + align-items: stretch; + display: grid; + gap: 8px; + grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); + min-width: 0; +} + +.pipeline-step { + background: var(--surface2); + border: 1px solid var(--border); + border-radius: 8px; + min-width: 0; + padding: 10px 12px; + text-align: left; +} + +.pipeline-step .step-name { + font-size: 12px; + font-weight: 700; + margin-bottom: 4px; +} + +.pipeline-step .step-detail { + color: var(--text-dim); + font-size: 10px; + line-height: 1.4; + overflow-wrap: anywhere; +} + +.pipeline-step[data-kind='no-llm'] { border-color: color-mix(in srgb, var(--teal) 40%, var(--border)); } +.pipeline-step[data-kind='llm'] { border-color: color-mix(in srgb, var(--green) 40%, var(--border)); } +.pipeline-step[data-kind='embedding'] { border-color: color-mix(in srgb, var(--orange) 40%, var(--border)); } +.pipeline-step[data-kind='db'] { border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); } + +.pipeline-arrow { + display: none; +} + +.three-col { + display: grid; + gap: 14px; + grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); +} + +.node-list { + display: grid; + gap: 5px; + list-style: none; +} + +.node-list li { + font-size: 12px; + line-height: 1.5; + min-width: 0; + overflow-wrap: break-word; + padding-left: 14px; + position: relative; +} + +.node-list li::before { + color: var(--text-dim); + content: '›'; + font-weight: 700; + left: 0; + position: absolute; +} + +.callout { + background: var(--surface2); + border: 1px solid var(--border); + border-left: 3px solid var(--accent); + border-radius: 0 8px 8px 0; + color: var(--text-dim); + font-size: 13px; + line-height: 1.6; + padding: 14px 16px; +} + +.callout strong { + color: var(--text); +} + +.meta { + color: var(--text-dim); + font-family: var(--font-mono); + font-size: 11px; + margin-top: 20px; +} + +/* Inline code */ +code { + background: color-mix(in srgb, var(--accent) 8%, var(--surface2)); + border: 1px solid color-mix(in srgb, var(--accent) 15%, var(--border)); + border-radius: 3px; + font-family: var(--font-mono); + font-size: 0.87em; + overflow-wrap: anywhere; + padding: 1px 5px; + word-break: break-word; +} + +/* KPI strip */ +.kpi-strip { display: flex; flex-wrap: wrap; gap: 12px; margin-bottom: 24px; } +.kpi-card { + background: var(--surface); border: 1px solid var(--border); border-radius: 10px; + flex: 1 1 110px; min-width: 0; padding: 14px 16px; text-align: center; +} +.kpi-value { + font-size: 28px; + font-weight: 700; + letter-spacing: -1.5px; + line-height: 1; + overflow-wrap: anywhere; +} +.kpi-label { + color: var(--text-dim); font-family: var(--font-mono); font-size: 10px; + letter-spacing: 1px; margin-top: 4px; text-transform: uppercase; +} +.kpi-delta { font-family: var(--font-mono); font-size: 11px; margin-top: 5px; } + +.tone-accent .kpi-value { color: var(--accent); } +.tone-green .kpi-value { color: var(--green); } +.tone-teal .kpi-value { color: var(--teal); } +.tone-orange .kpi-value { color: var(--orange); } +.tone-plum .kpi-value { color: var(--plum); } +.tone-accent .kpi-delta, .tone-orange .kpi-delta { color: var(--orange); } +.tone-green .kpi-delta, .tone-teal .kpi-delta { color: var(--green); } +.tone-plum .kpi-delta { color: var(--plum); } + +/* Legend badge row */ +.legend-row { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 20px; } +.legend-badge { + border-radius: 6px; font-family: var(--font-mono); + font-size: 11px; font-weight: 600; max-width: 100%; padding: 3px 10px; + overflow-wrap: anywhere; word-break: break-word; +} +.legend-badge--accent { background: var(--accent-dim); color: var(--accent); } +.legend-badge--green { background: var(--green-dim); color: var(--green); } +.legend-badge--teal { background: var(--teal-dim); color: var(--teal); } +.legend-badge--orange { background: var(--orange-dim); color: var(--orange); } +.legend-badge--plum { background: var(--plum-dim); color: var(--plum); } + +/* Card tag pills */ +.card-tags { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 8px; } +.tag-pill { + background: var(--surface-elevated); border: 1px solid var(--border-bright); + border-radius: 4px; font-family: var(--font-mono); font-size: 10px; max-width: 100%; + overflow-wrap: anywhere; padding: 2px 7px; word-break: break-word; +} + +/* Section blurb */ +.section-blurb { + color: var(--text-dim); font-size: 12px; line-height: 1.5; + margin-bottom: 10px; margin-top: -4px; +} + +@media (max-width: 768px) { + body { padding: 16px; } + .pipeline { grid-template-columns: 1fr; } + .kpi-strip { gap: 8px; } +} diff --git a/quick/render-architecture.mjs b/quick/render-architecture.mjs new file mode 100755 index 0000000..3dc0926 --- /dev/null +++ b/quick/render-architecture.mjs @@ -0,0 +1,233 @@ +#!/usr/bin/env node +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +function usage() { + console.error('Usage: node render-architecture.mjs '); + process.exit(1); +} + +/** HTML-escape a raw value. Always run before inserting into HTML. */ +function esc(value) { + return String(value ?? '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +/** + * Format text for HTML: escape first (XSS-safe), then convert `backtick spans` + * to elements. The inner content is already escaped so injection-safe. + */ +function fmt(value) { + return esc(value).replace(/`([^`]+)`/g, '$1'); +} + +function pickTheme(theme) { + const allowed = new Set(['terracotta', 'teal', 'rose', 'blueprint']); + return allowed.has(theme) ? theme : 'terracotta'; +} + +function renderSourcePills(items = []) { + return items + .map((item) => { + const icon = item.icon ? `${esc(item.icon)}` : ''; + return `
${icon}${esc(item.label)}
`; + }) + .join('\n'); +} + +/** Cards with optional tags: string[] rendered as pill badges. */ +function renderCards(cards = []) { + return cards + .map((card) => { + const tags = (card.tags || []).length + ? `
${card.tags.map((t) => `${esc(t)}`).join('')}
` + : ''; + return ` +
+
${fmt(card.title)}
+
${fmt(card.desc)}
+ ${tags} +
`; + }) + .join('\n'); +} + +/** KPI strip: [{value, label, tone?, delta?}] */ +function renderKpis(kpis = []) { + if (!kpis.length) return ''; + const cards = kpis + .map((k) => { + const tone = esc(k.tone || 'accent'); + const delta = k.delta ? `
${esc(k.delta)}
` : ''; + return `
+
${esc(k.value)}
+
${esc(k.label)}
+ ${delta} +
`; + }) + .join('\n'); + return `
${cards}
`; +} + +/** Legend: [{label, tone}] rendered as compact badge row. */ +function renderLegend(legend = []) { + if (!legend.length) return ''; + const badges = legend + .map((l) => `${esc(l.label)}`) + .join('\n'); + return `
${badges}
`; +} + +function renderPipeline(steps = []) { + return steps + .map((step, idx) => { + const stepHtml = ` +
+
${esc(step.name)}
+
${fmt(step.detail || '')}
+
`; + if (idx === steps.length - 1) return stepHtml; + return `${stepHtml}\n
`; + }) + .join('\n'); +} + +/** Output sections with optional blurb string displayed under section label. */ +function renderOutputSections(outputs = []) { + return outputs + .map((output, index) => { + const accentByIndex = ['green', 'plum', 'teal', 'orange', 'accent']; + const accent = output.accent || accentByIndex[index % accentByIndex.length]; + const blurb = output.blurb + ? `

${fmt(output.blurb)}

` + : ''; + const list = (output.items || []) + .map((item) => `
  • ${fmt(item)}
  • `) + .join('\n'); + return ` +
    + + ${blurb} +
      + ${list} +
    +
    `; + }) + .join('\n'); +} + +function renderHtml(spec, cssText) { + const theme = pickTheme(spec.theme); + const title = spec.title || 'Architecture Overview'; + const subtitle = spec.subtitle || ''; + const generatedAt = spec.generatedAt || new Date().toISOString(); + + return ` + + + + + ${esc(title)} + + + + + + +
    +

    ${esc(title)}

    +

    ${esc(subtitle)}

    + ${renderKpis(spec.kpis || [])} + ${renderLegend(spec.legend || [])} + +
    +
    + +
    + ${renderSourcePills(spec.inputSources || [])} +
    +
    + +
    + + ${esc(spec.gatewayFlowLabel || 'incoming events')} +
    + +
    + +
    + ${renderCards(spec.gateway?.cards || [])} +
    +
    + +
    + + ${esc(spec.pipelineFlowLabel || 'pipeline entry')} +
    + +
    + +
    + ${renderPipeline(spec.pipeline?.steps || [])} +
    +
    + +
    + + ${esc(spec.databaseFlowLabel || 'stored and queryable')} +
    + +
    + +
    + ${renderCards(spec.database?.cards || [])} +
    +
    + +
    + ${renderOutputSections(spec.outputs || [])} +
    + +
    + ${esc(spec.calloutTitle || 'Note')} — ${fmt(spec.callout || 'No callout provided.')} +
    +
    + +

    Generated via static-assembler prototype · ${esc(generatedAt)}

    +
    + +`; +} + +async function main() { + const [specPath, outPath] = process.argv.slice(2); + if (!specPath || !outPath) usage(); + + const cssPath = path.join(__dirname, 'base.css'); + const [specText, cssText] = await Promise.all([ + fs.readFile(specPath, 'utf8'), + fs.readFile(cssPath, 'utf8'), + ]); + + const spec = JSON.parse(specText); + const html = renderHtml(spec, cssText); + + await fs.mkdir(path.dirname(outPath), { recursive: true }); + await fs.writeFile(outPath, html, 'utf8'); + console.log(`✓ Rendered → ${outPath}`); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +});