@invisibleloop/pulse 0.20.4 → 0.20.6

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 (80) hide show
  1. package/.claude/pulse-checklist.md +1 -2
  2. package/.github/workflows/ci.yml +3 -0
  3. package/CLAUDE.md +1 -2
  4. package/docs/.claude/pulse-checklist.md +1 -2
  5. package/docs/public/coutumes.css +21 -44
  6. package/docs/public/docs.css +67 -0
  7. package/docs/public/pulse-ui.css +8 -1037
  8. package/docs/public/retrowheels.css +23 -0
  9. package/docs/public/shop.css +35 -1
  10. package/docs/public/sparkys.css +56 -23
  11. package/docs/server.js +0 -11
  12. package/docs/src/lib/nav.js +0 -11
  13. package/docs/src/pages/components/charts.js +2 -1
  14. package/docs/src/pages/components/grid.js +9 -5
  15. package/docs/src/pages/components.js +0 -4
  16. package/docs/src/pages/home.js +16 -1
  17. package/docs/src/pages/how-it-works.js +20 -14
  18. package/docs/src/pages/templates/coutumes.js +83 -72
  19. package/docs/src/pages/templates/mobile-app.js +36 -9
  20. package/docs/src/pages/templates/retrowheels.js +17 -4
  21. package/docs/src/pages/templates/shop.js +21 -10
  22. package/docs/src/pages/templates/sparkys.js +35 -20
  23. package/examples/.claude/pulse-checklist.md +1 -2
  24. package/examples/examples.test.js +4 -4
  25. package/examples/pricing.js +21 -3
  26. package/examples/public/pulse-ui.css +8 -1037
  27. package/examples/retrowheels.css +23 -0
  28. package/examples/retrowheels.js +17 -4
  29. package/package.json +5 -1
  30. package/public/.pulse-ui-version +1 -1
  31. package/public/pulse-ui.css +8 -1037
  32. package/src/agent/checklist.md +1 -2
  33. package/src/agent/guide-components.md +43 -67
  34. package/src/agent/guide-design-gallery.md +14 -14
  35. package/src/agent/guide-design-references.md +21 -21
  36. package/src/agent/guide-styles.md +2 -2
  37. package/src/agent/guide-templates.md +6 -6
  38. package/src/agent/identity.md +1 -1
  39. package/src/agent/workflow.md +1 -2
  40. package/src/mcp/server.js +31 -24
  41. package/src/ui/index.js +0 -27
  42. package/src/ui/ui.test.js +0 -137
  43. package/types/charts.d.ts +61 -0
  44. package/types/ui.d.ts +1 -129
  45. package/docs/src/pages/components/banner.js +0 -44
  46. package/docs/src/pages/components/carousel.js +0 -71
  47. package/docs/src/pages/components/code-window.js +0 -56
  48. package/docs/src/pages/components/decorate.js +0 -74
  49. package/docs/src/pages/components/gallery.js +0 -102
  50. package/docs/src/pages/components/list.js +0 -111
  51. package/docs/src/pages/components/marquee.js +0 -76
  52. package/docs/src/pages/components/photo-card.js +0 -74
  53. package/docs/src/pages/components/pricing.js +0 -67
  54. package/docs/src/pages/components/testimonial.js +0 -48
  55. package/docs/src/pages/components/timeline.js +0 -226
  56. package/src/ui/banner.js +0 -41
  57. package/src/ui/brand-statement.js +0 -58
  58. package/src/ui/carousel.js +0 -59
  59. package/src/ui/code-window.js +0 -39
  60. package/src/ui/colophon.js +0 -29
  61. package/src/ui/contact-row.js +0 -41
  62. package/src/ui/decorate.js +0 -55
  63. package/src/ui/display-heading.js +0 -44
  64. package/src/ui/drop-cap.js +0 -29
  65. package/src/ui/editorial-hero.js +0 -63
  66. package/src/ui/entry-list.js +0 -36
  67. package/src/ui/gallery.js +0 -60
  68. package/src/ui/image-pair.js +0 -72
  69. package/src/ui/list.js +0 -36
  70. package/src/ui/marquee.js +0 -48
  71. package/src/ui/photo-card.js +0 -44
  72. package/src/ui/pricing.js +0 -56
  73. package/src/ui/product-card.js +0 -65
  74. package/src/ui/section-label.js +0 -38
  75. package/src/ui/service-card.js +0 -47
  76. package/src/ui/stat-grid.js +0 -40
  77. package/src/ui/testimonial.js +0 -59
  78. package/src/ui/timeline.js +0 -72
  79. package/src/ui/top-strip.js +0 -47
  80. package/src/ui/trust-list.js +0 -46
@@ -1,12 +1,20 @@
1
1
  // Recreation of coutumes.com for Pulse capability testing
