@invisibleloop/pulse 0.12.2 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/.claude/commands/verify.md +9 -5
  2. package/.claude/pulse-checklist.md +21 -1
  3. package/.github/workflows/publish.yml +1 -0
  4. package/.pulse-verified +1 -0
  5. package/CLAUDE.md +56 -15
  6. package/docs/.claude/commands/verify.md +4 -0
  7. package/docs/.claude/pulse-checklist.md +36 -2
  8. package/docs/public/.pulse-ui-version +1 -1
  9. package/docs/public/andypixel.css +237 -0
  10. package/docs/public/bookworms.css +422 -0
  11. package/docs/public/coutumes.css +584 -0
  12. package/docs/public/images/bookworms/hero-books.jpg +0 -0
  13. package/docs/public/images/bookworms/how-we-read-now.jpg +0 -0
  14. package/docs/public/images/bookworms/lantern-keepers.jpg +0 -0
  15. package/docs/public/images/bookworms/provincial-bookseller.jpg +0 -0
  16. package/docs/public/images/journey/178.jpg +0 -0
  17. package/docs/public/images/journey/29.jpg +0 -0
  18. package/docs/public/images/retrowheels/hero.jpg +0 -0
  19. package/docs/public/images/shop/devbook-pro.jpg +0 -0
  20. package/docs/public/images/suzukibento/1015.jpg +0 -0
  21. package/docs/public/images/suzukibento/1025.jpg +0 -0
  22. package/docs/public/images/suzukibento/1043.jpg +0 -0
  23. package/docs/public/images/suzukibento/1059.jpg +0 -0
  24. package/docs/public/images/suzukibento/1062.jpg +0 -0
  25. package/docs/public/images/suzukibento/1084.jpg +0 -0
  26. package/docs/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
  27. package/docs/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
  28. package/docs/public/images/swish/1485518882345-15568b007407.jpg +0 -0
  29. package/docs/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
  30. package/docs/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
  31. package/docs/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
  32. package/docs/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
  33. package/docs/public/images/swish/1551803091-e20673f15770.jpg +0 -0
  34. package/docs/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
  35. package/docs/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
  36. package/docs/public/journey.css +528 -0
  37. package/docs/public/pulse-ui.css +1051 -25
  38. package/docs/public/pulse-ui.js +6 -0
  39. package/docs/public/retrowheels.css +402 -0
  40. package/docs/public/shop.css +235 -0
  41. package/docs/public/sparkys.css +349 -0
  42. package/docs/public/suzukibento.css +517 -0
  43. package/docs/public/swish.css +394 -0
  44. package/docs/server.js +19 -4
  45. package/docs/src/lib/nav.js +14 -3
  46. package/docs/src/pages/components/decorate.js +74 -0
  47. package/docs/src/pages/components/gallery.js +102 -0
  48. package/docs/src/pages/components/hero.js +1 -0
  49. package/docs/src/pages/components/marquee.js +76 -0
  50. package/docs/src/pages/components/phone-frame.js +26 -7
  51. package/docs/src/pages/components/photo-card.js +74 -0
  52. package/docs/src/pages/components/section.js +40 -1
  53. package/docs/src/pages/meta.js +26 -0
  54. package/docs/src/pages/templates/andypixel.js +260 -0
  55. package/docs/src/pages/templates/bookworms.js +227 -0
  56. package/docs/src/pages/templates/coutumes.js +226 -0
  57. package/docs/src/pages/templates/journey.js +186 -0
  58. package/docs/src/pages/templates/retrowheels.js +342 -0
  59. package/docs/src/pages/templates/shop.js +257 -0
  60. package/docs/src/pages/templates/sparkys.js +249 -0
  61. package/docs/src/pages/templates/suzukibento.js +200 -0
  62. package/docs/src/pages/templates/swish.js +313 -0
  63. package/docs/src/pages/templates.js +313 -2
  64. package/docs/src/pages/testing.js +2 -1
  65. package/examples/andypixel.css +237 -0
  66. package/examples/andypixel.js +258 -0
  67. package/examples/bookworms.css +422 -0
  68. package/examples/bookworms.js +225 -0
  69. package/examples/contact.js +1 -0
  70. package/examples/counter.js +1 -0
  71. package/examples/dev.server.js +17 -0
  72. package/examples/journey.css +528 -0
  73. package/examples/journey.js +186 -0
  74. package/examples/pricing.js +1 -0
  75. package/examples/quiz.js +1 -0
  76. package/examples/retrowheels.css +402 -0
  77. package/examples/retrowheels.js +340 -0
  78. package/examples/shared.js +6 -0
  79. package/examples/suzukibento.css +513 -0
  80. package/examples/suzukibento.js +200 -0
  81. package/examples/swish.css +394 -0
  82. package/examples/swish.js +311 -0
  83. package/examples/todos.js +1 -0
  84. package/package.json +1 -1
  85. package/public/.pulse-ui-version +1 -1
  86. package/public/images/bookworms/hero-books.jpg +0 -0
  87. package/public/images/bookworms/how-we-read-now.jpg +0 -0
  88. package/public/images/bookworms/lantern-keepers.jpg +0 -0
  89. package/public/images/bookworms/provincial-bookseller.jpg +0 -0
  90. package/public/images/journey/178.jpg +0 -0
  91. package/public/images/journey/29.jpg +0 -0
  92. package/public/images/retrowheels/hero.jpg +0 -0
  93. package/public/images/shop/devbook-pro.jpg +0 -0
  94. package/public/images/suzukibento/1015.jpg +0 -0
  95. package/public/images/suzukibento/1025.jpg +0 -0
  96. package/public/images/suzukibento/1043.jpg +0 -0
  97. package/public/images/suzukibento/1059.jpg +0 -0
  98. package/public/images/suzukibento/1062.jpg +0 -0
  99. package/public/images/suzukibento/1084.jpg +0 -0
  100. package/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
  101. package/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
  102. package/public/images/swish/1485518882345-15568b007407.jpg +0 -0
  103. package/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
  104. package/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
  105. package/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
  106. package/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
  107. package/public/images/swish/1551803091-e20673f15770.jpg +0 -0
  108. package/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
  109. package/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
  110. package/public/pulse-ui.css +1051 -22
  111. package/public/pulse-ui.js +6 -0
  112. package/scripts/build.js +72 -18
  113. package/src/agent/checklist.md +20 -2
  114. package/src/agent/commands/verify.md +4 -0
  115. package/src/agent/guide-components.md +146 -8
  116. package/src/agent/guide-design-gallery.md +288 -0
  117. package/src/agent/guide-design-references.md +172 -0
  118. package/src/agent/guide-explore.md +313 -0
  119. package/src/agent/guide-styles.md +197 -11
  120. package/src/agent/guide-templates.md +220 -10
  121. package/src/agent/guide.md +9 -4
  122. package/src/agent/workflow.md +208 -58
  123. package/src/cli/dev.js +39 -7
  124. package/src/cli/fmt.js +113 -0
  125. package/src/cli/index.js +63 -30
  126. package/src/cli/scaffold.js +2 -2
  127. package/src/html.js +6 -1
  128. package/src/mcp/server.js +2415 -101
  129. package/src/mcp/validate-worker.js +34 -5
  130. package/src/runtime/ssr.js +4 -1
  131. package/src/server/index.js +22 -2
  132. package/src/spec/schema.js +5 -0
  133. package/src/testing/html.js +130 -4
  134. package/src/testing/testing.test.js +58 -0
  135. package/src/ui/banner.js +24 -9
  136. package/src/ui/brand-statement.js +58 -0
  137. package/src/ui/button.js +12 -1
  138. package/src/ui/card.js +20 -1
  139. package/src/ui/colophon.js +29 -0
  140. package/src/ui/contact-row.js +41 -0
  141. package/src/ui/cta.js +18 -4
  142. package/src/ui/decorate.js +55 -0
  143. package/src/ui/display-heading.js +44 -0
  144. package/src/ui/drop-cap.js +29 -0
  145. package/src/ui/editorial-hero.js +63 -0
  146. package/src/ui/entry-list.js +36 -0
  147. package/src/ui/feature.js +20 -3
  148. package/src/ui/footer.js +78 -9
  149. package/src/ui/gallery.js +56 -0
  150. package/src/ui/hero.js +68 -18
  151. package/src/ui/image-pair.js +72 -0
  152. package/src/ui/index.js +20 -0
  153. package/src/ui/input.js +1 -0
  154. package/src/ui/marquee.js +48 -0
  155. package/src/ui/nav.js +19 -9
  156. package/src/ui/phone-frame.js +6 -5
  157. package/src/ui/photo-card.js +44 -0
  158. package/src/ui/product-card.js +65 -0
  159. package/src/ui/pullquote.js +14 -3
  160. package/src/ui/search.js +1 -0
  161. package/src/ui/section-label.js +38 -0
  162. package/src/ui/section.js +1 -1
  163. package/src/ui/service-card.js +47 -0
  164. package/src/ui/stat-grid.js +40 -0
  165. package/src/ui/stat.js +16 -2
  166. package/src/ui/testimonial.js +17 -6
  167. package/src/ui/textarea.js +1 -0
  168. package/src/ui/top-strip.js +47 -0
  169. package/src/ui/trust-list.js +46 -0
  170. package/src/ui/ui.test.js +35 -0
