@invisibleloop/pulse 0.20.5 → 0.20.7
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.
- package/.claude/pulse-checklist.md +1 -2
- package/CLAUDE.md +1 -2
- package/docs/.claude/pulse-checklist.md +1 -2
- package/docs/public/coutumes.css +21 -44
- package/docs/public/docs.css +73 -6
- package/docs/public/home-brut.css +175 -191
- package/docs/public/pulse-ui.css +8 -1037
- package/docs/public/retrowheels.css +23 -0
- package/docs/public/shop.css +35 -1
- package/docs/public/sparkys.css +56 -23
- package/docs/public/theme.css +51 -7
- package/docs/server.js +0 -11
- package/docs/src/lib/component-page.js +2 -1
- package/docs/src/lib/nav.js +0 -11
- package/docs/src/pages/accessibility.js +2 -1
- package/docs/src/pages/actions.js +2 -1
- package/docs/src/pages/auth.js +2 -1
- package/docs/src/pages/caching.js +2 -1
- package/docs/src/pages/components/accordion.js +2 -1
- package/docs/src/pages/components/alert.js +2 -1
- package/docs/src/pages/components/app-badge.js +2 -1
- package/docs/src/pages/components/avatar.js +2 -1
- package/docs/src/pages/components/badge.js +2 -1
- package/docs/src/pages/components/breadcrumbs.js +2 -1
- package/docs/src/pages/components/button.js +2 -1
- package/docs/src/pages/components/card.js +2 -1
- package/docs/src/pages/components/charts.js +4 -2
- package/docs/src/pages/components/checkbox.js +2 -1
- package/docs/src/pages/components/cluster.js +2 -1
- package/docs/src/pages/components/container.js +2 -1
- package/docs/src/pages/components/cta.js +2 -1
- package/docs/src/pages/components/divider.js +2 -1
- package/docs/src/pages/components/empty.js +2 -1
- package/docs/src/pages/components/feature.js +2 -1
- package/docs/src/pages/components/fieldset.js +2 -1
- package/docs/src/pages/components/fileupload.js +2 -1
- package/docs/src/pages/components/footer.js +2 -1
- package/docs/src/pages/components/grid.js +11 -6
- package/docs/src/pages/components/heading.js +2 -1
- package/docs/src/pages/components/hero.js +2 -1
- package/docs/src/pages/components/icons.js +2 -1
- package/docs/src/pages/components/image.js +2 -1
- package/docs/src/pages/components/input.js +2 -1
- package/docs/src/pages/components/media.js +2 -1
- package/docs/src/pages/components/modal.js +2 -1
- package/docs/src/pages/components/nav.js +2 -1
- package/docs/src/pages/components/phone-frame.js +2 -1
- package/docs/src/pages/components/progress.js +2 -1
- package/docs/src/pages/components/prose.js +2 -1
- package/docs/src/pages/components/pullquote.js +2 -1
- package/docs/src/pages/components/radio.js +2 -1
- package/docs/src/pages/components/rating.js +2 -1
- package/docs/src/pages/components/search.js +2 -1
- package/docs/src/pages/components/section.js +2 -1
- package/docs/src/pages/components/segmented.js +2 -1
- package/docs/src/pages/components/select.js +2 -1
- package/docs/src/pages/components/slider.js +2 -1
- package/docs/src/pages/components/spinner.js +2 -1
- package/docs/src/pages/components/stack.js +2 -1
- package/docs/src/pages/components/stat.js +2 -1
- package/docs/src/pages/components/stepper.js +2 -1
- package/docs/src/pages/components/table.js +2 -1
- package/docs/src/pages/components/textarea.js +2 -1
- package/docs/src/pages/components/toggle.js +2 -1
- package/docs/src/pages/components/tooltip.js +2 -1
- package/docs/src/pages/components.js +2 -5
- package/docs/src/pages/config.js +2 -1
- package/docs/src/pages/constraints.js +2 -1
- package/docs/src/pages/deploy.js +2 -1
- package/docs/src/pages/error-pages.js +2 -1
- package/docs/src/pages/extending.js +2 -1
- package/docs/src/pages/faq.js +2 -1
- package/docs/src/pages/forms.js +2 -1
- package/docs/src/pages/getting-started.js +2 -1
- package/docs/src/pages/guard.js +2 -1
- package/docs/src/pages/home.js +24 -15
- package/docs/src/pages/how-it-works.js +22 -15
- package/docs/src/pages/hydration.js +2 -1
- package/docs/src/pages/images.js +2 -1
- package/docs/src/pages/markdown.js +2 -1
- package/docs/src/pages/meta.js +2 -1
- package/docs/src/pages/mutations.js +2 -1
- package/docs/src/pages/navigation.js +2 -1
- package/docs/src/pages/performance.js +2 -1
- package/docs/src/pages/persist.js +2 -1
- package/docs/src/pages/project-structure.js +2 -1
- package/docs/src/pages/prompt-examples.js +2 -1
- package/docs/src/pages/raw-responses.js +2 -1
- package/docs/src/pages/routing.js +2 -1
- package/docs/src/pages/server-api.js +2 -1
- package/docs/src/pages/server-data.js +2 -1
- package/docs/src/pages/slash-commands.js +2 -1
- package/docs/src/pages/spec.js +2 -1
- package/docs/src/pages/state.js +2 -1
- package/docs/src/pages/store.js +2 -1
- package/docs/src/pages/streaming.js +2 -1
- package/docs/src/pages/stripe.js +2 -1
- package/docs/src/pages/supabase.js +2 -1
- package/docs/src/pages/templates/coutumes.js +83 -72
- package/docs/src/pages/templates/mobile-app.js +36 -9
- package/docs/src/pages/templates/retrowheels.js +17 -4
- package/docs/src/pages/templates/shop.js +21 -10
- package/docs/src/pages/templates/sparkys.js +35 -20
- package/docs/src/pages/templates.js +2 -2
- package/docs/src/pages/testing.js +2 -1
- package/docs/src/pages/validation.js +2 -1
- package/examples/.claude/pulse-checklist.md +1 -2
- package/examples/examples.test.js +4 -4
- package/examples/pricing.js +21 -3
- package/examples/public/pulse-ui.css +8 -1037
- package/examples/retrowheels.css +23 -0
- package/examples/retrowheels.js +17 -4
- package/package.json +5 -1
- package/public/.pulse-ui-version +1 -1
- package/public/pulse-ui.css +8 -1037
- package/src/agent/checklist.md +1 -2
- package/src/agent/guide-components.md +43 -67
- package/src/agent/guide-design-gallery.md +14 -14
- package/src/agent/guide-design-references.md +21 -21
- package/src/agent/guide-styles.md +2 -2
- package/src/agent/guide-templates.md +6 -6
- package/src/agent/identity.md +1 -1
- package/src/agent/workflow.md +1 -2
- package/src/mcp/server.js +31 -24
- package/src/ui/index.js +0 -27
- package/src/ui/ui.test.js +0 -137
- package/types/charts.d.ts +61 -0
- package/types/ui.d.ts +1 -129
- package/docs/src/pages/components/banner.js +0 -44
- package/docs/src/pages/components/carousel.js +0 -71
- package/docs/src/pages/components/code-window.js +0 -56
- package/docs/src/pages/components/decorate.js +0 -74
- package/docs/src/pages/components/gallery.js +0 -102
- package/docs/src/pages/components/list.js +0 -111
- package/docs/src/pages/components/marquee.js +0 -76
- package/docs/src/pages/components/photo-card.js +0 -74
- package/docs/src/pages/components/pricing.js +0 -67
- package/docs/src/pages/components/testimonial.js +0 -48
- package/docs/src/pages/components/timeline.js +0 -226
- package/src/ui/banner.js +0 -41
- package/src/ui/brand-statement.js +0 -58
- package/src/ui/carousel.js +0 -59
- package/src/ui/code-window.js +0 -39
- package/src/ui/colophon.js +0 -29
- package/src/ui/contact-row.js +0 -41
- package/src/ui/decorate.js +0 -55
- package/src/ui/display-heading.js +0 -44
- package/src/ui/drop-cap.js +0 -29
- package/src/ui/editorial-hero.js +0 -63
- package/src/ui/entry-list.js +0 -36
- package/src/ui/gallery.js +0 -60
- package/src/ui/image-pair.js +0 -72
- package/src/ui/list.js +0 -36
- package/src/ui/marquee.js +0 -48
- package/src/ui/photo-card.js +0 -44
- package/src/ui/pricing.js +0 -56
- package/src/ui/product-card.js +0 -65
- package/src/ui/section-label.js +0 -38
- package/src/ui/service-card.js +0 -47
- package/src/ui/stat-grid.js +0 -40
- package/src/ui/testimonial.js +0 -59
- package/src/ui/timeline.js +0 -72
- package/src/ui/top-strip.js +0 -47
- package/src/ui/trust-list.js +0 -46
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
// Recreation of coutumes.com for Pulse capability testing
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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,
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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 • 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 —
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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 —
|
|
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 —
|
|
92
|
-
|
|
93
|
-
|
|
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 =>
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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 —
|
|
132
|
-
media() is image + text
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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’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 —
|
|
167
|
-
|
|
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
|
|
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
|
-
|
|
20
|
-
|
|
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">“${quote}”</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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(),
|
|
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
|
-
* -
|
|
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,
|
|
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">“${r.quote}”</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(
|
|
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
|
-
* -
|
|
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,
|
|
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
|
-
${
|
|
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'
|
|
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'
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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) =>
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
226
|
-
|
|
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>
|
|
@@ -517,8 +517,8 @@ export default {
|
|
|
517
517
|
meta: {
|
|
518
518
|
title: 'Templates — Pulse',
|
|
519
519
|
description: 'Production-ready Pulse templates. Every template uses only Pulse UI components and ships with 100/100/100 Lighthouse scores.',
|
|
520
|
-
theme: '
|
|
521
|
-
styles:
|
|
520
|
+
theme: 'light',
|
|
521
|
+
styles: ['/pulse-ui.css', '/theme.css', '/docs.css'],
|
|
522
522
|
},
|
|
523
523
|
|
|
524
524
|
view: () => {
|
|
@@ -7,9 +7,10 @@ const { prev, next } = prevNext('/testing')
|
|
|
7
7
|
export default {
|
|
8
8
|
route: '/testing',
|
|
9
9
|
meta: {
|
|
10
|
+
theme: 'light',
|
|
10
11
|
title: 'Testing — Pulse Docs',
|
|
11
12
|
description: 'Test Pulse view functions with renderSync and render — query the HTML output with CSS-like selectors, no DOM required.',
|
|
12
|
-
styles: ['/docs.css'],
|
|
13
|
+
styles: ['/theme.css', '/docs.css'],
|
|
13
14
|
},
|
|
14
15
|
view: () => renderLayout({
|
|
15
16
|
currentHref: '/testing',
|
|
@@ -7,9 +7,10 @@ const { prev, next } = prevNext('/validation')
|
|
|
7
7
|
export default {
|
|
8
8
|
route: '/validation',
|
|
9
9
|
meta: {
|
|
10
|
+
theme: 'light',
|
|
10
11
|
title: 'Validation — Pulse Docs',
|
|
11
12
|
description: 'Declarative validation rules in Pulse — syntax, formats, dot-path notation, and error handling.',
|
|
12
|
-
styles: ['/docs.css'],
|
|
13
|
+
styles: ['/theme.css', '/docs.css'],
|
|
13
14
|
},
|
|
14
15
|
view: () => renderLayout({
|
|
15
16
|
currentHref: '/validation',
|
|
@@ -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 → `
|
|
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('.
|
|
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('.
|
|
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('.
|
|
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('.
|
|
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
|
|