2
- // This spec deliberately uses Pulse UI components to document:
3
- // - which components cover the design requirements, and
4
- // - which new components were needed (marked ★ NEW)
2
+ //
3
+ // component-free — creative override: this editorial fashion recreation relies
4
+ // on several highly custom sections (dual-image blend-mode hero, hover-reveal
5
+ // product cards, 50/50 image pair, large serif brand statement, dismissible
6
+ // announcement bar) that have no generic component equivalent — the former
7
+ // editorialHero/productCard/imagePair/brandStatement/banner components were
8
+ // cut from the library as narrow single-purpose wrappers an agent can
9
+ // hand-roll; this file now hand-rolls them directly with scoped CSS in
10
+ // coutumes.css, using var() tokens throughout. nav(), pullquote() (variant:
11
+ // 'editorial'), grid(), uiImage(), and footer() (rich layout) remain kept
12
+ // components and are unchanged.
5
13
  //
6
14
  // Substituted fonts: Bebas Neue (→ Plaak 3), Cormorant Garamond (→ Exposure Regular)
7
15
  // All photography: placeholder images (originals are proprietary Coutumes photos)
8
16
 
9
- import { nav, banner, editorialHero, pullquote, grid, productCard, imagePair, brandStatement, uiImage, footer, input, button } from '@invisibleloop/pulse/ui'
17
+ import { nav, pullquote, grid, uiImage, footer } from '@invisibleloop/pulse/ui'
10
18
  import { asset } from '../../lib/layout.js'
11
19
 