@@ -42,21 +42,25 @@ Use `mcp__chrome-devtools__list_console_messages` — report any errors or unexp
42
42
 
43
43
  ### 9. Code review
44
44
 
45
- Check the spec against the checklist in `.claude/pulse-checklist.md`. Work through every item. Fix anything that fails before proceeding to the next step.
45
+ Call `pulse_review` with the spec file path. This runs a full code review AND atomically writes the `.pulse-verified` stamp server-side, eliminating any race condition with the stop hook.
46
46
 
47
- ### 10. Close the browser
47
+ Work through every item the review returns. Fix anything that fails before proceeding.
48
48
 
49
- Use `mcp__chrome-devtools__list_pages` to get all open pages, then call `mcp__chrome-devtools__close_page` for every page ID returned. This closes the entire browser, not just the tab. `pageId` must be a number, not a string.
49
+ ### 10. Close extra browser tabs
50
+
51
+ Use `mcp__chrome-devtools__list_pages` to get all open pages. Close every page **except the last one** — `close_page` refuses to close the final tab. `pageId` must be a number, not a string.
52
+
53
+ If there is only one page open, skip this step — there is nothing to close.
50
54
 
51
55
  ### 11. Write verification stamp
52
56
 
53
- Run:
57
+ `pulse_review` writes the stamp automatically. As a belt-and-suspenders backup, also run:
54
58
 
