mirror of
https://github.com/HananoshikaYomaru/openfreebooks.git
synced 2026-11-12 20:45:52 +00:00
Update .gitignore to include additional static assets for KaTeX, add KaTeX and Commander dependencies in package.json and bun.lock, enhance footer with an About link in base.html, and improve work card links in index.html. Update Vite configuration for asset handling and add new components in SASS files.
This commit is contained in:
parent
3e6c53b928
commit
0d36f3a2d6
114 changed files with 9743 additions and 14 deletions
11
.agents/skills/visual-explainer/.claude-plugin/plugin.json
Normal file
11
.agents/skills/visual-explainer/.claude-plugin/plugin.json
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"name": "visual-explainer",
|
||||||
|
"description": "Generate beautiful HTML pages for diagrams, diff reviews, plan reviews, slides, and data tables",
|
||||||
|
"version": "0.7.1",
|
||||||
|
"author": {
|
||||||
|
"name": "nicobailon"
|
||||||
|
},
|
||||||
|
"repository": "https://github.com/nicobailon/visual-explainer",
|
||||||
|
"license": "MIT",
|
||||||
|
"skills": ["./"]
|
||||||
|
}
|
||||||
478
.agents/skills/visual-explainer/SKILL.md
Normal file
478
.agents/skills/visual-explainer/SKILL.md
Normal file
|
|
@ -0,0 +1,478 @@
|
||||||
|
---
|
||||||
|
name: visual-explainer
|
||||||
|
description: Generate beautiful, self-contained HTML pages that visually explain systems, code changes, plans, and data. Use when the user asks for a diagram, architecture overview, diff review, plan review, project recap, comparison table, or any visual explanation of technical concepts. Also use proactively when you are about to render a complex ASCII table (4+ rows or 3+ columns) — present it as a styled HTML page instead.
|
||||||
|
license: MIT
|
||||||
|
compatibility: Requires a browser to view generated HTML files. Optional surf-cli for AI image generation.
|
||||||
|
metadata:
|
||||||
|
author: nicobailon
|
||||||
|
version: "0.6.3"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Visual Explainer
|
||||||
|
|
||||||
|
Generate self-contained HTML files for technical diagrams, visualizations, and data tables. Always open the result in the browser. Never fall back to ASCII art when this skill is loaded.
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
## Available Commands
|
||||||
|
|
||||||
|
Detailed prompt templates in `./commands/`. In Pi, these are slash commands (`/diff-review`). In Claude Code, namespaced (`/visual-explainer:diff-review`). In Codex, use `/prompts:diff-review` (if installed to `~/.codex/prompts/`) or invoke `$visual-explainer` and describe the workflow.
|
||||||
|
|
||||||
|
| Command | What it does |
|
||||||
|
|---------|-------------|
|
||||||
|
| `generate-web-diagram` | Generate an HTML diagram for any topic |
|
||||||
|
| `generate-visual-plan` | Generate a visual implementation plan for a feature |
|
||||||
|
| `generate-slides` | Generate a magazine-quality slide deck |
|
||||||
|
| `diff-review` | Visual diff review with architecture comparison and code review |
|
||||||
|
| `plan-review` | Compare a plan against the codebase with risk assessment |
|
||||||
|
| `project-recap` | Mental model snapshot for context-switching back to a project |
|
||||||
|
| `fact-check` | Verify accuracy of a document against actual code |
|
||||||
|
| `share-page` | Deploy an HTML page to Vercel and get a live URL |
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
### 1. Think (5 seconds, not 5 minutes)
|
||||||
|
|
||||||
|
Before writing HTML, commit to a direction. Don't default to "dark theme with blue accents" every time.
|
||||||
|
|
||||||
|
**Visual is always default.** Even essays, blog posts, and articles get visual treatment — extract structure into cards, diagrams, grids, tables.
|
||||||
|
|
||||||
|
Prose patterns (lead paragraphs, pull quotes, callout boxes) are **accent elements** within visual pages, not a separate mode. Use them to highlight key points or provide breathing room, but the page structure remains visual.
|
||||||
|
|
||||||
|
For prose accents, see "Prose Page Elements" in `./references/css-patterns.md`. For everything else, use the standard freeform approach with aesthetic directions below.
|
||||||
|
|
||||||
|
**Who is looking?** A developer understanding a system? A PM seeing the big picture? A team reviewing a proposal? This shapes information density and visual complexity.
|
||||||
|
|
||||||
|
**What type of content?** Architecture, flowchart, sequence, data flow, schema/ER, state machine, mind map, class diagram, C4 architecture, data table, timeline, dashboard, or prose-first page. Each has distinct layout needs and rendering approaches (see Diagram Types below).
|
||||||
|
|
||||||
|
**What aesthetic?** Pick one and commit. The constrained aesthetics (Blueprint, Editorial, Paper/ink) are safer — they have specific requirements that prevent generic output. The flexible ones (IDE-inspired) require more discipline.
|
||||||
|
|
||||||
|
**Constrained aesthetics (prefer these):**
|
||||||
|
- Blueprint (technical drawing feel, subtle grid background, deep slate/blue palette, monospace labels, precise borders) — see `websocket-implementation-plan.html` for reference
|
||||||
|
- Editorial (serif headlines like Instrument Serif or Crimson Pro, generous whitespace, muted earth tones or deep navy + gold)
|
||||||
|
- Paper/ink (warm cream `#faf7f5` background, terracotta/sage accents, informal feel)
|
||||||
|
- Monochrome terminal (green/amber on near-black, monospace everything, CRT glow optional)
|
||||||
|
|
||||||
|
**Flexible aesthetics (use with caution):**
|
||||||
|
- IDE-inspired (borrow a real, named color scheme: Dracula, Nord, Catppuccin Mocha/Latte, Solarized Dark/Light, Gruvbox, One Dark, Rosé Pine) — commit to the actual palette, don't approximate
|
||||||
|
- Data-dense (small type, tight spacing, maximum information, muted colors)
|
||||||
|
|
||||||
|
**Explicitly forbidden:**
|
||||||
|
- Neon dashboard (cyan + magenta + purple on dark) — always produces AI slop
|
||||||
|
- Gradient mesh (pink/purple/cyan blobs) — too generic
|
||||||
|
- Any combination of Inter font + violet/indigo accents + gradient text
|
||||||
|
|
||||||
|
Vary the choice each time. If the last diagram was dark and technical, make the next one light and editorial. The swap test: if you replaced your styling with a generic dark theme and nobody would notice the difference, you haven't designed anything.
|
||||||
|
|
||||||
|
### 2. Structure
|
||||||
|
|
||||||
|
**Read the reference material** before generating. Don't memorize it — read it each time to absorb the patterns.
|
||||||
|
- For text-heavy architecture overviews (card content matters more than topology): read `./templates/architecture.html`
|
||||||
|
- For flowcharts, sequence diagrams, ER, state machines, mind maps, class diagrams, C4: read `./templates/mermaid-flowchart.html`
|
||||||
|
- For data tables, comparisons, audits, feature matrices: read `./templates/data-table.html`
|
||||||
|
- For slide deck presentations (when `--slides` flag is present or `/generate-slides` is invoked): read `./templates/slide-deck.html` and `./references/slide-patterns.md`
|
||||||
|
- For prose-heavy publishable pages (READMEs, articles, blog posts, essays): read the "Prose Page Elements" section in `./references/css-patterns.md` and "Typography by Content Voice" in `./references/libraries.md`
|
||||||
|
|
||||||
|
**For CSS/layout patterns and SVG connectors**, read `./references/css-patterns.md`.
|
||||||
|
|
||||||
|
**For pages with 4+ sections** (reviews, recaps, dashboards), also read `./references/responsive-nav.md` for section navigation with sticky sidebar TOC on desktop and horizontal scrollable bar on mobile.
|
||||||
|
|
||||||
|
**Choosing a rendering approach:**
|
||||||
|
|
||||||
|
| Content type | Approach | Why |
|
||||||
|
|---|---|---|
|
||||||
|
| Architecture (text-heavy) | CSS Grid cards + flow arrows | Rich card content (descriptions, code, tool lists) needs CSS control |
|
||||||
|
| Architecture (topology-focused) | **Mermaid** | Visible connections between components need automatic edge routing |
|
||||||
|
| Flowchart / pipeline | **Mermaid** | Automatic node positioning and edge routing |
|
||||||
|
| Sequence diagram | **Mermaid** | Lifelines, messages, and activation boxes need automatic layout |
|
||||||
|
| Data flow | **Mermaid** with edge labels | Connections and data descriptions need automatic edge routing |
|
||||||
|
| ER / schema diagram | **Mermaid** | Relationship lines between many entities need auto-routing |
|
||||||
|
| State machine | **Mermaid** | State transitions with labeled edges need automatic layout |
|
||||||
|
| Mind map | **Mermaid** | Hierarchical branching needs automatic positioning |
|
||||||
|
| Class diagram | **Mermaid** | Inheritance, composition, aggregation lines with automatic routing |
|
||||||
|
| C4 architecture | **Mermaid** | Use `graph TD` + `subgraph` for C4 (not native `C4Context` — it ignores themes) |
|
||||||
|
| Data table | HTML `<table>` | Semantic markup, accessibility, copy-paste behavior |
|
||||||
|
| Timeline | CSS (central line + cards) | Simple linear layout doesn't need a layout engine |
|
||||||
|
| Dashboard | CSS Grid + Chart.js | Card grid with embedded charts |
|
||||||
|
|
||||||
|
**Mermaid theming:** Always use `theme: 'base'` with custom `themeVariables` so colors match your page palette. Use `layout: 'elk'` for complex graphs (requires the `@mermaid-js/layout-elk` package — see `./references/libraries.md` for the CDN import). Override Mermaid's SVG classes with CSS for pixel-perfect control. See `./references/libraries.md` for full theming guide.
|
||||||
|
|
||||||
|
**Mermaid containers:** Always center Mermaid diagrams with `display: flex; justify-content: center;`. Add zoom controls (+/−/reset/expand) to every `.mermaid-wrap` container. Include the click-to-expand JavaScript so clicking the diagram (or the ⛶ button) opens it full-size in a new tab.
|
||||||
|
|
||||||
|
**⚠️ Never use bare `<pre class="mermaid">`.** It renders but has no zoom/pan controls — diagrams become tiny and unusable. Always use the full `diagram-shell` pattern from `templates/mermaid-flowchart.html`: the HTML structure (`.diagram-shell` > `.mermaid-wrap` > `.zoom-controls` + `.mermaid-viewport` > `.mermaid-canvas`), the CSS, and the ~200-line JS module for zoom/pan/fit. Copy it wholesale.
|
||||||
|
|
||||||
|
**Mermaid scaling:** Diagrams with 10+ nodes render too small by default. For 10-12 nodes, increase `fontSize` in themeVariables to 18-20px and set `INITIAL_ZOOM` to 1.5-1.6. For 15+ elements, don't try to scale — use the hybrid pattern instead (simple Mermaid overview + CSS Grid cards). See "Architecture / System Diagrams" below.
|
||||||
|
|
||||||
|
**Mermaid layout direction:** Prefer `flowchart TD` (top-down) over `flowchart LR` (left-to-right) for complex diagrams. LR spreads horizontally and makes labels unreadable when there are many nodes. Use LR only for simple 3-4 node linear flows. See `./references/libraries.md` "Layout Direction: TD vs LR".
|
||||||
|
|
||||||
|
**Mermaid line breaks in flowchart labels:** Use `<br/>` inside quoted labels. Never use escaped newlines like `\n` (Mermaid renders them as literal text in HTML output). Example: `A["Copilot Backend<br/>/api + /api/voicebot"]`.
|
||||||
|
|
||||||
|
**Mermaid CSS class collision constraint:** Never define `.node` as a page-level CSS class. Mermaid.js uses `.node` internally on SVG `<g>` elements with `transform: translate(x, y)` for positioning. Page-level `.node` styles (hover transforms, box-shadows) leak into diagrams and break layout. Use the namespaced `.ve-card` class for card components instead. The only safe way to style Mermaid's `.node` is scoped under `.mermaid` (e.g., `.mermaid .node rect`).
|
||||||
|
|
||||||
|
**AI-generated illustrations (optional).** If [surf-cli](https://github.com/nicobailon/surf-cli) is available, you can generate images via Gemini and embed them in the page for creative, illustrative, explanatory, educational, or decorative purposes. Check availability with `which surf`. If available:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate to a temp file (use --aspect-ratio for control)
|
||||||
|
surf gemini "descriptive prompt" --generate-image /tmp/ve-img.png --aspect-ratio 16:9
|
||||||
|
|
||||||
|
# Base64 encode for self-containment (macOS)
|
||||||
|
IMG=$(base64 -i /tmp/ve-img.png)
|
||||||
|
# Linux: IMG=$(base64 -w 0 /tmp/ve-img.png)
|
||||||
|
|
||||||
|
# Embed in HTML and clean up
|
||||||
|
# <img src="data:image/png;base64,${IMG}" alt="descriptive alt text">
|
||||||
|
rm /tmp/ve-img.png
|
||||||
|
```
|
||||||
|
|
||||||
|
See `./references/css-patterns.md` for image container styles (hero banners, inline illustrations, captions).
|
||||||
|
|
||||||
|
**When to use:** Hero banners that establish the page's visual tone. Conceptual illustrations for abstract systems that Mermaid can't express (physical infrastructure, user journeys, mental models). Educational diagrams that benefit from artistic or photorealistic rendering. Decorative accents that reinforce the aesthetic.
|
||||||
|
|
||||||
|
**When to skip:** Anything Mermaid or CSS handles well. Generic decoration that doesn't convey meaning. Data-heavy pages where images would distract. Always degrade gracefully — if surf isn't available, skip images without erroring. The page should stand on its own with CSS and typography alone.
|
||||||
|
|
||||||
|
**Prompt craft:** Match the image to the page's palette and aesthetic direction. Specify the style (3D render, technical illustration, watercolor, isometric, flat vector, etc.) and mention dominant colors from your CSS variables. Use `--aspect-ratio 16:9` for hero banners, `--aspect-ratio 1:1` for inline illustrations. Keep prompts specific — "isometric illustration of a message queue with cyan nodes on dark navy background" beats "a diagram of a queue."
|
||||||
|
|
||||||
|
### 3. Style
|
||||||
|
|
||||||
|
Apply these principles to every diagram:
|
||||||
|
|
||||||
|
**Typography is the diagram.** Pick a distinctive font pairing from the list in `./references/libraries.md`. Every page should use a different pairing from recent generations.
|
||||||
|
|
||||||
|
**Forbidden as `--font-body`:** Inter, Roboto, Arial, Helvetica, system-ui alone. These are AI slop signals.
|
||||||
|
|
||||||
|
**Good pairings (use these):**
|
||||||
|
- DM Sans + Fira Code (technical, precise)
|
||||||
|
- Instrument Serif + JetBrains Mono (editorial, refined)
|
||||||
|
- IBM Plex Sans + IBM Plex Mono (reliable, readable)
|
||||||
|
- Bricolage Grotesque + Fragment Mono (bold, characterful)
|
||||||
|
- Plus Jakarta Sans + Azeret Mono (rounded, approachable)
|
||||||
|
|
||||||
|
Load via `<link>` in `<head>`. Include a system font fallback in the `font-family` stack for offline resilience.
|
||||||
|
|
||||||
|
**Color tells a story.** Use CSS custom properties for the full palette. Define at minimum: `--bg`, `--surface`, `--border`, `--text`, `--text-dim`, and 3-5 accent colors. Each accent should have a full and a dim variant (for backgrounds). Name variables semantically when possible (`--pipeline-step` not `--blue-3`). Support both themes.
|
||||||
|
|
||||||
|
**Forbidden accent colors:** `#8b5cf6` `#7c3aed` `#a78bfa` (indigo/violet), `#d946ef` (fuchsia), the cyan-magenta-pink combination. These are Tailwind defaults that signal zero design intent.
|
||||||
|
|
||||||
|
**Good accent palettes (use these):**
|
||||||
|
- Terracotta + sage (`#c2410c`, `#65a30d`) — warm, earthy
|
||||||
|
- Teal + slate (`#0891b2`, `#0369a1`) — technical, precise
|
||||||
|
- Rose + cranberry (`#be123c`, `#881337`) — editorial, refined
|
||||||
|
- Amber + emerald (`#d97706`, `#059669`) — data-focused
|
||||||
|
- Deep blue + gold (`#1e3a5f`, `#d4a73a`) — premium, sophisticated
|
||||||
|
|
||||||
|
Put your primary aesthetic in `:root` and the alternate in the media query:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Light-first (editorial, paper/ink, blueprint): */
|
||||||
|
:root { /* light values */ }
|
||||||
|
@media (prefers-color-scheme: dark) { :root { /* dark values */ } }
|
||||||
|
|
||||||
|
/* Dark-first (neon, IDE-inspired, terminal): */
|
||||||
|
:root { /* dark values */ }
|
||||||
|
@media (prefers-color-scheme: light) { :root { /* light values */ } }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Surfaces whisper, they don't shout.** Build depth through subtle lightness shifts (2-4% between levels), not dramatic color changes. Borders should be low-opacity rgba (`rgba(255,255,255,0.08)` in dark mode, `rgba(0,0,0,0.08)` in light) — visible when you look, invisible when you don't.
|
||||||
|
|
||||||
|
**Backgrounds create atmosphere.** Don't use flat solid colors for the page background. Subtle gradients, faint grid patterns via CSS, or gentle radial glows behind focal areas. The background should feel like a space, not a void.
|
||||||
|
|
||||||
|
**Visual weight signals importance.** Not every section deserves equal visual treatment. Executive summaries and key metrics should dominate the viewport on load (larger type, more padding, subtle accent-tinted background zone). Reference sections (file maps, dependency lists, decision logs) should be compact and stay out of the way. Use `<details>/<summary>` for sections that are useful but not primary — the collapsible pattern is in `./references/css-patterns.md`.
|
||||||
|
|
||||||
|
**Surface depth creates hierarchy.** Vary card depth to signal what matters. Hero sections get elevated shadows and accent-tinted backgrounds (`ve-card--hero` pattern). Body content stays flat (default `.ve-card`). Code blocks and secondary content feel recessed (`ve-card--recessed`). See the depth tiers in `./references/css-patterns.md`. Don't make everything elevated — when everything pops, nothing does.
|
||||||
|
|
||||||
|
**Animation earns its place.** Staggered fade-ins on page load are almost always worth it — they guide the eye through the diagram's hierarchy. Mix animation types by role: `fadeUp` for cards, `fadeScale` for KPIs and badges, `drawIn` for SVG connectors, `countUp` for hero numbers. Hover transitions on interactive-feeling elements make the diagram feel alive. Always respect `prefers-reduced-motion`. CSS transitions and keyframes handle most cases. For orchestrated multi-element sequences, anime.js via CDN is available (see `./references/libraries.md`).
|
||||||
|
|
||||||
|
**Forbidden animations:**
|
||||||
|
- Animated glowing box-shadows (`@keyframes glow { box-shadow: 0 0 20px... }`) — this is AI slop
|
||||||
|
- Pulsing/breathing effects on static content
|
||||||
|
- Continuous animations that run after page load (except for progress indicators)
|
||||||
|
|
||||||
|
Keep animations purposeful: entrance reveals, hover feedback, and user-initiated interactions. Nothing should glow or pulse on its own.
|
||||||
|
|
||||||
|
### 4. Deliver
|
||||||
|
|
||||||
|
**Output location:** Write to `~/.agent/diagrams/`. Use a descriptive filename based on content: `modem-architecture.html`, `pipeline-flow.html`, `schema-overview.html`. The directory persists across sessions.
|
||||||
|
|
||||||
|
**Open in browser:**
|
||||||
|
- macOS: `open ~/.agent/diagrams/filename.html`
|
||||||
|
- Linux: `xdg-open ~/.agent/diagrams/filename.html`
|
||||||
|
|
||||||
|
**Tell the user** the file path so they can re-open or share it.
|
||||||
|
|
||||||
|
## Diagram Types
|
||||||
|
|
||||||
|
### Architecture / System Diagrams
|
||||||
|
Three approaches depending on complexity:
|
||||||
|
|
||||||
|
**Simple topology (under 10 elements):** Use Mermaid. A `graph TD` with custom `themeVariables` produces readable diagrams with automatic edge routing.
|
||||||
|
|
||||||
|
**Text-heavy overviews (under 15 elements):** CSS Grid with explicit row/column placement. Sections as rounded cards with colored borders and monospace labels. Vertical flow arrows between sections. The reference template at `./templates/architecture.html` demonstrates this pattern. Use when cards need descriptions, code references, tool lists, or other rich content that Mermaid nodes can't hold.
|
||||||
|
|
||||||
|
**Complex architectures (15+ elements):** Use the **hybrid pattern** — a simple Mermaid overview (5-8 nodes showing module relationships) followed by detailed CSS Grid cards for each module's internals. This gives you visual topology AND readable details. The overview diagram uses module names with `<small>` tags for key function names. The cards below show full function lists with new/modified badges. Never try to cram 15+ elements into a single Mermaid diagram — it will render unreadably small even with zoom controls.
|
||||||
|
|
||||||
|
### Flowcharts / Pipelines
|
||||||
|
**Use Mermaid.** Automatic node positioning and edge routing produces proper diagrams with connecting lines, decision diamonds, and parallel branches — dramatically better than CSS flexbox with arrow characters. Prefer `graph TD` (top-down); use `graph LR` only for simple 3-4 node linear flows. Color-code node types with Mermaid's `classDef` or rely on `themeVariables` for automatic styling.
|
||||||
|
|
||||||
|
### Sequence Diagrams
|
||||||
|
**Use Mermaid.** Lifelines, messages, activation boxes, notes, and loops all need automatic layout. Use Mermaid's `sequenceDiagram` syntax. Style actors and messages via CSS overrides on `.actor`, `.messageText`, `.activation` classes.
|
||||||
|
|
||||||
|
### Data Flow Diagrams
|
||||||
|
**Use Mermaid.** Data flow diagrams emphasize connections over boxes — exactly what Mermaid excels at. Use `graph TD` (or `graph LR` for simple linear flows) with edge labels for data descriptions. Thicker, colored edges for primary flows. Source/sink nodes styled differently from transform nodes via Mermaid's `classDef`.
|
||||||
|
|
||||||
|
### Schema / ER Diagrams
|
||||||
|
**Use Mermaid.** Relationship lines between entities need automatic routing. Use Mermaid's `erDiagram` syntax with entity attributes. Style via `themeVariables` and CSS overrides on `.er.entityBox` and `.er.relationshipLine`.
|
||||||
|
|
||||||
|
### State Machines / Decision Trees
|
||||||
|
**Use Mermaid.** Use `stateDiagram-v2` for states with labeled transitions. Supports nested states, forks, joins, and notes. Decision trees can use `graph TD` with diamond decision nodes.
|
||||||
|
|
||||||
|
**`stateDiagram-v2` label caveat:** Transition labels have a strict parser — colons, parentheses, `<br/>`, HTML entities, and most special characters cause silent parse failures ("Syntax error in text"). If your labels need any of these (e.g., `cancel()`, `curate: true`, multi-line labels), use `flowchart TD` instead with rounded nodes and quoted edge labels (`|"label text"|`). Flowcharts handle all special characters and support `<br/>` for line breaks. Reserve `stateDiagram-v2` for simple single-word or plain-text labels.
|
||||||
|
|
||||||
|
### Mind Maps / Hierarchical Breakdowns
|
||||||
|
**Use Mermaid.** Use `mindmap` syntax for hierarchical branching from a root node. Mermaid handles the radial layout automatically. Style with `themeVariables` to control node colors at each depth level.
|
||||||
|
|
||||||
|
### Class Diagrams
|
||||||
|
**Use Mermaid.** Use `classDiagram` syntax for domain modeling, OOP design, and entity relationships with typed properties and methods. Supports relationships: association (`-->`), composition (`*--`), aggregation (`o--`), and inheritance (`<|--`). Add multiplicity labels (e.g., `"1" --> "*"`) and abstract/interface markers (`<<interface>>`, `<<abstract>>`). For simple entity boxes without OOP semantics (no methods, no inheritance), prefer `erDiagram` instead — it produces cleaner output for pure data modeling.
|
||||||
|
|
||||||
|
### C4 Architecture Diagrams
|
||||||
|
**Use Mermaid flowchart syntax — NOT native C4.** Use `graph TD` with `subgraph` blocks for C4 boundaries. Native `C4Context` hardcodes sharp corners, its own font, blue icons, and inline SVG colors that ignore `themeVariables` — it always clashes with custom palettes.
|
||||||
|
|
||||||
|
**Flowchart-as-C4 pattern:** Persons → rounded nodes `(("Name"))`, systems → rectangles `["Name"]`, databases → cylinders `[("Name")]`, boundaries → `subgraph` blocks, relationships → labeled arrows `-->|"protocol"|`. Use `classDef` + `:::className` to visually differentiate external systems (e.g., dashed borders). This inherits `themeVariables`, `fontFamily`, and CSS overrides like every other Mermaid diagram.
|
||||||
|
|
||||||
|
### Data Tables / Comparisons / Audits
|
||||||
|
Use a real `<table>` element — not CSS Grid pretending to be a table. Tables get accessibility, copy-paste behavior, and column alignment for free. The reference template at `./templates/data-table.html` demonstrates all patterns below.
|
||||||
|
|
||||||
|
**Use proactively.** Any time you'd render an ASCII box-drawing table in the terminal, generate an HTML table instead. This includes: requirement audits (request vs plan), feature comparisons, status reports, configuration matrices, test result summaries, dependency lists, permission tables, API endpoint inventories — any structured rows and columns.
|
||||||
|
|
||||||
|
Layout patterns:
|
||||||
|
- Sticky `<thead>` so headers stay visible when scrolling long tables
|
||||||
|
- Alternating row backgrounds via `tr:nth-child(even)` (subtle, 2-3% lightness shift)
|
||||||
|
- First column optionally sticky for wide tables with horizontal scroll
|
||||||
|
- Responsive wrapper with `overflow-x: auto` for tables wider than the viewport
|
||||||
|
- Column width hints via `<colgroup>` or `th` widths — let text-heavy columns breathe
|
||||||
|
- Row hover highlight for scanability
|
||||||
|
|
||||||
|
Status indicators (use styled `<span>` elements, never emoji):
|
||||||
|
- Match/pass/yes: colored dot or checkmark with green background
|
||||||
|
- Gap/fail/no: colored dot or cross with red background
|
||||||
|
- Partial/warning: amber indicator
|
||||||
|
- Neutral/info: dim text or muted badge
|
||||||
|
|
||||||
|
Cell content:
|
||||||
|
- Wrap long text naturally — don't truncate or force single-line
|
||||||
|
- Use `<code>` for technical references within cells
|
||||||
|
- Secondary detail text in `<small>` with dimmed color
|
||||||
|
- Keep numeric columns right-aligned with `tabular-nums`
|
||||||
|
|
||||||
|
### Timeline / Roadmap Views
|
||||||
|
Vertical or horizontal timeline with a central line (CSS pseudo-element). Phase markers as circles on the line. Content cards branching left/right (alternating) or all to one side. Date labels on the line. Color progression from past (muted) to future (vivid).
|
||||||
|
|
||||||
|
### Dashboard / Metrics Overview
|
||||||
|
Card grid layout. Hero numbers large and prominent. Sparklines via inline SVG `<polyline>`. Progress bars via CSS `linear-gradient` on a div. For real charts (bar, line, pie), use **Chart.js via CDN** (see `./references/libraries.md`). KPI cards with trend indicators (up/down arrows, percentage deltas).
|
||||||
|
|
||||||
|
### Implementation Plans
|
||||||
|
|
||||||
|
For visualizing implementation plans, extension designs, or feature specifications. The goal is **understanding the approach**, not reading the full source code.
|
||||||
|
|
||||||
|
**Don't dump full files.** Displaying entire source files inline overwhelms the page and defeats the purpose of a visual explanation. Instead:
|
||||||
|
- Show **file structure with descriptions** — list functions/exports with one-line explanations
|
||||||
|
- Show **key snippets only** — the 5-10 lines that illustrate the core logic
|
||||||
|
- Use **collapsible sections** for full code if truly needed
|
||||||
|
|
||||||
|
**Code blocks require explicit formatting.** Without `white-space: pre-wrap`, code runs together into an unreadable wall. See the "Code Blocks" section in `./references/css-patterns.md` for the correct pattern.
|
||||||
|
|
||||||
|
**Structure for implementation plans:**
|
||||||
|
1. Overview/purpose (what problem does this solve?)
|
||||||
|
2. Flow diagram (Mermaid or CSS cards)
|
||||||
|
3. File structure with descriptions (not full code)
|
||||||
|
4. Key implementation details (snippets)
|
||||||
|
5. API/interface summary
|
||||||
|
6. Usage examples
|
||||||
|
|
||||||
|
### Documentation (READMEs, Library Docs, API References)
|
||||||
|
|
||||||
|
When visualizing documentation, extract structure into visual elements:
|
||||||
|
|
||||||
|
| Content | Visual Treatment |
|
||||||
|
|---------|------------------|
|
||||||
|
| Features | Card grid (2-3 columns) |
|
||||||
|
| Install/setup steps | Numbered cards or vertical flow |
|
||||||
|
| API endpoints/commands | Table with sticky header |
|
||||||
|
| Config options | Table |
|
||||||
|
| Architecture | Mermaid diagram or CSS card layout |
|
||||||
|
| Comparisons | Side-by-side panels or table |
|
||||||
|
| Warnings/notes | Callout boxes |
|
||||||
|
|
||||||
|
Don't just format the prose — transform it. A feature list becomes a card grid. Install steps become a numbered flow. An API reference becomes a table.
|
||||||
|
|
||||||
|
### Prose Accent Elements
|
||||||
|
|
||||||
|
Use these sparingly within visual pages to highlight key points or provide breathing room. See "Prose Page Elements" in `./references/css-patterns.md` for CSS patterns.
|
||||||
|
|
||||||
|
- **Lead paragraph** — larger intro text to set context before diving into cards/grids
|
||||||
|
- **Pull quote** — highlight a key insight; one per page maximum
|
||||||
|
- **Callout box** — warnings, tips, important notes
|
||||||
|
- **Section divider** — visual break between major sections
|
||||||
|
|
||||||
|
**When to use:** A visual page explaining an essay might use a lead paragraph for the thesis, then cards for key arguments. A README visualization might use callout boxes for warnings but otherwise stay card/table-focused.
|
||||||
|
|
||||||
|
## Slide Deck Mode
|
||||||
|
|
||||||
|
An alternative output format for presenting content as a magazine-quality slide presentation instead of a scrollable page. **Opt-in only** — the agent generates slides when the user invokes `/generate-slides`, passes `--slides` to an existing prompt (e.g., `/diff-review --slides`), or explicitly asks for a slide deck. Never auto-select slide format.
|
||||||
|
|
||||||
|
**Before generating slides**, read `./references/slide-patterns.md` (engine CSS, slide types, transitions, nav chrome, presets) and `./templates/slide-deck.html` (reference template showing all 10 types). Also read `./references/css-patterns.md` for shared patterns and `./references/libraries.md` for Mermaid/Chart.js theming.
|
||||||
|
|
||||||
|
**Slides are not pages reformatted.** They're a different medium. Each slide is exactly one viewport tall (100dvh) with no scrolling. Typography is 2–3× larger. Compositions are bolder. The agent composes a narrative arc (impact → context → deep dive → resolution) rather than mechanically paginating the source.
|
||||||
|
|
||||||
|
**Content completeness.** Changing the medium does not mean dropping content. Follow the "Planning a Deck from a Source Document" process in `slide-patterns.md` before writing any HTML: inventory the source, map every item to slides, verify coverage. Every section, decision, data point, specification, and collapsible detail from the source must appear in the deck. If a plan has 7 sections, the deck covers all 7. If there are 6 decisions, present all 6 — not the 2 that fit on one slide. Collapsible details in the source become their own slides. Add more slides rather than cutting content. A 22-slide deck that covers everything beats a 13-slide deck that looks polished but is missing 40% of the source.
|
||||||
|
|
||||||
|
**Slide types (10):** Title, Section Divider, Content, Split, Diagram, Dashboard, Table, Code, Quote, Full-Bleed. Each has a defined layout in `slide-patterns.md`. Content that exceeds a slide's density limit splits across multiple slides — never scrolls within a slide.
|
||||||
|
|
||||||
|
**Visual richness:** Check `which surf` at the start. If surf-cli is available, generate 2–4 images (title slide background, full-bleed background, optional content illustrations) before writing HTML — see the Proactive Imagery section in `slide-patterns.md` for the workflow. Also use SVG decorative accents, per-slide background gradients, inline sparklines, and small Mermaid diagrams. Visual-first, text-second.
|
||||||
|
|
||||||
|
**Compositional variety:** Consecutive slides must vary spatial approach — centered, left-heavy, right-heavy, split, edge-aligned, full-bleed. Three centered slides in a row means push one off-axis.
|
||||||
|
|
||||||
|
**Curated presets:** Four slide-specific presets as starting points (Midnight Editorial, Warm Signal, Terminal Mono, Swiss Clean) plus the existing 8 aesthetic directions adapted for slides. Pick one and commit. See `slide-patterns.md` for preset CSS values.
|
||||||
|
|
||||||
|
**`--slides` flag on existing prompts:** When a user passes `--slides` to `/diff-review`, `/plan-review`, `/project-recap`, or other prompts, the agent gathers data using the prompt's normal data-gathering instructions, then presents the content as a slide deck instead of a scrollable page. The slide version tells the same story with different structure and pacing — but the same breadth of coverage. Don't use the slide format as an excuse to summarize or skip sections that the scrollable version would have included.
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
Every diagram is a single self-contained `.html` file. No external assets except CDN links (fonts, optional libraries). Structure:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Descriptive Title</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=...&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
/* CSS custom properties, theme, layout, components — all inline */
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Semantic HTML: sections, headings, lists, tables, inline SVG -->
|
||||||
|
<!-- No script needed for static CSS-only diagrams -->
|
||||||
|
<!-- Optional: <script> for Mermaid, Chart.js, or anime.js when used -->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sharing Pages
|
||||||
|
|
||||||
|
Share visual explainer pages instantly via Vercel when a Pi-compatible `vercel-deploy` skill is available. No account or authentication required.
|
||||||
|
|
||||||
|
**Usage with the installed skill path:**
|
||||||
|
```bash
|
||||||
|
bash ~/.pi/agent/skills/visual-explainer/scripts/share.sh <html-file>
|
||||||
|
```
|
||||||
|
|
||||||
|
If the skill lives somewhere else, use that install path instead, such as `~/.codex/skills/visual-explainer/scripts/share.sh`, `~/.config/opencode/skill/visual-explainer/scripts/share.sh`, or `./plugins/visual-explainer/scripts/share.sh` from a repository checkout.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
bash ~/.pi/agent/skills/visual-explainer/scripts/share.sh ~/.agent/diagrams/my-diagram.html
|
||||||
|
|
||||||
|
# Output:
|
||||||
|
# ✓ Shared successfully!
|
||||||
|
# Live URL: https://skill-deploy-abc123.vercel.app
|
||||||
|
# Claim URL: https://vercel.com/claim-deployment?code=...
|
||||||
|
```
|
||||||
|
|
||||||
|
**How it works:**
|
||||||
|
1. Runs the `share.sh` script from the installed `visual-explainer` skill directory
|
||||||
|
2. Copies HTML file to temp directory as `index.html`
|
||||||
|
3. Deploys via the Pi-compatible `vercel-deploy` skill
|
||||||
|
4. URL is live immediately — works in any browser
|
||||||
|
|
||||||
|
**Requirements:**
|
||||||
|
- vercel-deploy skill in a standard Pi-compatible skill location (in Pi: `pi install npm:vercel-deploy`)
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
- Deployments are public — anyone with the URL can view
|
||||||
|
- Preview deployments have configurable retention (default: 30 days)
|
||||||
|
- Claim URL lets you transfer the deployment to your Vercel account
|
||||||
|
- Other harnesses can generate and open HTML normally; `/share-page` depends on the Pi-compatible `vercel-deploy` script being available
|
||||||
|
|
||||||
|
See `./commands/share-page.md` for the `/share-page` command template.
|
||||||
|
|
||||||
|
## Quality Checks
|
||||||
|
|
||||||
|
Before delivering, verify:
|
||||||
|
- **The squint test**: Blur your eyes. Can you still perceive hierarchy? Are sections visually distinct?
|
||||||
|
- **The swap test**: Would replacing your fonts and colors with a generic dark theme make this indistinguishable from a template? If yes, push the aesthetic further.
|
||||||
|
- **Both themes**: Toggle your OS between light and dark mode. Both should look intentional, not broken.
|
||||||
|
- **Information completeness**: Does the diagram actually convey what the user asked for? Pretty but incomplete is a failure.
|
||||||
|
- **No overflow**: Resize the browser to different widths. No content should clip or escape its container. Every grid and flex child needs `min-width: 0`. Side-by-side panels need `overflow-wrap: break-word`. Never use `display: flex` on `<li>` for marker characters — it creates anonymous flex items that can't shrink, causing lines with many inline `<code>` badges to overflow. Use absolute positioning for markers instead. See the Overflow Protection section in `./references/css-patterns.md`.
|
||||||
|
- **Mermaid zoom controls**: Every `.mermaid-wrap` container must have zoom controls (+/−/reset/expand buttons), Ctrl/Cmd+scroll zoom, click-and-drag panning, and click-to-expand (clicking without dragging opens the diagram full-size in a new tab). The expand button (⛶) provides the same functionality. See `./references/css-patterns.md` for the full pattern including the `openMermaidInNewTab()` function.
|
||||||
|
- **File opens cleanly**: No console errors, no broken font loads, no layout shifts.
|
||||||
|
|
||||||
|
## Anti-Patterns (AI Slop)
|
||||||
|
|
||||||
|
These patterns are explicitly forbidden. They signal "AI-generated template" and undermine the skill's purpose of producing distinctive, high-quality diagrams. Review every generated page against this list.
|
||||||
|
|
||||||
|
### Typography
|
||||||
|
|
||||||
|
**Forbidden fonts as primary `--font-body`:**
|
||||||
|
- Inter — the single most overused AI default
|
||||||
|
- Roboto, Arial, Helvetica — generic system fallbacks promoted to primary
|
||||||
|
- system-ui, sans-serif alone — no character, no intent
|
||||||
|
|
||||||
|
**Required:** Pick from the font pairings in `./references/libraries.md`. Every generation should use a different pairing from the last.
|
||||||
|
|
||||||
|
### Color Palette
|
||||||
|
|
||||||
|
**Forbidden accent colors:**
|
||||||
|
- Indigo-500/violet-500 (`#8b5cf6`, `#7c3aed`, `#a78bfa`) — Tailwind's default purple range
|
||||||
|
- The cyan + magenta + pink neon gradient combination (`#06b6d4` → `#d946ef` → `#f472b6`)
|
||||||
|
- Any palette that could be described as "Tailwind defaults with purple/pink/cyan accents"
|
||||||
|
|
||||||
|
**Forbidden color effects:**
|
||||||
|
- Gradient text on headings (`background: linear-gradient(...); background-clip: text;`) — this screams AI-generated
|
||||||
|
- Animated glowing box-shadows on cards (`box-shadow: 0 0 20px var(--glow); animation: glow 2s...`)
|
||||||
|
- Multiple overlapping radial glows in accent colors creating a "neon haze"
|
||||||
|
|
||||||
|
**Required:** Build palettes from the reference templates (terracotta/sage, teal/cyan, rose/cranberry, slate/blue) or derive from real IDE themes (Dracula, Nord, Solarized, Gruvbox, Catppuccin). Accents should feel intentional, not default.
|
||||||
|
|
||||||
|
### Section Headers
|
||||||
|
|
||||||
|
**Forbidden:**
|
||||||
|
- Emoji icons in section headers (🏗️, ⚙️, 📁, 💻, 📅, 🔗, ⚡, 🔧, 📦, 🚀, etc.)
|
||||||
|
- Section headers that all use the same icon-in-rounded-box pattern
|
||||||
|
|
||||||
|
**Required:** Use styled monospace labels with colored dot indicators (see `.section-label` in templates), numbered badges (`section__num` pattern), or asymmetric section dividers. If an icon is genuinely needed, use an inline SVG that matches the palette — not emoji.
|
||||||
|
|
||||||
|
### Layout & Hierarchy
|
||||||
|
|
||||||
|
**Forbidden:**
|
||||||
|
- Perfectly centered everything with uniform padding
|
||||||
|
- All cards styled identically with the same border-radius, shadow, and spacing
|
||||||
|
- Every section getting equal visual treatment — no hero/primary vs. secondary distinction
|
||||||
|
- Symmetric layouts where left and right halves mirror each other
|
||||||
|
|
||||||
|
**Required:** Vary visual weight. Hero sections should dominate (larger type, more padding, accent-tinted background). Reference sections should feel compact. Use the depth tiers (hero → elevated → default → recessed). Asymmetric layouts create interest.
|
||||||
|
|
||||||
|
### Template Patterns
|
||||||
|
|
||||||
|
**Forbidden:**
|
||||||
|
- Three-dot window chrome (red/yellow/green dots) on code blocks — this is a cliché
|
||||||
|
- KPI cards where every metric has identical gradient text treatment
|
||||||
|
- "Neon Dashboard" as an aesthetic choice — it always produces generic results
|
||||||
|
- Gradient meshes with pink/purple/cyan blobs in the background
|
||||||
|
|
||||||
|
**Required:** Code blocks use a simple header with filename or language label. KPI cards vary by importance — hero numbers for the primary metric, subdued treatment for supporting metrics. Pick aesthetics with natural constraints: Blueprint (must feel technical/precise), Editorial (must have generous whitespace and serif typography), Paper/ink (must feel warm and informal).
|
||||||
|
|
||||||
|
### The Slop Test
|
||||||
|
|
||||||
|
Before delivering, apply this test: **Would a developer looking at this page immediately think "AI generated this"?** The telltale signs:
|
||||||
|
|
||||||
|
1. Inter or Roboto font with purple/violet gradient accents
|
||||||
|
2. Every heading has `background-clip: text` gradient
|
||||||
|
3. Emoji icons leading every section
|
||||||
|
4. Glowing cards with animated shadows
|
||||||
|
5. Cyan-magenta-pink color scheme on dark background
|
||||||
|
6. Perfectly uniform card grid with no visual hierarchy
|
||||||
|
7. Three-dot code block chrome
|
||||||
|
|
||||||
|
If two or more of these are present, the page is slop. Regenerate with a different aesthetic direction — Editorial, Blueprint, Paper/ink, or a specific IDE theme. These constrained aesthetics are harder to mess up because they have specific visual requirements that prevent defaulting to generic patterns.
|
||||||
68
.agents/skills/visual-explainer/commands/diff-review.md
Normal file
68
.agents/skills/visual-explainer/commands/diff-review.md
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
---
|
||||||
|
description: Generate a visual HTML diff review — before/after architecture comparison with code review analysis
|
||||||
|
---
|
||||||
|
Load the visual-explainer skill, then generate a comprehensive visual diff review as a self-contained HTML page.
|
||||||
|
|
||||||
|
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`:
|
||||||
|
- Branch name (e.g. `main`, `develop`): working tree vs that branch
|
||||||
|
- Commit hash: that specific commit's diff (`git show <hash>`)
|
||||||
|
- `HEAD`: uncommitted changes only (`git diff` and `git diff --staged`)
|
||||||
|
- PR number (e.g. `#42`): `gh pr diff 42`
|
||||||
|
- Range (e.g. `abc123..def456`): diff between two commits
|
||||||
|
- No argument: default to `main`
|
||||||
|
|
||||||
|
**Data gathering phase** — run these first to understand the full scope:
|
||||||
|
- `git diff --stat <ref>` for file-level overview
|
||||||
|
- `git diff --name-status <ref> --` for new/modified/deleted files (separate src from tests)
|
||||||
|
- Line counts: compare key files between `<ref>` and working tree (`git show <ref>:file | wc -l` vs `wc -l`)
|
||||||
|
- New public API surface: grep added lines for exported symbols, public functions, classes, interfaces (adapt the pattern to the project's language — `export`/`function`/`class`/`interface` for TS/JS, `def`/`class` for Python, `func`/`type` for Go, etc.)
|
||||||
|
- Feature inventory: grep for new actions, keybindings, config fields, event types on both sides
|
||||||
|
- Read all changed files in full — include surrounding code paths needed to validate behavior
|
||||||
|
- Check whether `CHANGELOG.md` has an entry for these changes
|
||||||
|
- Check whether `README.md` or `docs/*.md` need updates given any new or changed features
|
||||||
|
- Reconstruct decision rationale: if this work was done in the current session, mine the conversation for approaches discussed, alternatives rejected, and trade-offs made. Check for progress docs (`~/.agent/memory/{project}/progress.md`, `~/.pi/agent/memory/{project}/progress.md`) or plan files that may contain reasoning. For committed changes, read commit messages and PR descriptions.
|
||||||
|
|
||||||
|
**Verification checkpoint** — before generating HTML, produce a structured fact sheet of every claim you will present in the review:
|
||||||
|
- Every quantitative figure: line counts, file counts, function counts, test counts
|
||||||
|
- Every function, type, and module name you will reference
|
||||||
|
- Every behavior description: what code does, what changed, before vs. after
|
||||||
|
- For each, cite the source: the git command output that produced it, or the file:line where you read it
|
||||||
|
Verify each claim against the code. If something cannot be verified, mark it as uncertain rather than stating it as fact. This fact sheet is your source of truth during HTML generation — do not deviate from it.
|
||||||
|
|
||||||
|
**Diagram structure** — the page should include:
|
||||||
|
1. **Executive summary** — not just a dry before/after. Lead with the *intuition*: why do these changes exist? What problem were they solving, what was the core insight? Then the factual scope (X files, Y lines, Z new modules). Aim for "aha moment" clarity — a reader who only sees this section should understand the essence of the change. *Visual treatment: this is the visual anchor — use hero depth (larger type 20-24px, subtle accent-tinted background, more padding than other sections).*
|
||||||
|
2. **KPI dashboard** — lines added/removed, files changed, new modules, test counts. Include a **housekeeping** indicator: whether CHANGELOG.md was updated (green/red badge) and whether docs need changes (green/yellow/red).
|
||||||
|
3. **Module architecture** — how the file structure changed, with a Mermaid dependency graph of the current state. Wrap in `.mermaid-wrap` with zoom controls (+/−/reset/expand buttons), Ctrl/Cmd+scroll zoom, click-and-drag panning, and click-to-expand (opens diagram full-size in new tab). See css-patterns.md "Mermaid Zoom Controls" for the full pattern including the `openMermaidInNewTab()` function.
|
||||||
|
4. **Major feature comparisons** — side-by-side before/after panels for each significant area of change (UI, data flow, API surface, config, etc.). Overflow prevention: apply `min-width: 0` on all grid/flex children and `overflow-wrap: break-word` on panels. Never use `display: flex` on `<li>` for marker characters — use absolute positioning instead (see css-patterns.md Overflow Protection).
|
||||||
|
5. **Flow diagrams** — Mermaid flowchart, sequence, or state diagrams for any new lifecycle/pipeline/interaction patterns. Same zoom controls and click-to-expand as section 3.
|
||||||
|
6. **File map** — full tree with color-coded new/modified/deleted indicators. *Visual treatment: compact — consider `<details>` collapsed by default for pages with many sections.*
|
||||||
|
7. **Test coverage** — before/after test file counts and what's covered
|
||||||
|
8. **Code review** — structured Good/Bad/Ugly analysis of the changes:
|
||||||
|
- **Good**: Solid choices, improvements, clean patterns worth calling out
|
||||||
|
- **Bad**: Concrete issues — bugs, regressions, missing error handling, logic errors
|
||||||
|
- **Ugly**: Subtle problems — tech debt introduced, maintainability concerns, things that work now but will bite later
|
||||||
|
- **Questions**: Anything unclear or that needs the author's clarification
|
||||||
|
- Use styled cards with green/red/amber/blue left-border accents matching the diff color language. Each item should reference specific files and line ranges. If nothing to flag in a category, say "None found" rather than omitting the section.
|
||||||
|
9. **Decision log** — for each significant design choice in the diff, a styled card with:
|
||||||
|
- **Decision**: one-line summary of what was decided (e.g., "Promise-based deferred resolution instead of event emitters for cleanup signaling")
|
||||||
|
- **Rationale**: why this approach — constraints, trade-offs, what it enables. Pull from conversation context if available, infer from code structure if not.
|
||||||
|
- **Alternatives considered**: what was rejected and why, if recoverable
|
||||||
|
- **Confidence**: whether this rationale was explicitly discussed (high — sourced from conversation/docs) or inferred from the code (medium — flagged as inference). Low confidence means the rationale couldn't be recovered at all.
|
||||||
|
- Visual treatment by confidence level — use left-border accent colors consistent with the diff color language: **High** (sourced from conversation/docs): green left border. **Medium** (inferred from code): blue left border, labeled "inferred." **Low** (not recoverable): amber left border, "rationale not recoverable — document before committing" warning. Low-confidence cards are cognitive debt hotspots — tell the user to document the reasoning before committing.
|
||||||
|
10. **Re-entry context** — a concise "note from present-you to future-you" covering the following. *Visual treatment: compact — consider `<details>` collapsed by default for pages with many sections.*
|
||||||
|
- **Key invariants**: assumptions the changed code relies on that aren't enforced by types or tests (e.g., "cleanup must be called before session switch or artifacts leak")
|
||||||
|
- **Non-obvious coupling**: files or behaviors that are connected in ways that aren't visible from imports alone (e.g., "the feed renderer reads events written by the overlay — changing the event schema requires updating both")
|
||||||
|
- **Gotchas**: things that would surprise someone modifying this code in two weeks. Edge cases, ordering dependencies, implicit contracts.
|
||||||
|
- **Don't forget**: if the changes require follow-up work (migration, config update, docs), list it here.
|
||||||
|
|
||||||
|
**Visual hierarchy**: Sections 1-3 should dominate the viewport on load (hero depth, larger type, more padding). Sections 6+ are reference material and should feel lighter (flat or recessed depth, compact layout, collapsible where appropriate).
|
||||||
|
|
||||||
|
**Optional illustrations** — if `surf` CLI is available (`which surf`), consider generating a hero banner or conceptual illustration via `surf gemini --generate-image` when it would enhance the page. Embed as base64 data URI. See css-patterns.md "Generated Images" for container styles. Skip if surf isn't available or the diff is purely structural.
|
||||||
|
|
||||||
|
Include responsive section navigation. Use diff-style visual language throughout: red for removed/before, green for added/after, yellow for modified, blue for neutral context. Write to `~/.agent/diagrams/` and open in browser.
|
||||||
|
|
||||||
|
Ultrathink.
|
||||||
|
|
||||||
|
$@
|
||||||
63
.agents/skills/visual-explainer/commands/fact-check.md
Normal file
63
.agents/skills/visual-explainer/commands/fact-check.md
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
---
|
||||||
|
description: Verify the factual accuracy of a document against the actual codebase, correct inaccuracies in place
|
||||||
|
---
|
||||||
|
Load the visual-explainer skill, then verify the factual accuracy of a document that makes claims about a codebase. Read the file, extract every verifiable claim, check each against the actual code and git history, correct inaccuracies in place, and add a verification summary.
|
||||||
|
|
||||||
|
For HTML files: read `./references/css-patterns.md` to match the existing page's styling when inserting the verification summary.
|
||||||
|
|
||||||
|
**Target file** — determine what to verify from `$1`:
|
||||||
|
- Explicit path: verify that specific file (`.html`, `.md`, or any text document)
|
||||||
|
- No argument: verify the most recently modified `.html` file in `~/.agent/diagrams/` (`ls -t ~/.agent/diagrams/*.html | head -1`)
|
||||||
|
|
||||||
|
Auto-detect the document type and adjust the verification strategy:
|
||||||
|
- **HTML review pages** (diff-review, plan-review, project-recap): detect from page content, verify against the git ref or plan file the review was based on
|
||||||
|
- **Plan/spec documents** (markdown): verify file references, function/type names, behavior descriptions, and architecture claims against the current codebase
|
||||||
|
- **Any other document**: extract and verify whatever factual claims about code it contains
|
||||||
|
|
||||||
|
**Phase 1: Extract claims.** Read the file. Extract every verifiable factual claim:
|
||||||
|
- **Quantitative**: line counts, file counts, function counts, module counts, test counts, any numeric metrics
|
||||||
|
- **Naming**: function names, type names, module names, file paths referenced in the document
|
||||||
|
- **Behavioral**: descriptions of what code does, how things work, before/after comparisons
|
||||||
|
- **Structural**: architecture claims, dependency relationships, import chains, module boundaries
|
||||||
|
- **Temporal**: git history claims, commit attributions, timeline entries
|
||||||
|
|
||||||
|
Skip subjective analysis (opinions, design judgments, readability assessments) — these aren't verifiable facts.
|
||||||
|
|
||||||
|
**Phase 2: Verify against source.** For each extracted claim, go to the source:
|
||||||
|
- Re-read every file referenced in the document — check function signatures, type definitions, behavior descriptions against the actual code
|
||||||
|
- For claims about git history: re-run git commands (`git diff --stat`, `git log`, `git diff --name-status`, etc.) and compare output against the document's numbers
|
||||||
|
- For diff-reviews: read both the ref version (`git show <ref>:file`) and working tree version to verify before/after claims aren't swapped or fabricated
|
||||||
|
- For plan docs: verify that files, functions, and types the plan references actually exist and behave as described
|
||||||
|
- For project-recaps: re-run `git log` commands to verify activity narrative and timeline
|
||||||
|
|
||||||
|
Classify each claim:
|
||||||
|
- **Confirmed**: claim matches the code/output exactly
|
||||||
|
- **Corrected**: claim was inaccurate — note what was wrong and what the correct value is
|
||||||
|
- **Unverifiable**: claim can't be checked (e.g., references a file that doesn't exist, or a behavior that requires runtime testing)
|
||||||
|
|
||||||
|
**Phase 3: Correct in place.** Edit the file directly using surgical text replacements:
|
||||||
|
- Fix incorrect numbers, function names, file paths, behavior descriptions
|
||||||
|
- Fix before/after swaps (a common error class in review pages)
|
||||||
|
- If a section is fundamentally wrong (not just a detail error), rewrite that section's content while preserving the surrounding structure
|
||||||
|
- For HTML: preserve layout, CSS, animations, Mermaid diagrams (unless they contain factual errors in node labels or edge descriptions)
|
||||||
|
- For markdown: preserve heading structure, formatting, and document organization
|
||||||
|
|
||||||
|
**Phase 4: Add verification summary.**
|
||||||
|
- **HTML files**: insert a verification section as a banner at the top or final section, matching the page's existing styling. Use a subtle card with muted colors.
|
||||||
|
- **Markdown files**: append a `## Verification Summary` section at the end of the document.
|
||||||
|
|
||||||
|
Include in the summary:
|
||||||
|
- Total claims checked
|
||||||
|
- Claims confirmed (with count)
|
||||||
|
- Corrections made (with brief list of what was fixed: "Changed `processCleanup` to `runCleanup` to match actual function name in `worker.ts:45`")
|
||||||
|
- Unverifiable claims flagged (if any)
|
||||||
|
|
||||||
|
**Phase 5: Report.** Tell the user what was checked, what was corrected, and open the file (HTML in browser, markdown path in chat). If nothing needed correction, say so — the verification still has value as confirmation.
|
||||||
|
|
||||||
|
This is not a re-review. It does not second-guess analysis, opinions, or design judgments. It does not change the document's structure or organization. It is a fact-checker — it verifies that the data presented matches reality, corrects what doesn't, and leaves everything else alone.
|
||||||
|
|
||||||
|
Write corrections to the original file.
|
||||||
|
|
||||||
|
Ultrathink.
|
||||||
|
|
||||||
|
$@
|
||||||
18
.agents/skills/visual-explainer/commands/generate-slides.md
Normal file
18
.agents/skills/visual-explainer/commands/generate-slides.md
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
---
|
||||||
|
description: Generate a stunning magazine-quality slide deck as a self-contained HTML page
|
||||||
|
---
|
||||||
|
Load the visual-explainer skill, then generate a slide deck for: $@
|
||||||
|
|
||||||
|
Follow the visual-explainer skill workflow. Read the reference template at `./templates/slide-deck.html` and slide patterns at `./references/slide-patterns.md` before generating. Also read `./references/css-patterns.md` for shared patterns (Mermaid zoom controls, depth tiers, overflow protection) and `./references/libraries.md` for Mermaid theming, Chart.js, and font pairings.
|
||||||
|
|
||||||
|
**Slide output is always opt-in.** Only generate slides when this command is invoked or the user explicitly asks for a slide deck.
|
||||||
|
|
||||||
|
**Aesthetic:** Pick a distinctive direction from the 4 slide presets in slide-patterns.md (Midnight Editorial, Warm Signal, Terminal Mono, Swiss Clean) or riff on the existing 8 aesthetic directions adapted for slides. Vary from previous decks. Commit to one direction and carry it through every slide.
|
||||||
|
|
||||||
|
**Narrative structure:** Slides have a temporal dimension — compose a story arc, not a list of sections. Start with impact (title), build context (overview), deep dive (content, diagrams, data), resolve (summary/next steps). Plan the slide sequence and assign a composition (centered, left-heavy, split, full-bleed) to each slide before writing HTML.
|
||||||
|
|
||||||
|
**Visual richness:** Proactively reach for visuals. If `surf` CLI is available (`which surf`), generate images for title slide backgrounds and full-bleed slides via `surf gemini --generate-image`. Add SVG decorative accents, inline sparklines, mini-charts, and small Mermaid diagrams where they make the story more compelling. Visual-first, text-second.
|
||||||
|
|
||||||
|
**Compositional variety:** Consecutive slides must vary their spatial approach. Alternate between centered, left-heavy, right-heavy, split, edge-aligned, and full-bleed. Three centered slides in a row means push one off-axis.
|
||||||
|
|
||||||
|
Write to `~/.agent/diagrams/` and open the result in the browser.
|
||||||
107
.agents/skills/visual-explainer/commands/generate-visual-plan.md
Normal file
107
.agents/skills/visual-explainer/commands/generate-visual-plan.md
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
---
|
||||||
|
description: Generate a visual HTML implementation plan — detailed feature specification with state machines, code snippets, and edge cases
|
||||||
|
---
|
||||||
|
Load the visual-explainer skill, then generate a comprehensive visual implementation plan for `$@` as a self-contained HTML page.
|
||||||
|
|
||||||
|
Follow the visual-explainer skill workflow. Read the reference template, CSS patterns, and mermaid theming references before generating. Use an editorial or blueprint aesthetic, but vary fonts and palette from previous diagrams.
|
||||||
|
|
||||||
|
**Data gathering phase** — understand the context before designing:
|
||||||
|
|
||||||
|
1. **Parse the feature request.** Extract:
|
||||||
|
- The core problem being solved
|
||||||
|
- Desired user-facing behavior
|
||||||
|
- Any constraints or requirements mentioned
|
||||||
|
- Scope boundaries (what's explicitly out of scope)
|
||||||
|
|
||||||
|
2. **Read the relevant codebase.** Identify:
|
||||||
|
- Files that will need modification
|
||||||
|
- Existing patterns to follow (code style, architecture, naming conventions)
|
||||||
|
- Related functionality that the feature should integrate with
|
||||||
|
- Types, interfaces, and APIs the feature must conform to
|
||||||
|
|
||||||
|
3. **Understand the extension points.** Look for:
|
||||||
|
- Hook points, event systems, or plugin architectures
|
||||||
|
- Configuration options or flags
|
||||||
|
- Public APIs that might need extension
|
||||||
|
- Test patterns used in the codebase
|
||||||
|
|
||||||
|
4. **Check for prior art.** Search for:
|
||||||
|
- Similar features already implemented
|
||||||
|
- Related issues or discussions
|
||||||
|
- Existing code that can be reused or extended
|
||||||
|
|
||||||
|
**Design phase** — work through the implementation before writing HTML:
|
||||||
|
|
||||||
|
1. **State design.** What new state variables are needed? What existing state is affected? Draw the state machine if behavior has multiple modes.
|
||||||
|
|
||||||
|
2. **API design.** What commands, functions, or endpoints are added? What are the signatures? What are the error cases?
|
||||||
|
|
||||||
|
3. **Integration design.** How does this feature interact with existing functionality? What hooks or events are involved?
|
||||||
|
|
||||||
|
4. **Edge cases.** Walk through unusual scenarios: concurrent operations, error conditions, boundary values, user mistakes.
|
||||||
|
|
||||||
|
**Verification checkpoint** — before generating HTML, produce a structured fact sheet:
|
||||||
|
- Every state variable (new and modified) with its type and purpose
|
||||||
|
- Every function/command/API with its signature
|
||||||
|
- Every file that needs modification with the specific changes
|
||||||
|
- Every edge case with expected behavior
|
||||||
|
- Every assumption about the codebase that the plan relies on
|
||||||
|
Verify each against the code. If something cannot be verified, mark it as uncertain. This fact sheet is your source of truth during HTML generation.
|
||||||
|
|
||||||
|
**Diagram structure** — the page should include:
|
||||||
|
|
||||||
|
1. **Header** — feature name, one-line description, scope summary. *Visual treatment: use a distinctive header with monospace label ("Feature Plan", "Implementation Spec", etc.), large italic title, and muted subtitle. Set the tone for the page.*
|
||||||
|
|
||||||
|
2. **The Problem** — side-by-side comparison panels showing current behavior vs. desired behavior. Use concrete examples, not abstract descriptions. Show what the user experiences or what the code does, step by step. *Visual treatment: two-column grid with rose-tinted "Before" header and sage-tinted "After" header. Numbered flow steps with arrows between them.*
|
||||||
|
|
||||||
|
3. **State Machine** — Mermaid flowchart or stateDiagram showing the states and transitions. Label edges with the triggers (commands, events, conditions). *Wrap in `.mermaid-wrap` with zoom controls (+/−/reset/expand) and click-to-expand. Use `flowchart TD` instead of `stateDiagram-v2` if labels need special characters like colons or parentheses. Add explanatory caption below the diagram.*
|
||||||
|
|
||||||
|
4. **State Variables** — card grid showing new state and existing state (if modified). Use code blocks with proper `white-space: pre-wrap`. *Visual treatment: two cards side-by-side, elevated depth, monospace labels.*
|
||||||
|
|
||||||
|
5. **Modified Functions** — for each function that needs changes, show:
|
||||||
|
- Function name and file path
|
||||||
|
- Key code snippet (not full implementation — 10-20 lines showing the pattern)
|
||||||
|
- Explanation of what changed and why
|
||||||
|
*Visual treatment: file path as monospace dim text above code block, code in recessed card with accent-dim background.*
|
||||||
|
|
||||||
|
6. **Commands / API** — table with command/function name, parameters, and behavior description. Use `<code>` for technical names. *Visual treatment: bordered table with sticky header, alternating row backgrounds.*
|
||||||
|
|
||||||
|
7. **Edge Cases** — table listing scenarios and expected behaviors. Be thorough — include error conditions, concurrent operations, boundary values. *Visual treatment: same table style as Commands section.*
|
||||||
|
|
||||||
|
8. **Test Requirements** — table or card grid showing test categories and specific tests to add. Group by: unit tests, integration tests, edge case tests. *Visual treatment: compact table with file references.*
|
||||||
|
|
||||||
|
9. **File References** — table mapping files to the changes needed. Include file paths and brief descriptions. *Visual treatment: compact reference table, can use `<details>` if many files.*
|
||||||
|
|
||||||
|
10. **Implementation Notes** — callout boxes for:
|
||||||
|
- Backward compatibility considerations (gold border)
|
||||||
|
- Critical implementation warnings (rose border)
|
||||||
|
- Performance considerations if relevant (amber border)
|
||||||
|
*Visual treatment: callout boxes with colored left borders, strong labels.*
|
||||||
|
|
||||||
|
**Visual hierarchy:**
|
||||||
|
- Sections 1-3 should dominate the viewport on load (hero depth for header, elevated for problem comparison and state machine)
|
||||||
|
- Sections 4-6 are core implementation details (elevated cards, readable code blocks)
|
||||||
|
- Sections 7-10 are reference material (flat or recessed depth, compact layout)
|
||||||
|
|
||||||
|
**Typography and color:**
|
||||||
|
- Pick a distinctive font pairing (not Inter/Roboto)
|
||||||
|
- Use semantic accent colors: gold for primary accents, sage for "after"/success states, rose for "before"/warning states
|
||||||
|
- Both light and dark themes must work
|
||||||
|
|
||||||
|
**Optional hero image** — if `surf` CLI is available (`which surf`), consider generating a conceptual illustration that captures the feature's essence. Use for abstract concepts that benefit from visual metaphor. Skip for purely structural changes. Embed as base64 data URI using the `.hero-img-wrap` pattern from css-patterns.md.
|
||||||
|
|
||||||
|
**Code block requirements:**
|
||||||
|
- Always use `white-space: pre-wrap` and `word-break: break-word`
|
||||||
|
- Include file path headers where relevant
|
||||||
|
- Use syntax-appropriate highlighting via CSS classes if desired
|
||||||
|
- Keep snippets focused — show the pattern, not the full implementation
|
||||||
|
|
||||||
|
**Overflow prevention:**
|
||||||
|
- Apply `min-width: 0` on all grid/flex children
|
||||||
|
- Use `overflow-wrap: break-word` on all text containers
|
||||||
|
- Never use `display: flex` on `<li>` for markers — use absolute positioning
|
||||||
|
- Test tables with wide content don't overflow their container
|
||||||
|
|
||||||
|
Write to `~/.agent/diagrams/` with a descriptive filename (e.g., `feature-name-plan.html`). Open the result in the browser. Tell the user the file path.
|
||||||
|
|
||||||
|
Ultrathink.
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
description: Generate a beautiful standalone HTML diagram and open it in the browser
|
||||||
|
---
|
||||||
|
Load the visual-explainer skill, then generate an HTML diagram for: $@
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Write to `~/.agent/diagrams/` and open the result in the browser.
|
||||||
86
.agents/skills/visual-explainer/commands/plan-review.md
Normal file
86
.agents/skills/visual-explainer/commands/plan-review.md
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
---
|
||||||
|
description: Generate a visual HTML plan review — current codebase state vs. proposed implementation plan
|
||||||
|
---
|
||||||
|
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.
|
||||||
|
|
||||||
|
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:**
|
||||||
|
- Plan file: `$1` (path to a markdown plan, spec, or RFC document)
|
||||||
|
- Codebase: `$2` if provided, otherwise the current working directory
|
||||||
|
|
||||||
|
**Data gathering phase** — read and cross-reference these before generating:
|
||||||
|
|
||||||
|
1. **Read the plan file in full.** Extract:
|
||||||
|
- The problem statement and motivation
|
||||||
|
- Each proposed change (files to modify, new files, deletions)
|
||||||
|
- Rejected alternatives and their reasoning
|
||||||
|
- Any explicit scope boundaries or non-goals
|
||||||
|
|
||||||
|
2. **Read every file the plan references.** For each file mentioned in the plan, read the current version in full. Also read files that import or depend on those files — the plan may not mention all ripple effects.
|
||||||
|
|
||||||
|
3. **Map the blast radius.** From the codebase, identify:
|
||||||
|
- What imports/requires the files being changed (grep for import paths)
|
||||||
|
- What tests exist for the affected files (look for corresponding `.test.*` / `.spec.*` files)
|
||||||
|
- Config files, types, or schemas that might need updates
|
||||||
|
- Public API surface that callers depend on
|
||||||
|
|
||||||
|
4. **Cross-reference plan vs. code.** For each change the plan proposes, verify:
|
||||||
|
- Does the file/function/type the plan references actually exist in the current code?
|
||||||
|
- Does the plan's description of current behavior match what the code actually does?
|
||||||
|
- Are there implicit assumptions about code structure that don't hold?
|
||||||
|
|
||||||
|
**Verification checkpoint** — before generating HTML, produce a structured fact sheet of every claim you will present in the review:
|
||||||
|
- Every quantitative figure: file counts, estimated lines, function counts, test counts
|
||||||
|
- Every function, type, and module name you will reference from both the plan and the codebase
|
||||||
|
- Every behavior description: what the code currently does vs. what the plan proposes
|
||||||
|
- For each, cite the source: the plan section or the file:line where you read it
|
||||||
|
Verify each claim against the code and the plan. If something cannot be verified, mark it as uncertain rather than stating it as fact. This fact sheet is your source of truth during HTML generation — do not deviate from it.
|
||||||
|
|
||||||
|
**Diagram structure** — the page should include:
|
||||||
|
|
||||||
|
1. **Plan summary** — lead with the *intuition*: what problem does this plan solve, and what's the core insight behind the approach? Then the scope: how many files touched, estimated scale of changes, new modules or tests planned. A reader who only sees this section should understand the plan's essence. *Visual treatment: this is the visual anchor — use hero depth (larger type 20-24px, subtle accent-tinted background, more padding than other sections).*
|
||||||
|
|
||||||
|
2. **Impact dashboard** — files to modify, files to create, files to delete, estimated lines added/removed, new test files planned, dependencies affected. Include a **completeness** indicator: whether the plan covers tests (green/red), docs updates (green/yellow/red), and migration/rollback (green/grey for N/A).
|
||||||
|
|
||||||
|
3. **Current architecture** — Mermaid diagram of how the affected subsystem works *today*. Focus only on the parts the plan touches — don't diagram the entire codebase. Show the data flow, dependencies, and call paths that will change. Wrap in `.mermaid-wrap` with zoom controls (+/−/reset/expand buttons), Ctrl/Cmd+scroll zoom, click-and-drag panning, and click-to-expand (opens diagram full-size in new tab). See css-patterns.md "Mermaid Zoom Controls" for the full pattern including the `openMermaidInNewTab()` function. *Visual treatment: use matching Mermaid layout direction and node names as section 4 so the visual diff is obvious.*
|
||||||
|
|
||||||
|
4. **Planned architecture** — Mermaid diagram of how the subsystem will work *after* the plan is implemented. Use the same node names and layout direction as the current architecture diagram so the differences are visually obvious. Same zoom controls and click-to-expand as section 3. *Highlight new nodes with a glow or accent border, removed nodes with strikethrough or reduced opacity, changed edges with a different stroke color.*
|
||||||
|
|
||||||
|
5. **Change-by-change breakdown** — for each change in the plan, a side-by-side panel. Overflow prevention: apply `min-width: 0` on all grid/flex children and `overflow-wrap: break-word` on panels. Never use `display: flex` on `<li>` for marker characters — use absolute positioning instead (see css-patterns.md Overflow Protection).
|
||||||
|
- **Left (current):** what the code does now, with relevant snippets or function signatures
|
||||||
|
- **Right (planned):** what the plan proposes, with the plan's own code examples if provided
|
||||||
|
- **Rationale:** below each side-by-side panel, extract _why_ the plan chose this approach. Pull from the plan's reasoning, rejected alternatives section, or inline justifications. If the plan includes a "rejected alternatives" section, map those rejections to the specific changes they apply to. Flag changes where the plan says _what_ to do but not _why_ — these are pre-implementation cognitive debt.
|
||||||
|
- Flag any discrepancies where the plan's description of current behavior doesn't match the actual code
|
||||||
|
|
||||||
|
6. **Dependency & ripple analysis** — *visual treatment: compact — consider `<details>` collapsed by default for pages with many sections.* What other code depends on the files being changed. Table or Mermaid graph showing callers, importers, and downstream effects the plan may not explicitly address. Color-code: covered by plan (green), not mentioned but likely affected (amber), definitely missed (red).
|
||||||
|
|
||||||
|
7. **Risk assessment** — styled cards for:
|
||||||
|
- **Edge cases** the plan doesn't address
|
||||||
|
- **Assumptions** the plan makes about the codebase that should be verified
|
||||||
|
- **Ordering risks** if changes need to be applied in a specific sequence
|
||||||
|
- **Rollback complexity** if things go wrong
|
||||||
|
- **Cognitive complexity** — areas where the plan introduces non-obvious coupling, action-at-a-distance behavior, implicit ordering requirements, or contracts that exist only in the developer's memory. Distinct from bug risk — these are "you'll forget how this works in a month" risks. Each cognitive complexity flag gets a brief mitigation suggestion (e.g., "add a comment explaining the ordering requirement" or "consider a runtime assertion that validates the invariant"). Note: cognitive complexity flags belong here when they're about specific code patterns; broader concerns about the plan's overall approach (overengineering, lock-in, maintenance burden) belong in section 8's Ugly category.
|
||||||
|
- Each risk gets a severity indicator (low/medium/high)
|
||||||
|
|
||||||
|
8. **Plan review** — structured Good/Bad/Ugly analysis of the plan itself:
|
||||||
|
- **Good**: Solid design decisions, things the plan gets right, well-reasoned tradeoffs
|
||||||
|
- **Bad**: Gaps in the plan — missing files, unaddressed edge cases, incorrect assumptions about current code
|
||||||
|
- **Ugly**: Subtle concerns — complexity being introduced, maintenance burden, things that will work initially but cause problems at scale
|
||||||
|
- **Questions**: Ambiguities that need the plan author's clarification before implementation begins
|
||||||
|
- Use styled cards with green/red/amber/blue left-border accents. Each item should reference specific plan sections and code files. If nothing to flag in a category, say "None found" rather than omitting the section.
|
||||||
|
9. **Understanding gaps** — a closing dashboard that rolls up decision-rationale gaps from section 5 and cognitive complexity flags from section 7:
|
||||||
|
- Count of changes with clear rationale vs. missing rationale (visual bar chart or progress indicator)
|
||||||
|
- List of cognitive complexity flags with severity
|
||||||
|
- Explicit recommendations: "Before implementing, document the rationale for changes X and Y — the plan doesn't explain why these approaches were chosen over alternatives"
|
||||||
|
- This section makes cognitive debt visible _before_ the work starts, when it's cheapest to address.
|
||||||
|
|
||||||
|
**Visual hierarchy**: Sections 1-4 should dominate the viewport on load (hero depth for summary, elevated for architecture diagrams). Sections 6+ are reference material and should feel lighter (flat or recessed depth, compact layout, collapsible where appropriate).
|
||||||
|
|
||||||
|
**Optional illustrations** — if `surf` CLI is available (`which surf`), consider generating a conceptual illustration of the planned system via `surf gemini --generate-image` when it would help the reader visualize the change. Embed as base64 data URI. See css-patterns.md "Generated Images" for container styles. Skip if surf isn't available or the plan is purely structural.
|
||||||
|
|
||||||
|
Include responsive section navigation. Use a current-vs-planned visual language throughout: blue/neutral for current state, green/purple for planned additions, amber for areas of concern, red for gaps or risks. Write to `~/.agent/diagrams/` and open in browser.
|
||||||
|
|
||||||
|
Ultrathink.
|
||||||
|
|
||||||
|
$@
|
||||||
61
.agents/skills/visual-explainer/commands/project-recap.md
Normal file
61
.agents/skills/visual-explainer/commands/project-recap.md
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
---
|
||||||
|
description: Generate a visual HTML project recap — rebuild mental model of a project's current state, recent decisions, and cognitive debt hotspots
|
||||||
|
---
|
||||||
|
Load the visual-explainer skill, then generate a comprehensive visual project recap as a self-contained HTML page.
|
||||||
|
|
||||||
|
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`:
|
||||||
|
- Shorthand like `2w`, `30d`, `3m`: parse to git's `--since` format (`2w` → `"2 weeks ago"`, `30d` → `"30 days ago"`, `3m` → `"3 months ago"`)
|
||||||
|
- If `$1` doesn't match a time pattern, treat it as free-form context and use the default window
|
||||||
|
- No argument: default to `2w` (2 weeks)
|
||||||
|
|
||||||
|
**Data gathering phase** — run these first to understand the project:
|
||||||
|
|
||||||
|
1. **Project identity.** Read `README.md`, `CHANGELOG.md`, `package.json` / `Cargo.toml` / `pyproject.toml` / `go.mod` for name, description, version, dependencies. Read the top-level file structure.
|
||||||
|
|
||||||
|
2. **Recent activity.** `git log --oneline --since=<window>` for commit history. `git log --stat --since=<window>` for file-level change scope. `git shortlog -sn --since=<window>` for contributor activity. Identify which areas of the codebase were most active.
|
||||||
|
|
||||||
|
3. **Current state.** Check for uncommitted changes (`git status`). Check for stale branches (`git branch --no-merged`). Look for TODO/FIXME comments in recently changed files. Read progress docs if they exist (`~/.agent/memory/{project}/progress.md`, `~/.pi/agent/memory/{project}/progress.md`, `.pi/todos/`, or similar).
|
||||||
|
|
||||||
|
4. **Decision context.** Read recent commit messages for rationale. If running in the same session as recent work, mine the conversation history. Read any plan docs, RFCs, or ADRs in the project directory.
|
||||||
|
|
||||||
|
5. **Architecture scan.** Read key source files to understand the module structure and dependencies. Focus on entry points, public API surface, and the files most frequently changed in the time window.
|
||||||
|
|
||||||
|
**Verification checkpoint** — before generating HTML, produce a structured fact sheet of every claim you will present in the recap:
|
||||||
|
- Every quantitative figure: commit counts, file counts, line counts, branch counts
|
||||||
|
- Every module, function, and type name you will reference
|
||||||
|
- Every behavior and architecture description
|
||||||
|
- For each, cite the source: the git command output that produced it, or the file:line where you read it
|
||||||
|
Verify each claim against the code. If something cannot be verified, mark it as uncertain rather than stating it as fact. This fact sheet is your source of truth during HTML generation — do not deviate from it.
|
||||||
|
|
||||||
|
**Optional hero image** — if `surf` CLI is available (`which surf`), generate a hero banner via `surf gemini --generate-image --aspect-ratio 16:9` that visually captures the project's identity or domain. Match the style to the page's palette. Embed as base64 data URI using the `.hero-img-wrap` pattern from css-patterns.md. Place above or just below the title. Skip if surf isn't available — the page should stand on its own.
|
||||||
|
|
||||||
|
**Diagram structure** — the page should include:
|
||||||
|
1. **Project identity** — not the README blurb. A *current-state* summary: what this project does, who uses it, what stage it's at (early dev, stable, actively shipping features). Include version, key dependencies, and the one-sentence "elevator pitch" for someone who forgot what they were building.
|
||||||
|
2. **Architecture snapshot** — Mermaid diagram of the system as it exists today. Focus on the conceptual modules and their relationships, not every file. Label nodes with what they do, not just file names. Wrap in `.mermaid-wrap` with zoom controls (+/−/reset/expand buttons), Ctrl/Cmd+scroll zoom, click-and-drag panning, and click-to-expand (opens diagram full-size in new tab). See css-patterns.md "Mermaid Zoom Controls" for the full pattern including the `openMermaidInNewTab()` function. *Visual treatment: this is the visual anchor — use hero depth (elevated container, larger padding, subtle accent-tinted background). The rest of the page hangs off this diagram.*
|
||||||
|
3. **Recent activity** — not raw git log. A human-readable narrative grouped by theme: feature work, bug fixes, refactors, infrastructure. Timeline visualization with the most significant changes called out. For each theme, a one-sentence summary of what happened and why it mattered.
|
||||||
|
4. **Decision log** — key design decisions from the time window. Extracted from commit messages, conversation history, plan docs, progress docs. Each entry: what was decided, why, what was considered. This is the highest-value section for fighting cognitive debt — the reasoning that evaporates first.
|
||||||
|
5. **State of things** — *visual treatment: use the KPI card pattern from css-patterns.md — large hero numbers for working/broken/blocked/in-progress counts, with color-coded trend indicators.* A dashboard of:
|
||||||
|
- What's working (stable, shipped, tested)
|
||||||
|
- What's in progress (uncommitted work, open branches, active TODOs)
|
||||||
|
- What's broken or degraded (known bugs, failing tests, tech debt items)
|
||||||
|
- What's blocked (waiting on external input, dependencies, decisions)
|
||||||
|
6. **Mental model essentials** — the 5-10 things you need to hold in your head to work on this project effectively:
|
||||||
|
- Key invariants and contracts (what must always be true)
|
||||||
|
- Non-obvious coupling (things connected in ways you wouldn't guess from the file tree)
|
||||||
|
- Gotchas (common mistakes, easy-to-forget requirements, things that break silently)
|
||||||
|
- Naming conventions or patterns the codebase follows
|
||||||
|
7. **Cognitive debt hotspots** — *visual treatment: use amber-tinted cards with severity indicators (colored left border: red for high, amber for medium, blue for low).* Areas where understanding is weakest:
|
||||||
|
- Code that changed recently but has no documented rationale
|
||||||
|
- Complex modules with no tests
|
||||||
|
- Areas where multiple people (or agents) made overlapping changes
|
||||||
|
- Files that are frequently modified but poorly understood
|
||||||
|
- Flag each with a severity and a concrete suggestion (e.g., "add a doc comment to `buildCoordinationInstructions` explaining the 4 coordination levels — this function is called from 3 places and the behavior is non-obvious")
|
||||||
|
8. **Next steps** — inferred from recent activity, open TODOs, project trajectory. Not prescriptive — just "here's where the momentum was pointing when you left." Include any explicit next-step notes from progress docs or plan files.
|
||||||
|
|
||||||
|
Include responsive section navigation. Use a warm, approachable visual language: muted blues and greens for architecture, amber callouts for cognitive debt hotspots, green/blue/amber/red for state-of-things status. Overflow prevention on any side-by-side or grid-based sections: apply `min-width: 0` on all grid/flex children and `overflow-wrap: break-word`. Never use `display: flex` on `<li>` for marker characters — use absolute positioning instead (see css-patterns.md Overflow Protection). Write to `~/.agent/diagrams/` and open in browser.
|
||||||
|
|
||||||
|
Ultrathink.
|
||||||
|
|
||||||
|
$@
|
||||||
68
.agents/skills/visual-explainer/commands/share-page.md
Normal file
68
.agents/skills/visual-explainer/commands/share-page.md
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
---
|
||||||
|
description: Deploy a generated visual-explainer HTML page and return a live Vercel URL
|
||||||
|
---
|
||||||
|
# Share Visual Explainer Page
|
||||||
|
|
||||||
|
Share a visual explainer HTML file instantly via Vercel. Returns a live URL with no authentication required when a Pi-compatible `vercel-deploy` skill is installed.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/share-page <file-path>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Arguments:**
|
||||||
|
- `file-path` - Path to the HTML file to share (required)
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
```
|
||||||
|
/share-page ~/.agent/diagrams/my-diagram.html
|
||||||
|
/share-page /tmp/visual-explainer-output.html
|
||||||
|
```
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. Finds the `visual-explainer` skill directory for the current harness
|
||||||
|
2. Copies your HTML file to a temp directory as `index.html`
|
||||||
|
3. Deploys via the Pi-compatible `vercel-deploy` skill
|
||||||
|
4. Returns a live URL immediately
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- **vercel-deploy skill** - Required for deployment. In Pi, install with: `pi install npm:vercel-deploy`
|
||||||
|
|
||||||
|
No Vercel account, Cloudflare account, or API keys needed. The deployment is "claimable" — you can transfer it to your Vercel account later if you want.
|
||||||
|
|
||||||
|
## Script Location
|
||||||
|
|
||||||
|
Resolve the script from the installed skill directory, then run it with the HTML file path:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash ~/.pi/agent/skills/visual-explainer/scripts/share.sh <file>
|
||||||
|
```
|
||||||
|
|
||||||
|
If the skill is installed somewhere else, use that install path instead. Common locations include `~/.codex/skills/visual-explainer/scripts/share.sh`, `~/.config/opencode/skill/visual-explainer/scripts/share.sh`, or `./plugins/visual-explainer/scripts/share.sh` from a repository checkout.
|
||||||
|
|
||||||
|
The script currently looks for the Pi-compatible `vercel-deploy` script in the standard Pi skill locations. Other harnesses can generate and open HTML normally, but sharing requires that dependency to be available in a compatible location.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
```
|
||||||
|
Sharing my-diagram.html...
|
||||||
|
|
||||||
|
✓ Shared successfully!
|
||||||
|
|
||||||
|
Live URL: https://skill-deploy-abc123.vercel.app
|
||||||
|
Claim URL: https://vercel.com/claim-deployment?code=...
|
||||||
|
```
|
||||||
|
|
||||||
|
The script also outputs JSON for programmatic use:
|
||||||
|
```json
|
||||||
|
{"previewUrl":"https://...","claimUrl":"https://...","deploymentId":"...","projectId":"..."}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Deployments are **public** — anyone with the URL can view
|
||||||
|
- Preview deployments have a configurable retention period (default: 30 days)
|
||||||
|
- Each share creates a new deployment with a unique URL
|
||||||
1813
.agents/skills/visual-explainer/references/css-patterns.md
Normal file
1813
.agents/skills/visual-explainer/references/css-patterns.md
Normal file
File diff suppressed because it is too large
Load diff
612
.agents/skills/visual-explainer/references/libraries.md
Normal file
612
.agents/skills/visual-explainer/references/libraries.md
Normal file
|
|
@ -0,0 +1,612 @@
|
||||||
|
# External Libraries (CDN)
|
||||||
|
|
||||||
|
Optional CDN libraries for cases where pure CSS/HTML isn't enough. Only include what the diagram actually needs — most diagrams need zero external JS.
|
||||||
|
|
||||||
|
## Mermaid.js — Diagramming Engine
|
||||||
|
|
||||||
|
Use for flowcharts, sequence diagrams, ER diagrams, state machines, mind maps, class diagrams, and any diagram where automatic node positioning and edge routing saves effort. Mermaid handles layout — you handle theming.
|
||||||
|
|
||||||
|
Do NOT use for dashboards — CSS Grid card layouts with Chart.js look better for those. Data tables use `<table>` elements.
|
||||||
|
|
||||||
|
**CDN:**
|
||||||
|
```html
|
||||||
|
<script type="module">
|
||||||
|
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
||||||
|
|
||||||
|
mermaid.initialize({ startOnLoad: true, /* ... */ });
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
**With ELK layout** (required for `layout: 'elk'` — it's a separate package, not bundled in core):
|
||||||
|
```html
|
||||||
|
<script type="module">
|
||||||
|
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
||||||
|
import elkLayouts from 'https://cdn.jsdelivr.net/npm/@mermaid-js/layout-elk/dist/mermaid-layout-elk.esm.min.mjs';
|
||||||
|
|
||||||
|
mermaid.registerLayoutLoaders(elkLayouts);
|
||||||
|
mermaid.initialize({ startOnLoad: true, layout: 'elk', /* ... */ });
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Without the ELK import and registration, `layout: 'elk'` silently falls back to dagre. Only import ELK when you actually need it — it adds significant bundle weight. Most simple diagrams render fine with dagre.
|
||||||
|
|
||||||
|
### Deep Theming
|
||||||
|
|
||||||
|
Always use `theme: 'base'` — it's the only theme where all `themeVariables` are fully customizable. The built-in themes (`default`, `dark`, `forest`, `neutral`) ignore most variable overrides.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script type="module">
|
||||||
|
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
||||||
|
|
||||||
|
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
mermaid.initialize({
|
||||||
|
startOnLoad: true,
|
||||||
|
theme: 'base',
|
||||||
|
look: 'classic',
|
||||||
|
themeVariables: {
|
||||||
|
// Background and surfaces — teal/slate palette (not violet/indigo!)
|
||||||
|
primaryColor: isDark ? '#134e4a' : '#ccfbf1',
|
||||||
|
primaryBorderColor: isDark ? '#14b8a6' : '#0d9488',
|
||||||
|
primaryTextColor: isDark ? '#f0fdfa' : '#134e4a',
|
||||||
|
secondaryColor: isDark ? '#1e293b' : '#f0fdf4',
|
||||||
|
secondaryBorderColor: isDark ? '#059669' : '#16a34a',
|
||||||
|
secondaryTextColor: isDark ? '#f1f5f9' : '#1e293b',
|
||||||
|
tertiaryColor: isDark ? '#27201a' : '#fef3c7',
|
||||||
|
tertiaryBorderColor: isDark ? '#d97706' : '#f59e0b',
|
||||||
|
tertiaryTextColor: isDark ? '#fef3c7' : '#27201a',
|
||||||
|
// Lines and edges
|
||||||
|
lineColor: isDark ? '#64748b' : '#94a3b8',
|
||||||
|
// Text
|
||||||
|
fontSize: '16px',
|
||||||
|
fontFamily: 'var(--font-body)',
|
||||||
|
// Notes and labels
|
||||||
|
noteBkgColor: isDark ? '#1e293b' : '#fefce8',
|
||||||
|
noteTextColor: isDark ? '#f1f5f9' : '#1e293b',
|
||||||
|
noteBorderColor: isDark ? '#fbbf24' : '#d97706',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
**FORBIDDEN in Mermaid themeVariables:** `#8b5cf6`, `#7c3aed`, `#a78bfa` (indigo/violet), `#d946ef` (fuchsia). Use teal, slate, amber, emerald, or colors from your page's palette.
|
||||||
|
|
||||||
|
### CSS Overrides on Mermaid SVG
|
||||||
|
|
||||||
|
Mermaid renders SVG. Override its classes for pixel-perfect control that `themeVariables` can't reach:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Container — see css-patterns.md "Mermaid Zoom Controls" for the full zoom pattern */
|
||||||
|
.mermaid-wrap {
|
||||||
|
position: relative;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CRITICAL: Force node/edge text to follow the page's color scheme.
|
||||||
|
Without this, themeVariables.primaryTextColor works for DEFAULT nodes,
|
||||||
|
but any classDef that sets color: will hardcode a single value that
|
||||||
|
breaks in the opposite color scheme. Fix: never set color: in classDef,
|
||||||
|
and always include these CSS overrides. */
|
||||||
|
.mermaid .nodeLabel { color: var(--text) !important; }
|
||||||
|
.mermaid .edgeLabel { color: var(--text-dim) !important; background-color: var(--bg) !important; }
|
||||||
|
.mermaid .edgeLabel rect { fill: var(--bg) !important; }
|
||||||
|
|
||||||
|
/* Node shapes */
|
||||||
|
.mermaid .node rect,
|
||||||
|
.mermaid .node circle,
|
||||||
|
.mermaid .node polygon {
|
||||||
|
stroke-width: 1.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Edge paths */
|
||||||
|
.mermaid .edge-pattern-solid {
|
||||||
|
stroke-width: 1.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Edge labels — smaller than node labels for visual hierarchy */
|
||||||
|
.mermaid .edgeLabel {
|
||||||
|
font-family: var(--font-mono) !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Node labels — 16px default; drop to 14px for complex diagrams (20+ nodes) */
|
||||||
|
.mermaid .nodeLabel {
|
||||||
|
font-family: var(--font-body) !important;
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sequence diagram actors */
|
||||||
|
.mermaid .actor {
|
||||||
|
stroke-width: 1.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sequence diagram messages */
|
||||||
|
.mermaid .messageText {
|
||||||
|
font-family: var(--font-mono) !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ER diagram entities */
|
||||||
|
.mermaid .er.entityBox {
|
||||||
|
stroke-width: 1.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mind map nodes */
|
||||||
|
.mermaid .mindmap-node rect {
|
||||||
|
stroke-width: 1.5px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### classDef and style Gotchas
|
||||||
|
|
||||||
|
`classDef` values and per-node `style` directives are static text inside `<pre>` — they can't use CSS variables or JS ternaries. Two rules:
|
||||||
|
|
||||||
|
1. **Never set `color:` in classDef or per-node `style` directives.** It hardcodes a text color that breaks in the opposite color scheme. This applies to both `classDef highlight fill:...,color:#2c2a25` and `style I fill:...,color:#2c2a25`. Let the CSS overrides above handle text color via `var(--text)`.
|
||||||
|
|
||||||
|
2. **Use semi-transparent fills (8-digit hex) for node backgrounds.** They layer over whatever Mermaid's base theme background is, producing a tint that works in both light and dark modes. Use `20`–`44` alpha for subtle, `55`–`77` for prominent:
|
||||||
|
|
||||||
|
```
|
||||||
|
classDef highlight fill:#b5761433,stroke:#b57614,stroke-width:2px
|
||||||
|
classDef muted fill:#7c6f6411,stroke:#7c6f6444,stroke-width:1px
|
||||||
|
```
|
||||||
|
|
||||||
|
### Node Label Special Characters
|
||||||
|
|
||||||
|
Mermaid uses certain characters for shape syntax. Node labels containing these characters cause syntax errors unless quoted.
|
||||||
|
|
||||||
|
**Shape characters to watch:**
|
||||||
|
- `[/text/]` — parallelogram
|
||||||
|
- `[\text\]` — trapezoid (alt)
|
||||||
|
- `[/text\]` — trapezoid
|
||||||
|
- `[\text/]` — trapezoid (alt)
|
||||||
|
- `[(text)]` — cylindrical
|
||||||
|
- `[[text]]` — subroutine
|
||||||
|
- `((text))` — circle
|
||||||
|
- `{{text}}` — hexagon
|
||||||
|
|
||||||
|
**If your node label starts with `/`, `\`, `(`, or `{`, wrap it in quotes:**
|
||||||
|
|
||||||
|
```
|
||||||
|
%% WRONG — syntax error (/ starts parallelogram shape)
|
||||||
|
CMD[/gallery command] --> SRV[server]
|
||||||
|
|
||||||
|
%% RIGHT — quotes escape the special character
|
||||||
|
CMD["/gallery command"] --> SRV[server]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Edge labels with special characters also need quotes:**
|
||||||
|
|
||||||
|
```
|
||||||
|
%% WRONG — quotes inside edge label
|
||||||
|
UI -->|"Use as Reference"| RET
|
||||||
|
|
||||||
|
%% RIGHT — use single quotes or escape
|
||||||
|
UI -->|'Use as Reference'| RET
|
||||||
|
UI -->|Use as Reference| RET
|
||||||
|
```
|
||||||
|
|
||||||
|
Avoid opaque light fills like `fill:#fefce8` — they render as bright boxes in dark mode.
|
||||||
|
|
||||||
|
### stateDiagram-v2 Label Limitations
|
||||||
|
|
||||||
|
State diagram transition labels have a strict parser. Avoid:
|
||||||
|
- `<br/>` — only works in flowcharts; causes a parse error in state diagrams
|
||||||
|
- Parentheses in labels — `cancel()` can confuse the parser
|
||||||
|
- Multiple colons — the first `:` is the label delimiter; extra colons in the label text may break parsing
|
||||||
|
|
||||||
|
If you need multi-line labels or special characters, use a `flowchart` instead of `stateDiagram-v2`. Flowcharts support quoted labels (`|"label with: special chars"|`) and `<br/>` for line breaks.
|
||||||
|
|
||||||
|
### Writing Valid Mermaid
|
||||||
|
|
||||||
|
Most Mermaid failures come from a few recurring issues. Follow these rules to avoid invalid diagrams:
|
||||||
|
|
||||||
|
**For multi-line flowchart node labels, use `<br/>` (not `\n`).** Mermaid flowcharts interpret `<br/>` as a line break, but escaped `\n` in labels often renders as literal text:
|
||||||
|
|
||||||
|
```
|
||||||
|
%% WRONG — renders literal "\n" in node text
|
||||||
|
A["Copilot Backend\n/api + /api/voicebot"] --> B["Redis"]
|
||||||
|
|
||||||
|
%% RIGHT — renders on two lines
|
||||||
|
A["Copilot Backend<br/>/api + /api/voicebot"] --> B["Redis"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Quote labels with special characters.** Parentheses, colons, commas, brackets, and ampersands break the parser when unquoted. Wrap any label containing special characters in double quotes:
|
||||||
|
|
||||||
|
```
|
||||||
|
A["handleRequest(ctx)"] --> B["DB: query users"]
|
||||||
|
A[handleRequest] --> B[query users]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Keep IDs simple.** Node IDs should be alphanumeric with no spaces or punctuation. Put the readable name in the label, not the ID:
|
||||||
|
|
||||||
|
```
|
||||||
|
userSvc["User Service"] --> authSvc["Auth Service"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Max 10-12 nodes per Mermaid diagram.** Beyond that, readability collapses even with zoom controls and increased fontSize. For complex architectures (15+ elements), use the **hybrid pattern**: a simple 5-8 node Mermaid overview showing module relationships, followed by CSS Grid cards with detailed function lists. Never cram everything into one diagram. Use `subgraph` blocks to group related nodes when under the limit:
|
||||||
|
|
||||||
|
```
|
||||||
|
subgraph Auth
|
||||||
|
login --> validate --> token
|
||||||
|
end
|
||||||
|
subgraph API
|
||||||
|
gateway --> router --> handler
|
||||||
|
end
|
||||||
|
Auth --> API
|
||||||
|
```
|
||||||
|
|
||||||
|
**Arrow styles for semantic meaning:**
|
||||||
|
|
||||||
|
| Arrow | Meaning | Use for |
|
||||||
|
|-------|---------|---------|
|
||||||
|
| `-->` | Solid | Primary flow |
|
||||||
|
| `-.->` | Dotted | Optional, async, or fallback paths |
|
||||||
|
| `==>` | Thick | Critical or highlighted path |
|
||||||
|
| `--x` | Cross | Rejected or blocked |
|
||||||
|
| `-->\|label\|` | Labeled | Decision branches, data descriptions |
|
||||||
|
|
||||||
|
**Escape pipes in labels.** If a label contains a literal `|`, use `#124;` (HTML entity) or rephrase to avoid it — pipes delimit edge labels in flowcharts.
|
||||||
|
|
||||||
|
**Sequence diagram messages must be plain text.** Unlike flowchart labels, sequence diagram messages (the text after `:`) cannot be quoted or escaped. Curly braces `{}`, square brackets `[]`, angle brackets `<>`, and `&` will silently break the parser and the entire diagram renders as raw text. Write human-readable descriptions, not code:
|
||||||
|
|
||||||
|
```
|
||||||
|
%% WRONG — parser chokes on braces, brackets, ampersand
|
||||||
|
A->>B: web_search({ queries: [...] })
|
||||||
|
B->>B: User removes query 2, keeps 1 & 3
|
||||||
|
B->>S: POST /submit { selected: [0, 2] }
|
||||||
|
|
||||||
|
%% RIGHT — plain English, no special characters
|
||||||
|
A->>B: Call web_search with queries
|
||||||
|
B->>B: User removes query 2, keeps 1 and 3
|
||||||
|
B->>S: POST /submit with selected indices
|
||||||
|
```
|
||||||
|
|
||||||
|
**Don't mix diagram syntax.** Each diagram type has its own syntax. `-->` works in flowcharts but not in sequence diagrams (`->>` instead). `:::className` works in flowcharts but not in ER diagrams. When in doubt, check the examples below for correct syntax per type.
|
||||||
|
|
||||||
|
### Layout Direction: TD vs LR
|
||||||
|
|
||||||
|
`flowchart LR` (left-to-right) spreads horizontally. With many nodes, Mermaid scales everything down to fit the width, making text unreadable. `flowchart TD` (top-down) is almost always better.
|
||||||
|
|
||||||
|
**When to use each:**
|
||||||
|
|
||||||
|
| Direction | Use when | Avoid when |
|
||||||
|
|-----------|----------|------------|
|
||||||
|
| `TD` (top-down) | Complex diagrams, 5+ nodes, hierarchies, architecture | Simple A→B→C linear flows |
|
||||||
|
| `LR` (left-to-right) | Simple linear flows, 3-4 nodes, pipelines | Complex graphs, many branches |
|
||||||
|
|
||||||
|
**Rule of thumb:** If the diagram has more than one row of nodes or any branching, use `TD`. The extra vertical space makes labels readable.
|
||||||
|
|
||||||
|
```
|
||||||
|
%% WRONG — LR with many nodes produces wide, short, unreadable diagram
|
||||||
|
flowchart LR
|
||||||
|
A --> B --> C --> D --> E
|
||||||
|
A --> F --> G --> H
|
||||||
|
|
||||||
|
%% RIGHT — TD uses vertical space, labels stay readable
|
||||||
|
flowchart TD
|
||||||
|
A --> B --> C --> D --> E
|
||||||
|
A --> F --> G --> H
|
||||||
|
```
|
||||||
|
|
||||||
|
### Diagram Type Examples
|
||||||
|
|
||||||
|
**Flowchart with decisions:**
|
||||||
|
```html
|
||||||
|
<pre class="mermaid">
|
||||||
|
graph TD
|
||||||
|
A[Request] --> B{Authenticated?}
|
||||||
|
B -->|Yes| C[Load Dashboard]
|
||||||
|
B -->|No| D[Login Page]
|
||||||
|
D --> E[Submit Credentials]
|
||||||
|
E --> B
|
||||||
|
C --> F{Role?}
|
||||||
|
F -->|Admin| G[Admin Panel]
|
||||||
|
F -->|User| H[User Dashboard]
|
||||||
|
</pre>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Sequence diagram:**
|
||||||
|
```html
|
||||||
|
<pre class="mermaid">
|
||||||
|
sequenceDiagram
|
||||||
|
participant C as Client
|
||||||
|
participant G as Gateway
|
||||||
|
participant S as Service
|
||||||
|
participant D as Database
|
||||||
|
C->>G: POST /api/data
|
||||||
|
G->>G: Validate JWT
|
||||||
|
G->>S: Forward request
|
||||||
|
S->>D: Query
|
||||||
|
D-->>S: Results
|
||||||
|
S-->>G: Response
|
||||||
|
G-->>C: 200 OK
|
||||||
|
</pre>
|
||||||
|
```
|
||||||
|
|
||||||
|
**ER diagram:**
|
||||||
|
```html
|
||||||
|
<pre class="mermaid">
|
||||||
|
erDiagram
|
||||||
|
USERS ||--o{ ORDERS : places
|
||||||
|
ORDERS ||--|{ LINE_ITEMS : contains
|
||||||
|
LINE_ITEMS }o--|| PRODUCTS : references
|
||||||
|
USERS { string email PK }
|
||||||
|
ORDERS { int id PK }
|
||||||
|
LINE_ITEMS { int quantity }
|
||||||
|
PRODUCTS { string name }
|
||||||
|
</pre>
|
||||||
|
```
|
||||||
|
|
||||||
|
**State diagram:**
|
||||||
|
```html
|
||||||
|
<pre class="mermaid">
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> Draft
|
||||||
|
Draft --> Review : submit
|
||||||
|
Review --> Approved : approve
|
||||||
|
Review --> Draft : request_changes
|
||||||
|
Approved --> Published : publish
|
||||||
|
Published --> Archived : archive
|
||||||
|
Archived --> [*]
|
||||||
|
</pre>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Mind map:**
|
||||||
|
```html
|
||||||
|
<pre class="mermaid">
|
||||||
|
mindmap
|
||||||
|
root((Project))
|
||||||
|
Frontend
|
||||||
|
React
|
||||||
|
Next.js
|
||||||
|
Tailwind
|
||||||
|
Backend
|
||||||
|
Node.js
|
||||||
|
PostgreSQL
|
||||||
|
Redis
|
||||||
|
Infrastructure
|
||||||
|
AWS
|
||||||
|
Docker
|
||||||
|
Terraform
|
||||||
|
</pre>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Class diagram:**
|
||||||
|
```html
|
||||||
|
<pre class="mermaid">
|
||||||
|
classDiagram
|
||||||
|
class User {
|
||||||
|
+string email
|
||||||
|
+string name
|
||||||
|
+login()
|
||||||
|
+logout()
|
||||||
|
}
|
||||||
|
class Order {
|
||||||
|
+int id
|
||||||
|
+decimal total
|
||||||
|
+submit()
|
||||||
|
}
|
||||||
|
class Product {
|
||||||
|
+string name
|
||||||
|
+decimal price
|
||||||
|
}
|
||||||
|
User "1" --> "*" Order : places
|
||||||
|
Order "*" --> "*" Product : contains
|
||||||
|
</pre>
|
||||||
|
```
|
||||||
|
|
||||||
|
**C4 architecture (flowchart-as-C4):**
|
||||||
|
```html
|
||||||
|
<pre class="mermaid">
|
||||||
|
graph TD
|
||||||
|
user("👤 User<br/><small>Browser client</small>")
|
||||||
|
subgraph boundary["Web Platform"]
|
||||||
|
app["Web App<br/><small>Node.js</small>"]
|
||||||
|
db[("Database<br/><small>PostgreSQL</small>")]
|
||||||
|
end
|
||||||
|
email["📧 Email Service"]:::ext
|
||||||
|
payment["💳 Payment Gateway"]:::ext
|
||||||
|
user -->|"HTTPS"| app
|
||||||
|
app -->|"SQL"| db
|
||||||
|
app -->|"SMTP"| email
|
||||||
|
app -->|"API"| payment
|
||||||
|
classDef ext fill:none,stroke-dasharray:5 5
|
||||||
|
</pre>
|
||||||
|
```
|
||||||
|
|
||||||
|
Do NOT use native `C4Context` / `C4Container` syntax — it hardcodes sharp corners, its own font, and inline colors that ignore `themeVariables`. Use `graph TD` + `subgraph` for C4 boundaries instead; it inherits all theme settings automatically.
|
||||||
|
|
||||||
|
### Which Mermaid Diagram Type?
|
||||||
|
|
||||||
|
Quick-reference for choosing the right Mermaid syntax:
|
||||||
|
|
||||||
|
| You want to show... | Use | Syntax keyword |
|
||||||
|
|---|---|---|
|
||||||
|
| Process flow, decisions, pipelines | Flowchart | `graph TD` / `graph LR` |
|
||||||
|
| Request/response, API calls, temporal interactions | Sequence diagram | `sequenceDiagram` |
|
||||||
|
| Database tables and relationships | ER diagram | `erDiagram` |
|
||||||
|
| OOP classes, domain models with methods | Class diagram | `classDiagram` |
|
||||||
|
| System architecture at multiple zoom levels | C4 diagram | `graph TD` + `subgraph` (not native `C4Context`) |
|
||||||
|
| State transitions, lifecycles | State diagram | `stateDiagram-v2` |
|
||||||
|
| Hierarchical breakdowns, brainstorms | Mind map | `mindmap` |
|
||||||
|
|
||||||
|
### Dark Mode Handling
|
||||||
|
|
||||||
|
Mermaid initializes once — it can't reactively switch themes. Read the preference at load time inside your `<script type="module">`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
// Use isDark to pick light or dark values in themeVariables
|
||||||
|
```
|
||||||
|
|
||||||
|
The CSS overrides on the container (`.mermaid-wrap`) and page will still respond to `prefers-color-scheme` normally — only the Mermaid SVG internals are static.
|
||||||
|
|
||||||
|
## Chart.js — Data Visualizations
|
||||||
|
|
||||||
|
Use for bar charts, line charts, pie/doughnut charts, radar charts, and other data-driven visualizations in dashboard-type diagrams. Overkill for static numbers — use pure SVG/CSS for simple progress bars and sparklines.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
|
||||||
|
|
||||||
|
<canvas id="myChart" width="600" height="300"></canvas>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
const textColor = isDark ? '#8b949e' : '#6b7280';
|
||||||
|
const gridColor = isDark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.06)';
|
||||||
|
const fontFamily = getComputedStyle(document.documentElement)
|
||||||
|
.getPropertyValue('--font-body').trim() || 'system-ui, sans-serif';
|
||||||
|
|
||||||
|
new Chart(document.getElementById('myChart'), {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
|
||||||
|
datasets: [{
|
||||||
|
label: 'Feedback Items',
|
||||||
|
data: [45, 62, 78, 91, 120],
|
||||||
|
backgroundColor: isDark ? 'rgba(129, 140, 248, 0.6)' : 'rgba(79, 70, 229, 0.6)',
|
||||||
|
borderColor: isDark ? '#818cf8' : '#4f46e5',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 4,
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
plugins: {
|
||||||
|
legend: { labels: { color: textColor, font: { family: fontFamily } } },
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: { ticks: { color: textColor, font: { family: fontFamily } }, grid: { color: gridColor } },
|
||||||
|
y: { ticks: { color: textColor, font: { family: fontFamily } }, grid: { color: gridColor } },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
Wrap the canvas in a styled container:
|
||||||
|
```css
|
||||||
|
.chart-container {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container canvas {
|
||||||
|
max-height: 300px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## anime.js — Orchestrated Animations
|
||||||
|
|
||||||
|
Use when a diagram has 10+ elements and you want a choreographed entrance sequence (staggered reveals, path drawing, count-up numbers). For simpler diagrams, CSS `animation-delay` staggering is sufficient.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/animejs@3.2.2/lib/anime.min.js"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
|
||||||
|
if (!prefersReduced) {
|
||||||
|
anime({
|
||||||
|
targets: '.ve-card',
|
||||||
|
opacity: [0, 1],
|
||||||
|
translateY: [20, 0],
|
||||||
|
delay: anime.stagger(80, { start: 200 }),
|
||||||
|
easing: 'easeOutCubic',
|
||||||
|
duration: 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
anime({
|
||||||
|
targets: '.connector path',
|
||||||
|
strokeDashoffset: [anime.setDashoffset, 0],
|
||||||
|
easing: 'easeInOutCubic',
|
||||||
|
duration: 800,
|
||||||
|
delay: anime.stagger(150, { start: 600 }),
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-count]').forEach(el => {
|
||||||
|
anime({
|
||||||
|
targets: { val: 0 },
|
||||||
|
val: parseInt(el.dataset.count),
|
||||||
|
round: 1,
|
||||||
|
duration: 1200,
|
||||||
|
delay: 400,
|
||||||
|
easing: 'easeOutExpo',
|
||||||
|
update: (anim) => { el.textContent = anim.animations[0].currentValue; }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
When using anime.js, set initial opacity to 0 in CSS so elements don't flash before the animation:
|
||||||
|
```css
|
||||||
|
.ve-card { opacity: 0; }
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.ve-card { opacity: 1 !important; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Google Fonts — Typography
|
||||||
|
|
||||||
|
Always load with `display=swap` for fast rendering. Pick a distinctive pairing — body + mono at minimum, optionally a display font for the title.
|
||||||
|
|
||||||
|
**FORBIDDEN as `--font-body` (AI slop signals):**
|
||||||
|
- Inter — the single most overused AI default font
|
||||||
|
- Roboto — generic Android/Google default
|
||||||
|
- Arial, Helvetica — system defaults with no character
|
||||||
|
- system-ui alone without a named font — signals zero design intent
|
||||||
|
|
||||||
|
```html
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Outfit:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
```
|
||||||
|
|
||||||
|
Define as CSS variables for easy reference:
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--font-body: 'Outfit', system-ui, sans-serif;
|
||||||
|
--font-mono: 'Space Mono', 'SF Mono', Consolas, monospace;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Font pairings** (rotate — never use the same pairing twice in a row):
|
||||||
|
|
||||||
|
| Body / Headings | Mono / Labels | Feel | Use for |
|
||||||
|
|---|---|---|---|
|
||||||
|
| DM Sans | Fira Code | Friendly, developer | Blueprint, technical docs |
|
||||||
|
| Instrument Serif | JetBrains Mono | Editorial, refined | Plan reviews, decision logs |
|
||||||
|
| IBM Plex Sans | IBM Plex Mono | Reliable, readable | Architecture diagrams |
|
||||||
|
| Bricolage Grotesque | Fragment Mono | Bold, characterful | Data tables, dashboards |
|
||||||
|
| Plus Jakarta Sans | Azeret Mono | Rounded, approachable | Status reports, audits |
|
||||||
|
| Outfit | Space Mono | Clean geometric, modern | Flowcharts, pipelines |
|
||||||
|
| Sora | IBM Plex Mono | Technical, precise | ER diagrams, schemas |
|
||||||
|
| Crimson Pro | Noto Sans Mono | Scholarly, serious | RFC reviews, specs |
|
||||||
|
| Fraunces | Source Code Pro | Warm, distinctive | Project recaps |
|
||||||
|
| Geist | Geist Mono | Vercel-inspired, sharp | Modern API docs |
|
||||||
|
| Red Hat Display | Red Hat Mono | Cohesive family | System overviews |
|
||||||
|
| Libre Franklin | Inconsolata | Classic, reliable | Data-dense tables |
|
||||||
|
| Playfair Display | Roboto Mono | Elegant contrast | Executive summaries |
|
||||||
|
|
||||||
|
The first 5 pairings are recommended for most use cases. Vary across consecutive diagrams.
|
||||||
|
|
||||||
|
### Typography by Content Voice
|
||||||
|
|
||||||
|
For prose-heavy pages (documentation, articles, essays), match typography to the content's voice:
|
||||||
|
|
||||||
|
| Voice | Fonts | Best For |
|
||||||
|
|-------|-------|----------|
|
||||||
|
| **Literary / Thoughtful** | Literata, Lora, Newsreader, Merriweather | Essays, personal posts, long-form articles |
|
||||||
|
| **Technical / Precise** | IBM Plex Sans + Mono, Geist + Geist Mono, Source family | Documentation, READMEs, API references |
|
||||||
|
| **Bold / Contemporary** | Bricolage Grotesque, Space Grotesk, DM Sans | Product pages, feature announcements |
|
||||||
|
| **Minimal / Focused** | Source Serif 4 + Source Sans 3, Karla + Inconsolata | Tutorials, how-tos, focused reading |
|
||||||
|
|
||||||
|
**Literata** deserves special mention — it has optical sizing designed specifically for screen reading. Google's answer to Georgia, but modernized.
|
||||||
212
.agents/skills/visual-explainer/references/responsive-nav.md
Normal file
212
.agents/skills/visual-explainer/references/responsive-nav.md
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
# Responsive Section Navigation
|
||||||
|
|
||||||
|
Navigation pattern for multi-section pages (reviews, recaps, dashboards). Provides a sticky sidebar TOC on desktop and a sticky horizontal scrollable bar on mobile.
|
||||||
|
|
||||||
|
## Layout Structure
|
||||||
|
|
||||||
|
The page uses a two-column CSS Grid: sidebar (TOC) + main content. On mobile it collapses to single-column with the TOC becoming a horizontal bar.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
|
||||||
|
<nav class="toc" id="toc">
|
||||||
|
<div class="toc-title">Contents</div>
|
||||||
|
<a href="#s1">1. First Section</a>
|
||||||
|
<a href="#s2">2. Second Section</a>
|
||||||
|
<!-- one link per section -->
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
<h1>Page Title</h1>
|
||||||
|
<p class="subtitle">Subtitle text</p>
|
||||||
|
|
||||||
|
<div id="s1" class="sec-head ...">1 — First Section</div>
|
||||||
|
<!-- section content -->
|
||||||
|
|
||||||
|
<div id="s2" class="sec-head ...">2 — Second Section</div>
|
||||||
|
<!-- section content -->
|
||||||
|
</div><!-- /main -->
|
||||||
|
|
||||||
|
</div><!-- /wrap -->
|
||||||
|
</body>
|
||||||
|
```
|
||||||
|
|
||||||
|
Key structural rules:
|
||||||
|
- `<nav class="toc">` is the **first child** of `.wrap`
|
||||||
|
- All page content goes inside `<div class="main">`
|
||||||
|
- Every section heading gets an `id="s1"`, `id="s2"`, etc.
|
||||||
|
- TOC links use `href="#s1"` matching those IDs
|
||||||
|
- Keep TOC link text short (truncate long section names)
|
||||||
|
|
||||||
|
## CSS
|
||||||
|
|
||||||
|
### Wrap (grid layout)
|
||||||
|
|
||||||
|
```css
|
||||||
|
.wrap {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 170px 1fr;
|
||||||
|
gap: 0 40px;
|
||||||
|
}
|
||||||
|
.main { min-width: 0; }
|
||||||
|
```
|
||||||
|
|
||||||
|
### TOC — Desktop (sticky sidebar)
|
||||||
|
|
||||||
|
```css
|
||||||
|
.toc {
|
||||||
|
position: sticky;
|
||||||
|
top: 24px;
|
||||||
|
align-self: start;
|
||||||
|
padding: 14px 0;
|
||||||
|
grid-row: 1 / -1;
|
||||||
|
max-height: calc(100dvh - 48px);
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.toc::-webkit-scrollbar { width: 3px; }
|
||||||
|
.toc::-webkit-scrollbar-thumb { background: var(--surface-elevated); border-radius: 2px; }
|
||||||
|
|
||||||
|
.toc-title {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
padding: 0 0 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc a {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border-left: 2px solid transparent;
|
||||||
|
transition: all 0.15s;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-bottom: 1px;
|
||||||
|
}
|
||||||
|
.toc a:hover { color: var(--text); background: var(--surface2); }
|
||||||
|
.toc a.active { color: var(--text); border-left-color: var(--accent); }
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `var(--accent)` with your page's primary accent color variable (e.g., `var(--orange)`, `var(--blue)`).
|
||||||
|
|
||||||
|
### TOC — Mobile (sticky horizontal bar)
|
||||||
|
|
||||||
|
```css
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.wrap { grid-template-columns: 1fr; padding-top: 0; }
|
||||||
|
body { padding-top: 0; }
|
||||||
|
|
||||||
|
.toc {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 200;
|
||||||
|
max-height: none;
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
background: var(--bg);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding: 10px 0;
|
||||||
|
margin: 0 -40px;
|
||||||
|
padding-left: 40px;
|
||||||
|
padding-right: 40px;
|
||||||
|
grid-row: auto;
|
||||||
|
}
|
||||||
|
.toc::-webkit-scrollbar { display: none; }
|
||||||
|
.toc-title { display: none; }
|
||||||
|
|
||||||
|
.toc a {
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-left: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
.toc a.active {
|
||||||
|
border-left: none;
|
||||||
|
border-bottom-color: var(--accent);
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main { padding-top: 20px; }
|
||||||
|
|
||||||
|
/* Offset scroll target so headings clear the sticky bar */
|
||||||
|
.sec-head { scroll-margin-top: 52px; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Adjust `margin: 0 -40px` and `padding-left/right: 40px` to match your `body` padding so the bar bleeds edge-to-edge.
|
||||||
|
|
||||||
|
## JavaScript — Scroll Spy
|
||||||
|
|
||||||
|
Place before `</body>`, after any Mermaid init:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
const toc = document.getElementById('toc');
|
||||||
|
const links = toc.querySelectorAll('a');
|
||||||
|
const sections = [];
|
||||||
|
|
||||||
|
links.forEach(link => {
|
||||||
|
const id = link.getAttribute('href').slice(1);
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) sections.push({ id, el, link });
|
||||||
|
});
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(entries => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
links.forEach(l => l.classList.remove('active'));
|
||||||
|
const match = sections.find(s => s.el === entry.target);
|
||||||
|
if (match) {
|
||||||
|
match.link.classList.add('active');
|
||||||
|
// On mobile, auto-scroll the active tab into view
|
||||||
|
if (window.innerWidth <= 1000) {
|
||||||
|
match.link.scrollIntoView({
|
||||||
|
behavior: 'smooth', block: 'nearest', inline: 'center'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, { rootMargin: '-10% 0px -80% 0px' });
|
||||||
|
|
||||||
|
sections.forEach(s => observer.observe(s.el));
|
||||||
|
|
||||||
|
links.forEach(link => {
|
||||||
|
link.addEventListener('click', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const id = link.getAttribute('href').slice(1);
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) {
|
||||||
|
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
history.replaceState(null, '', '#' + id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adaptation Notes
|
||||||
|
|
||||||
|
- The `.toc-title` text, link labels, accent color, and section IDs change per page. Everything else is copy-paste.
|
||||||
|
- For pages with fewer than 4 sections, skip the TOC entirely — it adds clutter without value.
|
||||||
|
- The `grid-template-columns: 170px 1fr` width works for most TOCs. If section names are longer, go up to `200px`.
|
||||||
|
- The `rootMargin: '-10% 0px -80% 0px'` means a section is "active" when its heading enters the top 10-20% of the viewport. This works well with sticky headers.
|
||||||
|
- On mobile, the horizontal bar uses `overflow-x: auto` with hidden scrollbar. The active tab auto-scrolls into the center of the bar as the user scrolls the page.
|
||||||
1406
.agents/skills/visual-explainer/references/slide-patterns.md
Normal file
1406
.agents/skills/visual-explainer/references/slide-patterns.md
Normal file
File diff suppressed because it is too large
Load diff
82
.agents/skills/visual-explainer/scripts/share.sh
Executable file
82
.agents/skills/visual-explainer/scripts/share.sh
Executable file
|
|
@ -0,0 +1,82 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Share Visual Explainer HTML via Vercel
|
||||||
|
# Usage: ./share.sh <html-file>
|
||||||
|
# Returns: Live URL instantly (no auth required)
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
HTML_FILE="${1}"
|
||||||
|
|
||||||
|
if [ -z "$HTML_FILE" ]; then
|
||||||
|
echo -e "${RED}Error: Please provide an HTML file to share${NC}" >&2
|
||||||
|
echo "Usage: $0 <html-file>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$HTML_FILE" ]; then
|
||||||
|
echo -e "${RED}Error: File not found: $HTML_FILE${NC}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find vercel-deploy skill
|
||||||
|
VERCEL_SCRIPT=""
|
||||||
|
for dir in ~/.pi/agent/skills/vercel-deploy/scripts /mnt/skills/user/vercel-deploy/scripts; do
|
||||||
|
if [ -f "$dir/deploy.sh" ]; then
|
||||||
|
VERCEL_SCRIPT="$dir/deploy.sh"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$VERCEL_SCRIPT" ]; then
|
||||||
|
echo -e "${RED}Error: vercel-deploy skill not found${NC}" >&2
|
||||||
|
echo "Install it with: pi install npm:vercel-deploy" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create temp directory with index.html
|
||||||
|
TEMP_DIR=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$TEMP_DIR"' EXIT
|
||||||
|
|
||||||
|
# Copy file as index.html (Vercel serves index.html at root)
|
||||||
|
cp "$HTML_FILE" "$TEMP_DIR/index.html"
|
||||||
|
|
||||||
|
echo -e "${CYAN}Sharing $(basename "$HTML_FILE")...${NC}" >&2
|
||||||
|
|
||||||
|
# Deploy via vercel-deploy skill
|
||||||
|
# Temporarily disable errexit to capture deployment errors
|
||||||
|
set +e
|
||||||
|
RESULT=$(bash "$VERCEL_SCRIPT" "$TEMP_DIR" 2>&1)
|
||||||
|
DEPLOY_EXIT=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ $DEPLOY_EXIT -ne 0 ]; then
|
||||||
|
echo -e "${RED}Error: Deployment failed${NC}" >&2
|
||||||
|
echo "$RESULT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract preview URL
|
||||||
|
PREVIEW_URL=$(echo "$RESULT" | grep -oE 'https://[^"]+\.vercel\.app' | head -1)
|
||||||
|
CLAIM_URL=$(echo "$RESULT" | grep -oE 'https://vercel\.com/claim-deployment[^"]+' | head -1)
|
||||||
|
|
||||||
|
if [ -z "$PREVIEW_URL" ]; then
|
||||||
|
echo -e "${RED}Error: Deployment failed${NC}" >&2
|
||||||
|
echo "$RESULT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >&2
|
||||||
|
echo -e "${GREEN}✓ Shared successfully!${NC}" >&2
|
||||||
|
echo "" >&2
|
||||||
|
echo -e "${GREEN}Live URL: ${PREVIEW_URL}${NC}" >&2
|
||||||
|
echo -e "${CYAN}Claim URL: ${CLAIM_URL}${NC}" >&2
|
||||||
|
echo "" >&2
|
||||||
|
|
||||||
|
# Output JSON for programmatic use (extract from vercel-deploy output)
|
||||||
|
echo "$RESULT" | grep -E '^\{' | head -1
|
||||||
596
.agents/skills/visual-explainer/templates/architecture.html
Normal file
596
.agents/skills/visual-explainer/templates/architecture.html
Normal file
|
|
@ -0,0 +1,596 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Architecture Diagram — Reference Template</title>
|
||||||
|
<!--
|
||||||
|
Reference template for the visual-explainer skill: CSS Grid architecture layout.
|
||||||
|
Warm terracotta/sage palette — distinctly different from the teal (mermaid)
|
||||||
|
and rose (data-table) templates so agents absorb variety, not a single palette.
|
||||||
|
Key patterns demonstrated:
|
||||||
|
- Warm non-default palette (terracotta + sage, NOT indigo/violet)
|
||||||
|
- Depth tiers: hero (input sources), default (mid sections), recessed (callout)
|
||||||
|
- Asymmetric background atmosphere (off-center gradient mesh)
|
||||||
|
- Large heading (38px) for typographic contrast
|
||||||
|
- Section cards with colored accent borders and dot labels
|
||||||
|
- Vertical flow arrows between sections (inline SVG)
|
||||||
|
- Horizontal pipeline with step boxes and arrow separators
|
||||||
|
- Parallel branch within a pipeline
|
||||||
|
- Color-coded legend, three-column output row
|
||||||
|
- Staggered fade-in via --i CSS variable (works with interleaved elements)
|
||||||
|
- Reduced motion respect, responsive single-column fallback
|
||||||
|
-->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
/* ============ THEME ============ */
|
||||||
|
:root {
|
||||||
|
--font-body: 'IBM Plex Sans', system-ui, sans-serif;
|
||||||
|
--font-mono: 'IBM Plex Mono', 'SF Mono', Consolas, monospace;
|
||||||
|
|
||||||
|
/* Light theme — warm terracotta + sage palette */
|
||||||
|
--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.07);
|
||||||
|
--green: #4d7c0f;
|
||||||
|
--green-dim: rgba(77, 124, 15, 0.07);
|
||||||
|
--orange: #b45309;
|
||||||
|
--orange-dim: rgba(180, 83, 9, 0.07);
|
||||||
|
--sage: #65a30d;
|
||||||
|
--sage-dim: rgba(101, 163, 13, 0.07);
|
||||||
|
--teal: #0f766e;
|
||||||
|
--teal-dim: rgba(15, 118, 110, 0.07);
|
||||||
|
--plum: #9f1239;
|
||||||
|
--plum-dim: rgba(159, 18, 57, 0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--bg: #1a1412;
|
||||||
|
--surface: #231d1a;
|
||||||
|
--surface2: #2e2622;
|
||||||
|
--surface-elevated: #352d28;
|
||||||
|
--border: rgba(255, 255, 255, 0.06);
|
||||||
|
--border-bright: rgba(255, 255, 255, 0.12);
|
||||||
|
--text: #ede5dd;
|
||||||
|
--text-dim: #a69889;
|
||||||
|
--accent: #fb923c;
|
||||||
|
--accent-dim: rgba(251, 146, 60, 0.12);
|
||||||
|
--green: #a3e635;
|
||||||
|
--green-dim: rgba(163, 230, 53, 0.1);
|
||||||
|
--orange: #fbbf24;
|
||||||
|
--orange-dim: rgba(251, 191, 36, 0.1);
|
||||||
|
--sage: #bef264;
|
||||||
|
--sage-dim: rgba(190, 242, 100, 0.1);
|
||||||
|
--teal: #5eead4;
|
||||||
|
--teal-dim: rgba(94, 234, 212, 0.1);
|
||||||
|
--plum: #fda4af;
|
||||||
|
--plum-dim: rgba(253, 164, 175, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ RESET + BASE ============ */
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
background-image:
|
||||||
|
radial-gradient(ellipse at 20% 0%, var(--accent-dim) 0%, transparent 50%),
|
||||||
|
radial-gradient(ellipse at 80% 100%, var(--sage-dim) 0%, transparent 40%);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: var(--font-body);
|
||||||
|
padding: 40px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ ANIMATION ============ */
|
||||||
|
@keyframes fadeUp {
|
||||||
|
from { opacity: 0; transform: translateY(12px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.section, .flow-arrow {
|
||||||
|
animation: fadeUp 0.4s ease-out both;
|
||||||
|
animation-delay: calc(var(--i, 0) * 0.06s);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*, *::before, *::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-delay: 0ms !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ TYPOGRAPHY ============ */
|
||||||
|
h1 {
|
||||||
|
font-size: 38px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ LAYOUT ============ */
|
||||||
|
.diagram {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 24px;
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ SECTION CARD ============ */
|
||||||
|
.section {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section--hero {
|
||||||
|
background: var(--surface-elevated);
|
||||||
|
border-color: color-mix(in srgb, var(--border) 50%, var(--accent) 50%);
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
|
||||||
|
padding: 28px 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section--recessed {
|
||||||
|
background: var(--surface2);
|
||||||
|
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-label {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-label .dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Color variants */
|
||||||
|
.section--accent { border-color: var(--accent-dim); }
|
||||||
|
.section--accent .section-label { color: var(--accent); }
|
||||||
|
.section--accent .section-label .dot { background: var(--accent); }
|
||||||
|
|
||||||
|
.section--green { border-color: var(--green-dim); }
|
||||||
|
.section--green .section-label { color: var(--green); }
|
||||||
|
.section--green .section-label .dot { background: var(--green); }
|
||||||
|
|
||||||
|
.section--orange { border-color: var(--orange-dim); }
|
||||||
|
.section--orange .section-label { color: var(--orange); }
|
||||||
|
.section--orange .section-label .dot { background: var(--orange); }
|
||||||
|
|
||||||
|
.section--sage { border-color: var(--sage-dim); }
|
||||||
|
.section--sage .section-label { color: var(--sage); }
|
||||||
|
.section--sage .section-label .dot { background: var(--sage); }
|
||||||
|
|
||||||
|
.section--teal { border-color: var(--teal-dim); }
|
||||||
|
.section--teal .section-label { color: var(--teal); }
|
||||||
|
.section--teal .section-label .dot { background: var(--teal); }
|
||||||
|
|
||||||
|
.section--plum { border-color: var(--plum-dim); }
|
||||||
|
.section--plum .section-label { color: var(--plum); }
|
||||||
|
.section--plum .section-label .dot { background: var(--plum); }
|
||||||
|
|
||||||
|
/* ============ INNER GRID ============ */
|
||||||
|
.inner-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-card {
|
||||||
|
background: var(--surface2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-card .title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-card .desc {
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-card code {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
background: var(--accent-dim);
|
||||||
|
color: var(--accent);
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ FLOW ARROW ============ */
|
||||||
|
.flow-arrow {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-arrow svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
fill: none;
|
||||||
|
stroke: var(--border-bright);
|
||||||
|
stroke-width: 2;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ PIPELINE ============ */
|
||||||
|
.pipeline {
|
||||||
|
display: flex;
|
||||||
|
gap: 0;
|
||||||
|
align-items: stretch;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pipeline-step {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
min-width: 120px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pipeline-step .step-num {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pipeline-step .step-name {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pipeline-step .step-detail {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Step color variants */
|
||||||
|
.pipeline-step--teal { border-color: var(--teal-dim); }
|
||||||
|
.pipeline-step--teal .step-num { color: var(--teal); }
|
||||||
|
|
||||||
|
.pipeline-step--sage { border-color: var(--sage-dim); }
|
||||||
|
.pipeline-step--sage .step-num { color: var(--sage); }
|
||||||
|
|
||||||
|
.pipeline-step--orange { border-color: var(--orange-dim); }
|
||||||
|
.pipeline-step--orange .step-num { color: var(--orange); }
|
||||||
|
|
||||||
|
.pipeline-step--green { border-color: var(--green-dim); }
|
||||||
|
.pipeline-step--green .step-num { color: var(--green); }
|
||||||
|
|
||||||
|
.pipeline-arrow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 2px;
|
||||||
|
color: var(--border-bright);
|
||||||
|
font-size: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pipeline-parallel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ THREE COLUMN ROW ============ */
|
||||||
|
.three-col {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ LIST ============ */
|
||||||
|
.node-list {
|
||||||
|
list-style: none;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-list li {
|
||||||
|
padding-left: 14px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-list li::before {
|
||||||
|
content: '›';
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-weight: 600;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-list code {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
background: var(--accent-dim);
|
||||||
|
color: var(--accent);
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ CALLOUT ============ */
|
||||||
|
.callout {
|
||||||
|
background: var(--surface2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-left: 3px solid var(--accent);
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
padding: 14px 18px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout strong { color: var(--text); font-weight: 600; }
|
||||||
|
|
||||||
|
.callout code {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
background: var(--accent-dim);
|
||||||
|
color: var(--accent);
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ LEGEND ============ */
|
||||||
|
.legend {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-swatch {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ SOURCE PILLS ============ */
|
||||||
|
.sources {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source {
|
||||||
|
background: var(--surface2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 18px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source:hover { border-color: var(--border-bright); }
|
||||||
|
|
||||||
|
/* ============ RESPONSIVE ============ */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body { padding: 20px; }
|
||||||
|
.inner-grid { grid-template-columns: 1fr; }
|
||||||
|
.three-col { grid-template-columns: 1fr; }
|
||||||
|
.pipeline { flex-wrap: wrap; gap: 6px; }
|
||||||
|
.pipeline-arrow { display: none; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>System Architecture</h1>
|
||||||
|
<p class="subtitle">reference template — architecture diagram pattern</p>
|
||||||
|
|
||||||
|
<div class="diagram">
|
||||||
|
|
||||||
|
<!-- Source pills row — hero depth for the entry point -->
|
||||||
|
<div class="section section--hero" style="--i:0">
|
||||||
|
<div class="section-label"><span class="dot" style="background:var(--text-dim)"></span> Input Sources</div>
|
||||||
|
<div class="sources">
|
||||||
|
<div class="source"><span>💬</span> Slack</div>
|
||||||
|
<div class="source"><span>🐙</span> GitHub</div>
|
||||||
|
<div class="source"><span>📧</span> Email</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Flow arrow -->
|
||||||
|
<div class="flow-arrow" style="--i:1">
|
||||||
|
<svg viewBox="0 0 20 20"><path d="M10 4 L10 16 M6 12 L10 16 L14 12"/></svg>
|
||||||
|
incoming events
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Gateway section with inner grid -->
|
||||||
|
<div class="section section--accent" style="--i:2">
|
||||||
|
<div class="section-label"><span class="dot"></span> Gateway Layer</div>
|
||||||
|
<div class="inner-grid">
|
||||||
|
<div class="inner-card">
|
||||||
|
<div class="title">Router</div>
|
||||||
|
<div class="desc">Routes messages to agents via <code>resolveRoute()</code></div>
|
||||||
|
</div>
|
||||||
|
<div class="inner-card">
|
||||||
|
<div class="title">HTTP Server</div>
|
||||||
|
<div class="desc">Plugin routes + handlers for webhooks and API</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flow-arrow" style="--i:3">
|
||||||
|
<svg viewBox="0 0 20 20"><path d="M10 4 L10 16 M6 12 L10 16 L14 12"/></svg>
|
||||||
|
pipeline entry
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pipeline section -->
|
||||||
|
<div class="section section--green" style="--i:4">
|
||||||
|
<div class="section-label"><span class="dot"></span> Processing Pipeline</div>
|
||||||
|
<div class="legend" style="margin-bottom:14px">
|
||||||
|
<div class="legend-item"><div class="legend-swatch" style="background:var(--teal-dim);border:1px solid var(--teal)"></div>no LLM</div>
|
||||||
|
<div class="legend-item"><div class="legend-swatch" style="background:var(--sage-dim);border:1px solid var(--sage)"></div>LLM call</div>
|
||||||
|
<div class="legend-item"><div class="legend-swatch" style="background:var(--orange-dim);border:1px solid var(--orange)"></div>embedding</div>
|
||||||
|
<div class="legend-item"><div class="legend-swatch" style="background:var(--green-dim);border:1px solid var(--green)"></div>DB write</div>
|
||||||
|
</div>
|
||||||
|
<div class="pipeline">
|
||||||
|
<div class="pipeline-step pipeline-step--teal">
|
||||||
|
<div class="step-num">STEP 0</div>
|
||||||
|
<div class="step-name">Pre-filter</div>
|
||||||
|
<div class="step-detail">Allowlist, bots,<br>dedup check</div>
|
||||||
|
</div>
|
||||||
|
<div class="pipeline-arrow">→</div>
|
||||||
|
<div class="pipeline-step pipeline-step--sage">
|
||||||
|
<div class="step-num">STEP 1</div>
|
||||||
|
<div class="step-name">Relevance</div>
|
||||||
|
<div class="step-detail">Cheap LLM<br>boolean check</div>
|
||||||
|
</div>
|
||||||
|
<div class="pipeline-arrow">→</div>
|
||||||
|
<div class="pipeline-step pipeline-step--sage">
|
||||||
|
<div class="step-num">STEP 2</div>
|
||||||
|
<div class="step-name">Classify</div>
|
||||||
|
<div class="step-detail">JSON schema<br>validated output</div>
|
||||||
|
</div>
|
||||||
|
<div class="pipeline-arrow">→</div>
|
||||||
|
<div class="pipeline-parallel">
|
||||||
|
<div class="pipeline-step pipeline-step--orange">
|
||||||
|
<div class="step-num">STEP 3</div>
|
||||||
|
<div class="step-name">Embed</div>
|
||||||
|
<div class="step-detail">Vector embedding</div>
|
||||||
|
</div>
|
||||||
|
<div class="pipeline-step pipeline-step--teal">
|
||||||
|
<div class="step-num">STEP 5</div>
|
||||||
|
<div class="step-name">Enrich</div>
|
||||||
|
<div class="step-detail">User resolution</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pipeline-arrow">→</div>
|
||||||
|
<div class="pipeline-step pipeline-step--green">
|
||||||
|
<div class="step-num">STEP 4</div>
|
||||||
|
<div class="step-name">Cluster</div>
|
||||||
|
<div class="step-detail">Cosine similarity<br>+ INSERT</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flow-arrow" style="--i:5">
|
||||||
|
<svg viewBox="0 0 20 20"><path d="M10 4 L10 16 M6 12 L10 16 L14 12"/></svg>
|
||||||
|
stored and queryable
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Database section -->
|
||||||
|
<div class="section section--orange" style="--i:6">
|
||||||
|
<div class="section-label"><span class="dot"></span> Database</div>
|
||||||
|
<div class="inner-grid">
|
||||||
|
<div class="inner-card">
|
||||||
|
<div class="title">feedback_items</div>
|
||||||
|
<div class="desc">Classification, embedding, cluster assignment, source dedup</div>
|
||||||
|
</div>
|
||||||
|
<div class="inner-card">
|
||||||
|
<div class="title">clusters</div>
|
||||||
|
<div class="desc">Centroid vectors, trends, ticket links, severity rollup</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flow-arrow" style="--i:7">
|
||||||
|
<svg viewBox="0 0 20 20"><path d="M10 4 L10 16 M6 12 L10 16 L14 12"/></svg>
|
||||||
|
consumed by
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Three column output -->
|
||||||
|
<div class="three-col">
|
||||||
|
<div class="section section--sage" style="--i:8">
|
||||||
|
<div class="section-label"><span class="dot"></span> Agent Tools</div>
|
||||||
|
<ul class="node-list">
|
||||||
|
<li><code>search</code> semantic vector search</li>
|
||||||
|
<li><code>clusters</code> browse and filter</li>
|
||||||
|
<li><code>stats</code> aggregate metrics</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="section section--plum" style="--i:9">
|
||||||
|
<div class="section-label"><span class="dot"></span> Actions</div>
|
||||||
|
<ul class="node-list">
|
||||||
|
<li>Create tickets from clusters</li>
|
||||||
|
<li>Notify customers on ship</li>
|
||||||
|
<li>Generate release notes</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="section section--teal" style="--i:10">
|
||||||
|
<div class="section-label"><span class="dot"></span> Dashboard</div>
|
||||||
|
<ul class="node-list">
|
||||||
|
<li>Metrics overview</li>
|
||||||
|
<li>Feedback stream</li>
|
||||||
|
<li>NL chat interface</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Callout — recessed depth for secondary info -->
|
||||||
|
<div class="callout section section--recessed" style="--i:11">
|
||||||
|
<strong>Multi-tenant</strong> — Each agent gets an isolated database at
|
||||||
|
<code>{agentDir}/intelligence/feedback.db</code> with per-agent config overlay
|
||||||
|
and credential isolation.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
540
.agents/skills/visual-explainer/templates/data-table.html
Normal file
540
.agents/skills/visual-explainer/templates/data-table.html
Normal file
|
|
@ -0,0 +1,540 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Requirements Audit — Reference Template</title>
|
||||||
|
<!--
|
||||||
|
Reference template for the visual-explainer skill: data tables.
|
||||||
|
Rose/cranberry palette — distinctly different from terracotta (architecture)
|
||||||
|
and teal (mermaid) templates so agents absorb variety.
|
||||||
|
Key patterns demonstrated:
|
||||||
|
- Rose/cranberry palette (NOT indigo/violet)
|
||||||
|
- KPI summary cards above the table (visual hook before the data)
|
||||||
|
- Collapsible <details> section (replaces static callout)
|
||||||
|
- Depth tiers: elevated KPIs, default table, collapsible for secondary
|
||||||
|
- Real <table> element with sticky header, alternating rows
|
||||||
|
- Status indicator badges (match, gap, partial)
|
||||||
|
- Text wrapping in wide columns, code references in cells
|
||||||
|
- Summary footer row with aggregate status
|
||||||
|
- Staggered row animation via --i variable
|
||||||
|
- Both light and dark themes via prefers-color-scheme
|
||||||
|
- Responsive horizontal scroll wrapper
|
||||||
|
-->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
/* ============ THEME ============ */
|
||||||
|
:root {
|
||||||
|
--font-body: 'Instrument Serif', 'Georgia', serif;
|
||||||
|
--font-mono: 'JetBrains Mono', 'SF Mono', Consolas, monospace;
|
||||||
|
--font-sans: system-ui, -apple-system, sans-serif;
|
||||||
|
|
||||||
|
--bg: #fff5f5;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--surface2: #fef0ee;
|
||||||
|
--surface-elevated: #fff8f7;
|
||||||
|
--border: rgba(0, 0, 0, 0.07);
|
||||||
|
--border-bright: rgba(0, 0, 0, 0.14);
|
||||||
|
--text: #1c1917;
|
||||||
|
--text-dim: #78716c;
|
||||||
|
--accent: #be123c;
|
||||||
|
--accent-dim: rgba(190, 18, 60, 0.06);
|
||||||
|
--green: #16a34a;
|
||||||
|
--green-dim: rgba(22, 163, 74, 0.08);
|
||||||
|
--red: #dc2626;
|
||||||
|
--red-dim: rgba(220, 38, 38, 0.08);
|
||||||
|
--orange: #d97706;
|
||||||
|
--orange-dim: rgba(217, 119, 6, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--bg: #1a0a0a;
|
||||||
|
--surface: #231414;
|
||||||
|
--surface2: #2e1b1b;
|
||||||
|
--surface-elevated: #351f1f;
|
||||||
|
--border: rgba(255, 255, 255, 0.06);
|
||||||
|
--border-bright: rgba(255, 255, 255, 0.12);
|
||||||
|
--text: #fde2e2;
|
||||||
|
--text-dim: #c9a3a3;
|
||||||
|
--accent: #fb7185;
|
||||||
|
--accent-dim: rgba(251, 113, 133, 0.12);
|
||||||
|
--green: #4ade80;
|
||||||
|
--green-dim: rgba(74, 222, 128, 0.1);
|
||||||
|
--red: #f87171;
|
||||||
|
--red-dim: rgba(248, 113, 113, 0.1);
|
||||||
|
--orange: #fbbf24;
|
||||||
|
--orange-dim: rgba(251, 191, 36, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ RESET + BASE ============ */
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
background-image:
|
||||||
|
radial-gradient(ellipse at 30% 0%, var(--accent-dim) 0%, transparent 50%),
|
||||||
|
radial-gradient(ellipse at 70% 100%, var(--green-dim) 0%, transparent 40%);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
padding: 40px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ ANIMATION ============ */
|
||||||
|
@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.04s);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*, *::before, *::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-delay: 0ms !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ CONTAINER ============ */
|
||||||
|
.container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ HEADER ============ */
|
||||||
|
h1 {
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: -0.3px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ LEGEND ============ */
|
||||||
|
.legend {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-swatch {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ TABLE WRAPPER ============ */
|
||||||
|
.table-wrap {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-scroll {
|
||||||
|
overflow-x: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ TABLE ============ */
|
||||||
|
.data-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.data-table thead {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table th {
|
||||||
|
background: var(--surface2);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1.2px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
text-align: left;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-bottom: 2px solid var(--border-bright);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cells */
|
||||||
|
.data-table td {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table tbody tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wide text column */
|
||||||
|
.data-table .wide {
|
||||||
|
min-width: 220px;
|
||||||
|
max-width: 440px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alternating rows */
|
||||||
|
.data-table tbody tr:nth-child(even) {
|
||||||
|
background: var(--accent-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Row hover + animation stagger */
|
||||||
|
.data-table tbody tr {
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
animation: fadeUp 0.35s ease-out both;
|
||||||
|
animation-delay: calc(var(--i, 0) * 0.04s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table tbody tr:hover {
|
||||||
|
background: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code in cells */
|
||||||
|
.data-table code {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
background: var(--accent-dim);
|
||||||
|
color: var(--accent);
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Secondary text */
|
||||||
|
.data-table small {
|
||||||
|
display: block;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 11px;
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ STATUS BADGES ============ */
|
||||||
|
.status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
white-space: nowrap;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status--match {
|
||||||
|
background: var(--green-dim);
|
||||||
|
color: var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status--gap {
|
||||||
|
background: var(--red-dim);
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status--partial {
|
||||||
|
background: var(--orange-dim);
|
||||||
|
color: var(--orange);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dot before status text */
|
||||||
|
.status::before {
|
||||||
|
content: '';
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ FOOTER ROW ============ */
|
||||||
|
.data-table tfoot td {
|
||||||
|
background: var(--surface2);
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
border-top: 2px solid var(--border-bright);
|
||||||
|
border-bottom: none;
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ KPI SUMMARY ============ */
|
||||||
|
.kpi-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-card {
|
||||||
|
background: var(--surface-elevated, var(--surface));
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 18px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-card__value {
|
||||||
|
font-family: var(--font-body);
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.1;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-card__value--green { color: var(--green); }
|
||||||
|
.kpi-card__value--red { color: var(--red); }
|
||||||
|
.kpi-card__value--orange { color: var(--orange); }
|
||||||
|
|
||||||
|
.kpi-card__label {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1.2px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ COLLAPSIBLE ============ */
|
||||||
|
details.collapsible {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
details.collapsible summary {
|
||||||
|
padding: 14px 20px;
|
||||||
|
background: var(--surface);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--text);
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
details.collapsible summary:hover {
|
||||||
|
background: var(--surface-elevated, var(--surface));
|
||||||
|
}
|
||||||
|
|
||||||
|
details.collapsible summary::-webkit-details-marker { display: none; }
|
||||||
|
|
||||||
|
details.collapsible summary::before {
|
||||||
|
content: '▸';
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
details.collapsible[open] summary::before {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
details.collapsible .collapsible__body {
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
details.collapsible .collapsible__body strong {
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
details.collapsible .collapsible__body code {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
background: var(--accent-dim);
|
||||||
|
color: var(--accent);
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ RESPONSIVE ============ */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body { padding: 16px; }
|
||||||
|
h1 { font-size: 24px; }
|
||||||
|
.data-table th,
|
||||||
|
.data-table td { padding: 10px 12px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<h1 class="animate" style="--i:0">Requirements Audit</h1>
|
||||||
|
<p class="subtitle animate" style="--i:1">victoria's email vs. implementation plan — point-by-point review</p>
|
||||||
|
|
||||||
|
<div class="kpi-row animate" style="--i:2">
|
||||||
|
<div class="kpi-card">
|
||||||
|
<div class="kpi-card__value">14</div>
|
||||||
|
<div class="kpi-card__label">Items Reviewed</div>
|
||||||
|
</div>
|
||||||
|
<div class="kpi-card">
|
||||||
|
<div class="kpi-card__value kpi-card__value--green">13</div>
|
||||||
|
<div class="kpi-card__label">Match</div>
|
||||||
|
</div>
|
||||||
|
<div class="kpi-card">
|
||||||
|
<div class="kpi-card__value kpi-card__value--red">1</div>
|
||||||
|
<div class="kpi-card__label">Gap</div>
|
||||||
|
</div>
|
||||||
|
<div class="kpi-card">
|
||||||
|
<div class="kpi-card__value" style="color:var(--accent)">93%</div>
|
||||||
|
<div class="kpi-card__label">Coverage</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="legend animate" style="--i:3">
|
||||||
|
<div class="legend-item"><div class="legend-swatch" style="background:var(--green)"></div> Match</div>
|
||||||
|
<div class="legend-item"><div class="legend-swatch" style="background:var(--red)"></div> Gap</div>
|
||||||
|
<div class="legend-item"><div class="legend-swatch" style="background:var(--orange)"></div> Partial</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-wrap animate" style="--i:4">
|
||||||
|
<div class="table-scroll">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="wide">Request</th>
|
||||||
|
<th class="wide">Plan</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr style="--i:4">
|
||||||
|
<td class="wide">Template: <code>yoth-special-edition</code> only</td>
|
||||||
|
<td class="wide">Plan scopes everything to this template.</td>
|
||||||
|
<td><span class="status status--match">Match</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="--i:5">
|
||||||
|
<td class="wide">EOD tomorrow (Friday), Tuesday AM launch</td>
|
||||||
|
<td class="wide">Plan header says same.</td>
|
||||||
|
<td><span class="status status--match">Match</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="--i:6">
|
||||||
|
<td class="wide">Update BIS button label and pop up text</td>
|
||||||
|
<td class="wide">Button text + modal description configurable via settings.</td>
|
||||||
|
<td><span class="status status--match">Match</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="--i:7">
|
||||||
|
<td class="wide">Use Stoq's API to trigger events</td>
|
||||||
|
<td class="wide">Uses <code>openInlineForm</code>, <code>openModal</code>, <code>removeInlineForm</code>.</td>
|
||||||
|
<td><span class="status status--match">Match</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="--i:8">
|
||||||
|
<td class="wide">Custom button + modal with text settings in buy buttons block</td>
|
||||||
|
<td class="wide">Schema settings in <code>buy_buttons</code> block.</td>
|
||||||
|
<td><span class="status status--match">Match</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="--i:9">
|
||||||
|
<td class="wide">Default values = current behavior when empty</td>
|
||||||
|
<td class="wide">Blank settings = Stoq default "Notify Me" behavior.</td>
|
||||||
|
<td><span class="status status--match">Match</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="--i:10">
|
||||||
|
<td class="wide">Only display for OOS variants</td>
|
||||||
|
<td class="wide">DOM-based sold-out detection.</td>
|
||||||
|
<td><span class="status status--match">Match</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="--i:11">
|
||||||
|
<td class="wide">Exclude products with <code>excludebis</code> tag</td>
|
||||||
|
<td class="wide">Checked in both PDP and PLP Liquid.</td>
|
||||||
|
<td><span class="status status--match">Match</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="--i:12">
|
||||||
|
<td class="wide"><code>openInlineForm</code> to load Stoq form in modal</td>
|
||||||
|
<td class="wide">PDP modal uses <code>openInlineForm</code>.</td>
|
||||||
|
<td><span class="status status--match">Match</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="--i:13">
|
||||||
|
<td class="wide">Updated Button Label: "Join the waitlist"</td>
|
||||||
|
<td class="wide">Pre-populated in template JSON.</td>
|
||||||
|
<td><span class="status status--match">Match</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="--i:14">
|
||||||
|
<td class="wide">Updated Pop Up Text: "Sign up to be notified when we restock this or other embroidered styles."</td>
|
||||||
|
<td class="wide"><code>bis_modal_description</code> setting.</td>
|
||||||
|
<td><span class="status status--match">Match</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="--i:15">
|
||||||
|
<td class="wide">Theme: Huha 2.0 - Giddy Up Collection D2C Launch</td>
|
||||||
|
<td class="wide">Clone ID <code>145580556374</code>.</td>
|
||||||
|
<td><span class="status status--match">Match</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="--i:16">
|
||||||
|
<td class="wide">Changes made locally</td>
|
||||||
|
<td class="wide">Local dev + theme push.</td>
|
||||||
|
<td><span class="status status--match">Match</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="--i:17">
|
||||||
|
<td class="wide">Run <code>stoq:restock-modal:submitted</code> when form is submitted</td>
|
||||||
|
<td class="wide">Not mentioned in plan.
|
||||||
|
<small>When using <code>openInlineForm</code> inside a custom modal, unclear if Stoq fires this event automatically or if manual dispatch is needed.</small>
|
||||||
|
</td>
|
||||||
|
<td><span class="status status--gap">Gap</span></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td>14 items reviewed</td>
|
||||||
|
<td></td>
|
||||||
|
<td>13 match · 1 gap</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<details class="collapsible animate" style="--i:19">
|
||||||
|
<summary>Gap Analysis Detail</summary>
|
||||||
|
<div class="collapsible__body">
|
||||||
|
<strong>Gap: <code>stoq:restock-modal:submitted</code> event.</strong>
|
||||||
|
Michael explicitly requests firing this event on form submission. The plan uses Stoq's <code>openInlineForm</code> inside a custom modal, but doesn't address whether Stoq dispatches this event automatically in that context. If other integrations (Klaviyo, analytics, theme JS) listen for it, missing it could silently break the submission pipeline. Recommend adding an explicit <code>dispatchEvent</code> call as a safety net.
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
724
.agents/skills/visual-explainer/templates/mermaid-flowchart.html
Normal file
724
.agents/skills/visual-explainer/templates/mermaid-flowchart.html
Normal file
|
|
@ -0,0 +1,724 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>CI/CD Pipeline — Reference Template</title>
|
||||||
|
<!--
|
||||||
|
Reference template for the visual-explainer skill: Mermaid diagrams.
|
||||||
|
Teal/cyan palette — distinctly different from terracotta (architecture)
|
||||||
|
and rose (data-table) templates so agents absorb variety.
|
||||||
|
Key patterns demonstrated:
|
||||||
|
- Teal/cyan palette (NOT indigo/violet)
|
||||||
|
- Dot-grid background atmosphere (different from radial gradient)
|
||||||
|
- Large heading (38px) for typographic contrast
|
||||||
|
- ESM import of Mermaid + @mermaid-js/layout-elk
|
||||||
|
- Mermaid theme: 'base' + full themeVariables, fontSize: 16px
|
||||||
|
- CSS overrides: .nodeLabel 16px, .edgeLabel 13px
|
||||||
|
- look: 'classic' for clean lines
|
||||||
|
- layout: 'elk' for better node positioning
|
||||||
|
- Zoom controls with scroll-to-zoom and drag-to-pan
|
||||||
|
- Both light and dark themes via prefers-color-scheme
|
||||||
|
- Staggered fade-in, reduced motion respect
|
||||||
|
-->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:wght@400;500;600;700&family=Fragment+Mono:wght@400&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
/* ============ THEME ============ */
|
||||||
|
:root {
|
||||||
|
--font-body: 'Bricolage Grotesque', system-ui, sans-serif;
|
||||||
|
--font-mono: 'Fragment Mono', 'SF Mono', Consolas, monospace;
|
||||||
|
|
||||||
|
--bg: #f0fdfa;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--border: rgba(0, 0, 0, 0.07);
|
||||||
|
--text: #134e4a;
|
||||||
|
--text-dim: #5f8a85;
|
||||||
|
|
||||||
|
--primary: #0d9488;
|
||||||
|
--primary-dim: rgba(13, 148, 136, 0.08);
|
||||||
|
--secondary: #0369a1;
|
||||||
|
--tertiary: #d97706;
|
||||||
|
--danger: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--bg: #042f2e;
|
||||||
|
--surface: #0a3d3a;
|
||||||
|
--border: rgba(255, 255, 255, 0.08);
|
||||||
|
--text: #ccfbf1;
|
||||||
|
--text-dim: #5eead4;
|
||||||
|
|
||||||
|
--primary: #2dd4bf;
|
||||||
|
--primary-dim: rgba(45, 212, 191, 0.14);
|
||||||
|
--secondary: #38bdf8;
|
||||||
|
--tertiary: #fbbf24;
|
||||||
|
--danger: #f87171;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ RESET + BASE ============ */
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--bg);
|
||||||
|
background-image: radial-gradient(circle, var(--border) 1px, transparent 1px);
|
||||||
|
background-size: 24px 24px;
|
||||||
|
color: var(--text);
|
||||||
|
font-family: var(--font-body);
|
||||||
|
padding: 40px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ ANIMATION ============ */
|
||||||
|
@keyframes fadeUp {
|
||||||
|
from { opacity: 0; transform: translateY(12px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate {
|
||||||
|
animation: fadeUp 0.4s ease-out both;
|
||||||
|
animation-delay: calc(var(--i, 0) * 0.06s);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*, *::before, *::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-delay: 0ms !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ LAYOUT ============ */
|
||||||
|
.container {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 38px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: var(--text-dim);
|
||||||
|
margin-bottom: 24px;
|
||||||
|
max-width: 700px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description code {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
background: var(--primary-dim);
|
||||||
|
color: var(--primary);
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ MERMAID CONTAINER ============ */
|
||||||
|
.mermaid-wrap {
|
||||||
|
position: relative;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 32px 24px;
|
||||||
|
overflow: auto;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-controls {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
z-index: 10;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-controls button {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background 0.15s ease, color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-controls button:hover {
|
||||||
|
background: var(--border);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid-wrap { cursor: grab; }
|
||||||
|
.mermaid-wrap.is-panning { cursor: grabbing; user-select: none; }
|
||||||
|
|
||||||
|
/* ============ MULTI-DIAGRAM STRUCTURE ============ */
|
||||||
|
.diagram-shell {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagram-shell__hint {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid-viewport {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid-canvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-label {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
padding: 0 6px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ MERMAID SVG OVERRIDES ============ */
|
||||||
|
.mermaid .nodeLabel {
|
||||||
|
font-family: var(--font-body) !important;
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid .edgeLabel {
|
||||||
|
font-family: var(--font-mono) !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid .node rect,
|
||||||
|
.mermaid .node circle,
|
||||||
|
.mermaid .node polygon {
|
||||||
|
stroke-width: 1.5px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mermaid .edge-pattern-solid {
|
||||||
|
stroke-width: 1.5px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ LEGEND ============ */
|
||||||
|
.legend {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-swatch {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ CALLOUT ============ */
|
||||||
|
.callout {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-left: 3px solid var(--primary);
|
||||||
|
border-radius: 0 10px 10px 0;
|
||||||
|
padding: 16px 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text-dim);
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout strong { color: var(--text); }
|
||||||
|
|
||||||
|
.callout code {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
background: var(--primary-dim);
|
||||||
|
color: var(--primary);
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ RESPONSIVE ============ */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body { padding: 16px; }
|
||||||
|
h1 { font-size: 22px; }
|
||||||
|
.mermaid-wrap { padding: 16px 12px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<h1 class="animate" style="--i:0">CI/CD Pipeline</h1>
|
||||||
|
<p class="subtitle animate" style="--i:1">github actions → staging → production</p>
|
||||||
|
|
||||||
|
<p class="description animate" style="--i:2">
|
||||||
|
Every push to <code>main</code> triggers the full pipeline. Tests and linting run in parallel,
|
||||||
|
then the build step produces a Docker image. Staging deploys automatically; production requires
|
||||||
|
manual approval via a GitHub environment gate.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<section class="diagram-shell animate" style="--i:3">
|
||||||
|
<p class="diagram-shell__hint">
|
||||||
|
Ctrl/Cmd + wheel to zoom. Scroll to pan. Drag to pan when zoomed. Double-click to fit.
|
||||||
|
</p>
|
||||||
|
<div class="mermaid-wrap">
|
||||||
|
<div class="zoom-controls">
|
||||||
|
<button type="button" data-action="zoom-in" title="Zoom in">+</button>
|
||||||
|
<button type="button" data-action="zoom-out" title="Zoom out">−</button>
|
||||||
|
<button type="button" data-action="zoom-fit" title="Smart fit">↺</button>
|
||||||
|
<button type="button" data-action="zoom-one" title="1:1 zoom">1:1</button>
|
||||||
|
<button type="button" data-action="zoom-expand" title="Open full size">⛶</button>
|
||||||
|
<span class="zoom-label">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<div class="mermaid-viewport">
|
||||||
|
<div class="mermaid mermaid-canvas"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script type="text/plain" class="diagram-source">
|
||||||
|
graph TD
|
||||||
|
A[Push to main] --> B{Branch?}
|
||||||
|
B -->|main| C[Run Tests]
|
||||||
|
B -->|feature| I[Run Tests]
|
||||||
|
I --> J[Preview Deploy]
|
||||||
|
C --> D[Lint + Type Check]
|
||||||
|
C --> E[Unit Tests]
|
||||||
|
C --> F[Integration Tests]
|
||||||
|
D --> G[Build Docker Image]
|
||||||
|
E --> G
|
||||||
|
F --> G
|
||||||
|
G --> H[Deploy to Staging]
|
||||||
|
H --> K{Smoke Tests Pass?}
|
||||||
|
K -->|Yes| L[Manual Approval]
|
||||||
|
K -->|No| M[Alert + Rollback]
|
||||||
|
L --> N[Deploy to Production]
|
||||||
|
N --> O[Health Check]
|
||||||
|
O --> P[Done]
|
||||||
|
</script>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="legend animate" style="--i:4">
|
||||||
|
<div class="legend-item"><div class="legend-swatch" style="background:var(--primary)"></div> Automated step</div>
|
||||||
|
<div class="legend-item"><div class="legend-swatch" style="background:var(--tertiary)"></div> Decision gate</div>
|
||||||
|
<div class="legend-item"><div class="legend-swatch" style="background:var(--danger)"></div> Failure path</div>
|
||||||
|
<div class="legend-item"><div class="legend-swatch" style="background:var(--secondary)"></div> Success</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="callout animate" style="--i:5">
|
||||||
|
<strong>ELK layout.</strong> The <code>layout: 'elk'</code> engine provides better node positioning
|
||||||
|
for complex graphs — it requires the separate <code>@mermaid-js/layout-elk</code> package (imported above).
|
||||||
|
Without it, Mermaid silently falls back to dagre.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
||||||
|
import elkLayouts from 'https://cdn.jsdelivr.net/npm/@mermaid-js/layout-elk/dist/mermaid-layout-elk.esm.min.mjs';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
fitPadding: 28,
|
||||||
|
minHeight: 360,
|
||||||
|
maxHeightPx: 960,
|
||||||
|
maxHeightVh: 0.84,
|
||||||
|
maxInitialZoom: 1.8,
|
||||||
|
minZoom: 0.08,
|
||||||
|
maxZoom: 6.5,
|
||||||
|
zoomStep: 0.14,
|
||||||
|
readabilityFloor: 0.58
|
||||||
|
};
|
||||||
|
|
||||||
|
const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
|
||||||
|
|
||||||
|
let activeDrag = null;
|
||||||
|
|
||||||
|
addEventListener('mousemove', (e) => activeDrag?.onMove(e));
|
||||||
|
addEventListener('mouseup', () => {
|
||||||
|
activeDrag?.onEnd();
|
||||||
|
activeDrag = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const isDark = matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
|
||||||
|
mermaid.registerLayoutLoaders(elkLayouts);
|
||||||
|
mermaid.initialize({
|
||||||
|
startOnLoad: false,
|
||||||
|
theme: 'base',
|
||||||
|
look: 'classic',
|
||||||
|
layout: 'elk',
|
||||||
|
themeVariables: {
|
||||||
|
fontFamily: "'Bricolage Grotesque', system-ui, sans-serif",
|
||||||
|
fontSize: '16px',
|
||||||
|
primaryColor: isDark ? '#115e59' : '#ccfbf1',
|
||||||
|
primaryBorderColor: isDark ? '#2dd4bf' : '#0d9488',
|
||||||
|
primaryTextColor: isDark ? '#ccfbf1' : '#134e4a',
|
||||||
|
secondaryColor: isDark ? '#0c4a6e' : '#e0f2fe',
|
||||||
|
secondaryBorderColor: isDark ? '#38bdf8' : '#0369a1',
|
||||||
|
secondaryTextColor: isDark ? '#ccfbf1' : '#134e4a',
|
||||||
|
tertiaryColor: isDark ? '#2e2618' : '#fffbeb',
|
||||||
|
tertiaryBorderColor: isDark ? '#fbbf24' : '#d97706',
|
||||||
|
tertiaryTextColor: isDark ? '#ccfbf1' : '#134e4a',
|
||||||
|
lineColor: isDark ? '#5eead4' : '#5f8a85',
|
||||||
|
noteBkgColor: isDark ? '#115e59' : '#fefce8',
|
||||||
|
noteTextColor: isDark ? '#ccfbf1' : '#134e4a',
|
||||||
|
noteBorderColor: isDark ? '#fbbf24' : '#d97706',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function initDiagram(shell) {
|
||||||
|
const wrap = shell.querySelector('.mermaid-wrap');
|
||||||
|
const viewport = shell.querySelector('.mermaid-viewport');
|
||||||
|
const canvas = shell.querySelector('.mermaid-canvas');
|
||||||
|
const source = shell.querySelector('.diagram-source');
|
||||||
|
const label = shell.querySelector('.zoom-label');
|
||||||
|
|
||||||
|
if (!wrap || !viewport || !canvas || !source || !label) {
|
||||||
|
console.error('initDiagram: missing required elements in', shell);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let zoom = 1;
|
||||||
|
let fitMode = 'contain';
|
||||||
|
let panX = 0;
|
||||||
|
let panY = 0;
|
||||||
|
let svgW = 0;
|
||||||
|
let svgH = 0;
|
||||||
|
|
||||||
|
let sx = 0;
|
||||||
|
let sy = 0;
|
||||||
|
let spx = 0;
|
||||||
|
let spy = 0;
|
||||||
|
|
||||||
|
let touchDist = 0;
|
||||||
|
let touchCx = 0;
|
||||||
|
let touchCy = 0;
|
||||||
|
|
||||||
|
function constrainPan() {
|
||||||
|
const vpW = viewport.clientWidth;
|
||||||
|
const vpH = viewport.clientHeight;
|
||||||
|
const rW = svgW * zoom;
|
||||||
|
const rH = svgH * zoom;
|
||||||
|
const pad = config.fitPadding;
|
||||||
|
|
||||||
|
panX = (rW + pad * 2 <= vpW)
|
||||||
|
? (vpW - rW) / 2
|
||||||
|
: clamp(panX, vpW - rW - pad, pad);
|
||||||
|
panY = (rH + pad * 2 <= vpH)
|
||||||
|
? (vpH - rH) / 2
|
||||||
|
: clamp(panY, vpH - rH - pad, pad);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTransform() {
|
||||||
|
const svg = canvas.querySelector('svg');
|
||||||
|
if (!svg || !svgW) return;
|
||||||
|
|
||||||
|
constrainPan();
|
||||||
|
svg.style.width = (svgW * zoom) + 'px';
|
||||||
|
svg.style.height = (svgH * zoom) + 'px';
|
||||||
|
canvas.style.transform = `translate(${panX}px, ${panY}px)`;
|
||||||
|
label.textContent = Math.round(zoom * 100) + '% \u2014 ' + fitMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canPan() {
|
||||||
|
const rW = svgW * zoom;
|
||||||
|
const rH = svgH * zoom;
|
||||||
|
return rW + config.fitPadding * 2 > viewport.clientWidth
|
||||||
|
|| rH + config.fitPadding * 2 > viewport.clientHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeSmartFit() {
|
||||||
|
const vpW = viewport.clientWidth;
|
||||||
|
const vpH = viewport.clientHeight;
|
||||||
|
const aW = Math.max(80, vpW - config.fitPadding * 2);
|
||||||
|
const aH = Math.max(80, vpH - config.fitPadding * 2);
|
||||||
|
const contain = Math.min(aW / svgW, aH / svgH);
|
||||||
|
|
||||||
|
let z = contain;
|
||||||
|
let mode = 'contain';
|
||||||
|
if (contain < config.readabilityFloor) {
|
||||||
|
const chartR = svgH / svgW;
|
||||||
|
const vpR = vpH / Math.max(vpW, 1);
|
||||||
|
if (chartR >= vpR) {
|
||||||
|
z = aW / svgW;
|
||||||
|
mode = 'width-priority';
|
||||||
|
} else {
|
||||||
|
z = aH / svgH;
|
||||||
|
mode = 'height-priority';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { zoom: clamp(z, config.minZoom, config.maxInitialZoom), mode };
|
||||||
|
}
|
||||||
|
|
||||||
|
function fitDiagram() {
|
||||||
|
if (!svgW) return;
|
||||||
|
const fit = computeSmartFit();
|
||||||
|
zoom = fit.zoom;
|
||||||
|
fitMode = fit.mode;
|
||||||
|
panX = (viewport.clientWidth - svgW * zoom) / 2;
|
||||||
|
panY = (viewport.clientHeight - svgH * zoom) / 2;
|
||||||
|
applyTransform();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOneToOne() {
|
||||||
|
zoom = clamp(1, config.minZoom, config.maxZoom);
|
||||||
|
fitMode = '1:1';
|
||||||
|
panX = (viewport.clientWidth - svgW * zoom) / 2;
|
||||||
|
panY = (viewport.clientHeight - svgH * zoom) / 2;
|
||||||
|
applyTransform();
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoomAround(factor, cx, cy) {
|
||||||
|
const next = clamp(zoom * factor, config.minZoom, config.maxZoom);
|
||||||
|
const ratio = next / zoom;
|
||||||
|
panX = cx - ratio * (cx - panX);
|
||||||
|
panY = cy - ratio * (cy - panY);
|
||||||
|
zoom = next;
|
||||||
|
fitMode = 'custom';
|
||||||
|
applyTransform();
|
||||||
|
}
|
||||||
|
|
||||||
|
function readSvgNaturalSize(svg) {
|
||||||
|
let w = 0;
|
||||||
|
let h = 0;
|
||||||
|
if (svg.viewBox?.baseVal?.width > 0) {
|
||||||
|
w = svg.viewBox.baseVal.width;
|
||||||
|
h = svg.viewBox.baseVal.height;
|
||||||
|
}
|
||||||
|
if (!w) {
|
||||||
|
w = parseFloat(svg.getAttribute('width')) || 0;
|
||||||
|
h = parseFloat(svg.getAttribute('height')) || 0;
|
||||||
|
}
|
||||||
|
if (!w) {
|
||||||
|
const b = svg.getBBox();
|
||||||
|
w = b.width;
|
||||||
|
h = b.height;
|
||||||
|
}
|
||||||
|
if (!w) {
|
||||||
|
const r = svg.getBoundingClientRect();
|
||||||
|
w = r.width || 1000;
|
||||||
|
h = r.height || 700;
|
||||||
|
}
|
||||||
|
if (!svg.getAttribute('viewBox')) {
|
||||||
|
svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
|
||||||
|
}
|
||||||
|
return { w, h };
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAdaptiveHeight() {
|
||||||
|
if (!svgW) return;
|
||||||
|
const usableW = Math.max(280, wrap.getBoundingClientRect().width - 2);
|
||||||
|
const idealH = (svgH / svgW) * usableW + config.fitPadding * 2;
|
||||||
|
const maxVp = Math.floor(innerHeight * config.maxHeightVh);
|
||||||
|
const hardMax = Math.min(config.maxHeightPx, Math.max(config.minHeight + 40, maxVp));
|
||||||
|
wrap.style.height = Math.round(clamp(idealH, config.minHeight, hardMax)) + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
function openInNewTab() {
|
||||||
|
const svg = canvas.querySelector('svg');
|
||||||
|
if (!svg) return;
|
||||||
|
|
||||||
|
const clone = svg.cloneNode(true);
|
||||||
|
clone.style.width = '';
|
||||||
|
clone.style.height = '';
|
||||||
|
|
||||||
|
// Use the same isDark value that was used to render the Mermaid theme
|
||||||
|
// This ensures the background matches the baked-in SVG colors
|
||||||
|
const bg = isDark ? '#042f2e' : '#f0fdfa';
|
||||||
|
|
||||||
|
const html = `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Diagram</title><style>
|
||||||
|
body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;
|
||||||
|
background:${bg};padding:40px;box-sizing:border-box}
|
||||||
|
svg{max-width:100%;max-height:90vh;height:auto}
|
||||||
|
</style></head><body>${clone.outerHTML}</body></html>`;
|
||||||
|
|
||||||
|
open(URL.createObjectURL(new Blob([html], { type: 'text/html' })), '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function render() {
|
||||||
|
try {
|
||||||
|
const code = source.textContent.trim();
|
||||||
|
if (!code) {
|
||||||
|
label.textContent = 'Error: Empty source';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = 'diagram-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8);
|
||||||
|
const { svg } = await mermaid.render(id, code);
|
||||||
|
canvas.innerHTML = svg;
|
||||||
|
|
||||||
|
const svgNode = canvas.querySelector('svg');
|
||||||
|
if (!svgNode) {
|
||||||
|
label.textContent = 'Error: No SVG';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size = readSvgNaturalSize(svgNode);
|
||||||
|
svgW = size.w;
|
||||||
|
svgH = size.h;
|
||||||
|
|
||||||
|
svgNode.removeAttribute('width');
|
||||||
|
svgNode.removeAttribute('height');
|
||||||
|
svgNode.style.maxWidth = 'none';
|
||||||
|
svgNode.style.display = 'block';
|
||||||
|
|
||||||
|
setAdaptiveHeight();
|
||||||
|
fitDiagram();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Mermaid render failed:', err);
|
||||||
|
label.textContent = 'Error: ' + (err.message || 'Render failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
'zoom-in': () => zoomAround(1 + config.zoomStep, viewport.clientWidth / 2, viewport.clientHeight / 2),
|
||||||
|
'zoom-out': () => zoomAround(1 / (1 + config.zoomStep), viewport.clientWidth / 2, viewport.clientHeight / 2),
|
||||||
|
'zoom-fit': fitDiagram,
|
||||||
|
'zoom-one': setOneToOne,
|
||||||
|
'zoom-expand': openInNewTab
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(actions).forEach(([action, handler]) => {
|
||||||
|
wrap.querySelector(`[data-action="${action}"]`)?.addEventListener('click', handler);
|
||||||
|
});
|
||||||
|
|
||||||
|
viewport.addEventListener('dblclick', fitDiagram);
|
||||||
|
|
||||||
|
viewport.addEventListener('wheel', (e) => {
|
||||||
|
if (e.ctrlKey || e.metaKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
const rect = viewport.getBoundingClientRect();
|
||||||
|
const factor = e.deltaY < 0 ? 1 + config.zoomStep : 1 / (1 + config.zoomStep);
|
||||||
|
zoomAround(factor, e.clientX - rect.left, e.clientY - rect.top);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (canPan()) {
|
||||||
|
e.preventDefault();
|
||||||
|
panX -= e.deltaX;
|
||||||
|
panY -= e.deltaY;
|
||||||
|
applyTransform();
|
||||||
|
}
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
|
viewport.addEventListener('mousedown', (e) => {
|
||||||
|
if (e.target.closest('.zoom-controls') || !canPan()) return;
|
||||||
|
wrap.classList.add('is-panning');
|
||||||
|
sx = e.clientX;
|
||||||
|
sy = e.clientY;
|
||||||
|
spx = panX;
|
||||||
|
spy = panY;
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
activeDrag = {
|
||||||
|
onMove: (ev) => {
|
||||||
|
panX = spx + (ev.clientX - sx);
|
||||||
|
panY = spy + (ev.clientY - sy);
|
||||||
|
applyTransform();
|
||||||
|
},
|
||||||
|
onEnd: () => {
|
||||||
|
wrap.classList.remove('is-panning');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
viewport.addEventListener('touchstart', (e) => {
|
||||||
|
if (e.touches.length === 1) {
|
||||||
|
sx = e.touches[0].clientX;
|
||||||
|
sy = e.touches[0].clientY;
|
||||||
|
spx = panX;
|
||||||
|
spy = panY;
|
||||||
|
} else if (e.touches.length === 2) {
|
||||||
|
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
||||||
|
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
||||||
|
touchDist = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
const r = viewport.getBoundingClientRect();
|
||||||
|
touchCx = (e.touches[0].clientX + e.touches[1].clientX) / 2 - r.left;
|
||||||
|
touchCy = (e.touches[0].clientY + e.touches[1].clientY) / 2 - r.top;
|
||||||
|
}
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
viewport.addEventListener('touchmove', (e) => {
|
||||||
|
if (e.touches.length === 1 && canPan()) {
|
||||||
|
if (touchDist > 0) {
|
||||||
|
sx = e.touches[0].clientX;
|
||||||
|
sy = e.touches[0].clientY;
|
||||||
|
spx = panX;
|
||||||
|
spy = panY;
|
||||||
|
touchDist = 0;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
panX = spx + (e.touches[0].clientX - sx);
|
||||||
|
panY = spy + (e.touches[0].clientY - sy);
|
||||||
|
applyTransform();
|
||||||
|
} else if (e.touches.length === 2 && touchDist > 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
||||||
|
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
||||||
|
const d = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
zoomAround(d / touchDist, touchCx, touchCy);
|
||||||
|
touchDist = d;
|
||||||
|
}
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
|
new ResizeObserver(() => {
|
||||||
|
if (svgW) {
|
||||||
|
setAdaptiveHeight();
|
||||||
|
fitDiagram();
|
||||||
|
}
|
||||||
|
}).observe(wrap);
|
||||||
|
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.diagram-shell').forEach(initDiagram);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
916
.agents/skills/visual-explainer/templates/slide-deck.html
Normal file
916
.agents/skills/visual-explainer/templates/slide-deck.html
Normal file
|
|
@ -0,0 +1,916 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>API Gateway Redesign — Reference Slide Deck</title>
|
||||||
|
<!--
|
||||||
|
Reference template for the visual-explainer skill: slide decks.
|
||||||
|
Midnight Editorial preset — deep navy, serif display, warm gold accents.
|
||||||
|
Distinctly different from the terracotta (architecture), teal (mermaid),
|
||||||
|
and rose (data-table) templates so agents absorb variety.
|
||||||
|
Key patterns demonstrated:
|
||||||
|
- All 10 slide types in a cohesive narrative
|
||||||
|
- SlideEngine JS: keyboard/touch/wheel nav, progress bar, dots, counter, hints
|
||||||
|
- Cinematic transitions: fade + translateY + scale, staggered child reveals
|
||||||
|
- Per-slide background variation (gradient direction, accent glow position)
|
||||||
|
- Decorative SVG accents (corner marks, quote mark, divider)
|
||||||
|
- Typography scale: 120px display → 48px heading → 22px body → 14px label
|
||||||
|
- Compositional variety: centered, left-heavy, split, full-bleed
|
||||||
|
- Mermaid at presentation scale (18px labels, 2px edges, 8 nodes)
|
||||||
|
- Dark-first with light mode via prefers-color-scheme
|
||||||
|
- Responsive height breakpoints for projection and small viewports
|
||||||
|
- Nav chrome with backdrop blur for mixed-background visibility
|
||||||
|
- Event delegation: Mermaid zoom and table scroll don't trigger slide nav
|
||||||
|
- prefers-reduced-motion respected
|
||||||
|
-->
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
/* ============ THEME: Midnight Editorial ============ */
|
||||||
|
:root {
|
||||||
|
--font-body: 'Instrument Serif', Georgia, serif;
|
||||||
|
--font-mono: 'JetBrains Mono', 'SF Mono', Consolas, monospace;
|
||||||
|
--bg: #0f1729;
|
||||||
|
--surface: #162040;
|
||||||
|
--surface2: #1d2b52;
|
||||||
|
--surface-elevated: #243362;
|
||||||
|
--border: rgba(200, 180, 140, 0.08);
|
||||||
|
--border-bright: rgba(200, 180, 140, 0.16);
|
||||||
|
--text: #e8e4d8;
|
||||||
|
--text-dim: #9a9484;
|
||||||
|
--accent: #d4a73a;
|
||||||
|
--accent-dim: rgba(212, 167, 58, 0.1);
|
||||||
|
--code-bg: #0a0f1e;
|
||||||
|
--code-text: #d4d0c4;
|
||||||
|
--green: #4ade80;
|
||||||
|
--green-dim: rgba(74, 222, 128, 0.1);
|
||||||
|
--red: #f87171;
|
||||||
|
--red-dim: rgba(248, 113, 113, 0.1);
|
||||||
|
--blue: #60a5fa;
|
||||||
|
--blue-dim: rgba(96, 165, 250, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
--bg: #faf8f2;
|
||||||
|
--surface: #ffffff;
|
||||||
|
--surface2: #f5f0e6;
|
||||||
|
--surface-elevated: #fffdf5;
|
||||||
|
--border: rgba(30, 30, 50, 0.08);
|
||||||
|
--border-bright: rgba(30, 30, 50, 0.16);
|
||||||
|
--text: #1a1814;
|
||||||
|
--text-dim: #7a7468;
|
||||||
|
--accent: #b8860b;
|
||||||
|
--accent-dim: rgba(184, 134, 11, 0.08);
|
||||||
|
--code-bg: #2a2520;
|
||||||
|
--code-text: #e8e4d8;
|
||||||
|
--green: #16a34a;
|
||||||
|
--green-dim: rgba(22, 163, 74, 0.08);
|
||||||
|
--red: #dc2626;
|
||||||
|
--red-dim: rgba(220, 38, 38, 0.08);
|
||||||
|
--blue: #2563eb;
|
||||||
|
--blue-dim: rgba(37, 99, 235, 0.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font-body);
|
||||||
|
color: var(--text);
|
||||||
|
background: var(--bg);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ DECK ENGINE ============ */
|
||||||
|
.deck {
|
||||||
|
height: 100dvh;
|
||||||
|
overflow-y: auto;
|
||||||
|
scroll-snap-type: y mandatory;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide {
|
||||||
|
height: 100dvh;
|
||||||
|
scroll-snap-align: start;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
padding: clamp(40px, 6vh, 80px) clamp(40px, 8vw, 120px);
|
||||||
|
isolation: isolate;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(40px) scale(0.98);
|
||||||
|
transition:
|
||||||
|
opacity 0.6s cubic-bezier(0.16, 1, 0.3, 1),
|
||||||
|
transform 0.6s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide.visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide .reveal {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
transition:
|
||||||
|
opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1),
|
||||||
|
transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide.visible .reveal { opacity: 1; transform: none; }
|
||||||
|
.slide.visible .reveal:nth-child(1) { transition-delay: 0.1s; }
|
||||||
|
.slide.visible .reveal:nth-child(2) { transition-delay: 0.2s; }
|
||||||
|
.slide.visible .reveal:nth-child(3) { transition-delay: 0.3s; }
|
||||||
|
.slide.visible .reveal:nth-child(4) { transition-delay: 0.4s; }
|
||||||
|
.slide.visible .reveal:nth-child(5) { transition-delay: 0.5s; }
|
||||||
|
.slide.visible .reveal:nth-child(6) { transition-delay: 0.6s; }
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.slide, .slide .reveal {
|
||||||
|
opacity: 1 !important;
|
||||||
|
transform: none !important;
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ NAV CHROME ============ */
|
||||||
|
.deck-progress {
|
||||||
|
position: fixed; top: 0; left: 0; height: 3px;
|
||||||
|
background: var(--accent); z-index: 100;
|
||||||
|
transition: width 0.3s ease; pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-dots {
|
||||||
|
position: fixed; right: clamp(12px, 2vw, 24px); top: 50%;
|
||||||
|
transform: translateY(-50%); display: flex; flex-direction: column;
|
||||||
|
gap: 8px; z-index: 100; padding: 8px;
|
||||||
|
background: color-mix(in srgb, var(--bg) 60%, transparent 40%);
|
||||||
|
border-radius: 20px;
|
||||||
|
backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-dot {
|
||||||
|
width: 8px; height: 8px; border-radius: 50%;
|
||||||
|
background: var(--text-dim); opacity: 0.3; border: none; padding: 0;
|
||||||
|
cursor: pointer; transition: opacity 0.2s ease, transform 0.2s ease;
|
||||||
|
}
|
||||||
|
.deck-dot:hover { opacity: 0.6; }
|
||||||
|
.deck-dot.active { opacity: 1; transform: scale(1.5); background: var(--accent); }
|
||||||
|
|
||||||
|
.deck-counter {
|
||||||
|
position: fixed; bottom: clamp(12px, 2vh, 24px); right: clamp(12px, 2vw, 24px);
|
||||||
|
font-family: var(--font-mono); font-size: 12px; color: var(--text-dim);
|
||||||
|
z-index: 100; font-variant-numeric: tabular-nums;
|
||||||
|
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deck-hints {
|
||||||
|
position: fixed; bottom: clamp(12px, 2vh, 24px); left: 50%;
|
||||||
|
transform: translateX(-50%); font-family: var(--font-mono);
|
||||||
|
font-size: 11px; color: var(--text-dim); opacity: 0.6; z-index: 100;
|
||||||
|
transition: opacity 0.5s ease; white-space: nowrap;
|
||||||
|
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
.deck-hints.faded { opacity: 0; pointer-events: none; }
|
||||||
|
|
||||||
|
/* ============ SHARED SLIDE ELEMENTS ============ */
|
||||||
|
.slide__display {
|
||||||
|
font-size: clamp(48px, 10vw, 120px);
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: -2px;
|
||||||
|
line-height: 0.95;
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide__heading {
|
||||||
|
font-size: clamp(28px, 5vw, 48px);
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
line-height: 1.15;
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide__body {
|
||||||
|
font-size: clamp(16px, 2.2vw, 22px);
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text-dim);
|
||||||
|
text-wrap: pretty;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide__subtitle {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: clamp(12px, 1.5vw, 18px);
|
||||||
|
color: var(--text-dim);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide__label {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: clamp(10px, 1.2vw, 13px);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
color: var(--accent);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ DECORATIVE SVG ============ */
|
||||||
|
.slide__decor {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ SLIDE TYPE: TITLE ============ */
|
||||||
|
.slide--title {
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
background-image: radial-gradient(ellipse at 50% 30%, var(--accent-dim) 0%, transparent 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide--title .slide__display { color: var(--accent); }
|
||||||
|
|
||||||
|
/* ============ SLIDE TYPE: DIVIDER ============ */
|
||||||
|
.slide--divider { justify-content: center; }
|
||||||
|
|
||||||
|
.slide--divider .slide__number {
|
||||||
|
font-size: clamp(100px, 22vw, 260px);
|
||||||
|
font-weight: 200;
|
||||||
|
line-height: 0.85;
|
||||||
|
opacity: 0.06;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%; left: 50%;
|
||||||
|
transform: translate(-50%, -55%);
|
||||||
|
pointer-events: none;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ SLIDE TYPE: CONTENT ============ */
|
||||||
|
.slide--content .slide__inner {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 3fr 2fr;
|
||||||
|
gap: clamp(24px, 4vw, 60px);
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide__bullets {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide__bullets li {
|
||||||
|
padding: 10px 0 10px 24px;
|
||||||
|
position: relative;
|
||||||
|
font-size: clamp(16px, 2vw, 22px);
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide__bullets li::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0; top: 20px;
|
||||||
|
width: 6px; height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide__aside {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ SLIDE TYPE: SPLIT ============ */
|
||||||
|
.slide--split { padding: 0; }
|
||||||
|
|
||||||
|
.slide--split .slide__panels {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 3fr 2fr;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide--split .slide__panel {
|
||||||
|
padding: clamp(40px, 6vh, 80px) clamp(32px, 4vw, 60px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide--split .slide__panel--primary { background: var(--surface); }
|
||||||
|
.slide--split .slide__panel--secondary {
|
||||||
|
background: var(--surface2);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ SLIDE TYPE: DIAGRAM ============ */
|
||||||
|
.slide--diagram {
|
||||||
|
padding: clamp(24px, 4vh, 48px) clamp(24px, 4vw, 60px);
|
||||||
|
}
|
||||||
|
.slide--diagram .slide__heading { margin-bottom: clamp(8px, 1.5vh, 20px); }
|
||||||
|
|
||||||
|
.mermaid-wrap {
|
||||||
|
position: relative;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
overflow: auto;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--border) transparent;
|
||||||
|
}
|
||||||
|
.mermaid-wrap::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||||
|
.mermaid-wrap::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.zoom-controls {
|
||||||
|
position: absolute; top: 8px; right: 8px;
|
||||||
|
display: flex; gap: 2px; z-index: 10;
|
||||||
|
background: var(--surface); border: 1px solid var(--border);
|
||||||
|
border-radius: 6px; padding: 2px;
|
||||||
|
}
|
||||||
|
.zoom-controls button {
|
||||||
|
width: 28px; height: 28px; border: none; background: transparent;
|
||||||
|
color: var(--text-dim); font-family: var(--font-mono); font-size: 14px;
|
||||||
|
cursor: pointer; border-radius: 4px;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
transition: background 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
.zoom-controls button:hover { background: var(--border); color: var(--text); }
|
||||||
|
.mermaid-wrap { cursor: grab; }
|
||||||
|
.mermaid-wrap.is-panning { cursor: grabbing; user-select: none; }
|
||||||
|
|
||||||
|
.mermaid .nodeLabel { color: var(--text) !important; }
|
||||||
|
.mermaid .edgeLabel { color: var(--text-dim) !important; background-color: var(--bg) !important; }
|
||||||
|
.mermaid .edgeLabel rect { fill: var(--bg) !important; }
|
||||||
|
.slide--diagram .mermaid svg {
|
||||||
|
width: 100% !important;
|
||||||
|
height: auto !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide--diagram .mermaid .nodeLabel { font-size: 18px !important; }
|
||||||
|
.slide--diagram .mermaid .edgeLabel { font-family: var(--font-mono) !important; font-size: 14px !important; }
|
||||||
|
.slide--diagram .mermaid .node rect,
|
||||||
|
.slide--diagram .mermaid .node circle,
|
||||||
|
.slide--diagram .mermaid .node polygon { stroke-width: 2px; }
|
||||||
|
.slide--diagram .mermaid .edge-pattern-solid { stroke-width: 2px; }
|
||||||
|
|
||||||
|
/* ============ SLIDE TYPE: DASHBOARD ============ */
|
||||||
|
.slide--dashboard .slide__kpis {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(clamp(140px, 20vw, 220px), 1fr));
|
||||||
|
gap: clamp(12px, 2vw, 24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide__kpi {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: clamp(16px, 3vh, 32px) clamp(16px, 2vw, 24px);
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide__kpi-val {
|
||||||
|
font-size: clamp(36px, 6vw, 64px);
|
||||||
|
font-weight: 400;
|
||||||
|
letter-spacing: -1.5px;
|
||||||
|
line-height: 1.1;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide__kpi-label {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: clamp(9px, 1.2vw, 13px);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide__kpi-trend {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ SLIDE TYPE: TABLE ============ */
|
||||||
|
.slide--table { padding: clamp(24px, 4vh, 48px) clamp(24px, 4vw, 60px); }
|
||||||
|
|
||||||
|
.table-wrap {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.table-scroll { overflow-x: auto; }
|
||||||
|
|
||||||
|
.data-table { width: 100%; border-collapse: collapse; }
|
||||||
|
|
||||||
|
.data-table th {
|
||||||
|
background: var(--surface2);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: clamp(10px, 1.3vw, 14px);
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
text-align: left;
|
||||||
|
padding: clamp(10px, 1.5vh, 16px) clamp(14px, 2vw, 24px);
|
||||||
|
border-bottom: 2px solid var(--border-bright);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table td {
|
||||||
|
padding: clamp(10px, 1.5vh, 16px) clamp(14px, 2vw, 24px);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
font-size: clamp(14px, 1.8vw, 20px);
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
.data-table tbody tr:last-child td { border-bottom: none; }
|
||||||
|
.data-table tbody tr:nth-child(even) { background: var(--surface2); }
|
||||||
|
.data-table tbody tr { transition: background 0.15s; }
|
||||||
|
.data-table tbody tr:hover { background: var(--accent-dim); }
|
||||||
|
.data-table code {
|
||||||
|
font-family: var(--font-mono); font-size: 0.85em;
|
||||||
|
background: var(--accent-dim); color: var(--accent);
|
||||||
|
padding: 1px 5px; border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ SLIDE TYPE: CODE ============ */
|
||||||
|
.slide--code { align-items: center; }
|
||||||
|
|
||||||
|
.slide__code-block {
|
||||||
|
background: var(--code-bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: clamp(24px, 4vh, 48px) clamp(24px, 4vw, 48px);
|
||||||
|
max-width: 900px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide__code-filename {
|
||||||
|
position: absolute;
|
||||||
|
top: -12px; left: 24px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px; font-weight: 600;
|
||||||
|
padding: 4px 12px; border-radius: 4px;
|
||||||
|
background: var(--accent); color: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide__code-block pre { margin: 0; overflow-x: auto; }
|
||||||
|
.slide__code-block code {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: clamp(14px, 1.6vw, 18px);
|
||||||
|
line-height: 1.7;
|
||||||
|
color: var(--code-text);
|
||||||
|
}
|
||||||
|
.slide__code-block .hl { color: var(--accent); }
|
||||||
|
.slide__code-block .cm { color: var(--text-dim); }
|
||||||
|
|
||||||
|
/* ============ SLIDE TYPE: QUOTE ============ */
|
||||||
|
.slide--quote {
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: clamp(60px, 10vh, 120px) clamp(60px, 12vw, 200px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide__quote-mark {
|
||||||
|
font-size: clamp(80px, 14vw, 180px);
|
||||||
|
line-height: 0.5;
|
||||||
|
opacity: 0.06;
|
||||||
|
font-family: Georgia, serif;
|
||||||
|
pointer-events: none;
|
||||||
|
margin-bottom: -20px;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide--quote blockquote {
|
||||||
|
font-size: clamp(24px, 4vw, 48px);
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.35;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide--quote cite {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: clamp(11px, 1.4vw, 14px);
|
||||||
|
font-style: normal;
|
||||||
|
margin-top: clamp(16px, 3vh, 32px);
|
||||||
|
display: block;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============ SLIDE TYPE: FULL-BLEED ============ */
|
||||||
|
.slide--bleed {
|
||||||
|
padding: 0;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide__bg {
|
||||||
|
position: absolute; inset: 0;
|
||||||
|
background-size: cover; background-position: center;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide__bg--gradient {
|
||||||
|
background: linear-gradient(135deg, #1a0f3c 0%, #0f1729 40%, #162040 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide__scrim {
|
||||||
|
position: absolute; inset: 0;
|
||||||
|
background: linear-gradient(to top, rgba(0,0,0,0.75) 0%, rgba(0,0,0,0.2) 40%, transparent 100%);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide--bleed .slide__content {
|
||||||
|
position: relative; z-index: 2;
|
||||||
|
padding: clamp(40px, 6vh, 80px) clamp(40px, 8vw, 120px);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.slide--bleed .slide__heading { color: #ffffff; }
|
||||||
|
.slide--bleed .slide__subtitle { color: rgba(255,255,255,0.7); }
|
||||||
|
|
||||||
|
/* ============ RESPONSIVE ============ */
|
||||||
|
@media (max-height: 700px) {
|
||||||
|
.slide { padding: clamp(24px, 4vh, 40px) clamp(32px, 6vw, 80px); }
|
||||||
|
.slide__display { font-size: clamp(36px, 8vw, 72px); }
|
||||||
|
.slide--divider .slide__number { font-size: clamp(80px, 16vw, 160px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-height: 600px) {
|
||||||
|
.slide__decor { display: none; }
|
||||||
|
.slide--quote { padding: clamp(32px, 6vh, 60px) clamp(40px, 8vw, 100px); }
|
||||||
|
.slide__quote-mark { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-height: 500px) {
|
||||||
|
.slide { padding: clamp(16px, 3vh, 24px) clamp(24px, 5vw, 48px); }
|
||||||
|
.deck-dots { display: none; }
|
||||||
|
.slide__display { font-size: clamp(28px, 7vw, 48px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.slide--content .slide__inner { grid-template-columns: 1fr; }
|
||||||
|
.slide--content .slide__aside { display: none; }
|
||||||
|
.slide--split .slide__panels { grid-template-columns: 1fr; }
|
||||||
|
.slide--dashboard .slide__kpis { grid-template-columns: repeat(2, 1fr); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="deck">
|
||||||
|
|
||||||
|
<!-- SLIDE 1: TITLE -->
|
||||||
|
<section class="slide slide--title">
|
||||||
|
<svg class="slide__decor" style="top:0;right:0;" width="120" height="120" viewBox="0 0 120 120">
|
||||||
|
<line x1="120" y1="0" x2="120" y2="40" stroke="var(--accent)" stroke-width="2" opacity="0.15"/>
|
||||||
|
<line x1="80" y1="0" x2="120" y2="0" stroke="var(--accent)" stroke-width="2" opacity="0.15"/>
|
||||||
|
</svg>
|
||||||
|
<svg class="slide__decor" style="bottom:0;left:0;" width="120" height="120" viewBox="0 0 120 120">
|
||||||
|
<line x1="0" y1="80" x2="0" y2="120" stroke="var(--accent)" stroke-width="2" opacity="0.15"/>
|
||||||
|
<line x1="0" y1="120" x2="40" y2="120" stroke="var(--accent)" stroke-width="2" opacity="0.15"/>
|
||||||
|
</svg>
|
||||||
|
<div class="reveal">
|
||||||
|
<p class="slide__subtitle" style="margin-bottom:clamp(16px,2vh,32px);">Engineering Review — Q1 2026</p>
|
||||||
|
</div>
|
||||||
|
<h1 class="slide__display reveal">API Gateway Redesign</h1>
|
||||||
|
<div class="reveal">
|
||||||
|
<p class="slide__subtitle" style="margin-top:clamp(16px,2vh,32px);">From monolith proxy to edge-native routing</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- SLIDE 2: SECTION DIVIDER -->
|
||||||
|
<section class="slide slide--divider" style="background-image:radial-gradient(ellipse at 80% 60%, var(--accent-dim) 0%, transparent 40%);">
|
||||||
|
<span class="slide__number">01</span>
|
||||||
|
<div>
|
||||||
|
<h2 class="slide__heading reveal">The Problem</h2>
|
||||||
|
<p class="slide__subtitle reveal" style="margin-top:12px;">Why the current gateway can't scale</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- SLIDE 3: CONTENT (left-heavy asymmetric) -->
|
||||||
|
<section class="slide slide--content" style="background-image:radial-gradient(ellipse at 20% 80%, var(--accent-dim) 0%, transparent 45%);">
|
||||||
|
<div class="slide__inner">
|
||||||
|
<div>
|
||||||
|
<p class="slide__label reveal">Current State</p>
|
||||||
|
<h2 class="slide__heading reveal">Single Point of Failure</h2>
|
||||||
|
<ul class="slide__bullets">
|
||||||
|
<li class="reveal">All traffic routes through one Node.js process</li>
|
||||||
|
<li class="reveal">Rate limiting is per-instance, not distributed</li>
|
||||||
|
<li class="reveal">Auth validation adds 40ms per request</li>
|
||||||
|
<li class="reveal">No circuit breaking — cascade failures hit everything</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="slide__aside reveal">
|
||||||
|
<svg viewBox="0 0 160 160" width="160" height="160">
|
||||||
|
<circle cx="80" cy="80" r="60" fill="none" stroke="var(--red)" stroke-width="2" opacity="0.3"/>
|
||||||
|
<circle cx="80" cy="80" r="40" fill="none" stroke="var(--red)" stroke-width="1.5" opacity="0.2" stroke-dasharray="4 4"/>
|
||||||
|
<circle cx="80" cy="80" r="8" fill="var(--red)" opacity="0.4"/>
|
||||||
|
<text x="80" y="130" text-anchor="middle" font-family="var(--font-mono)" font-size="11" fill="var(--text-dim)">SINGLE PROCESS</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- SLIDE 4: SPLIT (before/after) -->
|
||||||
|
<section class="slide slide--split">
|
||||||
|
<div class="slide__panels">
|
||||||
|
<div class="slide__panel slide__panel--primary">
|
||||||
|
<p class="slide__label reveal" style="color:var(--red);">Before</p>
|
||||||
|
<h2 class="slide__heading reveal" style="font-size:clamp(22px,3.5vw,36px);">Monolith Proxy</h2>
|
||||||
|
<ul class="slide__bullets" style="margin-top:16px;">
|
||||||
|
<li class="reveal">Express.js middleware chain</li>
|
||||||
|
<li class="reveal">In-memory rate limit counters</li>
|
||||||
|
<li class="reveal">Synchronous JWT validation</li>
|
||||||
|
<li class="reveal">Manual upstream health checks</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="slide__panel slide__panel--secondary">
|
||||||
|
<p class="slide__label reveal" style="color:var(--green);">After</p>
|
||||||
|
<h2 class="slide__heading reveal" style="font-size:clamp(22px,3.5vw,36px);">Edge-Native</h2>
|
||||||
|
<ul class="slide__bullets" style="margin-top:16px;">
|
||||||
|
<li class="reveal">Cloudflare Workers at the edge</li>
|
||||||
|
<li class="reveal">Durable Objects for distributed state</li>
|
||||||
|
<li class="reveal">Async JWT with key caching</li>
|
||||||
|
<li class="reveal">Automatic circuit breakers</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- SLIDE 5: SECTION DIVIDER -->
|
||||||
|
<section class="slide slide--divider" style="background-image:radial-gradient(ellipse at 30% 40%, var(--accent-dim) 0%, transparent 40%);">
|
||||||
|
<span class="slide__number">02</span>
|
||||||
|
<div>
|
||||||
|
<h2 class="slide__heading reveal">Architecture</h2>
|
||||||
|
<p class="slide__subtitle reveal" style="margin-top:12px;">How the new system works</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- SLIDE 6: DIAGRAM -->
|
||||||
|
<section class="slide slide--diagram">
|
||||||
|
<h2 class="slide__heading reveal">Request Flow</h2>
|
||||||
|
<div class="mermaid-wrap reveal">
|
||||||
|
<div class="zoom-controls">
|
||||||
|
<button onclick="zoomDiagram(this,1.2)" title="Zoom in">+</button>
|
||||||
|
<button onclick="zoomDiagram(this,0.8)" title="Zoom out">−</button>
|
||||||
|
<button onclick="resetZoom(this)" title="Reset">↺</button>
|
||||||
|
<button onclick="openDiagramFullscreen(this)" title="Open full size in new tab">⛶</button>
|
||||||
|
</div>
|
||||||
|
<pre class="mermaid">
|
||||||
|
graph LR
|
||||||
|
Client["Client"] --> Edge["Edge Worker"]
|
||||||
|
Edge --> Auth["Auth Cache"]
|
||||||
|
Edge --> RL["Rate Limiter<br/>Durable Object"]
|
||||||
|
Edge --> Router["Route Resolver"]
|
||||||
|
Router --> API["API Service"]
|
||||||
|
Router --> Static["Static Assets"]
|
||||||
|
API --> DB["Database"]
|
||||||
|
|
||||||
|
classDef primary fill:#d4a73a22,stroke:#d4a73a,stroke-width:2px
|
||||||
|
classDef secondary fill:#60a5fa22,stroke:#60a5fa,stroke-width:2px
|
||||||
|
classDef storage fill:#4ade8022,stroke:#4ade80,stroke-width:2px
|
||||||
|
|
||||||
|
class Client,Edge primary
|
||||||
|
class Auth,RL,Router secondary
|
||||||
|
class API,Static,DB storage
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- SLIDE 7: DASHBOARD -->
|
||||||
|
<section class="slide slide--dashboard" style="background-image:radial-gradient(ellipse at 70% 30%, var(--accent-dim) 0%, transparent 40%);">
|
||||||
|
<h2 class="slide__heading reveal">Performance Impact</h2>
|
||||||
|
<div class="slide__kpis">
|
||||||
|
<div class="slide__kpi reveal">
|
||||||
|
<div class="slide__kpi-val" style="color:var(--accent);">12ms</div>
|
||||||
|
<div class="slide__kpi-label">P99 Latency</div>
|
||||||
|
<div class="slide__kpi-trend" style="color:var(--green);">↓ from 142ms</div>
|
||||||
|
</div>
|
||||||
|
<div class="slide__kpi reveal">
|
||||||
|
<div class="slide__kpi-val" style="color:var(--green);">99.97%</div>
|
||||||
|
<div class="slide__kpi-label">Uptime</div>
|
||||||
|
<div class="slide__kpi-trend" style="color:var(--green);">↑ from 99.2%</div>
|
||||||
|
</div>
|
||||||
|
<div class="slide__kpi reveal">
|
||||||
|
<div class="slide__kpi-val" style="color:var(--blue);">340</div>
|
||||||
|
<div class="slide__kpi-label">Edge Locations</div>
|
||||||
|
<div class="slide__kpi-trend" style="color:var(--text-dim);">global coverage</div>
|
||||||
|
</div>
|
||||||
|
<div class="slide__kpi reveal">
|
||||||
|
<div class="slide__kpi-val" style="color:var(--accent);">$0.02</div>
|
||||||
|
<div class="slide__kpi-label">Per 10K Requests</div>
|
||||||
|
<div class="slide__kpi-trend" style="color:var(--green);">↓ 68% cost reduction</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- SLIDE 8: TABLE -->
|
||||||
|
<section class="slide slide--table">
|
||||||
|
<h2 class="slide__heading reveal">Migration Phases</h2>
|
||||||
|
<div class="table-wrap reveal" style="flex:1; min-height:0; margin-top:clamp(8px,1.5vh,20px);">
|
||||||
|
<div class="table-scroll">
|
||||||
|
<table class="data-table">
|
||||||
|
<thead><tr><th>Phase</th><th>Scope</th><th>Timeline</th><th>Risk</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>1. Shadow mode</td><td>Mirror traffic to edge, compare responses</td><td>Week 1–2</td><td style="color:var(--green);">Low</td></tr>
|
||||||
|
<tr><td>2. Canary rollout</td><td>5% traffic to edge, monitor errors</td><td>Week 3</td><td style="color:var(--green);">Low</td></tr>
|
||||||
|
<tr><td>3. Gradual shift</td><td>25% → 50% → 75% traffic</td><td>Week 4–5</td><td style="color:var(--accent);">Medium</td></tr>
|
||||||
|
<tr><td>4. Full cutover</td><td>100% traffic, decommission old proxy</td><td>Week 6</td><td style="color:var(--accent);">Medium</td></tr>
|
||||||
|
<tr><td>5. Cleanup</td><td>Remove feature flags, archive old code</td><td>Week 7</td><td style="color:var(--green);">Low</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- SLIDE 9: CODE -->
|
||||||
|
<section class="slide slide--code" style="background-image:radial-gradient(ellipse at 50% 80%, var(--accent-dim) 0%, transparent 40%);">
|
||||||
|
<h2 class="slide__heading reveal" style="text-align:center;">Edge Worker Entry Point</h2>
|
||||||
|
<div class="slide__code-block reveal" style="margin-top:clamp(12px,2vh,24px);">
|
||||||
|
<span class="slide__code-filename">gateway.ts</span>
|
||||||
|
<pre><code><span class="hl">export default</span> {
|
||||||
|
<span class="hl">async fetch</span>(req: Request, env: Env) {
|
||||||
|
<span class="cm">// Auth check with edge-cached keys</span>
|
||||||
|
const identity = <span class="hl">await</span> verifyAuth(req, env);
|
||||||
|
<span class="cm">// Distributed rate limiting</span>
|
||||||
|
const limit = env.RATE_LIMITER.get(identity.id);
|
||||||
|
<span class="hl">if</span> (<span class="hl">await</span> limit.check()) <span class="hl">return</span> tooMany();
|
||||||
|
<span class="cm">// Route to upstream</span>
|
||||||
|
<span class="hl">return</span> route(req, env.SERVICES);
|
||||||
|
}
|
||||||
|
};</code></pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- SLIDE 10: QUOTE -->
|
||||||
|
<section class="slide slide--quote" style="background-image:radial-gradient(ellipse at 50% 50%, var(--accent-dim) 0%, transparent 35%);">
|
||||||
|
<div class="slide__quote-mark reveal">“</div>
|
||||||
|
<blockquote class="reveal">
|
||||||
|
The fastest request is the one that never leaves the edge.
|
||||||
|
</blockquote>
|
||||||
|
<cite class="reveal">— Edge Computing Principle</cite>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- SLIDE 11: FULL-BLEED -->
|
||||||
|
<section class="slide slide--bleed">
|
||||||
|
<div class="slide__bg slide__bg--gradient"></div>
|
||||||
|
<div class="slide__scrim"></div>
|
||||||
|
<div class="slide__content">
|
||||||
|
<p class="slide__label reveal" style="color:rgba(255,255,255,0.6);">Next Steps</p>
|
||||||
|
<h2 class="slide__heading reveal">Ship Shadow Mode This Week</h2>
|
||||||
|
<p class="slide__subtitle reveal" style="color:rgba(255,255,255,0.6); margin-top:12px;">Full cutover targeted for end of Q1</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div><!-- /deck -->
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
||||||
|
|
||||||
|
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
mermaid.initialize({
|
||||||
|
startOnLoad: true,
|
||||||
|
theme: 'base',
|
||||||
|
look: 'classic',
|
||||||
|
themeVariables: {
|
||||||
|
primaryColor: isDark ? '#1d2b52' : '#fef3e0',
|
||||||
|
primaryBorderColor: isDark ? '#d4a73a' : '#b8860b',
|
||||||
|
primaryTextColor: isDark ? '#e8e4d8' : '#1a1814',
|
||||||
|
secondaryColor: isDark ? '#162040' : '#eff6ff',
|
||||||
|
secondaryBorderColor: isDark ? '#60a5fa' : '#2563eb',
|
||||||
|
secondaryTextColor: isDark ? '#e8e4d8' : '#1a1814',
|
||||||
|
tertiaryColor: isDark ? '#0f2620' : '#f0fdf4',
|
||||||
|
tertiaryBorderColor: isDark ? '#4ade80' : '#16a34a',
|
||||||
|
tertiaryTextColor: isDark ? '#e8e4d8' : '#1a1814',
|
||||||
|
lineColor: isDark ? '#9a9484' : '#7a7468',
|
||||||
|
fontSize: '18px',
|
||||||
|
fontFamily: 'var(--font-body)',
|
||||||
|
noteBkgColor: isDark ? '#1d2b52' : '#fef3e0',
|
||||||
|
noteTextColor: isDark ? '#e8e4d8' : '#1a1814',
|
||||||
|
noteBorderColor: isDark ? '#d4a73a' : '#b8860b',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function autoFit() {
|
||||||
|
document.querySelectorAll('.mermaid svg').forEach(function(svg) {
|
||||||
|
svg.removeAttribute('height');
|
||||||
|
svg.style.width = '100%';
|
||||||
|
svg.style.maxWidth = '100%';
|
||||||
|
svg.style.height = 'auto';
|
||||||
|
svg.parentElement.style.width = '100%';
|
||||||
|
});
|
||||||
|
document.querySelectorAll('.slide__kpi-val').forEach(function(el) {
|
||||||
|
if (el.scrollWidth > el.clientWidth) {
|
||||||
|
var s = el.clientWidth / el.scrollWidth;
|
||||||
|
el.style.transform = 'scale(' + s + ')';
|
||||||
|
el.style.transformOrigin = 'left top';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.querySelectorAll('.slide--quote blockquote').forEach(function(el) {
|
||||||
|
var len = el.textContent.trim().length;
|
||||||
|
if (len > 100) {
|
||||||
|
var scale = Math.max(0.5, 100 / len);
|
||||||
|
var fs = parseFloat(getComputedStyle(el).fontSize);
|
||||||
|
el.style.fontSize = Math.max(16, Math.round(fs * scale)) + 'px';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
mermaid.run().then(function() {
|
||||||
|
autoFit();
|
||||||
|
new SlideEngine();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Mermaid zoom controls + click-to-expand
|
||||||
|
var INITIAL_ZOOM=1;
|
||||||
|
function zoomDiagram(b,f){var w=b.closest('.mermaid-wrap');var t=w.querySelector('.mermaid');var c=parseFloat(t.dataset.zoom||INITIAL_ZOOM);var n=Math.min(Math.max(c*f,0.5),5);t.dataset.zoom=n;t.style.zoom=n;}
|
||||||
|
function resetZoom(b){var w=b.closest('.mermaid-wrap');var t=w.querySelector('.mermaid');t.dataset.zoom=INITIAL_ZOOM;t.style.zoom=INITIAL_ZOOM;}
|
||||||
|
function openDiagramFullscreen(b){openMermaidInNewTab(b.closest('.mermaid-wrap'));}
|
||||||
|
function openMermaidInNewTab(w){var svg=w.querySelector('.mermaid svg');if(!svg)return;var clone=svg.cloneNode(true);clone.style.zoom='';clone.style.transform='';var styles=getComputedStyle(document.documentElement);var bg=styles.getPropertyValue('--bg').trim()||'#ffffff';var html='<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Diagram</title><style>body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:'+bg+';padding:40px;box-sizing:border-box}svg{max-width:100%;max-height:90vh;height:auto}</style></head><body>'+clone.outerHTML+'</body></html>';window.open(URL.createObjectURL(new Blob([html],{type:'text/html'})),'_blank');}
|
||||||
|
document.querySelectorAll('.mermaid-wrap').forEach(function(w){w.addEventListener('wheel',function(e){if(!e.ctrlKey&&!e.metaKey)return;e.preventDefault();var t=w.querySelector('.mermaid');var c=parseFloat(t.dataset.zoom||INITIAL_ZOOM);var f=e.deltaY<0?1.1:0.9;var n=Math.min(Math.max(c*f,0.5),5);t.dataset.zoom=n;t.style.zoom=n;},{passive:false});var sX,sY,sL,sT,sTime,didPan;w.addEventListener('mousedown',function(e){if(e.target.closest('.zoom-controls'))return;w.classList.add('is-panning');sX=e.clientX;sY=e.clientY;sL=w.scrollLeft;sT=w.scrollTop;sTime=Date.now();didPan=false;});window.addEventListener('mousemove',function(e){if(!w.classList.contains('is-panning'))return;var dx=e.clientX-sX,dy=e.clientY-sY;if(Math.abs(dx)>5||Math.abs(dy)>5)didPan=true;w.scrollLeft=sL-dx;w.scrollTop=sT-dy;});window.addEventListener('mouseup',function(){if(!w.classList.contains('is-panning'))return;w.classList.remove('is-panning');if(!didPan&&Date.now()-sTime<300)openMermaidInNewTab(w);});});
|
||||||
|
|
||||||
|
// SlideEngine
|
||||||
|
function SlideEngine(){
|
||||||
|
this.deck=document.querySelector('.deck');
|
||||||
|
this.slides=[].slice.call(document.querySelectorAll('.slide'));
|
||||||
|
this.current=0;
|
||||||
|
this.total=this.slides.length;
|
||||||
|
this.buildChrome();
|
||||||
|
this.bindEvents();
|
||||||
|
this.observe();
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
SlideEngine.prototype.buildChrome=function(){
|
||||||
|
var bar=document.createElement('div');bar.className='deck-progress';document.body.appendChild(bar);this.bar=bar;
|
||||||
|
var dots=document.createElement('div');dots.className='deck-dots';var self=this;
|
||||||
|
this.slides.forEach(function(_,i){var d=document.createElement('button');d.className='deck-dot';d.title='Slide '+(i+1);d.onclick=function(){self.goTo(i);};dots.appendChild(d);});
|
||||||
|
document.body.appendChild(dots);this.dots=[].slice.call(dots.children);
|
||||||
|
var ctr=document.createElement('div');ctr.className='deck-counter';document.body.appendChild(ctr);this.counter=ctr;
|
||||||
|
var hints=document.createElement('div');hints.className='deck-hints';hints.textContent='\u2190 \u2192 or scroll to navigate';document.body.appendChild(hints);this.hints=hints;
|
||||||
|
this.hintTimer=setTimeout(function(){hints.classList.add('faded');},4000);
|
||||||
|
};
|
||||||
|
SlideEngine.prototype.bindEvents=function(){
|
||||||
|
var self=this;
|
||||||
|
document.addEventListener('keydown',function(e){
|
||||||
|
if(e.target.closest('.mermaid-wrap,.table-scroll,.code-scroll,input,textarea,[contenteditable]'))return;
|
||||||
|
if(['ArrowDown','ArrowRight',' ','PageDown'].indexOf(e.key)>-1){e.preventDefault();self.next();}
|
||||||
|
else if(['ArrowUp','ArrowLeft','PageUp'].indexOf(e.key)>-1){e.preventDefault();self.prev();}
|
||||||
|
else if(e.key==='Home'){e.preventDefault();self.goTo(0);}
|
||||||
|
else if(e.key==='End'){e.preventDefault();self.goTo(self.total-1);}
|
||||||
|
self.fadeHints();
|
||||||
|
});
|
||||||
|
var tY;
|
||||||
|
this.deck.addEventListener('touchstart',function(e){tY=e.touches[0].clientY;},{passive:true});
|
||||||
|
this.deck.addEventListener('touchend',function(e){var dy=tY-e.changedTouches[0].clientY;if(Math.abs(dy)>50){dy>0?self.next():self.prev();}});
|
||||||
|
};
|
||||||
|
SlideEngine.prototype.observe=function(){
|
||||||
|
var self=this;
|
||||||
|
var obs=new IntersectionObserver(function(entries){entries.forEach(function(entry){if(entry.isIntersecting){entry.target.classList.add('visible');self.current=self.slides.indexOf(entry.target);self.update();}});},{threshold:0.5});
|
||||||
|
this.slides.forEach(function(s){obs.observe(s);});
|
||||||
|
};
|
||||||
|
SlideEngine.prototype.goTo=function(i){this.slides[Math.max(0,Math.min(i,this.total-1))].scrollIntoView({behavior:'smooth'});};
|
||||||
|
SlideEngine.prototype.next=function(){if(this.current<this.total-1)this.goTo(this.current+1);};
|
||||||
|
SlideEngine.prototype.prev=function(){if(this.current>0)this.goTo(this.current-1);};
|
||||||
|
SlideEngine.prototype.update=function(){
|
||||||
|
this.bar.style.width=((this.current+1)/this.total*100)+'%';
|
||||||
|
var c=this.current;this.dots.forEach(function(d,i){d.classList.toggle('active',i===c);});
|
||||||
|
this.counter.textContent=(this.current+1)+' / '+this.total;
|
||||||
|
};
|
||||||
|
SlideEngine.prototype.fadeHints=function(){clearTimeout(this.hintTimer);this.hints.classList.add('faded');};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -19,9 +19,11 @@ Thumbs.db
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
# Theme static: only the Vite-built bundle is source-controlled.
|
# Theme static: only Vite-built JS/CSS assets (no HTML or main.css from Zola public/).
|
||||||
# Do not copy Zola public/ output here — it overrides the real homepage on build.
|
|
||||||
themes/openfreebooks/static/*
|
themes/openfreebooks/static/*
|
||||||
!themes/openfreebooks/static/js/
|
!themes/openfreebooks/static/js/
|
||||||
themes/openfreebooks/static/js/*
|
themes/openfreebooks/static/js/*
|
||||||
!themes/openfreebooks/static/js/bundle.js
|
!themes/openfreebooks/static/js/bundle.js
|
||||||
|
!themes/openfreebooks/static/js/*.js
|
||||||
|
!themes/openfreebooks/static/js/katex.css
|
||||||
|
!themes/openfreebooks/static/js/KaTeX_*
|
||||||
|
|
|
||||||
5
bun.lock
5
bun.lock
|
|
@ -5,6 +5,7 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "openfreebooks",
|
"name": "openfreebooks",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"katex": "^0.16.47",
|
||||||
"solid-js": "^1.9.5",
|
"solid-js": "^1.9.5",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -183,6 +184,8 @@
|
||||||
|
|
||||||
"caniuse-lite": ["caniuse-lite@1.0.30001793", "", {}, "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA=="],
|
"caniuse-lite": ["caniuse-lite@1.0.30001793", "", {}, "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA=="],
|
||||||
|
|
||||||
|
"commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||||
|
|
||||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||||
|
|
||||||
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
|
||||||
|
|
@ -213,6 +216,8 @@
|
||||||
|
|
||||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||||
|
|
||||||
|
"katex": ["katex@0.16.47", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg=="],
|
||||||
|
|
||||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||||
|
|
||||||
"merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="],
|
"merge-anything": ["merge-anything@5.1.7", "", { "dependencies": { "is-what": "^4.1.8" } }, "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ=="],
|
||||||
|
|
|
||||||
4
content/about.md
Normal file
4
content/about.md
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
+++
|
||||||
|
title = "Manifesto"
|
||||||
|
template = "about.html"
|
||||||
|
+++
|
||||||
6
content/math/_index.md
Normal file
6
content/math/_index.md
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
+++
|
||||||
|
title = "Hong Kong Mathematics"
|
||||||
|
description = "DSE Compulsory Part mathematics — Number & Algebra, Measures Shape & Space, and Data Handling."
|
||||||
|
template = "math/book.html"
|
||||||
|
sort_by = "weight"
|
||||||
|
+++
|
||||||
8
content/math/quadratic-equations/_index.md
Normal file
8
content/math/quadratic-equations/_index.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
+++
|
||||||
|
title = "Quadratic equations"
|
||||||
|
description = "The discriminant, nature of roots, and the graph of y = ax² + bx + c — DSE Compulsory Part."
|
||||||
|
template = "math/chapter.html"
|
||||||
|
weight = 1
|
||||||
|
[extra]
|
||||||
|
chapter_id = "quadratic-equations"
|
||||||
|
+++
|
||||||
46
data/manifest.json
Normal file
46
data/manifest.json
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
{
|
||||||
|
"intro": "Our guiding principles are set in stone.",
|
||||||
|
"lead": "Open Free Books is free internet textbooks for learners from elementary school through university — static, open source, and built for the web. These are the values we will not trade away as the catalog grows.",
|
||||||
|
"principles": [
|
||||||
|
{
|
||||||
|
"id": "free",
|
||||||
|
"title": "Free",
|
||||||
|
"body": "Every book, every chapter, every learner — no paywalls, no premium unlocks, no subscription that gates the syllabus. Free is not a trial; it is the product, forever."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "open",
|
||||||
|
"title": "Open",
|
||||||
|
"body": "Source on GitHub, improvements via pull requests, forks for classrooms and regions. Open means you can see how the book was made, fix a typo today, and ship a better edition tomorrow — not a PDF locked behind a login."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "curriculum",
|
||||||
|
"title": "Curriculum",
|
||||||
|
"body": "Not a wiki. Wikis excel at scattered facts; they do not carry a learner from prerequisite to mastery. We publish opinionated paths: ordered chapters, assessed skills, and a point of view about what to teach next — structure on purpose, not everything about X."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "independent",
|
||||||
|
"title": "Independent",
|
||||||
|
"body": "Built for learners and teachers, not for ad networks or investor growth charts. Sustainability comes from community contribution and shared infrastructure, not from turning students into the product."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "durable",
|
||||||
|
"title": "Durable",
|
||||||
|
"body": "Static HTML in an open repository — readable in a browser today, archivable tomorrow, forkable for decades. Your syllabus should not vanish when a vendor sunsets digital textbooks or changes the terms."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "crafted",
|
||||||
|
"title": "Crafted",
|
||||||
|
"body": "Books are authored HTML with care: figures, interactives, and JSON Canvas where they help understanding — not generated slop or print PDFs pasted on the web. Quality is a deliberate choice on every page."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "inspectable",
|
||||||
|
"title": "Inspectable",
|
||||||
|
"body": "Every change has a commit. Teachers can adapt a fork for their syllabus with diffs and review, not opaque exports from a CMS. If something is wrong, you can find who wrote it and fix it."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "web-native",
|
||||||
|
"title": "Web-native",
|
||||||
|
"body": "The medium is the open web: links, search, accessibility, and embeddable demos — not a file you download once and never update. Textbooks should behave like the rest of the internet learners already use."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
85
data/math-dse-curriculum.json
Normal file
85
data/math-dse-curriculum.json
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
{
|
||||||
|
"title": "Hong Kong Mathematics",
|
||||||
|
"subtitle": "DSE Compulsory Part (S4–S6)",
|
||||||
|
"intro": "Structured around the three core strands of the Hong Kong mathematics curriculum. Chapters marked as available can be read now; others are on the roadmap.",
|
||||||
|
"strands": [
|
||||||
|
{
|
||||||
|
"id": "number-algebra",
|
||||||
|
"title": "Number & Algebra",
|
||||||
|
"chapters": [
|
||||||
|
{
|
||||||
|
"slug": "quadratic-equations",
|
||||||
|
"title": "Quadratic equations",
|
||||||
|
"status": "live"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "sequences",
|
||||||
|
"title": "Sequences & series",
|
||||||
|
"status": "planned"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "functions-graphs",
|
||||||
|
"title": "Functions & graphs",
|
||||||
|
"status": "planned"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "linear-programming",
|
||||||
|
"title": "Linear programming",
|
||||||
|
"status": "planned"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "measures-shape-space",
|
||||||
|
"title": "Measures, Shape & Space",
|
||||||
|
"chapters": [
|
||||||
|
{
|
||||||
|
"slug": "trigonometry",
|
||||||
|
"title": "Advanced trigonometry",
|
||||||
|
"status": "planned"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "coordinate-geometry",
|
||||||
|
"title": "Coordinate geometry of circles",
|
||||||
|
"status": "planned"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "circle-properties",
|
||||||
|
"title": "Properties of circles",
|
||||||
|
"status": "planned"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "loci",
|
||||||
|
"title": "Loci",
|
||||||
|
"status": "planned"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "data-handling",
|
||||||
|
"title": "Data Handling",
|
||||||
|
"chapters": [
|
||||||
|
{
|
||||||
|
"slug": "dispersion",
|
||||||
|
"title": "Measures of dispersion",
|
||||||
|
"status": "planned"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "probability",
|
||||||
|
"title": "Permutations, combinations & probability",
|
||||||
|
"status": "planned"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "normal-distribution",
|
||||||
|
"title": "Normal distribution & z-scores",
|
||||||
|
"status": "planned"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "statistical-literacy",
|
||||||
|
"title": "Statistical literacy",
|
||||||
|
"status": "planned"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
558
frontend/src/components/quadratic-explorer.tsx
Normal file
558
frontend/src/components/quadratic-explorer.tsx
Normal file
|
|
@ -0,0 +1,558 @@
|
||||||
|
import katex from "katex";
|
||||||
|
import { createEffect, createMemo, createSignal, onCleanup, Show } from "solid-js";
|
||||||
|
|
||||||
|
const VIEW_SIZE = 400;
|
||||||
|
const UNIT = 20;
|
||||||
|
const ORIGIN = VIEW_SIZE / 2;
|
||||||
|
const X_RANGE = 10;
|
||||||
|
/** Target size on screen (CSS pixels), scaled into canvas space in hitTestHover. */
|
||||||
|
const VERTEX_HIT_SCREEN = 28;
|
||||||
|
const ROOT_HIT_SCREEN = 22;
|
||||||
|
const CURVE_HIT_SCREEN = 14;
|
||||||
|
const HOVER_RADIUS = 8;
|
||||||
|
const POINT_RADIUS = 5;
|
||||||
|
const CURVE_SAMPLE_STEP = 0.15;
|
||||||
|
|
||||||
|
type PointKind = "vertex" | "root" | "curve";
|
||||||
|
|
||||||
|
type GraphHover = {
|
||||||
|
kind: PointKind;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
label: string;
|
||||||
|
canvasX: number;
|
||||||
|
canvasY: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function round(n: number, digits = 2) {
|
||||||
|
const p = 10 ** digits;
|
||||||
|
return Math.round(n * p) / p;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNum(n: number) {
|
||||||
|
if (Number.isInteger(n)) return String(n);
|
||||||
|
return round(n, 2).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCoord(x: number, y: number) {
|
||||||
|
return `(${formatNum(x)}, ${formatNum(y)})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toCanvasX(x: number) {
|
||||||
|
return ORIGIN + x * UNIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toCanvasY(y: number) {
|
||||||
|
return ORIGIN - y * UNIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp(n: number, lo: number, hi: number) {
|
||||||
|
return Math.max(lo, Math.min(hi, n));
|
||||||
|
}
|
||||||
|
|
||||||
|
function discriminant(a: number, b: number, c: number) {
|
||||||
|
return b * b - 4 * a * c;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeRoots(a: number, b: number, c: number, delta: number) {
|
||||||
|
if (delta < 0 || Math.abs(a) < 1e-9) return [] as number[];
|
||||||
|
if (delta === 0) return [-b / (2 * a)];
|
||||||
|
const sqrtDelta = Math.sqrt(delta);
|
||||||
|
return [(-b + sqrtDelta) / (2 * a), (-b - sqrtDelta) / (2 * a)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function natureOfRoots(delta: number) {
|
||||||
|
if (delta > 0) return "Two distinct real roots";
|
||||||
|
if (delta === 0) return "One repeated real root (equal roots)";
|
||||||
|
return "No real roots";
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLatex(latex: string) {
|
||||||
|
return katex.renderToString(latex, { throwOnError: false, strict: "ignore" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCanvasColors(canvas: HTMLCanvasElement) {
|
||||||
|
const style = getComputedStyle(canvas);
|
||||||
|
return {
|
||||||
|
grid: style.getPropertyValue("--math-grid").trim() || "#e5e5e5",
|
||||||
|
axis: style.getPropertyValue("--math-axis").trim() || "#a8a29e",
|
||||||
|
curve: style.getPropertyValue("--math-curve").trim() || "#9a6b2e",
|
||||||
|
vertex: style.getPropertyValue("--math-vertex").trim() || "#4a6b4a",
|
||||||
|
root: style.getPropertyValue("--math-root").trim() || "#2563eb",
|
||||||
|
bg: style.getPropertyValue("--math-canvas-bg").trim() || "#ffffff",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function pointerToCanvas(
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
clientX: number,
|
||||||
|
clientY: number
|
||||||
|
) {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const scaleX = VIEW_SIZE / rect.width;
|
||||||
|
const scaleY = VIEW_SIZE / rect.height;
|
||||||
|
const px = (clientX - rect.left) * scaleX;
|
||||||
|
const py = (clientY - rect.top) * scaleY;
|
||||||
|
const x = (px - ORIGIN) / UNIT;
|
||||||
|
const y = -(py - ORIGIN) / UNIT;
|
||||||
|
return { px, py, x, y };
|
||||||
|
}
|
||||||
|
|
||||||
|
function distPx(px1: number, py1: number, px2: number, py2: number) {
|
||||||
|
return Math.hypot(px1 - px2, py1 - py2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function canvasHitRadius(canvas: HTMLCanvasElement, screenRadius: number) {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const scale = rect.width > 0 ? VIEW_SIZE / rect.width : 1;
|
||||||
|
return screenRadius * scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nearestCurveHover(
|
||||||
|
a: number,
|
||||||
|
b: number,
|
||||||
|
c: number,
|
||||||
|
px: number,
|
||||||
|
py: number
|
||||||
|
): { hover: GraphHover; dist: number } | null {
|
||||||
|
let bestX = 0;
|
||||||
|
let bestY = 0;
|
||||||
|
let bestDist = Infinity;
|
||||||
|
|
||||||
|
for (let x = -X_RANGE; x <= X_RANGE; x += CURVE_SAMPLE_STEP) {
|
||||||
|
const y = a * x * x + b * x + c;
|
||||||
|
const d = distPx(px, py, toCanvasX(x), toCanvasY(y));
|
||||||
|
if (d < bestDist) {
|
||||||
|
bestDist = d;
|
||||||
|
bestX = x;
|
||||||
|
bestY = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isFinite(bestDist)) return null;
|
||||||
|
|
||||||
|
const cx = toCanvasX(bestX);
|
||||||
|
const cy = toCanvasY(bestY);
|
||||||
|
return {
|
||||||
|
dist: bestDist,
|
||||||
|
hover: {
|
||||||
|
kind: "curve",
|
||||||
|
x: bestX,
|
||||||
|
y: bestY,
|
||||||
|
label: "On curve",
|
||||||
|
canvasX: cx,
|
||||||
|
canvasY: cy,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function hitTestHover(
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
a: number,
|
||||||
|
b: number,
|
||||||
|
c: number,
|
||||||
|
rootXs: number[],
|
||||||
|
px: number,
|
||||||
|
py: number
|
||||||
|
): GraphHover | null {
|
||||||
|
const vertexRadius = canvasHitRadius(canvas, VERTEX_HIT_SCREEN);
|
||||||
|
const rootRadius = canvasHitRadius(canvas, ROOT_HIT_SCREEN);
|
||||||
|
const curveRadius = canvasHitRadius(canvas, CURVE_HIT_SCREEN);
|
||||||
|
|
||||||
|
if (Math.abs(a) > 1e-9) {
|
||||||
|
const vx = -b / (2 * a);
|
||||||
|
const vy = a * vx * vx + b * vx + c;
|
||||||
|
const cx = toCanvasX(vx);
|
||||||
|
const cy = toCanvasY(vy);
|
||||||
|
const d = distPx(px, py, cx, cy);
|
||||||
|
if (d < vertexRadius) {
|
||||||
|
return {
|
||||||
|
kind: "vertex",
|
||||||
|
x: vx,
|
||||||
|
y: vy,
|
||||||
|
label: "Vertex",
|
||||||
|
canvasX: cx,
|
||||||
|
canvasY: cy,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const root of rootXs) {
|
||||||
|
const cx = toCanvasX(root);
|
||||||
|
const cy = toCanvasY(0);
|
||||||
|
const d = distPx(px, py, cx, cy);
|
||||||
|
if (d < rootRadius) {
|
||||||
|
return {
|
||||||
|
kind: "root",
|
||||||
|
x: root,
|
||||||
|
y: 0,
|
||||||
|
label: "Root",
|
||||||
|
canvasX: cx,
|
||||||
|
canvasY: cy,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const curve = nearestCurveHover(a, b, c, px, py);
|
||||||
|
if (curve && curve.dist < curveRadius) {
|
||||||
|
return curve.hover;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawGraph(
|
||||||
|
canvas: HTMLCanvasElement,
|
||||||
|
a: number,
|
||||||
|
b: number,
|
||||||
|
c: number,
|
||||||
|
rootXs: number[],
|
||||||
|
hover: GraphHover | null
|
||||||
|
) {
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
canvas.width = VIEW_SIZE * dpr;
|
||||||
|
canvas.height = VIEW_SIZE * dpr;
|
||||||
|
canvas.style.width = `${VIEW_SIZE}px`;
|
||||||
|
canvas.style.height = `${VIEW_SIZE}px`;
|
||||||
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
|
||||||
|
const colors = readCanvasColors(canvas);
|
||||||
|
|
||||||
|
ctx.fillStyle = colors.bg;
|
||||||
|
ctx.fillRect(0, 0, VIEW_SIZE, VIEW_SIZE);
|
||||||
|
|
||||||
|
ctx.strokeStyle = colors.grid;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
for (let i = -X_RANGE; i <= X_RANGE; i++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(toCanvasX(i), 0);
|
||||||
|
ctx.lineTo(toCanvasX(i), VIEW_SIZE);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, toCanvasY(i));
|
||||||
|
ctx.lineTo(VIEW_SIZE, toCanvasY(i));
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.strokeStyle = colors.axis;
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(ORIGIN, 0);
|
||||||
|
ctx.lineTo(ORIGIN, VIEW_SIZE);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, ORIGIN);
|
||||||
|
ctx.lineTo(VIEW_SIZE, ORIGIN);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.strokeStyle = colors.curve;
|
||||||
|
ctx.lineWidth = hover?.kind === "curve" ? 3.5 : 3;
|
||||||
|
ctx.lineCap = "round";
|
||||||
|
ctx.lineJoin = "round";
|
||||||
|
ctx.beginPath();
|
||||||
|
let started = false;
|
||||||
|
for (let x = -X_RANGE; x <= X_RANGE; x += 0.25) {
|
||||||
|
const y = a * x * x + b * x + c;
|
||||||
|
const px = toCanvasX(x);
|
||||||
|
const py = toCanvasY(y);
|
||||||
|
if (!started) {
|
||||||
|
ctx.moveTo(px, py);
|
||||||
|
started = true;
|
||||||
|
} else {
|
||||||
|
ctx.lineTo(px, py);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
const drawMarker = (
|
||||||
|
cx: number,
|
||||||
|
cy: number,
|
||||||
|
fill: string,
|
||||||
|
active: boolean
|
||||||
|
) => {
|
||||||
|
const r = active ? HOVER_RADIUS : POINT_RADIUS;
|
||||||
|
ctx.fillStyle = fill;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(cx, cy, r, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
if (active) {
|
||||||
|
ctx.strokeStyle = colors.bg;
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Math.abs(a) > 1e-9) {
|
||||||
|
const vx = -b / (2 * a);
|
||||||
|
const vy = a * vx * vx + b * vx + c;
|
||||||
|
if (vx >= -X_RANGE && vx <= X_RANGE && vy >= -X_RANGE && vy <= X_RANGE) {
|
||||||
|
const cx = toCanvasX(vx);
|
||||||
|
const cy = toCanvasY(vy);
|
||||||
|
const active = hover?.kind === "vertex";
|
||||||
|
drawMarker(cx, cy, colors.vertex, active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const root of rootXs) {
|
||||||
|
if (root >= -X_RANGE && root <= X_RANGE) {
|
||||||
|
const cx = toCanvasX(root);
|
||||||
|
const cy = toCanvasY(0);
|
||||||
|
const active = hover?.kind === "root" && Math.abs(hover.x - root) < 1e-6;
|
||||||
|
drawMarker(cx, cy, colors.root, active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hover?.kind === "curve") {
|
||||||
|
drawMarker(hover.canvasX, hover.canvasY, colors.curve, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function SliderRow(props: {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
step: number;
|
||||||
|
accentClass?: string;
|
||||||
|
onInput: (v: number) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div class="math-slider-row">
|
||||||
|
<label class="math-slider-label" for={props.id}>
|
||||||
|
<span>{props.label}</span>
|
||||||
|
<span class="math-slider-value">{formatNum(props.value)}</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id={props.id}
|
||||||
|
class={`math-slider ${props.accentClass ?? ""}`}
|
||||||
|
type="range"
|
||||||
|
min={props.min}
|
||||||
|
max={props.max}
|
||||||
|
step={props.step}
|
||||||
|
value={props.value}
|
||||||
|
onInput={(e) => props.onInput(Number(e.currentTarget.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function QuadraticExplorer() {
|
||||||
|
const [a, setA] = createSignal(1);
|
||||||
|
const [b, setB] = createSignal(0);
|
||||||
|
const [c, setC] = createSignal(-4);
|
||||||
|
const [hover, setHover] = createSignal<GraphHover | null>(null);
|
||||||
|
const [tooltipPos, setTooltipPos] = createSignal({ left: 0, top: 0 });
|
||||||
|
let canvasRef: HTMLCanvasElement | undefined;
|
||||||
|
let wrapRef: HTMLDivElement | undefined;
|
||||||
|
|
||||||
|
const aSafe = createMemo(() => {
|
||||||
|
const v = a();
|
||||||
|
if (v === 0) return a() > 0 ? 1 : -1;
|
||||||
|
return v;
|
||||||
|
});
|
||||||
|
|
||||||
|
const delta = createMemo(() => discriminant(aSafe(), b(), c()));
|
||||||
|
const nature = createMemo(() => natureOfRoots(delta()));
|
||||||
|
const rootValues = createMemo(() => computeRoots(aSafe(), b(), c(), delta()));
|
||||||
|
|
||||||
|
const equationHtml = createMemo(() =>
|
||||||
|
renderLatex(`y = ${formatNum(aSafe())}x^2 + ${formatNum(b())}x + ${formatNum(c())}`)
|
||||||
|
);
|
||||||
|
|
||||||
|
const deltaHtml = createMemo(() => {
|
||||||
|
const av = aSafe();
|
||||||
|
const bv = b();
|
||||||
|
const cv = c();
|
||||||
|
const d = delta();
|
||||||
|
const tone = d > 0 ? "pos" : d < 0 ? "neg" : "zero";
|
||||||
|
return {
|
||||||
|
html: renderLatex(`\\Delta = ${formatNum(bv)}^2 - 4(${formatNum(av)})(${formatNum(cv)}) = ${formatNum(d)}`),
|
||||||
|
tone,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAChange = (raw: number) => {
|
||||||
|
if (raw === 0) {
|
||||||
|
setA(a() > 0 ? -1 : 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setA(raw);
|
||||||
|
};
|
||||||
|
|
||||||
|
const redraw = () => {
|
||||||
|
if (canvasRef) {
|
||||||
|
drawGraph(canvasRef, aSafe(), b(), c(), rootValues(), hover());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
aSafe();
|
||||||
|
b();
|
||||||
|
c();
|
||||||
|
rootValues();
|
||||||
|
hover();
|
||||||
|
redraw();
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateTooltipPosition = (clientX: number, clientY: number) => {
|
||||||
|
if (!wrapRef) return;
|
||||||
|
const rect = wrapRef.getBoundingClientRect();
|
||||||
|
const pad = 12;
|
||||||
|
let left = clientX - rect.left + pad;
|
||||||
|
let top = clientY - rect.top - pad;
|
||||||
|
const maxLeft = rect.width - 140;
|
||||||
|
const maxTop = rect.height - 48;
|
||||||
|
left = clamp(left, pad, maxLeft);
|
||||||
|
top = clamp(top, pad, maxTop);
|
||||||
|
setTooltipPos({ left, top });
|
||||||
|
};
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const canvas = canvasRef;
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
const onMove = (e: PointerEvent) => {
|
||||||
|
const { px, py } = pointerToCanvas(canvas, e.clientX, e.clientY);
|
||||||
|
const hit = hitTestHover(canvas, aSafe(), b(), c(), rootValues(), px, py);
|
||||||
|
setHover(hit);
|
||||||
|
if (hit) {
|
||||||
|
updateTooltipPosition(e.clientX, e.clientY);
|
||||||
|
canvas.setAttribute("aria-label", `${hit.label} at ${formatCoord(hit.x, hit.y)}`);
|
||||||
|
} else {
|
||||||
|
canvas.setAttribute(
|
||||||
|
"aria-label",
|
||||||
|
`Graph of y equals ${formatNum(aSafe())} x squared plus ${formatNum(b())} x plus ${formatNum(c())}. Hover for coordinates.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onLeave = () => {
|
||||||
|
setHover(null);
|
||||||
|
canvas.setAttribute(
|
||||||
|
"aria-label",
|
||||||
|
`Graph of y equals ${formatNum(aSafe())} x squared plus ${formatNum(b())} x plus ${formatNum(c())}. Hover for coordinates.`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
canvas.addEventListener("pointermove", onMove);
|
||||||
|
canvas.addEventListener("pointerleave", onLeave);
|
||||||
|
canvas.addEventListener("pointerdown", onMove);
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
canvas.removeEventListener("pointermove", onMove);
|
||||||
|
canvas.removeEventListener("pointerleave", onLeave);
|
||||||
|
canvas.removeEventListener("pointerdown", onMove);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section class="math-widget" aria-labelledby="quadratic-explorer-title">
|
||||||
|
<h2 id="quadratic-explorer-title" class="math-widget__title">
|
||||||
|
Quadratic parabola & discriminant explorer
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="math-widget__layout">
|
||||||
|
<div class="math-widget__controls">
|
||||||
|
<div class="math-widget__equation-box" aria-live="polite">
|
||||||
|
<p class="math-widget__equation-label">Equation</p>
|
||||||
|
<div class="math-widget__equation" innerHTML={equationHtml()} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SliderRow
|
||||||
|
id="coef-a"
|
||||||
|
label="Value of a (cannot be 0)"
|
||||||
|
value={a()}
|
||||||
|
min={-5}
|
||||||
|
max={5}
|
||||||
|
step={1}
|
||||||
|
accentClass="math-slider--a"
|
||||||
|
onInput={handleAChange}
|
||||||
|
/>
|
||||||
|
<SliderRow
|
||||||
|
id="coef-b"
|
||||||
|
label="Value of b"
|
||||||
|
value={b()}
|
||||||
|
min={-10}
|
||||||
|
max={10}
|
||||||
|
step={1}
|
||||||
|
accentClass="math-slider--b"
|
||||||
|
onInput={setB}
|
||||||
|
/>
|
||||||
|
<SliderRow
|
||||||
|
id="coef-c"
|
||||||
|
label="Value of c"
|
||||||
|
value={c()}
|
||||||
|
min={-10}
|
||||||
|
max={10}
|
||||||
|
step={1}
|
||||||
|
accentClass="math-slider--c"
|
||||||
|
onInput={setC}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="math-widget__delta-panel" aria-live="polite">
|
||||||
|
<h3 class="math-widget__delta-title">Discriminant (Δ)</h3>
|
||||||
|
<div
|
||||||
|
class="math-widget__delta-formula"
|
||||||
|
classList={{ [`math-widget__delta-formula--${deltaHtml().tone}`]: true }}
|
||||||
|
innerHTML={deltaHtml().html}
|
||||||
|
/>
|
||||||
|
<p class="math-widget__delta-nature">{nature()}</p>
|
||||||
|
<Show when={rootValues().length > 0}>
|
||||||
|
<p class="math-widget__delta-roots">
|
||||||
|
Roots at x ≈ {rootValues().map((r) => formatNum(r)).join(", ")}
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<figure class="math-widget__graph">
|
||||||
|
<div
|
||||||
|
class="math-widget__canvas-wrap"
|
||||||
|
ref={(el) => {
|
||||||
|
wrapRef = el;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<canvas
|
||||||
|
ref={(el) => {
|
||||||
|
canvasRef = el;
|
||||||
|
}}
|
||||||
|
class="math-widget__canvas"
|
||||||
|
classList={{ "math-widget__canvas--active": !!hover() }}
|
||||||
|
width={VIEW_SIZE}
|
||||||
|
height={VIEW_SIZE}
|
||||||
|
role="img"
|
||||||
|
aria-label={`Graph of y equals ${formatNum(aSafe())} x squared plus ${formatNum(b())} x plus ${formatNum(c())}. Hover for coordinates.`}
|
||||||
|
/>
|
||||||
|
<Show when={hover()}>
|
||||||
|
{(h) => (
|
||||||
|
<div
|
||||||
|
class="math-graph-tooltip"
|
||||||
|
classList={{
|
||||||
|
[`math-graph-tooltip--${h().kind}`]: true,
|
||||||
|
}}
|
||||||
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
|
style={{
|
||||||
|
left: `${tooltipPos().left}px`,
|
||||||
|
top: `${tooltipPos().top}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="math-graph-tooltip__label">{h().label}</span>
|
||||||
|
<span class="math-graph-tooltip__coords">{formatCoord(h().x, h().y)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<figcaption class="math-widget__caption">
|
||||||
|
Hover the curve, vertex (green), or roots (blue) to read coordinates. Grid: x and y from
|
||||||
|
−10 to 10.
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
frontend/src/lib/render-math.ts
Normal file
18
frontend/src/lib/render-math.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import renderMathInElement from "katex/contrib/auto-render";
|
||||||
|
import "katex/dist/katex.min.css";
|
||||||
|
|
||||||
|
const DELIMITERS = [
|
||||||
|
{ left: "$$", right: "$$", display: true },
|
||||||
|
{ left: "\\(", right: "\\)", display: false },
|
||||||
|
{ left: "\\[", right: "\\]", display: true },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function renderBookMath() {
|
||||||
|
document.querySelectorAll<HTMLElement>(".book-prose, .book-callout").forEach((el) => {
|
||||||
|
renderMathInElement(el, {
|
||||||
|
delimiters: DELIMITERS,
|
||||||
|
throwOnError: false,
|
||||||
|
strict: "ignore",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -58,4 +58,16 @@ if (marqueeRoot) {
|
||||||
|
|
||||||
render(() => <ScrollRevealBootstrap />, document.body);
|
render(() => <ScrollRevealBootstrap />, document.body);
|
||||||
render(() => <ContributorsDialogBootstrap />, document.body);
|
render(() => <ContributorsDialogBootstrap />, document.body);
|
||||||
|
|
||||||
|
const quadraticRoot = document.getElementById("quadratic-explorer");
|
||||||
|
if (quadraticRoot) {
|
||||||
|
void Promise.all([
|
||||||
|
import("./components/quadratic-explorer"),
|
||||||
|
import("./lib/render-math"),
|
||||||
|
]).then(([{ QuadraticExplorer }, { renderBookMath }]) => {
|
||||||
|
render(() => <QuadraticExplorer />, quadraticRoot);
|
||||||
|
requestAnimationFrame(() => renderBookMath());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
mountFooterYear();
|
mountFooterYear();
|
||||||
|
|
|
||||||
1
frontend/src/types/css.d.ts
vendored
Normal file
1
frontend/src/types/css.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
declare module "*.css" {}
|
||||||
22
frontend/src/types/katex-contrib-auto-render.d.ts
vendored
Normal file
22
frontend/src/types/katex-contrib-auto-render.d.ts
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
declare module "katex/contrib/auto-render" {
|
||||||
|
import type { KatexOptions } from "katex";
|
||||||
|
|
||||||
|
export interface AutoRenderDelimiter {
|
||||||
|
left: string;
|
||||||
|
right: string;
|
||||||
|
display: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutoRenderOptions extends KatexOptions {
|
||||||
|
delimiters?: AutoRenderDelimiter[];
|
||||||
|
preProcess?: (math: string) => string;
|
||||||
|
ignoredTags?: string[];
|
||||||
|
ignoredClasses?: string[];
|
||||||
|
errorCallback?: (message: string, error: Error) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function renderMathInElement(
|
||||||
|
element: HTMLElement,
|
||||||
|
options?: AutoRenderOptions
|
||||||
|
): void;
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
"dev:js": "vite build --watch"
|
"dev:js": "vite build --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"katex": "^0.16.47",
|
||||||
"solid-js": "^1.9.5"
|
"solid-js": "^1.9.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,12 @@
|
||||||
"sourceType": "github",
|
"sourceType": "github",
|
||||||
"skillPath": "svg-animations/SKILL.md",
|
"skillPath": "svg-animations/SKILL.md",
|
||||||
"computedHash": "9be4b3e0afe9d3cbb1bcf238f8504f9a23552f742350fbef9483070112ac1977"
|
"computedHash": "9be4b3e0afe9d3cbb1bcf238f8504f9a23552f742350fbef9483070112ac1977"
|
||||||
|
},
|
||||||
|
"visual-explainer": {
|
||||||
|
"source": "nicobailon/visual-explainer",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "plugins/visual-explainer/SKILL.md",
|
||||||
|
"computedHash": "af1b98dda4cc5aeec848c2e97e0f8dcd4461ba22c34abc4c13852af4e8e41cb8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
4
spec.md
4
spec.md
|
|
@ -8,4 +8,6 @@ canvas feature use [jsoncanvas](https://jsoncanvas.org/spec/1.0/) standard by Ob
|
||||||
|
|
||||||
not markdown, html based. but in every page, we should have a copy as md button using [defuddle](https://github.com/kepano/defuddle).
|
not markdown, html based. but in every page, we should have a copy as md button using [defuddle](https://github.com/kepano/defuddle).
|
||||||
|
|
||||||
we don't i18n. i18n will be solved in future by big companies.
|
we don't i18n. i18n will be solved in future by big companies.
|
||||||
|
|
||||||
|
search is supported by pagefind.
|
||||||
89
themes/openfreebooks/sass/_about-manifest.scss
Normal file
89
themes/openfreebooks/sass/_about-manifest.scss
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
.about-page {
|
||||||
|
padding: 54px 0 72px;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-page__title {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-page__intro {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
max-width: var(--prose-width);
|
||||||
|
font-family: var(--font-heading);
|
||||||
|
font-size: clamp(1.35rem, 2.4vw, 1.75rem);
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.35;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-page__lead {
|
||||||
|
margin: 0 0 40px;
|
||||||
|
max-width: var(--prose-width);
|
||||||
|
color: var(--soft-ink);
|
||||||
|
font-size: 1.05rem;
|
||||||
|
line-height: 1.72;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manifest-principles {
|
||||||
|
margin: 0 0 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manifest-principles__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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manifest-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.manifest-card {
|
||||||
|
margin: 0;
|
||||||
|
padding: 24px 22px;
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--card-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 12px 32px var(--shadow-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.manifest-card__title {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-family: var(--font-heading);
|
||||||
|
font-size: clamp(1.5rem, 2.5vw, 1.85rem);
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.15;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.manifest-card__body {
|
||||||
|
margin: 0;
|
||||||
|
max-width: 42ch;
|
||||||
|
color: var(--soft-ink);
|
||||||
|
font-size: 0.98rem;
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-page__actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.manifest-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
259
themes/openfreebooks/sass/_book.scss
Normal file
259
themes/openfreebooks/sass/_book.scss
Normal file
|
|
@ -0,0 +1,259 @@
|
||||||
|
.book-page {
|
||||||
|
padding: 48px 0 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-breadcrumb {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-breadcrumb a {
|
||||||
|
color: var(--soft-ink);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-breadcrumb a:hover,
|
||||||
|
.book-breadcrumb a:focus-visible {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-breadcrumb__sep {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-header {
|
||||||
|
max-width: var(--prose-width);
|
||||||
|
margin-bottom: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-subtitle {
|
||||||
|
margin: 12px 0 0;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-intro {
|
||||||
|
margin: 20px 0 0;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
line-height: 1.65;
|
||||||
|
color: var(--soft-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-strands {
|
||||||
|
display: grid;
|
||||||
|
gap: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-strand__title {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-chapter-list {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-chapter-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border: 1px solid var(--card-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--card-bg);
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--ink);
|
||||||
|
transition: border-color 200ms ease, box-shadow 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.book-chapter-link:hover,
|
||||||
|
a.book-chapter-link:focus-visible {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: 0 6px 20px var(--shadow-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-chapter-link--disabled {
|
||||||
|
opacity: 0.72;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-chapter-item--planned .book-chapter-link__title {
|
||||||
|
color: var(--soft-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-chapter-link__title {
|
||||||
|
font-family: var(--font-heading);
|
||||||
|
font-size: 1.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-chapter-badge {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-chapter-badge--live {
|
||||||
|
color: var(--accent);
|
||||||
|
background: rgba(154, 107, 46, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-chapter-badge--planned {
|
||||||
|
color: var(--muted);
|
||||||
|
background: rgba(118, 118, 118, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-chapter {
|
||||||
|
max-width: var(--prose-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-chapter__header {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-chapter__lead {
|
||||||
|
margin: 16px 0 0;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
line-height: 1.65;
|
||||||
|
color: var(--soft-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-prose {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-prose > p {
|
||||||
|
margin: 0 0 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-prose__heading {
|
||||||
|
margin: 2em 0 0.6em;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-formula {
|
||||||
|
margin: 0;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-formula--inline {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-callout {
|
||||||
|
margin: 1.5em 0;
|
||||||
|
padding: 18px 20px;
|
||||||
|
border-left: 3px solid var(--accent);
|
||||||
|
border-radius: 0 10px 10px 0;
|
||||||
|
background: var(--card-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-callout--formula {
|
||||||
|
text-align: center;
|
||||||
|
border-left: none;
|
||||||
|
border: 1px solid var(--card-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-callout--dse {
|
||||||
|
border-left-color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-callout__label {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-table-wrap {
|
||||||
|
margin: 1.25em 0 1.5em;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-table th,
|
||||||
|
.book-table td {
|
||||||
|
padding: 12px 14px;
|
||||||
|
text-align: left;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-table thead th {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--ink);
|
||||||
|
background: rgba(154, 107, 46, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-table tbody td:first-child {
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-table tbody tr:nth-child(even) td {
|
||||||
|
background: rgba(154, 107, 46, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-prose .katex {
|
||||||
|
font-size: 1.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-formula .katex-display {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-widget-mount {
|
||||||
|
margin-top: 40px;
|
||||||
|
max-width: 960px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-page--chapter .book-callout--dse {
|
||||||
|
max-width: var(--prose-width);
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 900px) {
|
||||||
|
.book-page--chapter .book-chapter {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.book-prose {
|
||||||
|
max-width: var(--prose-width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -320,6 +320,24 @@ h3 {
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.work-card--link .work-card__anchor,
|
||||||
|
.work-card--link .work-card__anchor:any-link {
|
||||||
|
display: block;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-card__anchor h3,
|
||||||
|
.work-card__anchor p,
|
||||||
|
.work-card__anchor .card-subtitle {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.work-card__anchor:hover h3,
|
||||||
|
.work-card__anchor:focus-visible h3 {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
.card-subtitle {
|
.card-subtitle {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
|
|
||||||
247
themes/openfreebooks/sass/_math-widget.scss
Normal file
247
themes/openfreebooks/sass/_math-widget.scss
Normal file
|
|
@ -0,0 +1,247 @@
|
||||||
|
.math-widget {
|
||||||
|
--math-grid: #e5e5e5;
|
||||||
|
--math-axis: #a8a29e;
|
||||||
|
--math-curve: #9a6b2e;
|
||||||
|
--math-vertex: #4a6b4a;
|
||||||
|
--math-root: #2563eb;
|
||||||
|
--math-canvas-bg: #ffffff;
|
||||||
|
|
||||||
|
padding: 28px;
|
||||||
|
border: 1px solid var(--card-border);
|
||||||
|
border-radius: 14px;
|
||||||
|
background: var(--card-bg);
|
||||||
|
box-shadow: 0 10px 32px var(--shadow-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .math-widget {
|
||||||
|
--math-grid: #3a3834;
|
||||||
|
--math-axis: #8f8a80;
|
||||||
|
--math-curve: #c4924a;
|
||||||
|
--math-vertex: #84cc16;
|
||||||
|
--math-root: #60a5fa;
|
||||||
|
--math-canvas-bg: #242120;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-widget__title {
|
||||||
|
margin: 0 0 20px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
font-size: 1.35rem;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-widget__layout {
|
||||||
|
display: grid;
|
||||||
|
gap: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-widget__equation-box {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 16px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(154, 107, 46, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-widget__equation-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-widget__equation {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-widget__equation .katex {
|
||||||
|
font-size: 1.15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-widget__controls {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-slider-row {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-slider-label {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--soft-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-slider-value {
|
||||||
|
font-family: var(--mono);
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-slider {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-slider--a {
|
||||||
|
accent-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-slider--b {
|
||||||
|
accent-color: #4a6b4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-slider--c {
|
||||||
|
accent-color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-widget__delta-panel {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(154, 107, 46, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-widget__delta-title {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-widget__delta-formula {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-widget__delta-formula--pos .katex {
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-widget__delta-formula--neg .katex {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-widget__delta-nature {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--soft-ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-widget__delta-roots {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-widget__graph {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-widget__canvas-wrap {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: rgba(154, 107, 46, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-widget__canvas {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
box-shadow: inset 0 1px 4px var(--shadow-soft);
|
||||||
|
cursor: crosshair;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-widget__canvas--active {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-graph-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--card-border);
|
||||||
|
background: var(--card-bg);
|
||||||
|
box-shadow: 0 6px 18px var(--shadow-soft);
|
||||||
|
pointer-events: none;
|
||||||
|
font-family: var(--mono);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
animation: math-tooltip-in 120ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes math-tooltip-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.math-graph-tooltip {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-graph-tooltip__label {
|
||||||
|
font-size: 0.62rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-graph-tooltip__coords {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-graph-tooltip--vertex .math-graph-tooltip__label {
|
||||||
|
color: var(--math-vertex);
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-graph-tooltip--root .math-graph-tooltip__label {
|
||||||
|
color: var(--math-root);
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-graph-tooltip--curve .math-graph-tooltip__label {
|
||||||
|
color: var(--math-curve);
|
||||||
|
}
|
||||||
|
|
||||||
|
.math-widget__caption {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--muted);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.math-widget__layout {
|
||||||
|
grid-template-columns: minmax(240px, 1fr) minmax(280px, 1.2fr);
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,3 +5,6 @@
|
||||||
@import "why-sections";
|
@import "why-sections";
|
||||||
@import "contributors";
|
@import "contributors";
|
||||||
@import "credits";
|
@import "credits";
|
||||||
|
@import "about-manifest";
|
||||||
|
@import "book";
|
||||||
|
@import "math-widget";
|
||||||
|
|
|
||||||
BIN
themes/openfreebooks/static/js/KaTeX_AMS-Regular.ttf
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_AMS-Regular.ttf
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_AMS-Regular.woff
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_AMS-Regular.woff
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_AMS-Regular.woff2
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_AMS-Regular.woff2
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Caligraphic-Bold.ttf
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Caligraphic-Bold.ttf
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Caligraphic-Bold.woff
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Caligraphic-Bold.woff
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Caligraphic-Bold.woff2
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Caligraphic-Bold.woff2
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Caligraphic-Regular.ttf
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Caligraphic-Regular.ttf
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Caligraphic-Regular.woff
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Caligraphic-Regular.woff
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Caligraphic-Regular.woff2
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Caligraphic-Regular.woff2
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Fraktur-Bold.ttf
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Fraktur-Bold.ttf
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Fraktur-Bold.woff
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Fraktur-Bold.woff
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Fraktur-Bold.woff2
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Fraktur-Bold.woff2
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Fraktur-Regular.ttf
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Fraktur-Regular.ttf
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Fraktur-Regular.woff
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Fraktur-Regular.woff
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Fraktur-Regular.woff2
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Fraktur-Regular.woff2
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Main-Bold.ttf
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Main-Bold.ttf
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Main-Bold.woff
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Main-Bold.woff
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Main-Bold.woff2
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Main-Bold.woff2
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Main-BoldItalic.ttf
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Main-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Main-BoldItalic.woff
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Main-BoldItalic.woff
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Main-BoldItalic.woff2
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Main-BoldItalic.woff2
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Main-Italic.ttf
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Main-Italic.ttf
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Main-Italic.woff
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Main-Italic.woff
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Main-Italic.woff2
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Main-Italic.woff2
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Main-Regular.ttf
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Main-Regular.ttf
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Main-Regular.woff
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Main-Regular.woff
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Main-Regular.woff2
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Main-Regular.woff2
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Math-BoldItalic.ttf
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Math-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Math-BoldItalic.woff
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Math-BoldItalic.woff
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Math-BoldItalic.woff2
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Math-BoldItalic.woff2
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Math-Italic.ttf
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Math-Italic.ttf
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Math-Italic.woff
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Math-Italic.woff
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Math-Italic.woff2
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Math-Italic.woff2
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_SansSerif-Bold.ttf
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_SansSerif-Bold.ttf
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_SansSerif-Bold.woff
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_SansSerif-Bold.woff
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_SansSerif-Bold.woff2
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_SansSerif-Bold.woff2
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_SansSerif-Italic.ttf
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_SansSerif-Italic.ttf
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_SansSerif-Italic.woff
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_SansSerif-Italic.woff
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_SansSerif-Italic.woff2
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_SansSerif-Italic.woff2
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_SansSerif-Regular.ttf
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_SansSerif-Regular.ttf
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_SansSerif-Regular.woff
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_SansSerif-Regular.woff
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_SansSerif-Regular.woff2
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_SansSerif-Regular.woff2
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Script-Regular.ttf
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Script-Regular.ttf
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Script-Regular.woff
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Script-Regular.woff
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Script-Regular.woff2
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Script-Regular.woff2
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Size1-Regular.ttf
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Size1-Regular.ttf
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Size1-Regular.woff
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Size1-Regular.woff
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Size1-Regular.woff2
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Size1-Regular.woff2
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Size2-Regular.ttf
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Size2-Regular.ttf
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Size2-Regular.woff
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Size2-Regular.woff
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Size2-Regular.woff2
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Size2-Regular.woff2
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Size3-Regular.ttf
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Size3-Regular.ttf
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Size3-Regular.woff
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Size3-Regular.woff
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Size4-Regular.ttf
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Size4-Regular.ttf
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Size4-Regular.woff
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Size4-Regular.woff
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Size4-Regular.woff2
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Size4-Regular.woff2
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Typewriter-Regular.ttf
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Typewriter-Regular.ttf
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Typewriter-Regular.woff
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Typewriter-Regular.woff
Normal file
Binary file not shown.
BIN
themes/openfreebooks/static/js/KaTeX_Typewriter-Regular.woff2
Normal file
BIN
themes/openfreebooks/static/js/KaTeX_Typewriter-Regular.woff2
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
257
themes/openfreebooks/static/js/katex-Dc8nsIP1.js
Normal file
257
themes/openfreebooks/static/js/katex-Dc8nsIP1.js
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue