init
This commit is contained in:
149
.opencode/skills/preview/SKILL.md
Normal file
149
.opencode/skills/preview/SKILL.md
Normal file
@@ -0,0 +1,149 @@
|
||||
---
|
||||
name: ck:preview
|
||||
description: "View files/directories OR generate visual explanations, slides, diagrams (Markdown or self-contained HTML)."
|
||||
argument-hint: "[path] OR [--html] --explain|--slides|--diagram|--ascii [topic] OR --html --diff|--plan-review|--recap"
|
||||
metadata:
|
||||
author: claudekit
|
||||
version: "1.1.0"
|
||||
---
|
||||
|
||||
# Preview
|
||||
|
||||
Universal viewer + visual generator. View existing content OR generate new visual explanations.
|
||||
|
||||
## Default (No Arguments)
|
||||
|
||||
If invoked without arguments, use `AskUserQuestion` to present available preview operations:
|
||||
|
||||
| Operation | Description |
|
||||
|-----------|-------------|
|
||||
| `(view)` | View a file or directory |
|
||||
| `--explain` | Generate visual explanation |
|
||||
| `--slides` | Generate presentation slides |
|
||||
| `--diagram` | Generate architecture diagram |
|
||||
| `--ascii` | Terminal-friendly diagram |
|
||||
| `--stop` | Stop preview server |
|
||||
| `--html --explain` | Self-contained HTML explanation (opens in browser) |
|
||||
| `--html --diagram` | Self-contained HTML diagram with zoom controls |
|
||||
| `--html --slides` | Magazine-quality HTML slide deck |
|
||||
| `--html --diff` | Visual diff review (HTML) |
|
||||
| `--html --plan-review` | Plan vs codebase comparison (HTML) |
|
||||
| `--html --recap` | Project context snapshot (HTML) |
|
||||
|
||||
Present as options via `AskUserQuestion` with header "Preview Operation", question "What would you like to do?".
|
||||
|
||||
## Usage
|
||||
|
||||
### View Mode
|
||||
- `/ck:preview <file.md>` - View markdown file in novel-reader UI
|
||||
- `/ck:preview <directory/>` - Browse directory contents
|
||||
- `/ck:preview --stop` - Stop running server
|
||||
|
||||
### Generation Mode (Markdown)
|
||||
- `/ck:preview --explain <topic>` - Generate visual explanation (ASCII + Mermaid + prose)
|
||||
- `/ck:preview --slides <topic>` - Generate presentation slides (one concept per slide)
|
||||
- `/ck:preview --diagram <topic>` - Generate focused diagram (ASCII + Mermaid)
|
||||
- `/ck:preview --ascii <topic>` - Generate ASCII-only diagram (terminal-friendly)
|
||||
|
||||
### Generation Mode (HTML)
|
||||
- `/ck:preview --html --explain <topic>` - Self-contained HTML explanation
|
||||
- `/ck:preview --html --slides <topic>` - Magazine-quality HTML slide deck
|
||||
- `/ck:preview --html --diagram <topic>` - HTML diagram with zoom controls
|
||||
- `/ck:preview --html --diff [ref]` - Visual diff review
|
||||
- `/ck:preview --html --plan-review [plan-file]` - Plan vs codebase comparison
|
||||
- `/ck:preview --html --recap [timeframe]` - Project context snapshot
|
||||
|
||||
## Argument Resolution
|
||||
|
||||
When processing arguments, follow this priority order:
|
||||
|
||||
1. **`--stop`** → Stop server (exit)
|
||||
2. **`--html` flag present** → Set HTML output mode flag (continues to next step)
|
||||
3. **Generation flags** (`--explain`, `--slides`, `--diagram`, `--ascii`) → Generation mode. Load `references/generation-modes.md`
|
||||
4. **HTML-only flags** (`--diff`, `--plan-review`, `--recap`) → Auto-set HTML mode, then generation mode. Load `references/generation-modes.md`
|
||||
5. **Resolve path from argument:**
|
||||
- If argument is an explicit path → use directly
|
||||
- If argument is a contextual reference → resolve from recent conversation context
|
||||
6. **Resolved path exists on filesystem** → View mode. Load `references/view-mode.md`
|
||||
7. **Path doesn't exist or can't resolve** → Ask user to clarify
|
||||
|
||||
**Topic-to-slug conversion:**
|
||||
- Lowercase the topic
|
||||
- Replace spaces/special chars with hyphens
|
||||
- Remove non-alphanumeric except hyphens
|
||||
- Collapse multiple hyphens → single hyphen
|
||||
- Trim leading/trailing hyphens
|
||||
- **Max 80 chars** - truncate at word boundary if longer
|
||||
|
||||
**Multiple flags:** If multiple generation flags provided, use first one; remaining treated as topic.
|
||||
|
||||
**Placeholder `{topic}`:** Replaced with original user input in title case (not the slug).
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Error | Action |
|
||||
|-------|--------|
|
||||
| Invalid topic (empty) | Ask user to provide a topic |
|
||||
| Flag without topic | Ask user: "Please provide a topic: `/ck:preview --explain <topic>`" |
|
||||
| Topic becomes empty after sanitization | Ask for topic with alphanumeric characters |
|
||||
| File write failure | Report error, suggest checking disk space and permissions |
|
||||
| Server startup failure | Check if port in use, try `/ck:preview --stop` first |
|
||||
| No generation flag + unresolvable reference | Ask user to clarify which file they meant |
|
||||
| Existing file at output path | Overwrite with new content (no prompt) |
|
||||
| Server already running | Reuse existing server instance, just open new URL |
|
||||
| Parent `plans/` dir missing | Create directories recursively before write |
|
||||
| `--diff` without git context | Explain: "No git repo detected. Run inside a git repository." |
|
||||
| `--plan-review` without plan file or active plan | Explain: "Provide a plan file path or run from a session with an active plan." |
|
||||
| `--recap` without git history | Explain: "No git history found. Run inside a git repository with commits." |
|
||||
| `--html --ascii` combination | Not supported — `--ascii` is terminal-only by design. Suggest `--html --diagram` instead |
|
||||
| `--diff` with PR number but `gh` unavailable | Explain: "GitHub CLI (gh) is required for PR diffs. Install from https://cli.github.com/" |
|
||||
|
||||
## HTML Output Mode (`--html`)
|
||||
|
||||
Adding `--html` to any generation flag switches output from Markdown to a self-contained HTML file.
|
||||
|
||||
**Output:** Single `.html` file with all CSS/JS inline. Opens directly in browser — no server needed.
|
||||
**Location:** `{plan_dir}/visuals/{slug}.html` (same plan-aware logic as markdown mode)
|
||||
**Browser open:** `open` (macOS) / `xdg-open` (Linux) / `start` (Windows)
|
||||
**MANDATORY — Theme Toggle:** Every HTML page MUST include a light/dark theme toggle button. See `html-css-patterns.md` → "Theme Toggle Button" for the exact CSS, HTML, and JS to include. Pages without the toggle are considered incomplete.
|
||||
|
||||
### Reference Loading (HTML mode)
|
||||
|
||||
Before generating, agent MUST read these references:
|
||||
|
||||
| Mode | Always read | Mode-specific |
|
||||
|------|-------------|---------------|
|
||||
| All HTML modes | `html-design-guidelines.md` | — |
|
||||
| `--explain` | `html-css-patterns.md`, `html-libraries.md` | Template: `architecture.html` |
|
||||
| `--diagram` | `html-css-patterns.md`, `html-libraries.md` | Template: `mermaid-flowchart.html` or `architecture.html` |
|
||||
| `--slides` | `html-slide-patterns.md`, `html-css-patterns.md`, `html-libraries.md` | Template: `slide-deck.html` |
|
||||
| `--diff` | `html-css-patterns.md`, `html-libraries.md` | Templates: `data-table.html`, `architecture.html` |
|
||||
| `--plan-review` | `html-css-patterns.md`, `html-libraries.md` | Templates: `architecture.html`, `data-table.html` |
|
||||
| `--recap` | `html-css-patterns.md`, `html-libraries.md` | Templates: `architecture.html`, `data-table.html` |
|
||||
|
||||
Multi-section pages (`--explain`, `--diff`, `--plan-review`, `--recap`): also read `html-responsive-nav.md`.
|
||||
|
||||
Use `/ck:mermaidjs-v11` skill for Mermaid syntax validation.
|
||||
|
||||
### HTML-Only Modes
|
||||
|
||||
#### `--diff [ref]` (implies --html)
|
||||
Visual diff review. Scope detection: branch name, commit hash, HEAD, PR number, commit range, default=main.
|
||||
Data: git diff --stat, --name-status, changed files, new API surface, CHANGELOG.
|
||||
Output: executive summary, KPI dashboard, module architecture (Mermaid), feature comparisons (side-by-side), flow diagrams, file map, test coverage, code review cards (Good/Bad/Ugly/Questions), decision log, re-entry context.
|
||||
|
||||
#### `--plan-review [plan-file]` (implies --html)
|
||||
Plan vs codebase comparison. Input: plan file path or detect from active plan context.
|
||||
Data: read plan, read all referenced files, map blast radius, cross-reference assumptions.
|
||||
Output: plan summary, impact dashboard, current vs planned architecture (paired Mermaid), change breakdown (side-by-side), dependency analysis, risk assessment, review cards, understanding gaps.
|
||||
Visual language: blue=current, green=planned, amber=concern, red=gap.
|
||||
|
||||
#### `--recap [timeframe]` (implies --html)
|
||||
Project context snapshot. Time window: shorthand (2w, 30d, 3m) or default 2w.
|
||||
Data: project identity, git log, git status, decision context, architecture scan.
|
||||
Output: project identity, architecture snapshot (Mermaid), recent activity, decision log, state KPI cards, mental model essentials, cognitive debt hotspots, next steps.
|
||||
|
||||
### Style Strategy
|
||||
- Default: static anti-slop rules from `html-design-guidelines.md` (6 curated presets)
|
||||
- For `--slides`: consider invoking `/ck:ui-ux-pro-max` for richer style selection
|
||||
- Agent must vary aesthetics between consecutive HTML outputs (different font pair, palette)
|
||||
192
.opencode/skills/preview/references/generation-modes.md
Normal file
192
.opencode/skills/preview/references/generation-modes.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# Generation Modes
|
||||
|
||||
## Step 1: Determine Output Location
|
||||
|
||||
1. Check if there's an active plan context (from `## Plan Context` in hook injection)
|
||||
2. If active plan exists: save to `{plan_dir}/visuals/{topic-slug}.md`
|
||||
3. If no active plan: save to `plans/visuals/{topic-slug}.md`
|
||||
4. Create `visuals/` directory if it doesn't exist
|
||||
|
||||
## Step 2: Generate Content
|
||||
|
||||
**Mermaid Diagram Syntax:**
|
||||
When generating mermaid code blocks, use `/ck:mermaidjs-v11` skill for v11 syntax rules.
|
||||
|
||||
**Essential rules (always apply):**
|
||||
- Quote node text with special characters: `A["text with /slashes"]`
|
||||
- Escape brackets in labels: `A["array[0]"]`
|
||||
|
||||
Use the appropriate template based on flag:
|
||||
|
||||
### --explain (Visual Explanation)
|
||||
```markdown
|
||||
# Visual Explanation: {topic}
|
||||
|
||||
## Overview
|
||||
Brief description of what we're explaining.
|
||||
|
||||
## Quick View (ASCII)
|
||||
[ASCII diagram of component relationships]
|
||||
|
||||
## Detailed Flow
|
||||
[Mermaid sequence/flowchart diagram]
|
||||
|
||||
## Key Concepts
|
||||
1. **Concept A** - Explanation
|
||||
2. **Concept B** - Explanation
|
||||
|
||||
## Code Example (if applicable)
|
||||
[Relevant code snippet with comments]
|
||||
```
|
||||
|
||||
### --slides (Presentation Format)
|
||||
```markdown
|
||||
# {Topic} - Visual Presentation
|
||||
|
||||
---
|
||||
## Slide 1: Introduction
|
||||
- One concept per slide
|
||||
- Bullet points only
|
||||
|
||||
---
|
||||
## Slide 2: The Problem
|
||||
[Mermaid flowchart]
|
||||
|
||||
---
|
||||
## Slide 3: The Solution
|
||||
- Key point 1
|
||||
- Key point 2
|
||||
|
||||
---
|
||||
## Slide 4: Summary
|
||||
Key takeaways...
|
||||
```
|
||||
|
||||
### --diagram (Focused Diagram)
|
||||
```markdown
|
||||
# Diagram: {topic}
|
||||
|
||||
## ASCII Version
|
||||
[ASCII architecture diagram]
|
||||
|
||||
## Mermaid Version
|
||||
[Mermaid flowchart/graph]
|
||||
```
|
||||
|
||||
### --ascii (Terminal-Friendly Only)
|
||||
```
|
||||
[ASCII-only box diagram with legend]
|
||||
```
|
||||
|
||||
## Step 3: Save and Preview
|
||||
|
||||
1. Write generated content to determined path
|
||||
2. Start preview server with the generated file:
|
||||
```bash
|
||||
node .opencode/skills/markdown-novel-viewer/scripts/server.cjs \
|
||||
--file "<generated-file-path>" --host 0.0.0.0 --open --foreground
|
||||
```
|
||||
|
||||
## Step 4: Report to User
|
||||
|
||||
Report:
|
||||
- Generated file path
|
||||
- Preview URL (local + network)
|
||||
- Remind: file saved in plan's `visuals/` folder for future reference
|
||||
|
||||
---
|
||||
|
||||
## HTML Mode Generation
|
||||
|
||||
When `--html` flag is present (or implied by `--diff`, `--plan-review`, `--recap`), generate self-contained HTML instead of Markdown.
|
||||
|
||||
### HTML Step 1: Determine Output Location
|
||||
- Same plan-aware logic as markdown mode but with `.html` extension
|
||||
- Active plan: `{plan_dir}/visuals/{topic-slug}.html`
|
||||
- No plan: `plans/visuals/{topic-slug}.html`
|
||||
- Create `visuals/` directory if needed
|
||||
|
||||
### HTML Step 2: Read References
|
||||
Always read `html-design-guidelines.md` first (anti-slop rules, style presets).
|
||||
|
||||
Then read mode-specific references:
|
||||
|
||||
| Mode | References | Templates to study |
|
||||
|------|------------|-------------------|
|
||||
| --html --explain | html-css-patterns.md, html-libraries.md | architecture.html |
|
||||
| --html --diagram | html-css-patterns.md, html-libraries.md | mermaid-flowchart.html or architecture.html |
|
||||
| --html --slides | html-slide-patterns.md, html-css-patterns.md, html-libraries.md | slide-deck.html |
|
||||
| --html --diff | html-css-patterns.md, html-libraries.md | data-table.html, architecture.html |
|
||||
| --html --plan-review | html-css-patterns.md, html-libraries.md | architecture.html, data-table.html |
|
||||
| --html --recap | html-css-patterns.md, html-libraries.md | architecture.html, data-table.html |
|
||||
|
||||
For multi-section pages (explain, diff, plan-review, recap): also read `html-responsive-nav.md`.
|
||||
|
||||
### HTML Step 3: Generate Content
|
||||
|
||||
Follow the 4-phase workflow:
|
||||
|
||||
**Think:** Determine content-type routing:
|
||||
- Mermaid for topology (flowcharts, sequence, ER, state, mind maps, class, C4)
|
||||
- CSS Grid for text-heavy architecture (cards with descriptions, code references)
|
||||
- HTML `<table>` for data (requirement audits, comparisons, matrices)
|
||||
- Chart.js for real charts (KPI dashboards, sparklines)
|
||||
- Hybrid for complex systems (15+ elements): simple Mermaid overview + detailed CSS Grid cards
|
||||
|
||||
**Structure:** Pick template pattern, plan sections, assign depth tiers (hero/elevated/default/recessed).
|
||||
|
||||
**Style:** Select font pairing + palette from curated presets. Vary from previous outputs. Apply anti-slop checks:
|
||||
- No Inter/Roboto/system-ui alone as body font
|
||||
- No indigo/violet (#8b5cf6, #7c3aed) as accent
|
||||
- No animated glowing box-shadows
|
||||
- No gradient text on headings
|
||||
- No emoji icons in section headers
|
||||
- No three-dot window chrome on code blocks
|
||||
|
||||
**Deliver:** Write single self-contained `.html` file — all CSS and JavaScript inline. External resources: CDN only (Google Fonts, Mermaid.js v11, Chart.js, anime.js).
|
||||
|
||||
**MANDATORY — Theme Toggle:** Every HTML page MUST include the light/dark theme toggle button from `html-css-patterns.md` → "Theme Toggle Button" section. This is non-negotiable. The toggle button (`<button class="theme-toggle">`) must be the first child of `<body>`, with its CSS and JS inlined. Pages without the toggle are considered incomplete.
|
||||
|
||||
For `--slides`: recommend invoking `/ck:ui-ux-pro-max` for richer style selection.
|
||||
Must use `/ck:mermaidjs-v11` for any Mermaid diagrams.
|
||||
|
||||
### HTML Step 4: Open in Browser
|
||||
- macOS: `open "{output-path}"`
|
||||
- Linux: `xdg-open "{output-path}"`
|
||||
- Windows: `start "{output-path}"`
|
||||
- No server needed — file is self-contained
|
||||
- Report file path and confirm browser opened
|
||||
|
||||
### Data Gathering for HTML-Only Modes
|
||||
|
||||
#### --diff [ref]
|
||||
1. Detect scope: branch name → working tree diff; commit hash → `git show`; HEAD → uncommitted; PR number → `gh pr diff`; range → two commits; no arg → diff against main
|
||||
2. Run: `git diff --stat`, `git diff --name-status`, line counts
|
||||
3. Read all changed files + surrounding context
|
||||
4. Scan new public API surface (grep exports, functions, classes, interfaces)
|
||||
5. Check CHANGELOG.md, README.md, docs updates
|
||||
6. Reconstruct decision rationale from commits/conversation/progress docs
|
||||
|
||||
#### --plan-review [plan-file]
|
||||
1. Input: explicit plan file path OR detect from active plan context
|
||||
2. Read plan in full (problem, changes, rejected alternatives, scope)
|
||||
3. Read every file the plan references + their dependencies
|
||||
4. Map blast radius (imports, tests, config, public API)
|
||||
5. Cross-reference: plan assumptions vs actual code state
|
||||
|
||||
#### --recap [timeframe]
|
||||
1. Parse time window: shorthand (2w, 30d, 3m) → git `--since` format; default 2w
|
||||
2. Project identity: README, CHANGELOG, package.json, file structure
|
||||
3. Recent activity: `git log --oneline --since=...`, `git shortlog`
|
||||
4. Current state: `git status`, stale branches, TODOs, progress docs
|
||||
5. Decision context: commit messages, plans, ADRs
|
||||
6. Architecture scan: key files, module structure, frequently changed areas
|
||||
|
||||
### Quality Checklist
|
||||
Before delivering HTML output, verify:
|
||||
- [ ] **Squint test:** Visual hierarchy visible at arm's length?
|
||||
- [ ] **Swap test:** Would this look AI-generated? Check against forbidden patterns
|
||||
- [ ] **Theme toggle (MANDATORY):** Toggle button present as first child of `<body>`? Both light and dark modes render correctly? See `html-css-patterns.md` → "Theme Toggle Button".
|
||||
- [ ] **Overflow:** No horizontal scroll on content (tables excepted, wrapped in scroll container)
|
||||
- [ ] **Mermaid:** Zoom controls present? ELK layout for 10+ nodes?
|
||||
- [ ] **Responsiveness:** Readable on mobile width?
|
||||
1717
.opencode/skills/preview/references/html-css-patterns.md
Normal file
1717
.opencode/skills/preview/references/html-css-patterns.md
Normal file
File diff suppressed because it is too large
Load Diff
393
.opencode/skills/preview/references/html-design-guidelines.md
Normal file
393
.opencode/skills/preview/references/html-design-guidelines.md
Normal file
@@ -0,0 +1,393 @@
|
||||
# HTML Design Guidelines
|
||||
|
||||
Guidelines for generating distinctive, high-quality HTML pages. Read this before generating any HTML output.
|
||||
|
||||
---
|
||||
|
||||
## Anti-Slop: Forbidden Patterns
|
||||
|
||||
These patterns are explicitly forbidden — they signal "AI-generated template" and undermine quality. Check every page against this list before delivering.
|
||||
|
||||
### 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 `html-libraries.md`. Every generation should use a different pairing from the last.
|
||||
|
||||
### Color Palette — Forbidden Accents
|
||||
|
||||
- `#8b5cf6`, `#7c3aed`, `#a78bfa` — Tailwind's indigo/violet defaults
|
||||
- `#d946ef` — fuchsia
|
||||
- `#06b6d4` + `#d946ef` + `#f472b6` — the cyan + magenta + pink neon gradient combination
|
||||
- Any palette describable as "Tailwind defaults with purple/pink/cyan accents"
|
||||
|
||||
**Forbidden color effects:**
|
||||
- Gradient text on headings (`background: linear-gradient(...); background-clip: text;`) — screams AI-generated
|
||||
- Animated glowing box-shadows (`@keyframes glow { box-shadow: 0 0 20px... }`) — always produces AI slop
|
||||
- Multiple overlapping radial glows in accent colors creating a "neon haze"
|
||||
- Pulsing/breathing effects on static content
|
||||
- Continuous animations that run after page load (except progress indicators)
|
||||
|
||||
**Required accents (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
|
||||
|
||||
### 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 (`.section-label` + `.ve-card__label` pattern), numbered badges, or asymmetric section dividers. If an icon is genuinely needed, use inline SVG matching the palette.
|
||||
|
||||
### Layout — 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
|
||||
|
||||
### Template Clichés — Forbidden
|
||||
|
||||
- Three-dot window chrome (red/yellow/green dots) on code blocks
|
||||
- KPI cards where every metric has identical gradient text treatment
|
||||
- "Neon Dashboard" aesthetic
|
||||
- Gradient meshes with pink/purple/cyan blobs in the background
|
||||
|
||||
### The Slop Test
|
||||
|
||||
Before delivering, check: **Would a developer immediately think "AI generated this"?**
|
||||
|
||||
Signs of slop:
|
||||
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 are present: regenerate with Blueprint, Editorial, Paper/ink, or a specific IDE theme.
|
||||
|
||||
---
|
||||
|
||||
## Palette Cohesion Principles
|
||||
|
||||
**Every generated page must feel like one intentional color story.** This is the single most important readability rule.
|
||||
|
||||
1. **Background warmth must match accent warmth.** Terracotta accents need warm cream backgrounds (`#faf7f5`), not cool gray (`#f8f9fa`). Teal accents need cool-tinted backgrounds (`#f0fdfa`). Mixing warm accents on cool backgrounds (or vice versa) creates visual dissonance that makes pages feel generic.
|
||||
|
||||
2. **Text-dim must belong to the same family.** On warm backgrounds, use warm grays (`#8a7e72`, `#a69889`). On cool backgrounds, use cool grays (`#5f8a85`, `#8b949e`). Never use GitHub-style `#6b7280` on warm cream.
|
||||
|
||||
3. **Borders should be tinted, not neutral.** Use `rgba(0, 0, 0, 0.07)` or palette-tinted borders instead of flat `#e5e7eb`. Borders should be barely visible — felt, not seen.
|
||||
|
||||
4. **Surface layers create depth without fighting.** Define `--surface`, `--surface2`, and `--surface-elevated` as gradations of the same hue, not different colors.
|
||||
|
||||
5. **Extend every palette with semantic colors.** Every preset below should also define `--green`, `--red`, `--amber`, `--sage`, `--teal`, `--plum` (and their `*-dim` variants) that harmonize with the base palette. Richer semantic sets prevent monotony without clashing.
|
||||
|
||||
---
|
||||
|
||||
## 6 Curated Style Presets
|
||||
|
||||
Pick one and commit. The constrained presets (Blueprint, Editorial, Paper/Ink, Terminal Mono) are safer — they have specific requirements that prevent defaulting to generic patterns.
|
||||
|
||||
**IMPORTANT:** After choosing a preset, extend it with semantic colors (`--green`, `--red`, `--amber`, `--sage`, `--teal`, `--plum` + `*-dim` variants) that harmonize with the base palette. The default palette in `html-css-patterns.md` shows the full semantic structure — replicate that structure for whichever preset you choose.
|
||||
|
||||
### Blueprint
|
||||
|
||||
Technical drawing feel. Subtle grid background, deep slate/blue palette, monospace labels, precise borders.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--font-body: 'DM Sans', system-ui, sans-serif;
|
||||
--font-mono: 'Fira Code', 'SF Mono', monospace;
|
||||
--bg: #0d1421;
|
||||
--surface: #111d2e;
|
||||
--surface-elevated: #162438;
|
||||
--border: rgba(100, 160, 220, 0.12);
|
||||
--border-bright: rgba(100, 160, 220, 0.22);
|
||||
--text: #c8d8e8;
|
||||
--text-dim: #607080;
|
||||
--accent: #4a90d9;
|
||||
--accent-dim: rgba(74, 144, 217, 0.1);
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--bg: #f0f4f8;
|
||||
--surface: #ffffff;
|
||||
--surface-elevated: #e8eef4;
|
||||
--border: rgba(30, 60, 100, 0.1);
|
||||
--text: #1a2a3a;
|
||||
--text-dim: #5a7090;
|
||||
--accent: #1a5fa8;
|
||||
--accent-dim: rgba(26, 95, 168, 0.08);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Background: faint dot grid (`background-image: radial-gradient(circle, var(--border) 1px, transparent 1px); background-size: 24px 24px`). Monospace labels throughout.
|
||||
|
||||
### Editorial
|
||||
|
||||
Serif headlines (Instrument Serif or Crimson Pro), generous whitespace, muted earth tones or deep navy + gold.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--font-body: 'Instrument Serif', Georgia, serif;
|
||||
--font-mono: 'JetBrains Mono', 'SF Mono', monospace;
|
||||
--bg: #0f1729;
|
||||
--surface: #162040;
|
||||
--surface-elevated: #1d2b52;
|
||||
--border: rgba(200, 180, 140, 0.08);
|
||||
--text: #e8e4d8;
|
||||
--text-dim: #9a9484;
|
||||
--accent: #d4a73a;
|
||||
--accent-dim: rgba(212, 167, 58, 0.1);
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--bg: #faf8f2;
|
||||
--surface: #ffffff;
|
||||
--surface-elevated: #f5f0e6;
|
||||
--border: rgba(30, 30, 50, 0.08);
|
||||
--text: #1a1814;
|
||||
--text-dim: #7a7468;
|
||||
--accent: #b8860b;
|
||||
--accent-dim: rgba(184, 134, 11, 0.08);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Paper/Ink
|
||||
|
||||
Warm cream `#faf7f5` background, terracotta/sage accents, informal feel.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--font-body: 'Plus Jakarta Sans', system-ui, sans-serif;
|
||||
--font-mono: 'Azeret Mono', 'SF Mono', monospace;
|
||||
--bg: #faf6f0;
|
||||
--surface: #ffffff;
|
||||
--surface-elevated: #fffdf5;
|
||||
--border: rgba(60, 40, 20, 0.08);
|
||||
--text: #2c2a25;
|
||||
--text-dim: #7c756a;
|
||||
--accent: #c2410c;
|
||||
--accent-dim: rgba(194, 65, 12, 0.08);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #1c1916;
|
||||
--surface: #262220;
|
||||
--surface-elevated: #3a3430;
|
||||
--border: rgba(200, 180, 160, 0.08);
|
||||
--text: #f0e8dc;
|
||||
--text-dim: #a09888;
|
||||
--accent: #e85d2a;
|
||||
--accent-dim: rgba(232, 93, 42, 0.1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Terminal Mono
|
||||
|
||||
Green/amber on near-black, monospace everything. Developer-native.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--font-body: 'Geist Mono', 'SF Mono', Consolas, monospace;
|
||||
--font-mono: 'Geist Mono', 'SF Mono', Consolas, monospace;
|
||||
--bg: #0a0e14;
|
||||
--surface: #12161e;
|
||||
--surface-elevated: #222836;
|
||||
--border: rgba(80, 250, 123, 0.06);
|
||||
--text: #c8d6e5;
|
||||
--text-dim: #5a6a7a;
|
||||
--accent: #50fa7b;
|
||||
--accent-dim: rgba(80, 250, 123, 0.08);
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--bg: #f4f6f8;
|
||||
--surface: #ffffff;
|
||||
--border: rgba(0, 80, 40, 0.08);
|
||||
--text: #1a2332;
|
||||
--text-dim: #5a6a7a;
|
||||
--accent: #0d7a3e;
|
||||
--accent-dim: rgba(13, 122, 62, 0.08);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Background: faint dot grid. Everything monospace. CRT glow optional (CSS only, no animation).
|
||||
|
||||
### Swiss Clean
|
||||
|
||||
White, geometric sans, single bold accent, visible grid. Minimal and precise.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--font-body: 'DM Sans', system-ui, sans-serif;
|
||||
--font-mono: 'Fira Code', 'SF Mono', monospace;
|
||||
--bg: #ffffff;
|
||||
--surface: #f8f8f8;
|
||||
--surface-elevated: #ffffff;
|
||||
--border: rgba(0, 0, 0, 0.08);
|
||||
--text: #111111;
|
||||
--text-dim: #666666;
|
||||
--accent: #0055ff;
|
||||
--accent-dim: rgba(0, 85, 255, 0.06);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #111111;
|
||||
--surface: #1a1a1a;
|
||||
--surface-elevated: #2a2a2a;
|
||||
--border: rgba(255, 255, 255, 0.08);
|
||||
--text: #f0f0f0;
|
||||
--text-dim: #888888;
|
||||
--accent: #3b82f6;
|
||||
--accent-dim: rgba(59, 130, 246, 0.08);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Warm Signal
|
||||
|
||||
Cream paper, bold sans, terracotta accents. Confident, modern. Same as Paper/Ink but bolder.
|
||||
|
||||
Uses Plus Jakarta Sans + Azeret Mono, terracotta `#c2410c` accent. See Paper/Ink preset above — Warm Signal is the same palette with higher contrast headings and stronger section dividers.
|
||||
|
||||
---
|
||||
|
||||
## Typography Rules
|
||||
|
||||
### Font Pairings (12 options — rotate, never repeat consecutively)
|
||||
|
||||
| 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 |
|
||||
|
||||
The first 5 pairings are recommended for most use cases.
|
||||
|
||||
### Load via Google Fonts
|
||||
|
||||
```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=DM+Sans:wght@400;500;600;700&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
|
||||
```
|
||||
|
||||
Always use `display=swap` for fast rendering. Include system font fallback in `font-family` stack.
|
||||
|
||||
### Typography by Content Voice
|
||||
|
||||
For prose-heavy pages, match fonts to content voice:
|
||||
|
||||
| Voice | Fonts | Best For |
|
||||
|-------|-------|----------|
|
||||
| Literary / Thoughtful | Literata, Lora, Newsreader, Merriweather | Essays, personal posts, long-form |
|
||||
| Technical / Precise | IBM Plex Sans + Mono, Geist + Geist Mono | Documentation, READMEs, API refs |
|
||||
| Bold / Contemporary | Bricolage Grotesque, Space Grotesk, DM Sans | Product pages, announcements |
|
||||
| Minimal / Focused | Source Serif 4 + Source Sans 3, Karla + Inconsolata | Tutorials, focused reading |
|
||||
|
||||
---
|
||||
|
||||
## Quality Checklist
|
||||
|
||||
Before delivering any HTML page:
|
||||
|
||||
- **Squint test**: Blur your eyes. Can you still perceive hierarchy? Are sections visually distinct?
|
||||
- **Swap test**: Would replacing fonts and colors with a generic dark theme make this indistinguishable? If yes, push the aesthetic further.
|
||||
- **Theme toggle (MANDATORY)**: Toggle button MUST be present (first child of `<body>`). Switch between light and dark using the button. Both themes should look intentional, not broken. See `html-css-patterns.md` → "Theme Toggle Button".
|
||||
- **Information completeness**: Does the page actually convey what was asked? Pretty but incomplete is a failure.
|
||||
- **No overflow**: Resize the browser. No content should clip or escape its container. Every grid/flex child needs `min-width: 0`. Side-by-side panels need `overflow-wrap: break-word`.
|
||||
- **Mermaid zoom controls**: Every `.mermaid-wrap` must have zoom controls (+/−/reset/expand), Ctrl/Cmd+scroll zoom, click-and-drag panning, and click-to-expand. See `html-css-patterns.md`.
|
||||
- **File opens cleanly**: No console errors, no broken font loads, no layout shifts.
|
||||
|
||||
---
|
||||
|
||||
## Depth Tier System
|
||||
|
||||
Vary card depth to signal importance. Hero sections dominate; reference sections stay compact.
|
||||
|
||||
```css
|
||||
/* Default — flat, no shadow */
|
||||
.ve-card {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
/* Elevated — KPIs, key sections */
|
||||
.ve-card--elevated {
|
||||
background: var(--surface-elevated);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* Recessed — code blocks, secondary content */
|
||||
.ve-card--recessed {
|
||||
background: color-mix(in srgb, var(--bg) 70%, var(--surface) 30%);
|
||||
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* Hero — executive summaries, focal elements */
|
||||
.ve-card--hero {
|
||||
background: color-mix(in srgb, var(--surface) 92%, var(--accent) 8%);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||
border-color: color-mix(in srgb, var(--border) 50%, var(--accent) 50%);
|
||||
}
|
||||
```
|
||||
|
||||
Rule: Don't make everything elevated — when everything pops, nothing does.
|
||||
|
||||
---
|
||||
|
||||
## Content-Type Routing
|
||||
|
||||
When deciding how to render content:
|
||||
|
||||
| Content type | Approach | Why |
|
||||
|---|---|---|
|
||||
| Architecture (text-heavy) | CSS Grid cards + flow arrows | Rich card content needs CSS control |
|
||||
| Architecture (topology-focused) | **Mermaid** | Visible connections need automatic edge routing |
|
||||
| Flowchart / pipeline | **Mermaid** | Automatic node positioning |
|
||||
| Sequence diagram | **Mermaid** | Lifelines need automatic layout |
|
||||
| Data flow | **Mermaid** with edge labels | Connections need auto-routing |
|
||||
| ER / schema diagram | **Mermaid** | Relationship lines between entities |
|
||||
| State machine | **Mermaid** | State transitions with labeled edges |
|
||||
| Mind map | **Mermaid** | Hierarchical branching |
|
||||
| Class diagram | **Mermaid** | Inheritance lines with auto-routing |
|
||||
| C4 architecture | **Mermaid** `graph TD` + `subgraph` | Native C4 hardcodes its own styles |
|
||||
| Data table | HTML `<table>` | Semantic markup, accessibility, copy-paste |
|
||||
| Timeline | CSS (central line + cards) | Simple linear layout |
|
||||
| Dashboard | CSS Grid + Chart.js | Card grid with embedded charts |
|
||||
| Simple A→B→C flows in slides | CSS Pipeline cards | Mermaid renders too small for simple linear flows |
|
||||
|
||||
---
|
||||
|
||||
## AI Image Generation
|
||||
|
||||
If `/ck:ai-multimodal` skill is available and image generation is appropriate, it can be used for hero banners, conceptual illustrations, and decorative accents that establish the page's visual tone.
|
||||
|
||||
**When to use:** Hero banners, conceptual illustrations for abstract systems, educational diagrams benefiting from artistic rendering, decorative accents reinforcing 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 — the page should stand on its own with CSS and typography alone.
|
||||
|
||||
For `--slides` presentation-grade output, consider invoking `/ck:ui-ux-pro-max` for richer style selection and distinctive font/palette pairing.
|
||||
592
.opencode/skills/preview/references/html-libraries.md
Normal file
592
.opencode/skills/preview/references/html-libraries.md
Normal file
@@ -0,0 +1,592 @@
|
||||
# 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 html-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. 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)]` — 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
|
||||
UI -->|"Use as Reference"| RET
|
||||
|
||||
%% RIGHT — use single quotes or no quotes for simple text
|
||||
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 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.
|
||||
|
||||
**For multi-line flowchart node labels, use `<br/>` (not `\n`):**
|
||||
|
||||
```
|
||||
%% 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:
|
||||
|
||||
```
|
||||
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:
|
||||
|
||||
```
|
||||
userSvc["User Service"] --> authSvc["Auth Service"]
|
||||
```
|
||||
|
||||
**Max 10-12 nodes per Mermaid diagram.** Beyond that, readability collapses even with zoom controls. 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.
|
||||
|
||||
```
|
||||
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 |
|
||||
|
||||
**Sequence diagram messages must be plain text.** Unlike flowchart labels, sequence diagram messages cannot be quoted or escaped. Curly braces `{}`, square brackets `[]`, angle brackets `<>`, and `&` will silently break the parser:
|
||||
|
||||
```
|
||||
%% WRONG — parser chokes on braces, brackets, ampersand
|
||||
A->>B: web_search({ queries: [...] })
|
||||
B->>B: User removes query 2, keeps 1 & 3
|
||||
|
||||
%% RIGHT — plain English, no special characters
|
||||
A->>B: Call web_search with queries
|
||||
B->>B: User removes query 2, keeps 1 and 3
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
| Direction | Use when | Avoid when |
|
||||
|-----------|----------|------------|
|
||||
| `TD` (top-down) | Complex diagrams, 5+ nodes, hierarchies | Simple A→B→C linear flows |
|
||||
| `LR` (left-to-right) | Simple linear flows, 3-4 nodes | Complex graphs, many branches |
|
||||
|
||||
**Rule of thumb:** If the diagram has more than one row of nodes or any branching, use `TD`.
|
||||
|
||||
### 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.
|
||||
|
||||
### Which Mermaid Diagram Type?
|
||||
|
||||
| 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` |
|
||||
| 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
.opencode/skills/preview/references/html-responsive-nav.md
Normal file
212
.opencode/skills/preview/references/html-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.
|
||||
1401
.opencode/skills/preview/references/html-slide-patterns.md
Normal file
1401
.opencode/skills/preview/references/html-slide-patterns.md
Normal file
File diff suppressed because it is too large
Load Diff
42
.opencode/skills/preview/references/view-mode.md
Normal file
42
.opencode/skills/preview/references/view-mode.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# View Mode
|
||||
|
||||
## Execution
|
||||
|
||||
**IMPORTANT:** Run server as Claude Code background task using `run_in_background: true` with the Bash tool.
|
||||
|
||||
The skill is located at `.opencode/skills/markdown-novel-viewer/`.
|
||||
|
||||
### Stop Server
|
||||
|
||||
If `--stop` flag is provided:
|
||||
|
||||
```bash
|
||||
node .opencode/skills/markdown-novel-viewer/scripts/server.cjs --stop
|
||||
```
|
||||
|
||||
### Start Server
|
||||
|
||||
Run the `markdown-novel-viewer` server as CC background task with `--foreground` flag:
|
||||
|
||||
```bash
|
||||
INPUT_PATH="<resolved-path>"
|
||||
if [[ -d "$INPUT_PATH" ]]; then
|
||||
node .opencode/skills/markdown-novel-viewer/scripts/server.cjs \
|
||||
--dir "$INPUT_PATH" --host 0.0.0.0 --open --foreground
|
||||
else
|
||||
node .opencode/skills/markdown-novel-viewer/scripts/server.cjs \
|
||||
--file "$INPUT_PATH" --host 0.0.0.0 --open --foreground
|
||||
fi
|
||||
```
|
||||
|
||||
**Critical:** When calling the Bash tool:
|
||||
- Set `run_in_background: true`
|
||||
- Set `timeout: 300000` (5 minutes)
|
||||
- Parse JSON output and report URL to user
|
||||
|
||||
After starting, report:
|
||||
- Local URL for browser access
|
||||
- Network URL for remote device access
|
||||
- Inform user that server is now running as CC background task (visible in `/tasks`)
|
||||
|
||||
**CRITICAL:** MUST display the FULL URL including path and query string. NEVER truncate to just `host:port`.
|
||||
650
.opencode/skills/preview/templates/architecture.html
Normal file
650
.opencode/skills/preview/templates/architecture.html
Normal file
@@ -0,0 +1,650 @@
|
||||
<!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 ck:preview 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:not([data-theme="light"]) {
|
||||
--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);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Dark (manual toggle override) ── */
|
||||
[data-theme="dark"] {
|
||||
--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);
|
||||
}
|
||||
|
||||
/* ============ THEME TOGGLE ============ */
|
||||
.theme-toggle {
|
||||
position: fixed; top: 16px; right: 16px; z-index: 300;
|
||||
width: 36px; height: 36px; border-radius: 8px;
|
||||
border: 1px solid var(--border); background: var(--surface);
|
||||
color: var(--text-dim); cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 16px; transition: background 0.15s, color 0.15s;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.theme-toggle:hover { background: var(--surface2); color: var(--text); }
|
||||
|
||||
/* ============ 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>
|
||||
<button class="theme-toggle" id="themeToggle" title="Toggle theme" aria-label="Toggle light/dark theme"></button>
|
||||
<script>
|
||||
(function() {
|
||||
var t = document.getElementById('themeToggle');
|
||||
var s = localStorage.getItem('theme');
|
||||
var i = s || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||
if (s) document.documentElement.setAttribute('data-theme', i);
|
||||
t.textContent = i === 'dark' ? '\u2600' : '\u263E';
|
||||
t.addEventListener('click', function() {
|
||||
var c = document.documentElement.getAttribute('data-theme')
|
||||
|| (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||
var n = c === 'light' ? 'dark' : 'light';
|
||||
document.documentElement.setAttribute('data-theme', n);
|
||||
localStorage.setItem('theme', n);
|
||||
t.textContent = n === 'dark' ? '\u2600' : '\u263E';
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<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>
|
||||
590
.opencode/skills/preview/templates/data-table.html
Normal file
590
.opencode/skills/preview/templates/data-table.html
Normal file
@@ -0,0 +1,590 @@
|
||||
<!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 ck:preview 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:not([data-theme="light"]) {
|
||||
--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);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Dark (manual toggle override) ── */
|
||||
[data-theme="dark"] {
|
||||
--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);
|
||||
}
|
||||
|
||||
/* ============ THEME TOGGLE ============ */
|
||||
.theme-toggle {
|
||||
position: fixed; top: 16px; right: 16px; z-index: 300;
|
||||
width: 36px; height: 36px; border-radius: 8px;
|
||||
border: 1px solid var(--border); background: var(--surface);
|
||||
color: var(--text-dim); cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 16px; transition: background 0.15s, color 0.15s;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.theme-toggle:hover { background: var(--surface2); color: var(--text); }
|
||||
|
||||
/* ============ 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>
|
||||
<button class="theme-toggle" id="themeToggle" title="Toggle theme" aria-label="Toggle light/dark theme"></button>
|
||||
<script>
|
||||
(function() {
|
||||
var t = document.getElementById('themeToggle');
|
||||
var s = localStorage.getItem('theme');
|
||||
var i = s || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||
if (s) document.documentElement.setAttribute('data-theme', i);
|
||||
t.textContent = i === 'dark' ? '\u2600' : '\u263E';
|
||||
t.addEventListener('click', function() {
|
||||
var c = document.documentElement.getAttribute('data-theme')
|
||||
|| (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||
var n = c === 'light' ? 'dark' : 'light';
|
||||
document.documentElement.setAttribute('data-theme', n);
|
||||
localStorage.setItem('theme', n);
|
||||
t.textContent = n === 'dark' ? '\u2600' : '\u263E';
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<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>
|
||||
768
.opencode/skills/preview/templates/mermaid-flowchart.html
Normal file
768
.opencode/skills/preview/templates/mermaid-flowchart.html
Normal file
@@ -0,0 +1,768 @@
|
||||
<!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 ck:preview 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:not([data-theme="light"]) {
|
||||
--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;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Dark (manual toggle override) ── */
|
||||
[data-theme="dark"] {
|
||||
--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;
|
||||
}
|
||||
|
||||
/* ============ THEME TOGGLE ============ */
|
||||
.theme-toggle {
|
||||
position: fixed; top: 16px; right: 16px; z-index: 300;
|
||||
width: 36px; height: 36px; border-radius: 8px;
|
||||
border: 1px solid var(--border); background: var(--surface);
|
||||
color: var(--text-dim); cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 16px; transition: background 0.15s, color 0.15s;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.theme-toggle:hover { background: var(--surface); color: var(--text); }
|
||||
|
||||
/* ============ 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>
|
||||
<button class="theme-toggle" id="themeToggle" title="Toggle theme" aria-label="Toggle light/dark theme"></button>
|
||||
<script>
|
||||
(function() {
|
||||
var t = document.getElementById('themeToggle');
|
||||
var s = localStorage.getItem('theme');
|
||||
var i = s || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||
if (s) document.documentElement.setAttribute('data-theme', i);
|
||||
t.textContent = i === 'dark' ? '\u2600' : '\u263E';
|
||||
t.addEventListener('click', function() {
|
||||
var c = document.documentElement.getAttribute('data-theme')
|
||||
|| (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||
var n = c === 'light' ? 'dark' : 'light';
|
||||
document.documentElement.setAttribute('data-theme', n);
|
||||
localStorage.setItem('theme', n);
|
||||
t.textContent = n === 'dark' ? '\u2600' : '\u263E';
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<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>
|
||||
968
.opencode/skills/preview/templates/slide-deck.html
Normal file
968
.opencode/skills/preview/templates/slide-deck.html
Normal file
@@ -0,0 +1,968 @@
|
||||
<!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 ck:preview 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:not([data-theme="dark"]) {
|
||||
--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);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Light (manual toggle override) ── */
|
||||
[data-theme="light"] {
|
||||
--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);
|
||||
}
|
||||
|
||||
/* ============ THEME TOGGLE ============ */
|
||||
.theme-toggle {
|
||||
position: fixed; top: 16px; right: 16px; z-index: 300;
|
||||
width: 36px; height: 36px; border-radius: 8px;
|
||||
border: 1px solid var(--border); background: var(--surface);
|
||||
color: var(--text-dim); cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 16px; transition: background 0.15s, color 0.15s;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.theme-toggle:hover { background: var(--surface2); color: var(--text); }
|
||||
|
||||
* { 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>
|
||||
<button class="theme-toggle" id="themeToggle" title="Toggle theme" aria-label="Toggle light/dark theme"></button>
|
||||
<script>
|
||||
(function() {
|
||||
var t = document.getElementById('themeToggle');
|
||||
var s = localStorage.getItem('theme');
|
||||
var i = s || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||
if (s) document.documentElement.setAttribute('data-theme', i);
|
||||
t.textContent = i === 'dark' ? '\u2600' : '\u263E';
|
||||
t.addEventListener('click', function() {
|
||||
var c = document.documentElement.getAttribute('data-theme')
|
||||
|| (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||
var n = c === 'light' ? 'dark' : 'light';
|
||||
document.documentElement.setAttribute('data-theme', n);
|
||||
localStorage.setItem('theme', n);
|
||||
t.textContent = n === 'dark' ? '\u2600' : '\u263E';
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<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>
|
||||
Reference in New Issue
Block a user