55
59
  ```bash
56
60
  date +%s > .pulse-verified
57
61
  ```
58
62
 
59
- This records that verify completed successfully. The stop hook checks this stamp — do not skip this step.
63
+ This ensures the stamp is present even if `pulse_review` was not called with a file path.
60
64
 
61
65
  ### 12. Report
62
66
 
@@ -25,6 +25,20 @@ Before finishing any spec, verify every point below. Fix anything that fails.
25
25
  ### Components first
26
26
 
27
27
  - **Before writing any HTML by hand, check `src/ui/index.js`.** There are 50+ components. Use `button`, `card`, `alert`, `input`, `spinner`, `badge`, `modal`, `nav`, `pagination`, `table`, etc. before writing equivalent HTML from scratch.
28
+ - **Do not reinvent component patterns.** These patterns have dedicated components — never write custom HTML for them:
29
+ - Hero sections → `hero({ title, subtitle, actions, image, ... })`
30
+ - Product/service cards → `card({ title, body, image, footer, ... })`
31
+ - Image + text two-column layouts → `media({ image, content, reverse })`
32
+ - Horizontal strips/banners → `banner({ content, variant })`
33
+ - Feature tiles → `feature({ icon, title, description })`
34
+ - Testimonials → `testimonial({ quote, name, role, src })`
35
+ - CTAs → `cta({ title, subtitle, actions })`
36
+ - Navigation → `nav({ logo, links, actions })`
37
+ - Footers → `footer({ logo, links, columns, legal, ... })`
38
+
39
+ If you find yourself writing `class="hero"` or `class="product-card"` or `class="testimonial"`, stop — import the component instead.
40
+
41
+ - **Exception — brutalist, editorial, and playful vibes:** these vibes intentionally break from polished templates. `pulse://guide/explore` explicitly endorses zone-based raw HTML for distinctive layouts. For `brutalist`, `editorial`, `retro`, or `neon` vibes, raw structural HTML is correct — components can still be used for functional atoms (`button`, `input`, `modal`) but structural sections do not need to come from components. When reviewing a brutalist or editorial spec, do not flag raw section HTML as a violation.
28
42
 
29
43
  - **Never write App Store or Google Play download buttons by hand.** Always use `appBadge({ store: 'apple', href })` and `appBadge({ store: 'google', href })`. Raw `<a>` tags with badge images are incorrect — `appBadge` renders the correct accessible, styled badge for each store.
30
44
 
@@ -110,7 +124,13 @@ Before finishing any spec, verify every point below. Fix anything that fails.
110
124
  assert.equal(result.get('h1').text, mockProduct.name)
111
125
  assert.equal(result.attr('img', 'src'), mockProduct.image)
112
126
  ```
113
- Supported selectors: `tag`, `.class`, `#id`, `[attr]`, `[attr="value"]`, and combinations (`button.primary[disabled]`).
127
+ Supported selectors: `tag`, `.class`, `#id`, `[attr]`, `[attr="value"]`, combinations (`button.primary[disabled]`), and **descendant combinators** (`result.count('.parent li')`, `result.find('tbody tr')`). Descendant selectors match the rightmost token within the ancestor context — they do not return 0 if the ancestor doesn't exist, they simply find no matching descendants.
128
+
129
+ ### Markdown
130
+
131
+ - **Use `md()` from `@invisibleloop/pulse/md` for `.md` file content.** Never read and parse markdown manually. Pass the result's `html` to `prose()`.
132
+ - **Call the same `md()` fetcher in both `meta` and `server`** — it caches on `ctx._mdCache` so the file is only read once per request. Do not create two separate fetchers for the same file.
133
+ - **Always add `onViewError` on dynamic markdown routes** (routes with `:param` segments loading `.md` files). A missing file throws `{ status: 404 }` which must be caught gracefully.
114
134
 
115
135
  ### Markdown
116
136
 
@@ -40,5 +40,6 @@ jobs:
40
40
  git config user.email "github-actions[bot]@users.noreply.github.com"