12
20
  export const spec = {
@@ -30,18 +38,17 @@ export const spec = {
30
38
  view: (state) => `
31
39
  <main id="main-content">
32
40
 
33
- <!-- Announcement bar — fixed to bottom, dismissible -->
34
- ${state.bannerVisible ? banner({
35
- content: `<span>SILVER LANDS&nbsp;•&nbsp;NEW CAPSULE COLLECTION</span>`,
36
- variant: 'promo',
37
- position: 'bottom',
38
- dismissible: true,
39
- dismissEvent: 'dismissBanner',
40
- }) : ''}
41
-
42
- <!-- Navigation — nav() with utility links in action slot
43
- GAP: nav has no second link group; utility links (Search, Magazine, FAQ, Cart)
44
- are crammed into the action slot as raw HTML rather than a proper utilityLinks param -->
41
+ <!-- Announcement bar — fixed to bottom, dismissible. Raw HTML: the
42
+ removed banner() component previously rendered this; the dismiss
43
+ button still wires to the existing dismissBanner mutation. -->
44
+ ${state.bannerVisible ? `
45
+ <div class="coutumes-announce" role="banner">
46
+ <span>SILVER LANDS&nbsp;•&nbsp;NEW CAPSULE COLLECTION</span>
47
+ <button class="coutumes-announce-close" data-event="click:dismissBanner" aria-label="Close announcement">×</button>
48
+ </div>
49
+ ` : ''}
50
+
51
+ <!-- Navigation — nav() supports a second utilityLinks group natively. -->
45
52
  ${nav({
46
53
  logo: 'COUTUMES',
47
54
  logoHref: '/',
@@ -62,23 +69,26 @@ export const spec = {
62
69
  color: 'var(--coutumes-black)',
63
70
  })}
64
71
 
65
- <!-- Hero — ★ NEW: editorialHero() — dual split-image with overlapping blend-mode type.
66
- The hero() component could not do this: it only supports a single image side.
67
- editorialHero accepts leftImage + rightImage + lines array + blend mode. -->
68
- ${editorialHero({
69
- leftImage: `<img src="https://images.unsplash.com/photo-1617038260897-41a1f14a8ca0?w=700&q=80" alt="Hand holding a cream hat, wearing a dark ring" width="700" loading="eager" fetchpriority="high">`,
70
- rightImage: `<img src="https://images.unsplash.com/photo-1552374196-1ab2a1c593e8?w=700&q=80" alt="Man leaning against a Land Rover in the countryside" width="700" loading="eager">`,
71
- lines: ['INTRODUCING', 'SILVER LANDS', 'CAPSULE', 'COLLECTION'],
72
- heading: 'Silver Lands Capsule Collection',
73
- color: 'var(--coutumes-amber)',
74
- blend: 'none',
75
- ratio: '3/4',
76
- class: 'coutumes-hero',
77
- })}
72
+ <!-- Hero — dual split-image with overlapping blend-mode type. Raw HTML:
73
+ the removed editorialHero() component previously rendered this
74
+ mix-blend-mode overlay technique; a real h1 heading element carries
75
+ the accessible heading while the overlay lines are decorative (aria-hidden). -->
76
+ <section class="coutumes-hero">
77
+ <h1 class="coutumes-sr-only">Silver Lands Capsule Collection</h1>
78
+ <div class="coutumes-hero-images">
79
+ <div class="coutumes-hero-img">
80
+ <img src="https://images.unsplash.com/photo-1617038260897-41a1f14a8ca0?w=700&q=80" alt="Hand holding a cream hat, wearing a dark ring" width="700" loading="eager" fetchpriority="high">
81
+ </div>
82
+ <div class="coutumes-hero-img">
83
+ <img src="https://images.unsplash.com/photo-1552374196-1ab2a1c593e8?w=700&q=80" alt="Man leaning against a Land Rover in the countryside" width="700" loading="eager">
84
+ </div>
85
+ </div>
86
+ <div class="coutumes-hero-text" aria-hidden="true">
87
+ ${['INTRODUCING', 'SILVER LANDS', 'CAPSULE', 'COLLECTION'].map(l => `<span>${l}</span>`).join('')}
88
+ </div>
89
+ </section>
78
90
 
79
- <!-- Editorial quote — ★ ENHANCED: pullquote() with new variant: 'editorial'
80
- Previously pullquote only had a left accent border (wrong for this centred
81
- large-serif pattern). New variant='editorial' removes the border and centres. -->
91
+ <!-- Editorial quote — pullquote() variant='editorial' (centred, no border). -->
82
92
  <section id="collection">
83
93
  ${pullquote({
84
94
  quote: 'Open roads with no precise destination, passing landscapes, a few days spent outside with friends. Pieces designed to travel for a long time.',
@@ -88,9 +98,9 @@ export const spec = {
88
98
  })}
89
99
  </section>
90
100
 
91
- <!-- Product grid — grid() + ★ NEW: productCard()
92
- photoCard() is polaroid-styled (wrong for fashion e-commerce).
93
- productCard() is full-bleed with hover-reveal name overlay. -->
101
+ <!-- Product grid — raw anchor cards (full-bleed image + below-caption
102
+ name/price), replacing the removed productCard() component.
103
+ Laid out with the kept grid() component. -->
94
104
  ${grid({
95
105
  cols: 3,
96
106
  gap: 'sm',
@@ -116,43 +126,45 @@ export const spec = {
116
126
  price: '£62.00',
117
127
  badge: 'NEW',
118
128
  },
119
- ].map(p => productCard({
120
- src: p.img,
121
- alt: p.alt,
122
- name: p.name,
123
- price: p.price,
124
- badge: p.badge,
125
- ratio: '4/5',
126
- layout: 'below',
127
- })).join(''),
129
+ ].map(p => `
130
+ <a href="#" class="coutumes-product-card">
131
+ <img src="${p.img}" alt="${p.alt}" loading="lazy" decoding="async">
132
+ ${p.badge ? `<span class="coutumes-product-badge">${p.badge}</span>` : ''}
133
+ <div class="coutumes-product-caption">
134
+ <p class="coutumes-product-name">${p.name}</p>
135
+ <p class="coutumes-product-price">${p.price}</p>
136
+ </div>
137
+ </a>
138
+ `).join(''),
128
139
  class: 'coutumes-product-grid',
129
140
  })}
130
141
 
131
- <!-- 50/50 split editorial images — ★ NEW: imagePair()
132
- media() is image + text. There was no component for two editorial photos
133
- side by side with no text. imagePair() fills this gap. -->
134
- ${imagePair({
135
- leftSrc: 'https://picsum.photos/seed/craft/700/900',
136
- leftAlt: 'Craftsman working with tools in a jewelry workshop',
137
- rightSrc: 'https://images.unsplash.com/photo-1441984904996-e0b6ba687e04?w=700&q=80',
138
- rightAlt: 'Elegant boutique storefront with warm lighting',
139
- ratio: '3/4',
140
- label: 'Craftmanship and boutique',
141
- leftLabel: 'Craftmanship',
142
- leftHref: '#craftmanship',
143
- rightLabel: 'Store',
144
- rightHref: '#store',
145
- })}
146
-
147
- <!-- Large editorial statement — ★ NEW: brandStatement()
148
- pullquote() is for shorter attributed quotes (with left border).
149
- brandStatement() is for full-width brand-voice paragraphs: large,
150
- centred, italic serif. -->
151
- ${brandStatement({
152
- text: 'pieces that are both eclectic and everyday. Subtly precious, expressive, and refined, our jewelry complements an outfit or disrupts it, introducing new rituals into the modern man&rsquo;s wardrobe.',
153
- font: 'serif',
154
- size: 'md',
155
- })}
142
+ <!-- 50/50 split editorial images — raw HTML two-column grid, replacing
143
+ the removed imagePair() component. media() is image + text and
144
+ is not a fit for this text-free two-photo layout. -->
145
+ <div class="coutumes-split-images" aria-label="Craftmanship and boutique">
146
+ <div class="coutumes-split-col">
147
+ <div class="coutumes-split-img">
148
+ <img src="https://picsum.photos/seed/craft/700/900" alt="Craftsman working with tools in a jewelry workshop" loading="lazy" decoding="async">
149
+ </div>
150
+ <a href="#craftmanship" class="coutumes-split-link">Craftmanship →</a>
151
+ </div>
152
+ <div class="coutumes-split-col">
153
+ <div class="coutumes-split-img">
154
+ <img src="https://images.unsplash.com/photo-1441984904996-e0b6ba687e04?w=700&q=80" alt="Elegant boutique storefront with warm lighting" loading="lazy" decoding="async">
155
+ </div>
156
+ <a href="#store" class="coutumes-split-link">Store →</a>
157
+ </div>
158
+ </div>
159
+
160
+ <!-- Large editorial statement — raw HTML, replacing the removed
161
+ brandStatement() component. pullquote() is for shorter attributed
162
+ quotes; this is an unattributed full-width brand-voice paragraph. -->
163
+ <div class="coutumes-statement">
164
+ <div class="coutumes-statement-inner">
165
+ <p>pieces that are both eclectic and everyday. Subtly precious, expressive, and refined, our jewelry complements an outfit or disrupts it, introducing new rituals into the modern man&rsquo;s wardrobe.</p>
166
+ </div>
167
+ </div>
156
168
 
157
169
  <!-- Portrait — uiImage() works well here with maxWidth constraint -->
158
170
  ${uiImage({
@@ -163,9 +175,8 @@ export const spec = {
163
175
  class: 'coutumes-portrait',
164
176
  })}
165
177
 
166
- <!-- Footer — ★ ENHANCED: footer() with new columns + subscribe + wordmark + background params.
167
- The simple footer() only had logo + flat links + legal.
168
- New rich variant adds multi-column link grid, subscribe form, and giant wordmark. -->
178
+ <!-- Footer — footer() already supports the rich layout (columns,
179
+ subscribe, wordmark, background/color) used here; no changes needed. -->
169
180
  ${footer({
170
181
  background: 'var(--coutumes-amber)',
171
182
  color: 'var(--coutumes-black)',
@@ -3,7 +3,9 @@
3
3
  *
4
4
  * A complete landing page for a mobile app — nav, hero, features,
5
5
  * stats, testimonials, pricing, FAQ, CTA, footer.
6
- * Built entirely from Pulse UI components, zero custom HTML.
6
+ * Built entirely from Pulse UI components (testimonials and pricing tiers
7
+ * use card() — the former testimonial()/pricing() components were removed
8
+ * from the library as narrow single-purpose wrappers).
7
9
  */
8
10
 
9
11
  import { asset } from '../../lib/layout.js'
@@ -16,8 +18,8 @@ import {
16
18
  grid,
17
19
  feature,
18
20
  stat,
19
- testimonial,
20
- pricing,
21
+ card,
22
+ badge,
21
23
  accordion,
22
24
  cta,
23
25
  footer,
@@ -30,6 +32,7 @@ import {
30
32
  iconZap,
31
33
  iconRefresh,
32
34
  iconBarChart,
35
+ iconStar,
33
36
  } from '../../../../src/ui/index.js'
34
37
 
35
38
  // ── Navigation ────────────────────────────────────────────────────────────────
@@ -164,6 +167,15 @@ const featuresSection = section({
164
167
 
165
168
  // ── Testimonials ──────────────────────────────────────────────────────────────
166
169
 
170
+ // Testimonial card — replaces the removed testimonial() component with card()
171
+ const testimonialCard = ({ quote, name, role, rating }) => card({
172
+ content: `
173
+ <div class="u-flex u-gap-1" aria-hidden="true" style="color:var(--ui-accent)">${iconStar({ size: 14 }).repeat(rating)}</div>
174
+ <p class="u-mt-3" style="font-style:italic">&ldquo;${quote}&rdquo;</p>
175
+ `,
176
+ footer: `<p class="u-font-bold">${name}</p><p class="u-text-sm u-text-muted">${role}</p>`,
177
+ })
178
+
167
179
  const testimonialsSection = section({
168
180
  id: 'testimonials',
169
181
  variant: 'alt',
@@ -175,9 +187,9 @@ const testimonialsSection = section({
175
187
  cols: 3,
176
188
  gap: 'md',
177
189
  content: [
178
- testimonial({ quote: 'I\'ve tried every journaling app out there. Lumio is the first one that actually helped me understand why I was feeling burnt out. The AI insights are genuinely useful, not gimmicky.', name: 'Sarah K.', role: 'Product Designer', rating: 5 }),
179
- testimonial({ quote: 'The streak feature sounds simple but it completely changed my relationship with writing. 112 days in and I haven\'t missed one. My therapist noticed a difference before I did.', name: 'Marcus T.', role: 'Software Engineer', rating: 5 }),
180
- testimonial({ quote: 'I was sceptical about the AI part but it\'s surprisingly good at spotting patterns. It told me I write less when I skip my morning run — completely true and now I use it as motivation.', name: 'Priya R.', role: 'Clinical Psychologist', rating: 5 }),
190
+ testimonialCard({ quote: 'I\'ve tried every journaling app out there. Lumio is the first one that actually helped me understand why I was feeling burnt out. The AI insights are genuinely useful, not gimmicky.', name: 'Sarah K.', role: 'Product Designer', rating: 5 }),
191
+ testimonialCard({ quote: 'The streak feature sounds simple but it completely changed my relationship with writing. 112 days in and I haven\'t missed one. My therapist noticed a difference before I did.', name: 'Marcus T.', role: 'Software Engineer', rating: 5 }),
192
+ testimonialCard({ quote: 'I was sceptical about the AI part but it\'s surprisingly good at spotting patterns. It told me I write less when I skip my morning run — completely true and now I use it as motivation.', name: 'Priya R.', role: 'Clinical Psychologist', rating: 5 }),
181
193
  ].map(t => `<div>${t}</div>`).join(''),
182
194
  }),
183
195
  }),
@@ -185,6 +197,21 @@ const testimonialsSection = section({
185
197
 
186
198
  // ── Pricing ───────────────────────────────────────────────────────────────────
187
199
 
200
+ // Pricing tile — replaces the removed pricing() component with card()
201
+ const pricingCard = ({ name, price, period, description, features, badge: badgeLabel, highlighted, action }) => card({
202
+ variant: highlighted ? 'elevated' : 'bordered',
203
+ content: `
204
+ ${badgeLabel ? badge({ label: badgeLabel }) : ''}
205
+ <h3 class="u-text-xl u-font-bold u-mt-2">${name}</h3>
206
+ <p class="u-text-3xl u-font-bold u-mt-2">${price}<span class="u-text-sm u-text-muted">${period}</span></p>
207
+ <p class="u-text-sm u-text-muted u-mt-2">${description}</p>
208
+ <ul class="u-mt-4 u-flex u-flex-col u-gap-2">
209
+ ${features.map(f => `<li>${f}</li>`).join('')}
210
+ </ul>
211
+ `,
212
+ footer: action,
213
+ })
214
+
188
215
  const pricingSection = section({
189
216
  id: 'pricing',
190
217
  eyebrow: 'Simple pricing',
@@ -197,7 +224,7 @@ const pricingSection = section({
197
224
  cols: 3,
198
225
  gap: 'md',
199
226
  content: [
200
- pricing({
227
+ pricingCard({
201
228
  name: 'Free',
202
229
  price: '£0',
203
230
  period: 'forever',
@@ -211,7 +238,7 @@ const pricingSection = section({
211
238
  ],
212
239
  action: button({ label: 'Get started free', href: '#download', variant: 'outline', full: true }),
213
240
  }),
214
- pricing({
241
+ pricingCard({
215
242
  name: 'Pro',
216
243
  price: '£4.99',
217
244
  period: '/month',
@@ -228,7 +255,7 @@ const pricingSection = section({
228
255
  ],
229
256
  action: button({ label: 'Start free trial', href: '#download', full: true }),
230
257
  }),
231
- pricing({
258
+ pricingCard({
232
259
  name: 'Family',
233
260
  price: '£9.99',
234
261
  period: '/month',
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Demonstrates:
5
5
  * - Retro/vintage light-theme branding (1970s enamel-sign aesthetic)
6
- * - stat(), testimonial(), cta(), card(), badge() layout components
6
+ * - stat(), cta(), card(), badge() layout components
7
7
  * - Icon imports: iconMapPin, iconPhone, iconClock, iconSend, iconStar
8
8
  * - Async action with onStart / run / onSuccess / onError lifecycle
9
9
  * - State-driven form (idle → sending → sent) with disabled submit
@@ -11,14 +11,14 @@
11
11
  * - System web fonts (Rockwell + Courier New) — zero extra HTTP requests
12
12
  * - Full-bleed hero with background image + gradient overlay
13
13
  * - Stat strip with inverted navy background
14
- * - Drop cap typography for editorial prose section
14
+ * - Reviews rendered with card() (quote + name/role + star rating)
15
15
  * - Opening hours list with Courier monospace styling
16
16
  *
17
17
  * Run: node examples/dev.server.js → http://localhost:3001/retrowheels
18
18
  */
19
19
 
20
20
  import {
21
- nav, footer, button, input, stat, testimonial, cta, card, badge,
21
+ nav, footer, button, input, stat, cta, card, badge,
22
22
  } from '../../../../src/ui/index.js'
23
23
  import { iconMapPin, iconPhone, iconClock, iconSend, iconStar } from '../../../../src/ui/icons.js'
24
24
  import { asset } from '../../lib/layout.js'
@@ -120,6 +120,19 @@ const renderHourRow = (h) => `
120
120
  </li>
121
121
  `
122
122
 
123
+ // Review card — replaces the removed testimonial() component with card()
124
+ const renderReview = (r) => card({
125
+ content: `
126
+ <div class="rw-review-stars" aria-hidden="true">${iconStar({ size: 14 }).repeat(r.rating || 0)}</div>
127
+ <p class="rw-review-quote">&ldquo;${r.quote}&rdquo;</p>
128
+ `,
129
+ footer: `
130
+ <p class="rw-review-name">${r.name}</p>
131
+ <p class="rw-review-role">${r.role}</p>
132
+ `,
133
+ class: 'rw-review',
134
+ })
135
+
123
136
  // ---------------------------------------------------------------------------
124
137
  // Spec
125
138
  // ---------------------------------------------------------------------------
@@ -226,7 +239,7 @@ export default {
226
239
  <h2 class="rw-section-title">What folks have been saying.</h2>
227
240
  <p class="rw-section-deck">Reviews collected from customers, riders, and the occasional postcard pinned to the workshop wall.</p>
228
241
  <div class="rw-service-grid">
229
- ${REVIEWS.map(r => testimonial(r)).join('')}
242
+ ${REVIEWS.map(renderReview).join('')}
230
243
  </div>
231
244
  </div>
232
245
  </section>
@@ -11,16 +11,17 @@
11
11
  * - nav() with a custom cart icon trigger and dialog-open
12
12
  * - modal() basket with a live cart table
13
13
  * - Quantity stepper with min/max constraints
14
- * - displayHeading(), badge(), sectionLabel(), stat(), grid()
14
+ * - heading(), badge(), stat(), grid()
15
+ * - Section eyebrow + rule rendered as raw HTML (replaces removed sectionLabel())
16
+ * - Minimal end-of-page metadata footer rendered as raw HTML (replaces removed colophon())
15
17
  * - _toast on mutation success
16
18
  * - Light theme with custom accent override
17
19
  */
18
20
 
19
21
  import {
20
- nav, displayHeading, badge, sectionLabel, stat, grid,
22
+ nav, heading, badge, stat, grid,
21
23
  button, modal, empty,
22
24
  iconPlus, iconMinus, iconShoppingCart, iconCheck, iconPackage,
23
- colophon,
24
25
  } from '../../../../src/ui/index.js'
25
26
  import { asset } from '../../lib/layout.js'
26
27
 
@@ -48,6 +49,15 @@ const cartRows = (items) => items.map(({ name, qty, unitPrice }) => [
48
49
  formatPrice(qty * unitPrice),
49
50
  ])
50
51
 
52
+ // Section eyebrow + heading + rule — replaces the removed sectionLabel() component
53
+ const sectionLabel = ({ eyebrow, heading: headingText }) => `
54
+ <div class="shop-section-label">
55
+ <p class="shop-section-label-eyebrow">${eyebrow}</p>
56
+ ${heading({ text: headingText, level: 2, class: 'shop-section-label-heading' })}
57
+ <hr class="shop-section-label-rule" aria-hidden="true">
58
+ </div>
59
+ `
60
+
51
61
  export default {
52
62
  route: '/templates/shop',
53
63
  meta: {
@@ -121,7 +131,7 @@ export default {
121
131
  <main id="main-content">
122
132
 
123
133
  <div class="page-body product-display">
124
- ${displayHeading({ text: 'DevBook Pro 15', level: 1, tracking: 'tight' })}
134
+ ${heading({ text: 'DevBook Pro 15', level: 1, class: 'product-display-title' })}
125
135
  <p class="product-tagline">Premium developer laptop for people who build things.</p>
126
136
  <div class="product-meta-row">
127
137
  ${badge({ label: 'In Stock', variant: 'success' })}
@@ -216,7 +226,7 @@ export default {
216
226
  </div>
217
227
 
218
228
  <div class="page-body product-specs">
219
- ${sectionLabel({ eyebrow: 'Under the hood', heading: 'Full specifications', rule: true })}
229
+ ${sectionLabel({ eyebrow: 'Under the hood', heading: 'Full specifications' })}
220
230
  <div class="spec-grid">
221
231
  ${SPECS.map(({ label, value }) => `
222
232
  <div class="spec-item">
@@ -228,7 +238,7 @@ export default {
228
238
  </div>
229
239
 
230
240
  <div class="page-body product-stats">
231
- ${sectionLabel({ eyebrow: 'At a glance', heading: 'Key numbers', rule: true })}
241
+ ${sectionLabel({ eyebrow: 'At a glance', heading: 'Key numbers' })}
232
242
  <div class="product-stats-grid">
233
243
  ${grid({ cols: 3, content:
234
244
  stat({ label: 'Battery life', value: '18 hrs', trend: 'up' }) +
@@ -240,10 +250,11 @@ export default {
240
250
 
241
251
  </main>
242
252
 
243
- ${colophon({
244
- content: `<p>© 2026 Tech Stuff Inc. · <a href="#">Terms</a> · <a href="#">Privacy</a></p>`,
245
- align: 'center',
246
- })}
253
+ <footer class="shop-colophon">
254
+ <div class="shop-colophon-inner">
255
+ <p>© 2026 Tech Stuff Inc. · <a href="#">Terms</a> · <a href="#">Privacy</a></p>
256
+ </div>
257
+ </footer>
247
258
  `
248
259
  },
249
260
 
@@ -6,11 +6,15 @@
6
6
  * trust signals, testimonial, and contact strip.
7
7
  *
8
8
  * Vibe: corporate Theme: light Palette: navy / amber
9
+ *
10
+ * component-free — creative override: the top utility strip, trust list, and
11
+ * contact strip are small raw-HTML sections (icon+label pairs / a two-column
12
+ * bar) that replaced the now-removed topStrip/trustList/contactRow components.
13
+ * Services use the kept card() component instead of the removed serviceCard().
9
14
  */
10
15
 
11
16
  import {
12
- nav, button, footer,
13
- topStrip, serviceCard, trustList, contactRow,
17
+ nav, button, footer, card, grid,
14
18
  iconZap, iconShield, iconCheck, iconPhone, iconMail, iconHome, iconClock, iconSettings, iconAlertTriangle, iconStar,
15
19
  } from '../../../../src/ui/index.js'
16
20
  import { asset } from '../../lib/layout.js'
@@ -67,10 +71,13 @@ const why = [
67
71
  },
68
72
  ]
69
73
 
70
- const service = (s) => serviceCard({
71
- icon: s.icon,
72
- title: s.title,
73
- description: s.body,
74
+ const service = (s) => card({
75
+ content: `
76
+ <div class="ui-service-card-icon" aria-hidden="true">${s.icon}</div>
77
+ <h3 class="ui-service-card-title">${s.title}</h3>
78
+ <p class="ui-service-card-desc">${s.body}</p>
79
+ `,
80
+ class: 'service-card',
74
81
  })
75
82
 
76
83
  const whyItem = (w) => `
@@ -106,12 +113,10 @@ export default {
106
113
  },
107
114
 
108
115
  view: () => `
109
- ${topStrip({
110
- left: 'Mon–Sat 7am–7pm · 24/7 emergency line',
111
- right: `<a href="${PHONE_HREF}">Call ${PHONE}</a>`,
112
- background: 'var(--brand-navy-deep)',
113
- color: 'var(--brand-white)',
114
- })}
116
+ <div class="top-strip">
117
+ <div class="top-strip__left">Mon–Sat 7am–7pm · 24/7 emergency line</div>
118
+ <div class="top-strip__right"><a href="${PHONE_HREF}">Call ${PHONE}</a></div>
119
+ </div>
115
120
 
116
121
  ${nav({
117
122
  logo: '<strong class="brand-mark">Sparkys</strong>',
@@ -153,7 +158,14 @@ export default {
153
158
  })}
154
159
  </div>
155
160
  <div class="hero-split__trust">
156
- ${trustList({ items: ['Part P registered', 'NICEIC approved', '£10m insured'] })}
161
+ <ul class="trust-inline">
162
+ ${['Part P registered', 'NICEIC approved', '£10m insured'].map(t => `
163
+ <li class="trust-inline__item">
164
+ <span class="trust-inline__icon" aria-hidden="true">${iconCheck({ size: 14 })}</span>
165
+ <span>${t}</span>
166
+ </li>
167
+ `).join('')}
168
+ </ul>
157
169
  </div>
158
170
  </div>
159
171
  <aside class="hero-split__panel" aria-label="Sparkys at a glance">
@@ -186,9 +198,7 @@ export default {
186
198
  <h2 class="section__title">Domestic, commercial and everything in between.</h2>
187
199
  <p class="section__subtitle">One local team, certified for every job — so you don't end up juggling three different trades.</p>
188
200
  </header>
189
- <div class="services">
190
- ${services.map(service).join('')}
191
- </div>
201
+ ${grid({ cols: 3, gap: 'md', content: services.map(service).join(''), class: 'services' })}
192
202
  </div>
193
203
  </section>
194
204
 
@@ -222,13 +232,18 @@ export default {
222
232
  <p class="contact-strip__lead">Free, no-obligation quotes for homes and businesses across Ashfield, Mansfield and the wider NG postcodes.</p>
223
233
  </div>
224
234
  <div class="contact-strip__details">
225
- ${contactRow({
226
- items: [
235
+ <ul class="contact-inline">
236
+ ${[
227
237
  { icon: iconPhone({ size: 18 }), label: PHONE, href: PHONE_HREF },
228
238
  { icon: iconMail({ size: 18 }), label: EMAIL, href: `mailto:${EMAIL}` },
229
239
  { icon: iconClock({ size: 18 }), label: 'Mon–Sat 7am–7pm · 24/7 emergencies' },
230
- ],
231
- })}
240
+ ].map(({ icon, label, href }) => `
241
+ <li class="contact-inline__item">
242
+ <span class="contact-inline__icon" aria-hidden="true">${icon}</span>
243
+ ${href ? `<a href="${href}">${label}</a>` : `<span>${label}</span>`}
244
+ </li>
245
+ `).join('')}
246
+ </ul>
232
247
  </div>
233
248
  </div>
234
249
  </section>
@@ -31,9 +31,8 @@ Before finishing any spec, verify every point below. Fix anything that fails.
31
31
  - Hero sections → `hero({ title, subtitle, actions, image, ... })`
32
32
  - Product/service cards → `card({ title, body, image, footer, ... })`
33
33
  - Image + text two-column layouts → `media({ image, content, reverse })`
34
- - Horizontal strips/banners → `banner({ content, variant })`
35
34
  - Feature tiles → `feature({ icon, title, description })`
36
- - Testimonials → `testimonial({ quote, name, role, src })`
35
+ - Testimonials → `card({ content, footer })` with quote + avatar/name in the content slot
37
36
  - CTAs → `cta({ title, subtitle, actions })`
38
37
  - Navigation → `nav({ logo, links, actions })`
39
38
  - Footers → `footer({ logo, links, columns, legal, ... })`
@@ -353,23 +353,23 @@ test('FAQ has entries', () => {
353
353
 
354
354
  test('pricing: monthly view renders 3 plans', () => {
355
355
  const r = renderSync(pricing, { state: { billing: 'monthly' } })
356
- assert.equal(r.count('.ui-pricing'), 3)
356
+ assert.equal(r.count('.pr-plan'), 3)
357
357
  })
358
358
 
359
359
  test('pricing: annual view renders 3 plans', () => {
360
360
  const r = renderSync(pricing, { state: { billing: 'annual' } })
361
- assert.equal(r.count('.ui-pricing'), 3)
361
+ assert.equal(r.count('.pr-plan'), 3)
362
362
  })
363
363
 
364
364
  test('pricing: monthly Pro price is £12', () => {
365
365
  const r = renderSync(pricing, { state: { billing: 'monthly' } })
366
- const prices = r.findAll('.ui-pricing-amount')
366
+ const prices = r.findAll('.pr-plan-amount')
367
367
  assert(prices.some(p => p.text === '£12'), 'monthly Pro should be £12')
368
368
  })
369
369
 
370
370
  test('pricing: annual Pro price is £9', () => {
371
371
  const r = renderSync(pricing, { state: { billing: 'annual' } })
372
- const prices = r.findAll('.ui-pricing-amount')
372
+ const prices = r.findAll('.pr-plan-amount')
373
373
  assert(prices.some(p => p.text === '£9'), 'annual Pro should be £9')
374
374
  })
375
375
 
@@ -2,7 +2,9 @@
2
2
  * Pulse — Pricing page example
3
3
  *
4
4
  * Demonstrates:
5
- * - Landing page components (nav, pricing, accordion, cta, footer)
5
+ * - Landing page components (nav, accordion, cta, footer)
6
+ * - Pricing tiers rendered with card() — the former pricing() component
7
+ * was removed from the library as a narrow single-purpose wrapper
6
8
  * - Billing-period toggle (monthly / annual) via mutation
7
9
  * - Layout components (section, container, stack, cluster, grid)
8
10
  * - No server data — fully static, still gets streaming SSR
@@ -11,11 +13,27 @@
11
13
  */
12
14
 
13
15
  import {
14
- nav as uiNav, pricing, accordion, cta, footer as uiFooter,
16
+ nav as uiNav, card, badge, accordion, cta, footer as uiFooter,
15
17
  section as uiSection, container, stack, cluster,
16
18
  button, heading,
17
19
  } from '../src/ui/index.js'
18
20
 
21
+ // Pricing tile — replaces the removed pricing() component with card()
22
+ const pricingCard = ({ name, price, period, description, features, badge: badgeLabel, highlighted, action, level = 2 }) => card({
23
+ variant: highlighted ? 'elevated' : 'bordered',
24
+ class: 'pr-plan',
25
+ content: `
26
+ ${badgeLabel ? badge({ label: badgeLabel }) : ''}
27
+ <h${level} class="u-text-xl u-font-bold u-mt-2">${name}</h${level}>
28
+ <p class="u-text-3xl u-font-bold u-mt-2"><span class="pr-plan-amount">${price}</span><span class="u-text-sm u-text-muted">${period}</span></p>
29
+ <p class="u-text-sm u-text-muted u-mt-2">${description}</p>
30
+ <ul class="u-mt-4 u-flex u-flex-col u-gap-2">
31
+ ${features.map(f => `<li>${f}</li>`).join('')}
32
+ </ul>
33
+ `,
34
+ footer: action,
35
+ })
36
+
19
37
  const PLANS = {
20
38
  monthly: [
21
39
  {
@@ -181,7 +199,7 @@ export default {
181
199
  ${state.billing === 'annual' ? `<p class="pr-annual-note">Annual plans are billed yearly. Prices shown are monthly equivalents.</p>` : ''}
182
200
 
183
201
  <div class="pr-plans">
184
- ${plans.map(p => pricing({ ...p, level: 2 })).join('')}
202
+ ${plans.map(p => pricingCard({ ...p, level: 2 })).join('')}
185
203
  </div>
186
204
 
187
205
  ` }) }) })}