|
| 1 | +--- |
| 2 | +name: codecept-diagram |
| 3 | +description: Compose embeddable perspective-style SVG+PNG diagrams for codecept.site docs. Uses the site's honey-gold palette, Bricolage/Inter/JetBrains fonts, and the rehype-inject-figure plugin so diagrams land in upstream-synced markdown without editing the .md file. |
| 4 | +--- |
| 5 | + |
| 6 | +# /codecept-diagram |
| 7 | + |
| 8 | +Build a diagram for a docs page on codecept.site and wire it into the page via the rehype injection plugin. The style is locked to **perspective** (white card + side parallelograms + ambient drop shadow) in the site's honey-gold palette. No icons — typography only. Up to 5 elements per diagram. |
| 9 | + |
| 10 | +## When to use |
| 11 | + |
| 12 | +- Any docs page that needs a hero-style explanation graphic (one per major section, sometimes two) |
| 13 | +- The user names a section / `## Heading` to land the diagram under |
| 14 | +- The doc is upstream-synced from the CodeceptJS repo and **must not be edited** — use the rehype plugin |
| 15 | + |
| 16 | +If the user already gave you the concept + heading, **don't ask back**. Read the doc, design, render, embed. |
| 17 | + |
| 18 | +## Non-negotiables |
| 19 | + |
| 20 | +### 1 · Read the doc first |
| 21 | + |
| 22 | +The diagram **explains what the doc says**, not what you think the topic is. |
| 23 | + |
| 24 | +- Open the target `.md` under `src/content/docs/` |
| 25 | +- If the doc has a table (preference order, lifecycle order, …), the diagram **must follow that order exactly** |
| 26 | +- Use the doc's terminology verbatim — `BeforeSuite`, `Scenario`, `inject()`, `helpers`, `I.click()` — not invented buckets like "Hooks block" or "Tags block" |
| 27 | +- If you need to verify runtime behavior, read the CodeceptJS source at `/home/davert/.bun/install/cache/codeceptjs@*/lib/` — `container.js`, `actor.js`, `globals.js`, `mocha/inject.js` are the load-bearing files |
| 28 | + |
| 29 | +### 2 · Read existing diagrams for the visual grammar |
| 30 | + |
| 31 | +Before composing, open one or two of these and use them as the geometric template: |
| 32 | + |
| 33 | +- `/public/test-structure.svg` — container-with-inner-blocks, serpentine arrows |
| 34 | +- `/public/pageobjects-di.svg` — 3-card row with code blocks and arrows between |
| 35 | +- `/public/helpers-delegation.svg` — hub-and-spoke (1 hub + 4 satellites, fan-out arrows) |
| 36 | +- `/public/locators.svg` — 4-card preference row, no kickers, plain examples |
| 37 | +- `/public/agents-loop.svg` — cycle / loop |
| 38 | +- `/public/skills-bundle.svg` — bundle / set |
| 39 | +- `/public/retry-levels.svg` — nested-levels stack |
| 40 | + |
| 41 | +Don't reinvent geometry the existing diagrams already solve. Copy the structure and re-skin it for the new concept. |
| 42 | + |
| 43 | +### 3 · Verify by eye (per memory) |
| 44 | + |
| 45 | +`magick` dimension/transparency checks do **not** catch cut text, clipped content, or arrows pointing at nothing. After rendering the PNG you **must** `Read` it and trace each element by sight. If anything is clipped, increase the viewBox and re-render. |
| 46 | + |
| 47 | +## Style tokens |
| 48 | + |
| 49 | +### Fonts (always loaded via `@import` in `<style>`) |
| 50 | + |
| 51 | +``` |
| 52 | +Bricolage Grotesque — titles, hero headings (700, ss01+ss02, tight tracking) |
| 53 | +Inter — body text (500, slightly tight) |
| 54 | +JetBrains Mono — kicker, code, hero monospace |
| 55 | +``` |
| 56 | + |
| 57 | +`@import` line — paste verbatim at the top of `<style>`: |
| 58 | + |
| 59 | +```css |
| 60 | +@import url('https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400..800&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400..700&display=swap'); |
| 61 | +``` |
| 62 | + |
| 63 | +(In SVG, `&` must be written as `&` inside the `<style>` text node.) |
| 64 | + |
| 65 | +### Typography classes (use these names — the rehype plugin scopes them per-figure so they don't clash) |
| 66 | + |
| 67 | +```css |
| 68 | +.kicker { font-family: 'JetBrains Mono', monospace; font-weight: 700; font-size: 11px; letter-spacing: 0.2em; text-transform: uppercase; } |
| 69 | +.title-xl { font-family: 'Bricolage Grotesque', sans-serif; font-weight: 700; font-size: 32px; letter-spacing: -0.025em; font-feature-settings: 'ss01','ss02'; } |
| 70 | +.title { font-family: 'Bricolage Grotesque', sans-serif; font-weight: 700; font-size: 24px; letter-spacing: -0.02em; font-feature-settings: 'ss01','ss02'; } |
| 71 | +.body { font-family: 'Inter', sans-serif; font-weight: 500; font-size: 14px; letter-spacing: -0.005em; } |
| 72 | +.hero { font-family: 'JetBrains Mono', monospace; font-weight: 700; font-size: 28px; letter-spacing: -0.01em; } |
| 73 | +.mono-lg { font-family: 'JetBrains Mono', monospace; font-weight: 600; font-size: 14px; } |
| 74 | +.mono { font-family: 'JetBrains Mono', monospace; font-weight: 500; font-size: 13px; } |
| 75 | +.mono-sm { font-family: 'JetBrains Mono', monospace; font-weight: 500; font-size: 12px; } |
| 76 | +.backend { font-family: 'JetBrains Mono', monospace; font-weight: 500; font-size: 12px; } |
| 77 | +``` |
| 78 | + |
| 79 | +Title size is the only thing that wobbles between diagrams — 23–26 px is normal, 32 for a hero. Pick once per diagram, don't change mid-svg. |
| 80 | + |
| 81 | +### Palette |
| 82 | + |
| 83 | +Honey gold (accents, arrows, kickers-on-emphasis, code keywords): |
| 84 | +- `#F2A900` — light honey (rarely used; prefer #C88800) |
| 85 | +- `#C88800` — primary honey, all arrows and primary accents |
| 86 | + |
| 87 | +Neutral type: |
| 88 | +- `#111827` — main card title |
| 89 | +- `#1F2937` — code default |
| 90 | +- `#334155` — muted heading (e.g. optional-block title) |
| 91 | +- `#4B5563` — body text |
| 92 | +- `#6B7280` — italic subtitle next to title |
| 93 | +- `#64748B` — mono muted (backend / right-aligned annotation) |
| 94 | +- `#94A3B8` — kicker muted (optional blocks) |
| 95 | +- `#9CA3AF` — code comments, very subtle annotations |
| 96 | + |
| 97 | +Panel / surface: |
| 98 | +- `#FFFFFF` — card front (default) |
| 99 | +- `#F1F5F9` — perspective top face |
| 100 | +- `#E2E8F0` — perspective right face |
| 101 | +- `#FAFAF7` — code block fill |
| 102 | +- `#F0EDE5` — code block 1px border |
| 103 | + |
| 104 | +Honey container (only the outermost wrap-block when a diagram has nested children, like `test-structure`): |
| 105 | +- `#FFFBF0` — container front |
| 106 | +- `#FFF1D6` — container top face |
| 107 | +- `#F5E5B0` — container right face |
| 108 | + |
| 109 | +### Shadow filters |
| 110 | + |
| 111 | +Define both in `<defs>` and pick per card: |
| 112 | + |
| 113 | +```xml |
| 114 | +<filter id="ambient" x="-12%" y="-12%" width="124%" height="124%"> |
| 115 | + <feDropShadow dx="0" dy="4" stdDeviation="9" flood-color="#1a1d28" flood-opacity="0.12"/> |
| 116 | +</filter> |
| 117 | +<filter id="ambientSoft" x="-12%" y="-12%" width="124%" height="124%"> |
| 118 | + <feDropShadow dx="0" dy="3" stdDeviation="6" flood-color="#1a1d28" flood-opacity="0.08"/> |
| 119 | +</filter> |
| 120 | +``` |
| 121 | + |
| 122 | +`ambient` for hero / primary cards. `ambientSoft` for inner / secondary cards (e.g. the `Before` / `BeforeSuite` strips inside the Feature container). |
| 123 | + |
| 124 | +### Arrow marker |
| 125 | + |
| 126 | +```xml |
| 127 | +<marker id="arrowGold" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"> |
| 128 | + <path d="M0,1 L9,5 L0,9 z" fill="#C88800"/> |
| 129 | +</marker> |
| 130 | +``` |
| 131 | + |
| 132 | +Arrows are `<line>` or `<path>` with `stroke="#C88800" stroke-width="2.2-2.4" stroke-linecap="round" marker-end="url(#arrowGold)"`. **Never** use the literal `→` character for diagram arrows. |
| 133 | + |
| 134 | +## The perspective card |
| 135 | + |
| 136 | +A card of logical width `W` and height `H` placed at origin `(0,0)` of a `<g transform="translate(x, y)">`: |
| 137 | + |
| 138 | +```xml |
| 139 | +<g transform="translate(x, y)"> |
| 140 | + <!-- top face: 14px push-up, 14px right --> |
| 141 | + <path d="M0,0 L{W},0 L{W+14},-8 L14,-8 Z" fill="#F1F5F9"/> |
| 142 | + <!-- right face: 14px right, mirrors the top depth --> |
| 143 | + <path d="M{W},0 L{W+14},-8 L{W+14},{H-8} L{W},{H} Z" fill="#E2E8F0"/> |
| 144 | + <!-- front: white rect with ambient shadow --> |
| 145 | + <rect width="{W}" height="{H}" fill="#FFFFFF" filter="url(#ambient)"/> |
| 146 | + |
| 147 | + <!-- content --> |
| 148 | + <text x="24" y="..." class="title" fill="#111827">...</text> |
| 149 | +</g> |
| 150 | +``` |
| 151 | + |
| 152 | +The 14 px / 8 px offsets are fixed — they define the "perspective" feel. Don't tune them per card. |
| 153 | + |
| 154 | +For a honey container, swap fills to `#FFF1D6 / #F5E5B0 / #FFFBF0`. |
| 155 | + |
| 156 | +### Inner code block |
| 157 | + |
| 158 | +When a card shows code, wrap it in a soft rounded panel: |
| 159 | + |
| 160 | +```xml |
| 161 | +<rect x="24" y="76" width="{W-48}" height="..." rx="8" fill="#FAFAF7" stroke="#F0EDE5" stroke-width="1"/> |
| 162 | +<text x="40" y="..." class="mono" fill="#9CA3AF">// comment</text> |
| 163 | +<text x="40" y="..." class="mono"><tspan fill="#C88800">keyword</tspan><tspan fill="#1F2937">: value</tspan></text> |
| 164 | +``` |
| 165 | + |
| 166 | +Use `<tspan>` for inline colored tokens. Comments are `#9CA3AF`, keywords `#C88800`, default text `#1F2937`. |
| 167 | + |
| 168 | +## Layout patterns |
| 169 | + |
| 170 | +Pick one based on the relationship: |
| 171 | + |
| 172 | +| Doc concept | Shape | See | |
| 173 | +|------------------------------------------------|--------------------------------------|------------------------------| |
| 174 | +| Lifecycle / required container + inner phases | Honey container wrapping inner cards | `test-structure.svg` | |
| 175 | +| Preference order / strategy list (2–4 items) | Horizontal card row, no arrows | `locators.svg` | |
| 176 | +| 3-step pipeline | 3-card row with arrows between | `pageobjects-di.svg` | |
| 177 | +| One thing delegates to N things | 1 hub + N satellites, fan-out arrows | `helpers-delegation.svg` | |
| 178 | +| Cycle / loop | Circular ring of stages | `agents-loop.svg` | |
| 179 | +| Nested levels (inner → outer scope) | Concentric stack | `retry-levels.svg` | |
| 180 | + |
| 181 | +If the doc shape doesn't match any of these, copy the closest one and adapt. |
| 182 | + |
| 183 | +## Workflow |
| 184 | + |
| 185 | +1. **Read the target .md** under `src/content/docs/<page>.md`. Note the exact heading you'll inject under and the doc's terminology. |
| 186 | +2. **Pick a shape** by matching the doc concept to the table above. Open the reference SVG and read its geometry. |
| 187 | +3. **Compose the SVG** at `/public/<slug>.svg`. |
| 188 | + - `viewBox` sized to your geometry, with ~30 px top / 50 px bottom / 50–96 px L+R padding so nothing kisses the edge. |
| 189 | + - No full-page `<rect>` background (kills transparency on the page). |
| 190 | + - `width` and `height` attributes match the viewBox numerically (the rehype plugin strips them so the figure scales). |
| 191 | +4. **Render the PNG at 2× DPI**: |
| 192 | + ```bash |
| 193 | + google-chrome --headless --disable-gpu --no-sandbox --hide-scrollbars \ |
| 194 | + --window-size=<W>,<H> \ |
| 195 | + --force-device-scale-factor=2 \ |
| 196 | + --default-background-color=00000000 \ |
| 197 | + --screenshot=/home/davert/sites/codecept.site/public/<slug>.png \ |
| 198 | + file:///home/davert/sites/codecept.site/public/<slug>.svg |
| 199 | + ``` |
| 200 | + `<W>` and `<H>` are the logical viewBox dimensions. The PNG comes out 2W × 2H. |
| 201 | +5. **Verify by eye** — `Read` the PNG. Check: every arrow has both ends visible and points the right way, no text is clipped (especially the bottom row of the last card), no serif fallback (fonts loaded), palette matches. |
| 202 | +6. **Embed via the plugin** — add an entry to the `figureInjections` array in `astro.config.mjs`: |
| 203 | + ```js |
| 204 | + { |
| 205 | + slug: 'basics', // .md filename without extension |
| 206 | + afterHeading: 'How It Works', // case-insensitive heading text match |
| 207 | + src: '/helpers-delegation.svg', |
| 208 | + alt: 'one-paragraph description of what the diagram shows', |
| 209 | + width: 1120, |
| 210 | + height: 620, |
| 211 | + } |
| 212 | + ``` |
| 213 | + Optional fields: |
| 214 | + - `before: true` — insert *before* the heading instead of after |
| 215 | + - `replace: 'p'` — replace the first matching sibling within the section |
| 216 | + - `caption: '…'` or `captionHtml: '…'` — figcaption |
| 217 | +7. **Confirm the dev server picked it up** — should hot-reload via the `handleHotUpdate` hook in `astro.config.mjs`. Hit `/<page>` and visually confirm. |
| 218 | +8. **Wait for user approval before committing.** The pattern in this project: user reviews each diagram, may iterate, then says "commit & push". |
| 219 | + |
| 220 | +## Hard-won lessons (read every time) |
| 221 | + |
| 222 | +- **Don't invent terminology.** The doc uses `BeforeSuite` / `Before` / `Scenario` / `inject()` — use those exact words. No "Hooks" block, no "Tags" block, no generic categories the doc doesn't use. |
| 223 | +- **Match preference tables exactly.** If the doc lists locator strategies in the order Semantic → ARIA → CSS → XPath, the diagram cards go in that order. Period. |
| 224 | +- **No filler kickers.** Don't add labels like "DEFAULT" or "ACCESSIBLE" above cards just to fill space. If the kicker isn't carrying information the user can't already see, drop it. |
| 225 | +- **No bullshit captions.** Don't add italic notes restating what the title already says. If the body line is "obviously true given the title", cut it. |
| 226 | +- **Code samples must be runnable.** Use plain string locators (`I.click('Sign In')`) where the docs do — don't wrap them in `{ css: '…' }` notation unless the doc explicitly does. Inconsistency across cards is worse than imperfection on one. |
| 227 | +- **Consistency across cards beats variety.** If card 1 uses `I.click(...)`, cards 2/3/4 also use `I.click(...)` not `I.fillField` / `I.see` / etc. unless the variety carries meaning. |
| 228 | +- **Don't reinvent existing styles.** The page already styles default elements (`<a>`, `<code>`, etc.). Don't add per-section CSS for primitives — let site styling cover them. (This applies to the surrounding markdown, not the SVG itself.) |
| 229 | +- **Verify diagrams by eye.** Dimension/transparency checks via `magick` don't catch cut text or clipped content. Open the PNG with `Read` and trace each element. |
| 230 | +- **One container, not many top-level cards.** When the concept is "a Feature wraps Scenarios and hooks", use a honey container with inner cards — don't render six unrelated cards in a row and label them. |
| 231 | +- **Serpentine flow > straight line of arrows.** For a lifecycle diagram (`test-structure`), arrows should follow the actual execution order even if it means turning corners (BeforeSuite → Before → ↓ Scenario → ↓ After → AfterSuite). The shape of the arrows is information. |
| 232 | + |
| 233 | +## The integration plumbing (already wired) |
| 234 | + |
| 235 | +You don't need to set these up — they exist. Just be aware of how they work: |
| 236 | + |
| 237 | +- `src/lib/rehype-inject-figure.mjs` — the rehype plugin. Reads each entry's `src`, inlines the SVG (so fonts load), prefixes every CSS selector with a per-figure class (so `.title` / `.body` don't collide across diagrams on the same page), wraps in `starlight-image-zoom-zoomable` for the lightbox, and forces `visibility:visible` on `<marker>` so arrowheads survive the lightbox clone. |
| 238 | +- `astro.config.mjs` — calls `rehypeInjectFigure({ injections: [...] })` from `markdown.rehypePlugins`. Also registers a Vite `handleHotUpdate` hook that triggers a reload when any `.svg` under `/public/` changes — so editing an SVG hot-reloads in dev. |
| 239 | +- Dependencies — `unist-util-visit` and `hast-util-from-html` are explicit deps in `package.json`. Don't remove them; Vercel's pnpm build can't hoist them implicitly. |
| 240 | + |
| 241 | +## Quick checklist |
| 242 | + |
| 243 | +Before reporting done: |
| 244 | + |
| 245 | +- [ ] Read the target `.md` and matched its exact heading + terminology |
| 246 | +- [ ] viewBox padded so no card touches the edge |
| 247 | +- [ ] No emoji, no literal `→` chars, no full-page background rect |
| 248 | +- [ ] All `<text>` have a class (font-family applied) |
| 249 | +- [ ] Shadow `ambient` on primary cards, `ambientSoft` on secondaries |
| 250 | +- [ ] Arrow marker is `arrowGold` and arrows point in the direction of flow |
| 251 | +- [ ] PNG rendered at 2× via `--force-device-scale-factor=2` |
| 252 | +- [ ] PNG `Read` by eye — no clipping, no serif fallback, arrows land on cards |
| 253 | +- [ ] `astro.config.mjs` entry added with correct `slug` + `afterHeading` |
| 254 | +- [ ] Dev server shows it under the right heading |
0 commit comments