41
41
  git add package.json package-lock.json public/.pulse-ui-version
42
42
  git commit -m "chore: release $VERSION [skip ci]" || true
43
+ git pull --rebase
43
44
  git push
44
45
  npm publish --provenance --access public
@@ -0,0 +1 @@
1
+ 1780010184
package/CLAUDE.md CHANGED
@@ -318,23 +318,30 @@ For multi-brand setups, each brand theme file just overrides `--font` in `:root`
318
318
 
319
319
  ## Overriding Theme Colours
320
320
 
321
- `pulse-ui.css` defines light theme variables under `[data-theme="light"]`, which has **higher specificity than `:root`**. Overrides written only to `:root` will be silently beaten by those rules.
322
-
323
- Always target `[data-theme="light"]` when overriding colours for the light theme:
321
+ Use **input tokens** (unprefixed: `--accent`, `--bg`, `--text`, etc.) in `theme.css`. `pulse-ui.css` maps them to output tokens via `var()` in **both** the dark (`:root`) and light (`[data-theme="light"]`) blocks:
324
322
 
325
323
  ```css
326
- /* WRONG — loses to [data-theme="light"] rules in pulse-ui.css */
324
+ /* Dark theme — in :root */
327
325
  :root {
328
- --color-accent: #e25;
326
+ --accent: #9b8dff;
329
327
  }
330
328
 
331
- /* CORRECT */
329
+ /* Light theme — in [data-theme="light"] */
332
330
  [data-theme="light"] {
333
- --color-accent: #e25;
331
+ --accent: #e25; /* overrides the light default */
334
332
  }
335
333
  ```
336
334
 
337
- If a colour must apply in both themes, set it in both `:root` and `[data-theme="light"]`.
335
+ Setting `--accent` in `[data-theme="light"]` works correctly — `pulse-ui.css` resolves `var(--accent, ...)` there just like it does in `:root`.
336
+
337
+ If you need to override an output token directly (e.g. one with no input equivalent), target `[data-theme="light"]` explicitly:
338
+
339
+ ```css
340
+ /* Direct output token override for light theme only */
341
+ [data-theme="light"] {
342
+ --ui-nav-sticky-bg: rgba(255, 255, 255, 0.95);
343
+ }
344
+ ```
338
345
 
339
346
  ## CSS File Responsibilities
340
347
 
@@ -342,10 +349,10 @@ There are two CSS files and they have distinct roles — do not mix them:
342
349
 
343
350
  | File | Purpose | Allowed content |
344
351
  |---|---|---|
345
- | `public/theme.css` | Token definitions — hex values, raw colours, font URLs | Hex values, `rgb()`, raw values, `@font-face`, `@import` |
346
- | `app.css` (or per-page) | Layout and component overrides | `var()` tokens **only** — no hex, no raw colour values |
352
+ | `public/theme.css` | Token definitions — hex values, raw colours, font URLs | Hex values, `rgb()`, `rgba()`, `hsl()`, `hsla()`, raw values, `@font-face`, `@import` |
353
+ | `app.css` (or per-page) | Layout and component overrides | `var()` tokens **only** — no hex values |
347
354
 
348
- **`app.css` must never contain hex values or raw colour values.** A lint hook enforces this. If you need to define or override a colour token, put it in `public/theme.css` and reference it via `var()` in `app.css`.
355
+ **`app.css` must never contain hex values.** `rgba()` and `hsla()` are allowed for translucency — but if the same translucent colour is used more than once, extract it as a token in `theme.css` (e.g. `--brand-overlay-12: rgba(0,0,0,0.12)`). If you need to define or override a colour token, put it in `public/theme.css` and reference it via `var()` in `app.css`.
349
356
 
350
357
  ```css
351
358
  /* public/theme.css — hex values live here */
@@ -422,7 +429,8 @@ const result = await render(mySpec, { server: { product: mockProduct } })
422
429
  const result = await render(mySpec, { ctx: { params: { id: '1' } } }) // real fetchers
423
430
  ```
424
431
 
425
- **Supported selectors:** `button`, `.class`, `#id`, `[attr]`, `[attr="value"]`, and combinations: `button.primary[type="submit"]`.
432
+ **Supported selectors:** `button`, `.class`, `#id`, `[attr]`, `[attr="value"]`, and combinations: `button.primary[type="submit"]`. **Descendant combinators are supported:** `result.count('.parent li')` or `result.find('tbody tr')`.
433
+
426
434
  `Element` also has `.find()`, `.findAll()`, `.has()`, `.attr()`, `.text`, `.tag`, `.attrs`.
427
435
 
428
436
  ## Key Decisions (Do Not Reverse)
@@ -439,6 +447,25 @@ const result = await render(mySpec, { ctx: { params: { id: '1' } } }) // real fe
439
447
 
440
448
  ## Build Workflow
441
449
 
