@invisibleloop/pulse 0.20.10 → 0.20.19
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/package.json +6 -5
- package/public/.pulse-ui-version +1 -1
- package/scripts/build.js +26 -0
- package/scripts/release-version.js +17 -8
- package/scripts/strip-server.test.js +60 -0
- package/src/agent/checklist.md +8 -12
- package/src/agent/commands/pulse-start.md +2 -0
- package/src/agent/commands/verify.md +30 -8
- package/src/agent/guide-components.md +7 -25
- package/src/agent/guide-routing.md +0 -12
- package/src/agent/guide-server.md +102 -1
- package/src/agent/guide-spec.md +8 -1
- package/src/agent/guide-styles.md +23 -0
- package/src/agent/identity.md +9 -0
- package/src/agent/workflow.md +4 -23
- package/src/cli/check-bundles.js +113 -0
- package/src/cli/check-bundles.test.js +137 -0
- package/src/cli/cli.test.js +105 -0
- package/src/cli/dev.js +48 -3
- package/src/cli/diagnose.js +105 -0
- package/src/cli/diagnose.test.js +124 -0
- package/src/cli/discover.js +9 -2
- package/src/cli/index.js +171 -9
- package/src/cli/process-utils.js +57 -0
- package/src/cli/process-utils.test.js +114 -0
- package/src/cli/review.js +391 -0
- package/src/cli/review.test.js +143 -0
- package/src/cli/scaffold.js +9 -4
- package/src/cli/stamp.js +102 -0
- package/src/cli/stamp.test.js +149 -0
- package/src/cli/start.js +15 -2
- package/src/cli/validate.js +184 -0
- package/src/cli/validate.test.js +153 -0
- package/src/mcp/mcp.test.js +261 -30
- package/src/mcp/server.js +207 -515
- package/src/mcp/validate-worker.js +18 -1
- package/src/runtime/index.js +144 -11
- package/src/runtime/runtime.test.js +199 -0
- package/src/runtime/ssr.js +14 -7
- package/src/runtime/ssr.test.js +31 -0
- package/src/server/index.js +163 -10
- package/src/server/server.test.js +225 -2
- package/src/spec/schema.js +1 -1
- package/src/ui/index.js +0 -5
- package/src/ui/ui.test.js +0 -75
- package/types/ui.d.ts +0 -21
- package/.claude/commands/build-page.md +0 -63
- package/.claude/commands/new-doc-page.md +0 -45
- package/.claude/commands/pulse-start.md +0 -11
- package/.claude/commands/pulse-stop.md +0 -5
- package/.claude/commands/verify.md +0 -118
- package/.claude/pulse-checklist.md +0 -189
- package/.github/workflows/ci.yml +0 -25
- package/.github/workflows/publish.yml +0 -45
- package/.pulse/load-reports/home/1773432711417.json +0 -22
- package/.pulse-verified +0 -1
- package/AGENTS.md +0 -697
- package/CLAUDE.md +0 -697
- package/README.md +0 -95
- package/benchmark/public/dist/counter.boot-PO6SNSJZ.js +0 -8
- package/benchmark/public/dist/home.boot-YK6VAEQI.js +0 -6
- package/benchmark/public/dist/manifest.json +0 -5
- package/benchmark/public/dist/runtime-5Y7ESAIB.js +0 -2
- package/benchmark/public/dist/runtime-KO4BHUQ3.js +0 -49
- package/benchmark/results/2026-03-27-09-20.json +0 -188
- package/benchmark/results/2026-03-27-09-21.json +0 -188
- package/benchmark/results/2026-03-27-09-29.json +0 -188
- package/benchmark/results/2026-03-27-10-14.json +0 -188
- package/benchmark/results/2026-03-27-10-18.json +0 -232
- package/benchmark/src/pages/counter.js +0 -37
- package/benchmark/src/pages/home.js +0 -21
- package/docs/.claude/commands/pulse-build.md +0 -7
- package/docs/.claude/commands/pulse-dev.md +0 -7
- package/docs/.claude/commands/pulse-start.md +0 -9
- package/docs/.claude/commands/pulse-stop.md +0 -5
- package/docs/.claude/commands/verify.md +0 -84
- package/docs/.claude/coverage-check.js +0 -196
- package/docs/.claude/pulse-checklist.md +0 -189
- package/docs/public/.pulse-ui-version +0 -1
- package/docs/public/andypixel.css +0 -237
- package/docs/public/bookworms.css +0 -422
- package/docs/public/coutumes.css +0 -561
- package/docs/public/docs.css +0 -2388
- package/docs/public/favicon.svg +0 -4
- package/docs/public/home-brut.css +0 -982
- package/docs/public/images/bookworms/hero-books.jpg +0 -0
- package/docs/public/images/bookworms/how-we-read-now.jpg +0 -0
- package/docs/public/images/bookworms/lantern-keepers.jpg +0 -0
- package/docs/public/images/bookworms/provincial-bookseller.jpg +0 -0
- package/docs/public/images/journey/178.jpg +0 -0
- package/docs/public/images/journey/29.jpg +0 -0
- package/docs/public/images/retrowheels/hero.jpg +0 -0
- package/docs/public/images/shop/devbook-pro.jpg +0 -0
- package/docs/public/images/suzukibento/1015.jpg +0 -0
- package/docs/public/images/suzukibento/1025.jpg +0 -0
- package/docs/public/images/suzukibento/1043.jpg +0 -0
- package/docs/public/images/suzukibento/1059.jpg +0 -0
- package/docs/public/images/suzukibento/1062.jpg +0 -0
- package/docs/public/images/suzukibento/1084.jpg +0 -0
- package/docs/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
- package/docs/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
- package/docs/public/images/swish/1485518882345-15568b007407.jpg +0 -0
- package/docs/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
- package/docs/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
- package/docs/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
- package/docs/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
- package/docs/public/images/swish/1551803091-e20673f15770.jpg +0 -0
- package/docs/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
- package/docs/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
- package/docs/public/img/heic1501a.jpg +0 -0
- package/docs/public/img/placeholder-landscape.svg +0 -18
- package/docs/public/img/placeholder-square.svg +0 -14
- package/docs/public/img/placeholder-tall.svg +0 -22
- package/docs/public/journey.css +0 -528
- package/docs/public/menu.js +0 -327
- package/docs/public/pulse-ui.css +0 -3028
- package/docs/public/pulse-ui.js +0 -257
- package/docs/public/retrowheels.css +0 -425
- package/docs/public/shop.css +0 -269
- package/docs/public/sparkys.css +0 -382
- package/docs/public/suzukibento.css +0 -517
- package/docs/public/swish.css +0 -394
- package/docs/public/theme.css +0 -52
- package/docs/public/themes/lumio.css +0 -50
- package/docs/public/themes/observatory.css +0 -104
- package/docs/server.js +0 -161
- package/docs/src/lib/component-page.js +0 -49
- package/docs/src/lib/highlight.js +0 -2
- package/docs/src/lib/layout.js +0 -190
- package/docs/src/lib/metrics-store.js +0 -6
- package/docs/src/lib/nav.js +0 -171
- package/docs/src/lib/stats.js +0 -81
- package/docs/src/pages/accessibility.js +0 -157
- package/docs/src/pages/actions.js +0 -194
- package/docs/src/pages/auth.js +0 -176
- package/docs/src/pages/caching.js +0 -95
- package/docs/src/pages/components/accordion.js +0 -48
- package/docs/src/pages/components/alert.js +0 -35
- package/docs/src/pages/components/app-badge.js +0 -41
- package/docs/src/pages/components/avatar.js +0 -35
- package/docs/src/pages/components/badge.js +0 -36
- package/docs/src/pages/components/breadcrumbs.js +0 -94
- package/docs/src/pages/components/button.js +0 -84
- package/docs/src/pages/components/card.js +0 -225
- package/docs/src/pages/components/charts.js +0 -283
- package/docs/src/pages/components/checkbox.js +0 -129
- package/docs/src/pages/components/cluster.js +0 -47
- package/docs/src/pages/components/container.js +0 -40
- package/docs/src/pages/components/cta.js +0 -54
- package/docs/src/pages/components/divider.js +0 -37
- package/docs/src/pages/components/empty.js +0 -36
- package/docs/src/pages/components/feature.js +0 -60
- package/docs/src/pages/components/fieldset.js +0 -65
- package/docs/src/pages/components/fileupload.js +0 -128
- package/docs/src/pages/components/footer.js +0 -58
- package/docs/src/pages/components/grid.js +0 -169
- package/docs/src/pages/components/heading.js +0 -107
- package/docs/src/pages/components/hero.js +0 -123
- package/docs/src/pages/components/icons.js +0 -410
- package/docs/src/pages/components/image.js +0 -71
- package/docs/src/pages/components/input.js +0 -51
- package/docs/src/pages/components/media.js +0 -51
- package/docs/src/pages/components/modal.js +0 -111
- package/docs/src/pages/components/nav.js +0 -136
- package/docs/src/pages/components/phone-frame.js +0 -74
- package/docs/src/pages/components/progress.js +0 -102
- package/docs/src/pages/components/prose.js +0 -111
- package/docs/src/pages/components/pullquote.js +0 -71
- package/docs/src/pages/components/radio.js +0 -194
- package/docs/src/pages/components/rating.js +0 -106
- package/docs/src/pages/components/search.js +0 -61
- package/docs/src/pages/components/section.js +0 -98
- package/docs/src/pages/components/segmented.js +0 -121
- package/docs/src/pages/components/select.js +0 -45
- package/docs/src/pages/components/slider.js +0 -115
- package/docs/src/pages/components/spinner.js +0 -73
- package/docs/src/pages/components/stack.js +0 -48
- package/docs/src/pages/components/stat.js +0 -55
- package/docs/src/pages/components/stepper.js +0 -66
- package/docs/src/pages/components/table.js +0 -45
- package/docs/src/pages/components/textarea.js +0 -31
- package/docs/src/pages/components/toggle.js +0 -84
- package/docs/src/pages/components/tooltip.js +0 -48
- package/docs/src/pages/components.js +0 -200
- package/docs/src/pages/config.js +0 -193
- package/docs/src/pages/constraints.js +0 -100
- package/docs/src/pages/deploy.js +0 -233
- package/docs/src/pages/error-pages.js +0 -99
- package/docs/src/pages/extending.js +0 -198
- package/docs/src/pages/faq.js +0 -99
- package/docs/src/pages/forms.js +0 -129
- package/docs/src/pages/getting-started.js +0 -147
- package/docs/src/pages/guard.js +0 -120
- package/docs/src/pages/home.js +0 -345
- package/docs/src/pages/how-it-works.js +0 -244
- package/docs/src/pages/hydration.js +0 -108
- package/docs/src/pages/images.js +0 -120
- package/docs/src/pages/markdown.js +0 -156
- package/docs/src/pages/meta.js +0 -193
- package/docs/src/pages/mutations.js +0 -107
- package/docs/src/pages/navigation.js +0 -85
- package/docs/src/pages/performance.js +0 -157
- package/docs/src/pages/persist.js +0 -89
- package/docs/src/pages/project-structure.js +0 -91
- package/docs/src/pages/prompt-examples.js +0 -186
- package/docs/src/pages/raw-responses.js +0 -122
- package/docs/src/pages/routing.js +0 -120
- package/docs/src/pages/server-api.js +0 -291
- package/docs/src/pages/server-data.js +0 -203
- package/docs/src/pages/slash-commands.js +0 -55
- package/docs/src/pages/spec.js +0 -207
- package/docs/src/pages/state.js +0 -102
- package/docs/src/pages/store.js +0 -203
- package/docs/src/pages/streaming.js +0 -107
- package/docs/src/pages/stripe.js +0 -194
- package/docs/src/pages/supabase.js +0 -324
- package/docs/src/pages/templates/andypixel.js +0 -260
- package/docs/src/pages/templates/blog-post.js +0 -226
- package/docs/src/pages/templates/bookworms.js +0 -227
- package/docs/src/pages/templates/coutumes.js +0 -237
- package/docs/src/pages/templates/journey.js +0 -186
- package/docs/src/pages/templates/mobile-app.js +0 -371
- package/docs/src/pages/templates/retrowheels.js +0 -355
- package/docs/src/pages/templates/shop.js +0 -268
- package/docs/src/pages/templates/sparkys.js +0 -264
- package/docs/src/pages/templates/suzukibento.js +0 -200
- package/docs/src/pages/templates/swish.js +0 -313
- package/docs/src/pages/templates.js +0 -534
- package/docs/src/pages/testing.js +0 -199
- package/docs/src/pages/validation.js +0 -138
- package/examples/.claude/commands/pulse-build.md +0 -7
- package/examples/.claude/commands/pulse-dev.md +0 -7
- package/examples/.claude/commands/pulse-start.md +0 -9
- package/examples/.claude/commands/pulse-stop.md +0 -5
- package/examples/.claude/commands/verify.md +0 -84
- package/examples/.claude/coverage-check.js +0 -196
- package/examples/.claude/pulse-checklist.md +0 -189
- package/examples/andypixel.css +0 -237
- package/examples/andypixel.js +0 -258
- package/examples/bookworms.css +0 -422
- package/examples/bookworms.js +0 -225
- package/examples/contact.js +0 -166
- package/examples/counter.js +0 -94
- package/examples/dev.server.js +0 -110
- package/examples/examples.test.js +0 -394
- package/examples/journey.css +0 -528
- package/examples/journey.js +0 -186
- package/examples/pricing.js +0 -262
- package/examples/products.js +0 -190
- package/examples/public/.pulse-ui-version +0 -1
- package/examples/public/pulse-ui.css +0 -3028
- package/examples/public/pulse-ui.js +0 -257
- package/examples/quiz.js +0 -208
- package/examples/retrowheels.css +0 -425
- package/examples/retrowheels.js +0 -353
- package/examples/shared.js +0 -84
- package/examples/suzukibento.css +0 -513
- package/examples/suzukibento.js +0 -200
- package/examples/swish.css +0 -394
- package/examples/swish.js +0 -311
- package/examples/todos.js +0 -162
- package/src/ui/fieldset.js +0 -37
- package/src/ui/phone-frame.js +0 -40
- package/src/ui/search.js +0 -78
- package/src/ui/segmented.js +0 -50
- package/src/ui/tooltip.js +0 -28
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse — Retro Wheels Co. example
|
|
3
|
-
*
|
|
4
|
-
* Demonstrates:
|
|
5
|
-
* - Retro/vintage light-theme branding (1970s enamel-sign aesthetic)
|
|
6
|
-
* - stat(), cta(), card(), badge() layout components
|
|
7
|
-
* - Icon imports: iconMapPin, iconPhone, iconClock, iconSend, iconStar
|
|
8
|
-
* - Async action with onStart / run / onSuccess / onError lifecycle
|
|
9
|
-
* - State-driven form (idle → sending → sent) with disabled submit
|
|
10
|
-
* - input() fields inside a card() for the booking enquiry
|
|
11
|
-
* - System web fonts (Rockwell + Courier New) — zero extra HTTP requests
|
|
12
|
-
* - Full-bleed hero with background image + gradient overlay
|
|
13
|
-
* - Stat strip with inverted navy background
|
|
14
|
-
* - Reviews rendered with card() (quote + name/role + star rating)
|
|
15
|
-
* - Opening hours list with Courier monospace styling
|
|
16
|
-
*
|
|
17
|
-
* Run: node examples/dev.server.js → http://localhost:3001/retrowheels
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import {
|
|
21
|
-
nav, footer, button, input, stat, cta, card, badge,
|
|
22
|
-
} from '../../../../src/ui/index.js'
|
|
23
|
-
import { iconMapPin, iconPhone, iconClock, iconSend, iconStar } from '../../../../src/ui/icons.js'
|
|
24
|
-
import { asset } from '../../lib/layout.js'
|
|
25
|
-
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
// Static data
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
|
|
30
|
-
const SERVICES = [
|
|
31
|
-
{
|
|
32
|
-
num: 'No. 01',
|
|
33
|
-
title: 'Restored Vintage Bicycles',
|
|
34
|
-
body: 'Lovingly rebuilt Raleighs, Peugeots, and Bianchis from the workshop. Each one inspected, polished, and ready for the road.',
|
|
35
|
-
price: 'From £325',
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
num: 'No. 02',
|
|
39
|
-
title: 'Hand-Built City Cruisers',
|
|
40
|
-
body: 'Lugged steel frames, leather saddles, and brass bells. Built to your measurements over four to six weeks.',
|
|
41
|
-
price: 'From £1,100',
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
num: 'No. 03',
|
|
45
|
-
title: 'Full-Service Workshop',
|
|
46
|
-
body: 'Brakes, gears, wheels, headsets — whatever the trouble. We work on every era from penny-farthings to ten-speeds.',
|
|
47
|
-
price: 'From £45',
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
num: 'No. 04',
|
|
51
|
-
title: 'Steel-Frame Tune-Ups',
|
|
52
|
-
body: 'Bottom bracket overhaul, hub repack, true the wheels, polish the chrome. Your old steed will ride like 1976 again.',
|
|
53
|
-
price: '£95',
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
num: 'No. 05',
|
|
57
|
-
title: 'Saddles & Leather Goods',
|
|
58
|
-
body: 'Brooks saddles, twine-wrapped bar tape, canvas tool rolls, and waxed cotton panniers. Stocked and ready to fit.',
|
|
59
|
-
price: 'From £18',
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
num: 'No. 06',
|
|
63
|
-
title: 'Weekend Group Rides',
|
|
64
|
-
body: 'Every Saturday at 9 a.m. — twenty miles, a cafe stop, and home by lunch. All paces, all bikes, all welcome.',
|
|
65
|
-
price: 'Free · BYO bicycle',
|
|
66
|
-
},
|
|
67
|
-
]
|
|
68
|
-
|
|
69
|
-
const HOURS = [
|
|
70
|
-
{ day: 'Tuesday', time: '9 a.m. — 6 p.m.' },
|
|
71
|
-
{ day: 'Wednesday', time: '9 a.m. — 6 p.m.' },
|
|
72
|
-
{ day: 'Thursday', time: '9 a.m. — 7 p.m.' },
|
|
73
|
-
{ day: 'Friday', time: '9 a.m. — 6 p.m.' },
|
|
74
|
-
{ day: 'Saturday', time: '9 a.m. — 5 p.m.' },
|
|
75
|
-
{ day: 'Sunday', time: '10 a.m. — 4 p.m.' },
|
|
76
|
-
{ day: 'Monday', time: 'Closed', closed: true },
|
|
77
|
-
]
|
|
78
|
-
|
|
79
|
-
const REVIEWS = [
|
|
80
|
-
{
|
|
81
|
-
quote: 'They rebuilt my grandfather\u2019s 1972 Raleigh Twenty as if it had just left the factory. I cried a little. Worth every penny.',
|
|
82
|
-
name: 'Eleanor Bishop',
|
|
83
|
-
role: 'Brought back from the loft',
|
|
84
|
-
rating: 5,
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
quote: 'The custom cruiser I commissioned is the loveliest object I own. Hand-pinstriped name on the down tube. A proper bicycle.',
|
|
88
|
-
name: 'Marcus O\u2019Halloran',
|
|
89
|
-
role: 'Custom build, Spring 2026',
|
|
90
|
-
rating: 5,
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
quote: 'Saturday ride is the best thing in my week. Twenty miles, a cafe stop, and not a single person in lycra. Bliss.',
|
|
94
|
-
name: 'Priya Ramachandran',
|
|
95
|
-
role: 'Weekend rider since 2024',
|
|
96
|
-
rating: 5,
|
|
97
|
-
},
|
|
98
|
-
]
|
|
99
|
-
|
|
100
|
-
// Logo uses system web fonts — no external request needed
|
|
101
|
-
const LOGO = `<span class="rw-logo-text">Retro Wheels Co.</span>`
|
|
102
|
-
|
|
103
|
-
// ---------------------------------------------------------------------------
|
|
104
|
-
// Helpers
|
|
105
|
-
// ---------------------------------------------------------------------------
|
|
106
|
-
|
|
107
|
-
const renderService = (s) => `
|
|
108
|
-
<article class="rw-service">
|
|
109
|
-
<p class="rw-service-num">${s.num}</p>
|
|
110
|
-
<h3>${s.title}</h3>
|
|
111
|
-
<p>${s.body}</p>
|
|
112
|
-
<p class="rw-service-price">${s.price}</p>
|
|
113
|
-
</article>
|
|
114
|
-
`
|
|
115
|
-
|
|
116
|
-
const renderHourRow = (h) => `
|
|
117
|
-
<li class="rw-hours-row">
|
|
118
|
-
<span>${h.day}</span>
|
|
119
|
-
<span class="${h.closed ? 'rw-closed' : ''}">${h.time}</span>
|
|
120
|
-
</li>
|
|
121
|
-
`
|
|
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
|
-
|
|
136
|
-
// ---------------------------------------------------------------------------
|
|
137
|
-
// Spec
|
|
138
|
-
// ---------------------------------------------------------------------------
|
|
139
|
-
|
|
140
|
-
export default {
|
|
141
|
-
route: '/templates/retrowheels',
|
|
142
|
-
|
|
143
|
-
meta: {
|
|
144
|
-
title: 'Retro Wheels Co. — Restored vintage bicycles, est. 1976',
|
|
145
|
-
description: 'A neighbourhood bicycle shop specialising in restored vintage bikes, hand-built city cruisers, and old-school repairs. Established 1976.',
|
|
146
|
-
theme: 'light',
|
|
147
|
-
styles: ['/pulse-ui.css', '/retrowheels.css'],
|
|
148
|
-
},
|
|
149
|
-
|
|
150
|
-
state: {
|
|
151
|
-
status: 'idle', // idle | sending | sent
|
|
152
|
-
},
|
|
153
|
-
|
|
154
|
-
view: (state) => `
|
|
155
|
-
<a href="#main-content" class="rw-skip-link">Skip to main content</a>
|
|
156
|
-
|
|
157
|
-
${nav({
|
|
158
|
-
logo: LOGO,
|
|
159
|
-
logoHref: '/retrowheels',
|
|
160
|
-
sticky: true,
|
|
161
|
-
links: [
|
|
162
|
-
{ label: 'Workshop', href: '#workshop' },
|
|
163
|
-
{ label: 'Services', href: '#services' },
|
|
164
|
-
{ label: 'Reviews', href: '#reviews' },
|
|
165
|
-
{ label: 'Visit', href: '#visit' },
|
|
166
|
-
],
|
|
167
|
-
action: button({ label: 'Book a service', href: '#book', size: 'sm' }),
|
|
168
|
-
})}
|
|
169
|
-
|
|
170
|
-
<main id="main-content">
|
|
171
|
-
<section class="rw-hero" aria-labelledby="hero-title">
|
|
172
|
-
<div class="rw-hero-inner">
|
|
173
|
-
<p class="rw-hero-eyebrow">Established 1976</p>
|
|
174
|
-
<h1 id="hero-title" class="rw-hero-title">
|
|
175
|
-
Steel frames,<br>
|
|
176
|
-
<em>old roads,</em><br>
|
|
177
|
-
new adventures.
|
|
178
|
-
</h1>
|
|
179
|
-
<p class="rw-hero-sub">
|
|
180
|
-
A neighbourhood bicycle shop on Saddler Lane — restoring vintage bikes,
|
|
181
|
-
hand-building city cruisers, and keeping every steel-framed bicycle in town
|
|
182
|
-
rolling sweetly since the Carter administration.
|
|
183
|
-
</p>
|
|
184
|
-
<div class="rw-hero-actions">
|
|
185
|
-
${button({ label: 'Browse the workshop', href: '#services', variant: 'primary', size: 'lg' })}
|
|
186
|
-
${button({ label: 'Saturday ride', href: '#ride', variant: 'secondary', size: 'lg' })}
|
|
187
|
-
</div>
|
|
188
|
-
</div>
|
|
189
|
-
<p class="rw-hero-image-credit">Photo: Unsplash</p>
|
|
190
|
-
</section>
|
|
191
|
-
|
|
192
|
-
<section class="rw-stat-strip" aria-label="Shop facts">
|
|
193
|
-
<div class="rw-stat-strip-inner">
|
|
194
|
-
${stat({ label: 'Established', value: '1976', center: true })}
|
|
195
|
-
${stat({ label: 'Bikes restored', value: '2,400+', center: true })}
|
|
196
|
-
${stat({ label: 'Saturday riders', value: '60/wk', center: true })}
|
|
197
|
-
${stat({ label: 'Carbon fibre', value: 'None', center: true })}
|
|
198
|
-
</div>
|
|
199
|
-
</section>
|
|
200
|
-
|
|
201
|
-
<section id="workshop" class="rw-editorial">
|
|
202
|
-
<p class="rw-eyebrow">The Workshop</p>
|
|
203
|
-
<h2 class="rw-section-title u-mt-3">A bicycle is a promise that you’ll go outside.</h2>
|
|
204
|
-
<hr class="rw-rule">
|
|
205
|
-
<p>
|
|
206
|
-
We opened in the autumn of 1976 in a former ironmonger’s on Saddler Lane.
|
|
207
|
-
Fifty years on, the floorboards still creak, the tea is still strong, and the
|
|
208
|
-
workbench is still covered in the same small jars of cotter pins and brass
|
|
209
|
-
ferrules. What we build today is the same thing we built then: bicycles you
|
|
210
|
-
could leave to your grandchildren.
|
|
211
|
-
</p>
|
|
212
|
-
<p>
|
|
213
|
-
We don’t do carbon. We don’t do electronic shifting. We do lugged steel,
|
|
214
|
-
hand-built wheels, three-speed hubs, and a fair few French derailleurs that
|
|
215
|
-
require patience and the correct sort of grease. If your bicycle has a name,
|
|
216
|
-
we’d like to meet it.
|
|
217
|
-
</p>
|
|
218
|
-
${badge({ label: 'Repairs by appointment · Workshop open Tues — Sun', variant: 'default' })}
|
|
219
|
-
</section>
|
|
220
|
-
|
|
221
|
-
<section id="services" class="rw-section rw-section--alt">
|
|
222
|
-
<div class="rw-section-inner">
|
|
223
|
-
<p class="rw-eyebrow">Services & Goods</p>
|
|
224
|
-
<h2 class="rw-section-title">Six things we’re rather good at.</h2>
|
|
225
|
-
<p class="rw-section-deck">
|
|
226
|
-
Prices are honest, the work is guaranteed for a year, and every bicycle that
|
|
227
|
-
leaves the workshop has been ridden round the block by someone who actually
|
|
228
|
-
cares.
|
|
229
|
-
</p>
|
|
230
|
-
<div class="rw-service-grid">
|
|
231
|
-
${SERVICES.map(renderService).join('')}
|
|
232
|
-
</div>
|
|
233
|
-
</div>
|
|
234
|
-
</section>
|
|
235
|
-
|
|
236
|
-
<section id="reviews" class="rw-section">
|
|
237
|
-
<div class="rw-section-inner">
|
|
238
|
-
<p class="rw-eyebrow">From the Logbook</p>
|
|
239
|
-
<h2 class="rw-section-title">What folks have been saying.</h2>
|
|
240
|
-
<p class="rw-section-deck">Reviews collected from customers, riders, and the occasional postcard pinned to the workshop wall.</p>
|
|
241
|
-
<div class="rw-service-grid">
|
|
242
|
-
${REVIEWS.map(renderReview).join('')}
|
|
243
|
-
</div>
|
|
244
|
-
</div>
|
|
245
|
-
</section>
|
|
246
|
-
|
|
247
|
-
<section id="visit" class="rw-section rw-section--alt">
|
|
248
|
-
<div class="rw-section-inner">
|
|
249
|
-
<p class="rw-eyebrow">Visit the Shop</p>
|
|
250
|
-
<h2 class="rw-section-title">Find us, ring us, or write.</h2>
|
|
251
|
-
<hr class="rw-rule">
|
|
252
|
-
<div class="rw-visit-grid">
|
|
253
|
-
<div>
|
|
254
|
-
<h3 class="rw-hours-heading">
|
|
255
|
-
${iconClock({ size: 18 })} Opening hours
|
|
256
|
-
</h3>
|
|
257
|
-
<ul class="rw-hours" aria-label="Opening hours">
|
|
258
|
-
${HOURS.map(renderHourRow).join('')}
|
|
259
|
-
</ul>
|
|
260
|
-
<div class="rw-address">
|
|
261
|
-
<p><strong>${iconMapPin({ size: 16 })} 14 Saddler Lane</strong><br>
|
|
262
|
-
Trumpington, Cambridge<br>
|
|
263
|
-
CB2 9PQ</p>
|
|
264
|
-
<p class="u-mt-3">${iconPhone({ size: 16 })} 01223 555 0176<br>
|
|
265
|
-
hello@retrowheels.co</p>
|
|
266
|
-
</div>
|
|
267
|
-
</div>
|
|
268
|
-
|
|
269
|
-
<div id="book">
|
|
270
|
-
${card({
|
|
271
|
-
title: 'Book a service',
|
|
272
|
-
level: 3,
|
|
273
|
-
content: state.status === 'sent'
|
|
274
|
-
? `<p>Thanks — we’ll be in touch within a working day. Tea’s on when you arrive.</p>`
|
|
275
|
-
: `<form data-action="bookService" novalidate>
|
|
276
|
-
<p class="u-mb-4 u-text-muted">Drop us a line about your bicycle. We’ll reply with a slot and an estimate.</p>
|
|
277
|
-
${input({ name: 'name', label: 'Your name', required: true })}
|
|
278
|
-
${input({ name: 'email', label: 'Email', type: 'email', required: true, class: 'u-mt-3' })}
|
|
279
|
-
${input({ name: 'bike', label: 'Bicycle (make, year)', placeholder: 'e.g. 1972 Raleigh Twenty', class: 'u-mt-3' })}
|
|
280
|
-
<div class="u-mt-4">
|
|
281
|
-
${button({
|
|
282
|
-
label: state.status === 'sending' ? 'Sending\u2026' : 'Send enquiry',
|
|
283
|
-
type: 'submit',
|
|
284
|
-
icon: iconSend({ size: 16 }),
|
|
285
|
-
attrs: state.status === 'sending' ? { 'aria-busy': 'true', disabled: '' } : {},
|
|
286
|
-
})}
|
|
287
|
-
</div>
|
|
288
|
-
</form>`,
|
|
289
|
-
})}
|
|
290
|
-
</div>
|
|
291
|
-
</div>
|
|
292
|
-
</div>
|
|
293
|
-
</section>
|
|
294
|
-
|
|
295
|
-
<section id="ride" class="rw-ride-banner">
|
|
296
|
-
<p class="rw-eyebrow rw-eyebrow--on-orange">Every Saturday, 9 a.m. sharp</p>
|
|
297
|
-
<h2>${iconStar({ size: 28 })} Join the Saturday ride.</h2>
|
|
298
|
-
<p>Twenty miles, one cafe stop, no lycra required. Meet at the shop. Helmets sensible, bicycle of any vintage welcome.</p>
|
|
299
|
-
${button({ label: 'Add me to the list', href: '#book', variant: 'secondary', size: 'lg' })}
|
|
300
|
-
</section>
|
|
301
|
-
|
|
302
|
-
<section class="rw-section">
|
|
303
|
-
<div class="rw-section-inner">
|
|
304
|
-
${cta({
|
|
305
|
-
eyebrow: 'Workshop & Showroom',
|
|
306
|
-
title: 'Bring us a bicycle. Leave with a story.',
|
|
307
|
-
subtitle: 'Walk-ins welcome during opening hours. The kettle is always on.',
|
|
308
|
-
actions: button({ label: 'Find the shop', href: '#visit', variant: 'primary', size: 'lg' }),
|
|
309
|
-
})}
|
|
310
|
-
</div>
|
|
311
|
-
</section>
|
|
312
|
-
</main>
|
|
313
|
-
|
|
314
|
-
${footer({
|
|
315
|
-
logo: LOGO,
|
|
316
|
-
links: [
|
|
317
|
-
{ label: 'Workshop', href: '#workshop' },
|
|
318
|
-
{ label: 'Services', href: '#services' },
|
|
319
|
-
{ label: 'Visit', href: '#visit' },
|
|
320
|
-
{ label: 'Saturday ride', href: '#ride' },
|
|
321
|
-
],
|
|
322
|
-
legal: '\u00a9 1976 \u2014 2026 Retro Wheels Co. \u00b7 14 Saddler Lane, Cambridge \u00b7 A bicycle is forever.',
|
|
323
|
-
})}
|
|
324
|
-
<script src="${asset('/pulse-ui.js')}" defer></script>
|
|
325
|
-
`,
|
|
326
|
-
|
|
327
|
-
actions: {
|
|
328
|
-
bookService: {
|
|
329
|
-
onStart: () => ({ status: 'sending' }),
|
|
330
|
-
run: async (_state, _server, formData) => {
|
|
331
|
-
const name = (formData.get('name') || '').trim()
|
|
332
|
-
const email = (formData.get('email') || '').trim()
|
|
333
|
-
if (!name) throw new Error('Please tell us your name.')
|
|
334
|
-
if (!email) throw new Error('Please leave an email so we can reply.')
|
|
335
|
-
await new Promise(r => setTimeout(r, 350))
|
|
336
|
-
return { name }
|
|
337
|
-
},
|
|
338
|
-
onSuccess: () => ({
|
|
339
|
-
status: 'sent',
|
|
340
|
-
_toast: { message: 'Enquiry sent \u2014 we\u2019ll be in touch shortly.', variant: 'success' },
|
|
341
|
-
}),
|
|
342
|
-
onError: (_state, err) => ({
|
|
343
|
-
status: 'idle',
|
|
344
|
-
_toast: { message: err.message || 'Something went wrong. Try again?', variant: 'error' },
|
|
345
|
-
}),
|
|
346
|
-
},
|
|
347
|
-
},
|
|
348
|
-
|
|
349
|
-
onViewError: () => `
|
|
350
|
-
<main id="main-content" class="rw-editorial">
|
|
351
|
-
<h1>Spanner in the works</h1>
|
|
352
|
-
<p>Something went sideways. <a href="/retrowheels">Reload the page</a> or give us a ring on 01223 555 0176.</p>
|
|
353
|
-
</main>
|
|
354
|
-
`,
|
|
355
|
-
}
|
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Template: Shop — Product Page
|
|
3
|
-
*
|
|
4
|
-
* A clean, interactive e-commerce product page.
|
|
5
|
-
* Light theme, blue accent. Full add-to-cart flow with a basket modal,
|
|
6
|
-
* quantity controls, spec grid, and key stats.
|
|
7
|
-
*
|
|
8
|
-
* Vibe: minimal Theme: light Palette: blue / white
|
|
9
|
-
*
|
|
10
|
-
* Demonstrates:
|
|
11
|
-
* - nav() with a custom cart icon trigger and dialog-open
|
|
12
|
-
* - modal() basket with a live cart table
|
|
13
|
-
* - Quantity stepper with min/max constraints
|
|
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())
|
|
17
|
-
* - _toast on mutation success
|
|
18
|
-
* - Light theme with custom accent override
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import {
|
|
22
|
-
nav, heading, badge, stat, grid,
|
|
23
|
-
button, modal, empty,
|
|
24
|
-
iconPlus, iconMinus, iconShoppingCart, iconCheck, iconPackage,
|
|
25
|
-
} from '../../../../src/ui/index.js'
|
|
26
|
-
import { asset } from '../../lib/layout.js'
|
|
27
|
-
|
|
28
|
-
const SPECS = [
|
|
29
|
-
{ label: 'Display', value: '15.6" Retina, 2560×1664' },
|
|
30
|
-
{ label: 'Processor', value: 'M3 Pro, 12-core CPU' },
|
|
31
|
-
{ label: 'Memory', value: '32GB Unified Memory' },
|
|
32
|
-
{ label: 'Storage', value: '1TB SSD' },
|
|
33
|
-
{ label: 'Battery', value: 'Up to 18 hours' },
|
|
34
|
-
{ label: 'Chassis', value: 'Space Grey Aluminium' },
|
|
35
|
-
{ label: 'Ports', value: '3× USB-C, HDMI, SD card' },
|
|
36
|
-
{ label: 'Weight', value: '1.86 kg' },
|
|
37
|
-
]
|
|
38
|
-
|
|
39
|
-
const UNIT_PRICE = 1899
|
|
40
|
-
|
|
41
|
-
export const formatPrice = (p) => `£${p.toLocaleString('en-GB')}`
|
|
42
|
-
export const cartTotal = (items) => items.reduce((sum, i) => sum + i.qty * i.unitPrice, 0)
|
|
43
|
-
export const cartCount = (items) => items.reduce((sum, i) => sum + i.qty, 0)
|
|
44
|
-
|
|
45
|
-
const cartRows = (items) => items.map(({ name, qty, unitPrice }) => [
|
|
46
|
-
name,
|
|
47
|
-
String(qty),
|
|
48
|
-
formatPrice(unitPrice),
|
|
49
|
-
formatPrice(qty * unitPrice),
|
|
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
|
-
|
|
61
|
-
export default {
|
|
62
|
-
route: '/templates/shop',
|
|
63
|
-
meta: {
|
|
64
|
-
title: 'Shop — Product Page Template · Pulse',
|
|
65
|
-
description: 'Interactive e-commerce product page with cart modal, quantity controls, and spec grid. Built with Pulse.',
|
|
66
|
-
styles: [
|
|
67
|
-
asset('/pulse-ui.css'),
|
|
68
|
-
asset('/shop.css'),
|
|
69
|
-
],
|
|
70
|
-
theme: 'light',
|
|
71
|
-
},
|
|
72
|
-
state: {
|
|
73
|
-
qty: 1,
|
|
74
|
-
cartItems: [],
|
|
75
|
-
added: false,
|
|
76
|
-
},
|
|
77
|
-
mutations: {
|
|
78
|
-
increment: (state) => ({
|
|
79
|
-
qty: Math.min(state.qty + 1, 10),
|
|
80
|
-
added: false,
|
|
81
|
-
}),
|
|
82
|
-
decrement: (state) => ({
|
|
83
|
-
qty: Math.max(state.qty - 1, 1),
|
|
84
|
-
added: false,
|
|
85
|
-
}),
|
|
86
|
-
addToCart: (state) => {
|
|
87
|
-
const existing = state.cartItems.find(i => i.name === 'DevBook Pro 15')
|
|
88
|
-
const cartItems = existing
|
|
89
|
-
? state.cartItems.map(i =>
|
|
90
|
-
i.name === 'DevBook Pro 15'
|
|
91
|
-
? { ...i, qty: i.qty + state.qty }
|
|
92
|
-
: i
|
|
93
|
-
)
|
|
94
|
-
: [...state.cartItems, { name: 'DevBook Pro 15', qty: state.qty, unitPrice: UNIT_PRICE }]
|
|
95
|
-
return {
|
|
96
|
-
cartItems,
|
|
97
|
-
added: true,
|
|
98
|
-
_toast: { message: `${state.qty} × DevBook Pro 15 added to cart`, variant: 'success' },
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
},
|
|
102
|
-
view: (state) => {
|
|
103
|
-
const count = cartCount(state.cartItems)
|
|
104
|
-
const total = cartTotal(state.cartItems)
|
|
105
|
-
|
|
106
|
-
return `
|
|
107
|
-
${nav({
|
|
108
|
-
logo: `${iconPackage({ size: 18 })} Tech Stuff Inc.`,
|
|
109
|
-
logoHref: '/',
|
|
110
|
-
sticky: true,
|
|
111
|
-
links: [
|
|
112
|
-
{ label: 'Laptops', href: '/templates/shop' },
|
|
113
|
-
{ label: 'Accessories', href: '#' },
|
|
114
|
-
{ label: 'Support', href: '#' },
|
|
115
|
-
],
|
|
116
|
-
action: `
|
|
117
|
-
<button
|
|
118
|
-
type="button"
|
|
119
|
-
class="cart-trigger"
|
|
120
|
-
data-dialog-open="cart"
|
|
121
|
-
aria-label="${count > 0 ? `Open basket, ${count} item${count !== 1 ? 's' : ''}` : 'Open basket'}"
|
|
122
|
-
>
|
|
123
|
-
${iconShoppingCart({ size: 18 })}
|
|
124
|
-
${count > 0 ? `<span class="cart-bubble" aria-hidden="true">${count}</span>` : ''}
|
|
125
|
-
</button>
|
|
126
|
-
`,
|
|
127
|
-
})}
|
|
128
|
-
|
|
129
|
-
<script src="${asset('/pulse-ui.js')}" defer></script>
|
|
130
|
-
|
|
131
|
-
<main id="main-content">
|
|
132
|
-
|
|
133
|
-
<div class="page-body product-display">
|
|
134
|
-
${heading({ text: 'DevBook Pro 15', level: 1, class: 'product-display-title' })}
|
|
135
|
-
<p class="product-tagline">Premium developer laptop for people who build things.</p>
|
|
136
|
-
<div class="product-meta-row">
|
|
137
|
-
${badge({ label: 'In Stock', variant: 'success' })}
|
|
138
|
-
${badge({ label: 'Ships in 24 hours', variant: 'info' })}
|
|
139
|
-
</div>
|
|
140
|
-
</div>
|
|
141
|
-
|
|
142
|
-
${modal({
|
|
143
|
-
id: 'cart',
|
|
144
|
-
title: 'Your basket',
|
|
145
|
-
level: 2,
|
|
146
|
-
size: 'lg',
|
|
147
|
-
content: state.cartItems.length === 0
|
|
148
|
-
? empty({ title: 'Your basket is empty', description: 'Add a product to get started.' })
|
|
149
|
-
: `
|
|
150
|
-
<table class="ui-table cart-table">
|
|
151
|
-
<thead>
|
|
152
|
-
<tr>
|
|
153
|
-
<th scope="col">Product</th>
|
|
154
|
-
<th scope="col" class="cart-col-num">Qty</th>
|
|
155
|
-
<th scope="col" class="cart-col-num">Unit price</th>
|
|
156
|
-
<th scope="col" class="cart-col-num">Subtotal</th>
|
|
157
|
-
</tr>
|
|
158
|
-
</thead>
|
|
159
|
-
<tbody>
|
|
160
|
-
${cartRows(state.cartItems).map(([name, qty, unit, sub]) => `
|
|
161
|
-
<tr>
|
|
162
|
-
<td>${name}</td>
|
|
163
|
-
<td class="cart-col-num">${qty}</td>
|
|
164
|
-
<td class="cart-col-num">${unit}</td>
|
|
165
|
-
<td class="cart-col-num">${sub}</td>
|
|
166
|
-
</tr>
|
|
167
|
-
`).join('')}
|
|
168
|
-
</tbody>
|
|
169
|
-
<tfoot>
|
|
170
|
-
<tr class="cart-total-row">
|
|
171
|
-
<td colspan="3"><strong>Total</strong></td>
|
|
172
|
-
<td class="cart-col-num"><strong>${formatPrice(total)}</strong></td>
|
|
173
|
-
</tr>
|
|
174
|
-
</tfoot>
|
|
175
|
-
</table>
|
|
176
|
-
`,
|
|
177
|
-
footer: `
|
|
178
|
-
${button({ label: 'Continue shopping', variant: 'secondary', attrs: { 'data-dialog-close': '' } })}
|
|
179
|
-
${button({ label: 'Checkout', variant: 'primary', disabled: state.cartItems.length === 0 })}
|
|
180
|
-
`,
|
|
181
|
-
})}
|
|
182
|
-
|
|
183
|
-
<img
|
|
184
|
-
class="product-photo"
|
|
185
|
-
src="/images/shop/devbook-pro.jpg"
|
|
186
|
-
alt="DevBook Pro 15 open on a clean white desk showing a code editor, with headphones, a coffee mug, and a plant nearby"
|
|
187
|
-
width="1366"
|
|
188
|
-
height="912"
|
|
189
|
-
/>
|
|
190
|
-
|
|
191
|
-
<div class="page-body product-purchase">
|
|
192
|
-
<p class="product-price">£1,899</p>
|
|
193
|
-
|
|
194
|
-
<div class="qty-control">
|
|
195
|
-
${button({
|
|
196
|
-
label: 'Decrease quantity',
|
|
197
|
-
variant: 'secondary',
|
|
198
|
-
size: 'sm',
|
|
199
|
-
disabled: state.qty <= 1,
|
|
200
|
-
icon: iconMinus({ size: 14 }),
|
|
201
|
-
class: 'qty-btn',
|
|
202
|
-
attrs: { 'data-event': 'decrement', 'aria-label': 'Decrease quantity' },
|
|
203
|
-
})}
|
|
204
|
-
<span class="qty-value" aria-live="polite">${state.qty}</span>
|
|
205
|
-
${button({
|
|
206
|
-
label: 'Increase quantity',
|
|
207
|
-
variant: 'secondary',
|
|
208
|
-
size: 'sm',
|
|
209
|
-
disabled: state.qty >= 10,
|
|
210
|
-
icon: iconPlus({ size: 14 }),
|
|
211
|
-
class: 'qty-btn',
|
|
212
|
-
attrs: { 'data-event': 'increment', 'aria-label': 'Increase quantity' },
|
|
213
|
-
})}
|
|
214
|
-
</div>
|
|
215
|
-
|
|
216
|
-
<div class="cart-actions">
|
|
217
|
-
${button({
|
|
218
|
-
label: state.added ? 'Added to cart' : 'Add to cart',
|
|
219
|
-
variant: 'primary',
|
|
220
|
-
size: 'lg',
|
|
221
|
-
icon: state.added ? iconCheck({ size: 16 }) : iconShoppingCart({ size: 16 }),
|
|
222
|
-
attrs: { 'data-event': 'addToCart' },
|
|
223
|
-
})}
|
|
224
|
-
</div>
|
|
225
|
-
${state.added ? `<p class="added-confirm">${state.cartItems.find(i => i.name === 'DevBook Pro 15')?.qty ?? 0} item${(state.cartItems.find(i => i.name === 'DevBook Pro 15')?.qty ?? 0) !== 1 ? 's' : ''} in your basket.</p>` : ''}
|
|
226
|
-
</div>
|
|
227
|
-
|
|
228
|
-
<div class="page-body product-specs">
|
|
229
|
-
${sectionLabel({ eyebrow: 'Under the hood', heading: 'Full specifications' })}
|
|
230
|
-
<div class="spec-grid">
|
|
231
|
-
${SPECS.map(({ label, value }) => `
|
|
232
|
-
<div class="spec-item">
|
|
233
|
-
<span class="spec-label">${label}</span>
|
|
234
|
-
<span class="spec-value">${value}</span>
|
|
235
|
-
</div>
|
|
236
|
-
`).join('')}
|
|
237
|
-
</div>
|
|
238
|
-
</div>
|
|
239
|
-
|
|
240
|
-
<div class="page-body product-stats">
|
|
241
|
-
${sectionLabel({ eyebrow: 'At a glance', heading: 'Key numbers' })}
|
|
242
|
-
<div class="product-stats-grid">
|
|
243
|
-
${grid({ cols: 3, content:
|
|
244
|
-
stat({ label: 'Battery life', value: '18 hrs', trend: 'up' }) +
|
|
245
|
-
stat({ label: 'Unified memory', value: '32 GB' }) +
|
|
246
|
-
stat({ label: 'Delivery', value: '24 hrs', trend: 'up' })
|
|
247
|
-
})}
|
|
248
|
-
</div>
|
|
249
|
-
</div>
|
|
250
|
-
|
|
251
|
-
</main>
|
|
252
|
-
|
|
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>
|
|
258
|
-
`
|
|
259
|
-
},
|
|
260
|
-
|
|
261
|
-
onViewError: () => `
|
|
262
|
-
<main id="main-content">
|
|
263
|
-
<div class="view-error">
|
|
264
|
-
<p>Something went wrong loading this page. <a href="/templates/shop">Reload</a></p>
|
|
265
|
-
</div>
|
|
266
|
-
</main>
|
|
267
|
-
`,
|
|
268
|
-
}
|