@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
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Pulse UI — TopStrip
3
+ *
4
+ * A slim utility bar that sits above the nav — phone numbers, trading hours,
5
+ * social links, or any short announcements. Distinct from banner() which is
6
+ * for promotional/dismissible messages.
7
+ *
8
+ * Two layout modes:
9
+ * - left + right slots: `topStrip({ left: '...', right: '...' })`
10
+ * - single content: `topStrip({ content: '...' })`
11
+ *
12
+ * @param {object} opts
13
+ * @param {string} opts.content - Single centred content (overrides left/right)
14
+ * @param {string} opts.left - Left-aligned slot
15
+ * @param {string} opts.right - Right-aligned slot
16
+ * @param {string} opts.background - CSS background value
17
+ * @param {string} opts.color - Foreground/text colour
18
+ * @param {string} opts.class
19
+ */
20
+
21
+ import { escHtml as e } from '../html.js'
22
+
23
+ export function topStrip({
24
+ content = '',
25
+ left = '',
26
+ right = '',
27
+ background = '',
28
+ color = '',
29
+ class: cls = '',
30
+ } = {}) {
31
+ const classes = ['ui-top-strip', cls].filter(Boolean).join(' ')
32
+
33
+ const styles = [
34
+ background && `background:${background.replace(/"/g, "'")}`,
35
+ color && `color:${color.replace(/"/g, "'")}`,
36
+ ].filter(Boolean).join(';')
37
+ const styleAttr = styles ? ` style="${styles}"` : ''
38
+
39
+ if (content) {
40
+ return `<div class="${e(classes)}"${styleAttr}><div class="ui-top-strip-content">${content}</div></div>`
41
+ }
42
+
43
+ return `<div class="${e(classes)}"${styleAttr}>
44
+ ${left ? `<div class="ui-top-strip-left">${left}</div>` : ''}
45
+ ${right ? `<div class="ui-top-strip-right">${right}</div>` : ''}
46
+ </div>`
47
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Pulse UI — TrustList
3
+ *
4
+ * A compact row of trust/compliance badges — "Part P registered · NICEIC · £10m insured".
5
+ * Each item gets a leading icon (default: check mark). Common on trades, services,
6
+ * and professional-services sites.
7
+ *
8
+ * @param {object} opts
9
+ * @param {string[]} opts.items - List of labels
10
+ * @param {string} opts.icon - Raw HTML slot — icon shown before each item (default: ✓ check SVG)
11
+ * @param {string} opts.color - Icon colour CSS value (default: inherits --ui-accent)
12
+ * @param {'sm'|'md'} opts.size
13
+ * @param {string} opts.class
14
+ */
15
+
16
+ import { escHtml as e } from '../html.js'
17
+ import { iconCheck } from './icons.js'
18
+
19
+ const SIZES = new Set(['sm', 'md'])
20
+
21
+ export function trustList({
22
+ items = [],
23
+ icon = iconCheck({ size: 14 }),
24
+ color = '',
25
+ size = 'sm',
26
+ class: cls = '',
27
+ } = {}) {
28
+ if (!SIZES.has(size)) size = 'sm'
29
+
30
+ const classes = [
31
+ 'ui-trust-list',
32
+ size !== 'sm' && `ui-trust-list--${size}`,
33
+ cls,
34
+ ].filter(Boolean).join(' ')
35
+
36
+ const iconStyle = color ? ` style="color:${color.replace(/"/g, "'")}"` : ''
37
+
38
+ const itemsHtml = items.map(label =>
39
+ `<li class="ui-trust-list-item">
40
+ <span class="ui-trust-list-icon" aria-hidden="true"${iconStyle}>${icon}</span>
41
+ <span>${e(label)}</span>
42
+ </li>`
43
+ ).join('')
44
+
45
+ return `<ul class="${e(classes)}">${itemsHtml}</ul>`
46
+ }
package/src/ui/ui.test.js CHANGED
@@ -113,6 +113,17 @@ test('button: renders icon HTML inside element', () => {
113
113
  assert.match(html, /<svg\/>/)
114
114
  })
115
115
 
116
+ test('button: attrs with undefined values are omitted', () => {
117
+ const html = button({ label: 'Minus', attrs: { disabled: undefined, 'data-count': '3' } })
118
+ assert.doesNotMatch(html, /disabled="undefined"/)
119
+ assert.match(html, /data-count="3"/)
120
+ })
121
+
122
+ test('button: attrs with defined values are rendered', () => {
123
+ const html = button({ label: 'Add', attrs: { 'data-action': 'add' } })
124
+ assert.match(html, /data-action="add"/)
125
+ })
126
+
116
127
  // ─── badge ──────────────────────────────────────────────────────────────────
117
128
 
118
129
  test('badge: renders with default variant', () => {
@@ -656,6 +667,30 @@ test('nav: escapes link labels and hrefs', () => {
656
667
  assert.doesNotMatch(html, /<b>X<\/b>/)
657
668
  })
658
669
 
670
+ test('nav: aria-current="page" on current link', () => {
671
+ const html = nav({ logo: 'x', links: [
672
+ { label: 'Home', href: '/', current: true },
673
+ { label: 'About', href: '/about' },
674
+ ] })
675
+ assert.match(html, /aria-current="page"/)
676
+ // Appears in both desktop and mobile nav — once per nav, so 2 total
677
+ const matches = html.match(/aria-current="page"/g)
678
+ assert.ok(matches && matches.length >= 1, 'At least one link should have aria-current')
679
+ assert.doesNotMatch(html, /href="\/about"[^>]*aria-current/)
680
+ })
681
+
682
+ test('nav: no aria-current when current not set', () => {
683
+ const html = nav({ logo: 'x', links: [{ label: 'Home', href: '/' }] })
684
+ assert.doesNotMatch(html, /aria-current/)
685
+ })
686
+
687
+ test('nav: aria-current on utility link', () => {
688
+ const html = nav({ logo: 'x', links: [], utilityLinks: [
689
+ { label: 'Sign in', href: '/login', current: true },
690
+ ] })
691
+ assert.match(html, /aria-current="page"/)
692
+ })
693
+
659
694
  // ─── appBadge ────────────────────────────────────────────────────────────────
660
695
 
661
696
  test('appBadge: renders apple badge by default', () => {