450
+ ### New page or new site? Start with intake — always
451
+
452
+ For any **new page, landing page, or branded site**, run this sequence first. Do not skip to building.
453
+
454
+ ```
455
+ 0. pulse_extract_inspiration(url/image) → structured design brief [if user shares reference]
456
+ 1. pulse_intake(name, pitch, features, ...) → product brief + contrast check
457
+ 2. pulse_sketch(brief, vibe?, antiStyle?) → 3 layout directions to choose from
458
+ 3. pulse_intent(description) → archetype + scaffold + guide list
459
+ ```
460
+
461
+ **Before calling `pulse_intake`**, gather the required information by asking the user — one question at a time, in plain prose, no bullet lists. You need at minimum: the product name, what it does (pitch), and its key features. Ask for inspiration references, palette, vibe, and anti-style if the user hasn't mentioned them.
462
+
463
+ **`pulse_sketch` is mandatory** for `playful`, `bold`, `brutalist`, `retro`, or `neon` vibes — those vibes fight the default centred-hero layout. Skip it only for `corporate` or `minimal` when no structural preference has been stated.
464
+
465
+ For **small edits, bug fixes, or "add X to existing Y"** — skip intake/sketch and go straight to Understand below.
466
+
467
+ ---
468
+
442
469
  Every build task follows this sequence. Each phase has a pass gate — do not advance until it clears.
443
470
 
444
471
  **Before calling any slow tool (`pulse_build`, `lighthouse_audit`), output a status message to the user first** — e.g. "Building for production — ~30 s…" or "Running Lighthouse desktop audit…". Never call a slow tool silently.
@@ -449,14 +476,16 @@ Every build task follows this sequence. Each phase has a pass gate — do not ad
449
476
  | 2. Plan | Present plan to user, wait for confirmation | User confirms |
450
477
  | 3. Build | Write spec + related files | — |
451
478
  | 4. Validate | `pulse_validate` — fix all errors + warnings | Clean output |
452
- | 5. Browser | Screenshot + Lighthouse desktop + mobile | 100/100/100 (Accessibility, Best Practices, SEO) both strategies |
479
+ | 5. Browser | Screenshot → `pulse_design_review` (if intake ran) → `pulse_layout_review` → `/verify` (Lighthouse desktop + mobile + `pulse_review`) | ⛔ Do not skip: all gates must pass + `.pulse-verified` stamp written |
453
480
  | 6. Tests | Write tests, run them, fix failures | All pass |
454
- | 7. Review Agent | Invoke review — **only after phases 4–6 all pass** | — |
481
+ | 7. Code Review | `pulse_review` — only after phases 4–6 pass | — |
455
482
  | 8. Fix | Fix every review issue, re-run affected gates | All gates still pass |
456
483
 
457
484
  **Skip phase 2 confirmation only for trivially small, unambiguous tasks.** When in doubt, confirm.
458
485
 
459
- **The Review Agent is always last.** Never invoke it before validate, Lighthouse, and tests all pass.
486
+ **Template mode exception:** when adapting a pre-built template (see `pulse://guide/templates`), replace the full plan with a single compact confirmation block — template, substitutions, files. One turn is enough; do not run the full phase 2 ceremony.
487
+
488
+ **The Code Review is always last.** Never invoke `pulse_review` before validate, Lighthouse, design review, and tests all pass.
460
489
 
461
490
  The `/verify` command runs the browser check loop (phases 4–5) automatically. Use it.
462
491
 
@@ -521,6 +550,18 @@ export default {
521
550
 
522
551
  Before writing any UI HTML by hand, check `src/ui/index.js` — there are 50+ components available. Use them. Do not reinvent `button`, `card`, `alert`, `modal`, `spinner`, `badge`, `input`, etc.
523
552
 
553
+ **Common patterns with dedicated components — never write these from scratch:**
554
+
555
+ - Hero sections → `hero({ title, subtitle, actions, image, layout })`
556
+ - Product/service cards → `card({ title, body, image, footer })`
557
+ - Image + text layouts → `media({ image, content, reverse })`
558
+ - Horizontal strips → `banner({ content, variant })`
559
+ - Feature tiles → `feature({ icon, title, description })`
560
+ - Testimonials → `testimonial({ quote, name, role, src })`
561
+ - Call-to-action sections → `cta({ title, subtitle, actions })`
562
+
563
+ If you're about to write `class="hero"` or `class="product-card"`, stop — import the component instead. You can add custom CSS on top of components for brand-specific styling, but the structure and accessibility must come from the component.
564
+
524
565
  @src/agent/checklist.md
525
566
 
526
567
  ## Performance Baseline
@@ -16,6 +16,8 @@ Use `pulse_validate` with the spec file content. If validation fails, report eve
16
16
 
17
17
  Use `pulse_fetch_page` to confirm the server is responding for the route. If it errors, use `pulse_restart_server` and retry once.
18
18
 
19
+ **If `pulse_fetch_page` returns unexpected HTML (e.g. a different project's page title, a Next.js 404, or any non-Pulse response):** check the `X-Pulse` response header — Pulse servers always send this. If it is absent, the wrong server is running on that port. Stop and inform the user before continuing.
20
+
19
21
  ### 4. Screenshot
20
22
 
21
23
  Use `mcp__chrome-devtools__navigate_page` to load the page route, then `mcp__chrome-devtools__take_screenshot` to capture the result. Describe what you see — layout, content, any obvious rendering issues.
@@ -32,6 +34,8 @@ Run `mcp__chrome-devtools__lighthouse_audit` on the same route with `{ "strategy
32
34
 
33
35
  **Same pass bar: Accessibility, Best Practices, and SEO must all be 100.** Fix any failures and restart from step 2.
34
36
 
37
+ **If you need to visually inspect the layout at a mobile viewport width** (e.g. to debug a wrapping or overflow issue), use `mcp__chrome-devtools__emulate` with `viewport: "390x844x2,mobile,touch"` — not `resize_page`. The `emulate` tool reliably sets the viewport; `resize_page` does not reliably trigger responsive layout changes. Reset afterward with `emulate` using the desktop viewport: `"1440x900x1"`.
38
+
35
39
  ### 7. Performance
36
40
 
37
41
  Navigate to the page route, then run `mcp__chrome-devtools__performance_start_trace` with `reload: true` and `autoStop: true`. Report LCP and CLS. Flag any LCP insight that suggests a fixable problem (render-blocking resources, large image load delay, etc.).
@@ -20,9 +20,43 @@ Before finishing any spec, verify every point below. Fix anything that fails.
20
20
 
21
21
  - **Do not set `hydrate` in specs.** It is auto-derived by the framework from the URL entry passed to `createServer`. Specs with `mutations`, `actions`, or `persist` are hydrated automatically. Purely server-rendered specs get zero JavaScript — no configuration needed.
22
22
 
23
- ### Components first
23
+ - **Always restart the server and reload the browser after every file edit — before checking the result.** The dev server restarts on file changes but the browser tab stays stale. The required sequence is: `pulse_restart_server` → `navigate_page`. Never attempt to debug a visual problem without doing both steps first.
24
+
25
+ ### Components first — or creative override
24
26
 
25
27
  - **Before writing any HTML by hand, check `src/ui/index.js`.** There are 50+ components. Use `button`, `card`, `alert`, `input`, `spinner`, `badge`, `modal`, `nav`, `pagination`, `table`, etc. before writing equivalent HTML from scratch.
28
+ - **Do not reinvent component patterns.** These patterns have dedicated components — never write custom HTML for them unless a creative override applies (see below):
29
+ - Hero sections → `hero({ title, subtitle, actions, image, ... })`
30
+ - Product/service cards → `card({ title, body, image, footer, ... })`
31
+ - Image + text two-column layouts → `media({ image, content, reverse })`
32
+ - Horizontal strips/banners → `banner({ content, variant })`
33
+ - Feature tiles → `feature({ icon, title, description })`
34
+ - Testimonials → `testimonial({ quote, name, role, src })`
35
+ - CTAs → `cta({ title, subtitle, actions })`
36
+ - Navigation → `nav({ logo, links, actions })`
37
+ - Footers → `footer({ logo, links, columns, legal, ... })`
38
+
39
+ - **Creative override — raw HTML throughout is permitted** when the design intent genuinely calls for it: a highly custom visual identity, an asymmetric or typographically-driven layout, a brutalist/editorial/retro/neon/paper vibe where components would constrain the expression. This is a deliberate design decision, not a shortcut. When taking this path:
40
+ - State it explicitly in the build brief: *"Building component-free — raw HTML throughout for creative control."*
41
+ - Functional atoms (`button`, `input`, `badge`, `modal`) should still come from components unless there is a specific design reason not to.
42
+ - `/pulse-ui.css` is always required — it provides the token system your custom CSS will use.
43
+ - The quality gate replaces the component checklist: **100/100/100/100 Lighthouse on both desktop and mobile is the pass bar.** A component-free page that passes every audit is correct. A component-heavy page that fails accessibility is not.
44
+
45
+ - When reviewing a spec built under creative override, do not flag raw section HTML as a violation — check the Lighthouse results instead.
46
+
47
+ - **Never write App Store or Google Play download buttons by hand.** Always use `appBadge({ store: 'apple', href })` and `appBadge({ store: 'google', href })`. Raw `<a>` tags with badge images are incorrect — `appBadge` renders the correct accessible, styled badge for each store.
48
+
49
+ ```js
50
+ import { appBadge } from '@invisibleloop/pulse/ui'
51
+
52
+ // In a hero() actions slot:
53
+ ${hero({
54
+ actions: `
55
+ ${appBadge({ store: 'apple', href: 'https://apps.apple.com/...' })}
56
+ ${appBadge({ store: 'google', href: 'https://play.google.com/...' })}
57
+ `,
58
+ })}
59
+ ```
26
60
 
27
61
  ### Reuse (DRY)
28
62
 
@@ -94,7 +128,7 @@ Before finishing any spec, verify every point below. Fix anything that fails.
94
128
  assert.equal(result.get('h1').text, mockProduct.name)
95
129
  assert.equal(result.attr('img', 'src'), mockProduct.image)
96
130
  ```
97
- Supported selectors: `tag`, `.class`, `#id`, `[attr]`, `[attr="value"]`, and combinations (`button.primary[disabled]`).
131
+ Supported selectors: `tag`, `.class`, `#id`, `[attr]`, `[attr="value"]`, combinations (`button.primary[disabled]`), and **descendant combinators** (`result.count('.parent li')`, `result.find('tbody tr')`). Descendant selectors match the rightmost token within the ancestor context — they do not return 0 if the ancestor doesn't exist, they simply find no matching descendants.
98
132
 
99
133
  ### Markdown
100
134
 
@@ -1 +1 @@
1
- 0.10.2
1
+ 0.13.0
@@ -0,0 +1,237 @@
1
+ /*
2
+ * andypixel.css — Andy Pixel blog article example
3
+ *
4
+ * Token definitions (hex values) + layout styles (var() only).
5
+ * Orange accent palette — editorial light theme.
6
+ */
7
+
8
+ /* ─── Token definitions ──────────────────────────────────────── */
9
+
10
+ [data-theme="light"] {
11
+ --accent: #f97316;
12
+ --accent-hover: #ea6c0a;
13
+ --accent-dim: #fff3e8;
14
+ --surface-2: #f0f0f0;
15
+ --border: #e5e5e5;
16
+ --muted-bg: #f5f5f5;
17
+
18
+ /* Code block dark-shell tokens — defined here so app.css stays hex-free */
19
+ --ap-code-shell: #1e1e1e;
20
+ --ap-code-text: #d4d4d4;
21
+ --ap-btn-dark: #1a1a1a;
22
+ --ap-btn-dark-hover: #333333;
23
+ }
24
+
25
+ /* ─── Base ───────────────────────────────────────────────────── */
26
+
27
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
28
+
29
+ body {
30
+ font-family: var(--ui-font, system-ui, sans-serif);
31
+ line-height: 1.6;
32
+ background: var(--ui-bg);
33
+ color: var(--ui-text);
34
+ }
35
+
36
+ /* ─── Contrast fixes ─────────────────────────────────────────── */
37
+
38
+ /* Badge: muted on surface-2 fails WCAG AA at small size */
39
+ .ui-badge--default {
40
+ color: var(--ui-text);
41
+ }
42
+
43
+ /* Inline code: accent on surface-2 fails WCAG AA */
44
+ .ui-prose code {
45
+ color: var(--ui-text);
46
+ }
47
+
48
+ /* ─── Reading column ─────────────────────────────────────────── */
49
+
50
+ .ap-reading-col {
51
+ max-width: 72ch;
52
+ margin-left: auto;
53
+ margin-right: auto;
54
+ padding-left: var(--ui-space-6);
55
+ padding-right: var(--ui-space-6);
56
+ }
57
+
58
+ /* ─── Article header ─────────────────────────────────────────── */
59
+
60
+ .ap-article-header {
61
+ padding: var(--ui-space-12) 0 var(--ui-space-8);
62
+ border-bottom: 3px solid var(--ui-accent);
63
+ margin-bottom: var(--ui-space-10);
64
+ }
65
+
66
+ .ap-article-meta {
67
+ display: flex;
68
+ align-items: center;
69
+ gap: var(--ui-space-2);
70
+ flex-wrap: wrap;
71
+ margin-bottom: var(--ui-space-5);
72
+ }
73
+
74
+ .ap-article-title {
75
+ font-family: var(--ui-font-display, system-ui, serif);
76
+ font-size: clamp(2rem, 5vw, 3.25rem);
77
+ font-weight: 800;
78
+ line-height: 1.1;
79
+ letter-spacing: -0.02em;
80
+ color: var(--ui-text);
81
+ margin-bottom: var(--ui-space-4);
82
+ }
83
+
84
+ .ap-article-subtitle {
85
+ font-size: var(--ui-text-xl);
86
+ color: var(--ui-muted);
87
+ line-height: 1.55;
88
+ margin-bottom: var(--ui-space-6);
89
+ max-width: 56ch;
90
+ }
91
+
92
+ .ap-article-author-strip {
93
+ display: flex;
94
+ align-items: center;
95
+ gap: var(--ui-space-3);
96
+ }
97
+
98
+ /* ─── Article body ───────────────────────────────────────────── */
99
+
100
+ .ap-article-body {
101
+ padding-bottom: var(--ui-space-12);
102
+ }
103
+
104
+ .ap-article-lead {
105
+ font-size: var(--ui-text-xl) !important;
106
+ line-height: 1.65 !important;
107
+ color: var(--ui-text) !important;
108
+ }
109
+
110
+ /* ─── Code block ─────────────────────────────────────────────── */
111
+
112
+ .ap-code-block {
113
+ margin: var(--ui-space-6) 0;
114
+ border-radius: var(--ui-radius);
115
+ overflow: hidden;
116
+ border: 1px solid var(--ui-border);
117
+ }
118
+
119
+ .ap-code-block__header {
120
+ display: flex;
121
+ align-items: center;
122
+ gap: var(--ui-space-2);
123
+ padding: var(--ui-space-2) var(--ui-space-4);
124
+ background: var(--ui-text);
125
+ color: var(--ui-accent);
126
+ font-family: var(--ui-mono, monospace);
127
+ font-size: var(--ui-text-sm);
128
+ }
129
+
130
+ .ap-code-block__pre {
131
+ background: var(--ap-code-shell);
132
+ padding: var(--ui-space-5) var(--ui-space-6);
133
+ overflow-x: auto;
134
+ margin: 0;
135
+ }
136
+
137
+ .ap-code-block__code {
138
+ font-family: var(--ui-mono, monospace);
139
+ font-size: var(--ui-text-sm);
140
+ line-height: 1.75;
141
+ color: var(--ap-code-text);
142
+ }
143
+
144
+ /* ─── Pullquote ──────────────────────────────────────────────── */
145
+
146
+ .ap-pullquote {
147
+ margin: var(--ui-space-8) 0;
148
+ padding: var(--ui-space-5) var(--ui-space-6);
149
+ border-left: 4px solid var(--ui-accent);
150
+ background: var(--accent-dim);
151
+ border-radius: 0 var(--ui-radius) var(--ui-radius) 0;
152
+ }
153
+
154
+ .ap-pullquote p {
155
+ font-family: var(--ui-font-display, system-ui, serif);
156
+ font-size: var(--ui-text-xl);
157
+ font-style: italic;
158
+ color: var(--ui-text);
159
+ margin: 0;
160
+ line-height: 1.55;
161
+ }
162
+
163
+ /* ─── Author bio ─────────────────────────────────────────────── */
164
+
165
+ .ap-author-bio-wrap {
166
+ border-top: 1px solid var(--ui-border);
167
+ border-bottom: 1px solid var(--ui-border);
168
+ padding: var(--ui-space-8) 0;
169
+ margin-bottom: var(--ui-space-10);
170
+ }
171
+
172
+ .ap-author-bio {
173
+ display: flex;
174
+ gap: var(--ui-space-4);
175
+ align-items: flex-start;
176
+ }
177
+
178
+ .ap-author-bio__name {
179
+ font-weight: 700;
180
+ font-size: var(--ui-text-base);
181
+ margin-bottom: var(--ui-space-1);
182
+ }
183
+
184
+ /* ─── Newsletter ─────────────────────────────────────────────── */
185
+
186
+ .ap-newsletter-section {
187
+ max-width: 540px;
188
+ margin: 0 auto;
189
+ text-align: center;
190
+ padding: var(--ui-space-12) var(--ui-space-6);
191
+ border-top: 3px solid var(--ui-accent);
192
+ }
193
+
194
+ .ap-newsletter-eyebrow {
195
+ font-family: var(--ui-mono, monospace);
196
+ font-size: var(--ui-text-sm);
197
+ text-transform: uppercase;
198
+ letter-spacing: 0.1em;
199
+ color: var(--ui-text);
200
+ margin-bottom: var(--ui-space-2);
201
+ }
202
+
203
+ .ap-newsletter-title {
204
+ font-family: var(--ui-font-display, system-ui, serif);
205
+ font-size: clamp(1.5rem, 4vw, 2.25rem);
206
+ font-weight: 800;
207
+ letter-spacing: -0.02em;
208
+ color: var(--ui-text);
209
+ margin-bottom: var(--ui-space-3);
210
+ line-height: 1.15;
211
+ }
212
+
213
+ .ap-newsletter-form {
214
+ display: flex;
215
+ gap: var(--ui-space-2);
216
+ margin-top: var(--ui-space-6);
217
+ flex-wrap: wrap;
218
+ justify-content: center;
219
+ }
220
+
221
+ /* Orange bg on small text fails WCAG AA — use near-black for contrast (15:1) */
222
+ .ap-newsletter-form .ui-btn--primary {
223
+ background: var(--ap-btn-dark);
224
+ }
225
+ .ap-newsletter-form .ui-btn--primary:hover {
226
+ background: var(--ap-btn-dark-hover);
227
+ }
228
+
229
+ .ap-newsletter-error {
230
+ color: var(--ui-error, #dc2626);
231
+ margin-top: var(--ui-space-3);
232
+ }
233
+
234
+ .ap-newsletter-success {
235
+ padding: var(--ui-space-12) var(--ui-space-6);
236
+ text-align: center;
237
+ }