@invisibleloop/pulse 0.12.3 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/verify.md +9 -5
- package/.claude/pulse-checklist.md +21 -1
- package/.github/workflows/publish.yml +1 -0
- package/.pulse-verified +1 -0
- package/CLAUDE.md +56 -15
- package/docs/.claude/commands/verify.md +4 -0
- package/docs/.claude/pulse-checklist.md +20 -2
- package/docs/public/.pulse-ui-version +1 -1
- package/docs/public/andypixel.css +237 -0
- package/docs/public/bookworms.css +422 -0
- package/docs/public/coutumes.css +584 -0
- 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/journey.css +528 -0
- package/docs/public/pulse-ui.css +1036 -22
- package/docs/public/retrowheels.css +402 -0
- package/docs/public/shop.css +235 -0
- package/docs/public/sparkys.css +349 -0
- package/docs/public/suzukibento.css +517 -0
- package/docs/public/swish.css +394 -0
- package/docs/server.js +19 -4
- package/docs/src/lib/nav.js +14 -3
- package/docs/src/pages/components/decorate.js +74 -0
- package/docs/src/pages/components/gallery.js +102 -0
- package/docs/src/pages/components/hero.js +1 -0
- package/docs/src/pages/components/marquee.js +76 -0
- package/docs/src/pages/components/photo-card.js +74 -0
- package/docs/src/pages/components/section.js +40 -1
- package/docs/src/pages/meta.js +26 -0
- package/docs/src/pages/templates/andypixel.js +260 -0
- package/docs/src/pages/templates/bookworms.js +227 -0
- package/docs/src/pages/templates/coutumes.js +226 -0
- package/docs/src/pages/templates/journey.js +186 -0
- package/docs/src/pages/templates/retrowheels.js +342 -0
- package/docs/src/pages/templates/shop.js +257 -0
- package/docs/src/pages/templates/sparkys.js +249 -0
- package/docs/src/pages/templates/suzukibento.js +200 -0
- package/docs/src/pages/templates/swish.js +313 -0
- package/docs/src/pages/templates.js +313 -2
- package/docs/src/pages/testing.js +2 -1
- package/examples/andypixel.css +237 -0
- package/examples/andypixel.js +258 -0
- package/examples/bookworms.css +422 -0
- package/examples/bookworms.js +225 -0
- package/examples/contact.js +1 -0
- package/examples/counter.js +1 -0
- package/examples/dev.server.js +17 -0
- package/examples/journey.css +528 -0
- package/examples/journey.js +186 -0
- package/examples/pricing.js +1 -0
- package/examples/quiz.js +1 -0
- package/examples/retrowheels.css +402 -0
- package/examples/retrowheels.js +340 -0
- package/examples/shared.js +6 -0
- package/examples/suzukibento.css +513 -0
- package/examples/suzukibento.js +200 -0
- package/examples/swish.css +394 -0
- package/examples/swish.js +311 -0
- package/examples/todos.js +1 -0
- package/package.json +2 -3
- package/public/.pulse-ui-version +1 -1
- package/public/images/bookworms/hero-books.jpg +0 -0
- package/public/images/bookworms/how-we-read-now.jpg +0 -0
- package/public/images/bookworms/lantern-keepers.jpg +0 -0
- package/public/images/bookworms/provincial-bookseller.jpg +0 -0
- package/public/images/journey/178.jpg +0 -0
- package/public/images/journey/29.jpg +0 -0
- package/public/images/retrowheels/hero.jpg +0 -0
- package/public/images/shop/devbook-pro.jpg +0 -0
- package/public/images/suzukibento/1015.jpg +0 -0
- package/public/images/suzukibento/1025.jpg +0 -0
- package/public/images/suzukibento/1043.jpg +0 -0
- package/public/images/suzukibento/1059.jpg +0 -0
- package/public/images/suzukibento/1062.jpg +0 -0
- package/public/images/suzukibento/1084.jpg +0 -0
- package/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
- package/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
- package/public/images/swish/1485518882345-15568b007407.jpg +0 -0
- package/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
- package/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
- package/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
- package/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
- package/public/images/swish/1551803091-e20673f15770.jpg +0 -0
- package/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
- package/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
- package/public/pulse-ui.css +1036 -22
- package/scripts/build.js +72 -18
- package/src/agent/checklist.md +20 -2
- package/src/agent/commands/verify.md +4 -0
- package/src/agent/guide-components.md +145 -7
- package/src/agent/guide-design-gallery.md +288 -0
- package/src/agent/guide-design-references.md +172 -0
- package/src/agent/guide-explore.md +313 -0
- package/src/agent/guide-styles.md +197 -11
- package/src/agent/guide-templates.md +220 -10
- package/src/agent/guide.md +9 -4
- package/src/agent/workflow.md +208 -58
- package/src/cli/dev.js +39 -7
- package/src/cli/fmt.js +113 -0
- package/src/cli/index.js +63 -30
- package/src/cli/scaffold.js +2 -2
- package/src/html.js +6 -1
- package/src/mcp/server.js +2415 -101
- package/src/mcp/validate-worker.js +34 -5
- package/src/runtime/ssr.js +4 -1
- package/src/server/index.js +22 -2
- package/src/spec/schema.js +5 -0
- package/src/testing/html.js +130 -4
- package/src/testing/testing.test.js +58 -0
- package/src/ui/banner.js +24 -9
- package/src/ui/brand-statement.js +58 -0
- package/src/ui/button.js +12 -1
- package/src/ui/card.js +20 -1
- package/src/ui/colophon.js +29 -0
- package/src/ui/contact-row.js +41 -0
- package/src/ui/cta.js +18 -4
- package/src/ui/decorate.js +55 -0
- package/src/ui/display-heading.js +44 -0
- package/src/ui/drop-cap.js +29 -0
- package/src/ui/editorial-hero.js +63 -0
- package/src/ui/entry-list.js +36 -0
- package/src/ui/feature.js +20 -3
- package/src/ui/footer.js +78 -9
- package/src/ui/gallery.js +56 -0
- package/src/ui/hero.js +68 -18
- package/src/ui/image-pair.js +72 -0
- package/src/ui/index.js +20 -0
- package/src/ui/input.js +1 -0
- package/src/ui/marquee.js +48 -0
- package/src/ui/nav.js +19 -9
- package/src/ui/photo-card.js +44 -0
- package/src/ui/product-card.js +65 -0
- package/src/ui/pullquote.js +14 -3
- package/src/ui/search.js +1 -0
- package/src/ui/section-label.js +38 -0
- package/src/ui/section.js +1 -1
- package/src/ui/service-card.js +47 -0
- package/src/ui/stat-grid.js +40 -0
- package/src/ui/stat.js +16 -2
- package/src/ui/testimonial.js +17 -6
- package/src/ui/textarea.js +1 -0
- package/src/ui/top-strip.js +47 -0
- package/src/ui/trust-list.js +46 -0
- package/src/ui/ui.test.js +35 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse — BookWorms example
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates:
|
|
5
|
+
* - Light-theme editorial branding for a physical/independent business
|
|
6
|
+
* - nav() with logo text, anchor links, and a CTA action button
|
|
7
|
+
* - Pure HTML sections — no Pulse section/container wrappers (shows the
|
|
8
|
+
* framework is just strings; you can write vanilla HTML where it fits)
|
|
9
|
+
* - Book card grid (staff picks) with cover images from picsum.photos
|
|
10
|
+
* - Pull-quote / press quote breakout
|
|
11
|
+
* - Two-column feature grid
|
|
12
|
+
* - Bordered event list with date / title / host columns
|
|
13
|
+
* - Centred CTA section
|
|
14
|
+
* - footer() with flat link list
|
|
15
|
+
* - Google Fonts (Fraunces) for display headings
|
|
16
|
+
*
|
|
17
|
+
* Run: node examples/dev.server.js → http://localhost:3001/bookworms
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { nav, button, footer } from '../../../../src/ui/index.js'
|
|
21
|
+
import { asset } from '../../lib/layout.js'
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Static data
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
const picks = [
|
|
28
|
+
{
|
|
29
|
+
tag: 'Fiction · Staff pick',
|
|
30
|
+
title: 'The Lantern Keepers',
|
|
31
|
+
author: 'Iris Aldenmoor',
|
|
32
|
+
note: 'A quiet novel about three sisters, an unreliable lighthouse, and the long winter that follows a single decision.',
|
|
33
|
+
cover: '/images/bookworms/lantern-keepers.jpg',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
tag: 'Non-fiction · This week',
|
|
37
|
+
title: 'How We Read Now',
|
|
38
|
+
author: 'Dr. Samuel Okafor',
|
|
39
|
+
note: 'A short, generous history of attention — from monastic libraries to the open browser tab.',
|
|
40
|
+
cover: '/images/bookworms/how-we-read-now.jpg',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
tag: 'Rare find · Just arrived',
|
|
44
|
+
title: 'Letters from a Provincial Bookseller',
|
|
45
|
+
author: 'M. Halloran (1927)',
|
|
46
|
+
note: 'First edition, lightly foxed. A wry account of running a shop through a quiet decade.',
|
|
47
|
+
cover: '/images/bookworms/provincial-bookseller.jpg',
|
|
48
|
+
},
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
const features = [
|
|
52
|
+
{
|
|
53
|
+
title: 'Curated, not algorithmic',
|
|
54
|
+
body: 'Every recommendation comes from one of our booksellers. No bestseller lists, no engagement metrics — just books we\'ve read and would lend you ourselves.',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
title: 'Browse by mood',
|
|
58
|
+
body: 'Looking for something restorative? Something difficult? Our shelves are arranged the way readers actually think.',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
title: 'Author evenings & book clubs',
|
|
62
|
+
body: 'Small, considered events with writers we admire. Wine, conversation, and signed copies — no panels of four.',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
title: 'Free delivery over £25',
|
|
66
|
+
body: 'Wrapped by hand, delivered locally by bicycle. Posted further afield by the next morning.',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
title: 'Gift cards & reader subscriptions',
|
|
70
|
+
body: 'A monthly book chosen for one specific reader, with a note from the bookseller who picked it.',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
title: 'Open seven days',
|
|
74
|
+
body: 'Old Market Lane, opposite the bakery. Coffee from nine, books from ten.',
|
|
75
|
+
},
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
const events = [
|
|
79
|
+
{ date: '12 June', title: 'An evening with Iris Aldenmoor', host: 'In conversation with Priya Joshi' },
|
|
80
|
+
{ date: '21 June', title: 'Slow Reading Club — Middlemarch, part one', host: 'Hosted by the BookWorms team' },
|
|
81
|
+
{ date: '03 July', title: 'New voices in non-fiction', host: 'Three debut authors read and answer questions' },
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
const navLinks = [
|
|
85
|
+
{ label: 'Browse', href: '#browse' },
|
|
86
|
+
{ label: 'Staff picks', href: '#picks' },
|
|
87
|
+
{ label: 'Events', href: '#events' },
|
|
88
|
+
{ label: 'Visit', href: '#visit' },
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Spec
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
export default {
|
|
96
|
+
route: '/templates/bookworms',
|
|
97
|
+
|
|
98
|
+
meta: {
|
|
99
|
+
title: 'BookWorms — an independent bookstore for curious readers',
|
|
100
|
+
description: 'Handpicked fiction, non-fiction, and rare finds. Weekly staff picks, author evenings, and a slow reading club. Open seven days on Old Market Lane.',
|
|
101
|
+
theme: 'light',
|
|
102
|
+
styles: [
|
|
103
|
+
'https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,500;1,9..144,400&display=swap',
|
|
104
|
+
'/pulse-ui.css',
|
|
105
|
+
'/bookworms.css',
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
view: () => `
|
|
110
|
+
${nav({
|
|
111
|
+
logo: 'BookWorms',
|
|
112
|
+
links: navLinks,
|
|
113
|
+
action: button({ label: 'Visit the shop', href: '#visit', variant: 'primary', size: 'sm' }),
|
|
114
|
+
sticky: true,
|
|
115
|
+
})}
|
|
116
|
+
|
|
117
|
+
<main id="main-content">
|
|
118
|
+
<section class="bw-hero">
|
|
119
|
+
<div class="bw-shell bw-hero__inner">
|
|
120
|
+
<div class="bw-hero__content">
|
|
121
|
+
<p class="bw-eyebrow">Independent bookstore · Old Market Lane</p>
|
|
122
|
+
<h1 class="bw-hero-title">Books, handpicked for the curious.</h1>
|
|
123
|
+
<p class="bw-hero-lede">
|
|
124
|
+
BookWorms is a small shop with a long memory. Every title on our shelves has been read by
|
|
125
|
+
someone who works here — and is waiting for the right reader to find it.
|
|
126
|
+
</p>
|
|
127
|
+
<div class="bw-cta-actions bw-cta-actions--start">
|
|
128
|
+
${button({ label: 'See this week\u2019s picks', href: '#picks', variant: 'primary' })}
|
|
129
|
+
${button({ label: 'Upcoming events', href: '#events', variant: 'secondary' })}
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
<div class="bw-hero__image" aria-hidden="true">
|
|
133
|
+
<img src="/images/bookworms/hero-books.jpg" alt="" width="820" height="1080" />
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</section>
|
|
137
|
+
|
|
138
|
+
<section class="bw-section" id="picks">
|
|
139
|
+
<div class="bw-shell">
|
|
140
|
+
<p class="bw-section-label">This week</p>
|
|
141
|
+
<h2 class="bw-section-title">Three books we\u2019re pressing into people\u2019s hands.</h2>
|
|
142
|
+
<div class="bw-picks">
|
|
143
|
+
${picks.map((p, i) => `
|
|
144
|
+
<article>
|
|
145
|
+
<div class="bw-pick-cover-wrap">
|
|
146
|
+
<img class="bw-pick-cover" src="${p.cover}" alt="Cover of ${p.title} by ${p.author}" width="400" height="600" loading="lazy" />
|
|
147
|
+
<div class="bw-pick-overlay">
|
|
148
|
+
<h3 class="bw-pick-overlay-title">${p.title}</h3>
|
|
149
|
+
<p class="bw-pick-overlay-author">${i === 2 ? p.author.replace(/\s*\(\d{4}\)/, '') : 'by ' + p.author}</p>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
<p class="bw-pick-meta">${p.tag}</p>
|
|
153
|
+
<h3 class="bw-pick-title">${p.title}</h3>
|
|
154
|
+
<p class="bw-pick-author">by ${p.author}</p>
|
|
155
|
+
<p class="bw-pick-note">${p.note}</p>
|
|
156
|
+
</article>
|
|
157
|
+
`).join('')}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</section>
|
|
161
|
+
|
|
162
|
+
<section class="bw-pull">
|
|
163
|
+
<div class="bw-shell">
|
|
164
|
+
<p class="bw-pull-quote">\u201cThe kind of shop where you walk in for one book and leave with three you didn\u2019t know you needed.\u201d</p>
|
|
165
|
+
<p class="bw-pull-cite">\u2014 The Quarterly Review</p>
|
|
166
|
+
</div>
|
|
167
|
+
</section>
|
|
168
|
+
|
|
169
|
+
<section class="bw-section" id="browse">
|
|
170
|
+
<div class="bw-shell">
|
|
171
|
+
<p class="bw-section-label">What we do</p>
|
|
172
|
+
<h2 class="bw-section-title">A bookstore that still reads.</h2>
|
|
173
|
+
<div class="bw-features">
|
|
174
|
+
${features.map(f => `
|
|
175
|
+
<div>
|
|
176
|
+
<h3 class="bw-feature-title">${f.title}</h3>
|
|
177
|
+
<p class="bw-feature-body">${f.body}</p>
|
|
178
|
+
</div>
|
|
179
|
+
`).join('')}
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</section>
|
|
183
|
+
|
|
184
|
+
<section class="bw-section" id="events">
|
|
185
|
+
<div class="bw-shell">
|
|
186
|
+
<p class="bw-section-label">Upcoming</p>
|
|
187
|
+
<h2 class="bw-section-title">Evenings worth turning up for.</h2>
|
|
188
|
+
<div class="bw-events">
|
|
189
|
+
${events.map(e => `
|
|
190
|
+
<article class="bw-event">
|
|
191
|
+
<p class="bw-event-date">${e.date}</p>
|
|
192
|
+
<h3 class="bw-event-title">${e.title}</h3>
|
|
193
|
+
<p class="bw-event-host">${e.host}</p>
|
|
194
|
+
</article>
|
|
195
|
+
`).join('')}
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
</section>
|
|
199
|
+
|
|
200
|
+
<section class="bw-cta" id="visit">
|
|
201
|
+
<div class="bw-shell">
|
|
202
|
+
<h2 class="bw-cta-title">Come and browse.</h2>
|
|
203
|
+
<p class="bw-cta-sub">
|
|
204
|
+
14 Old Market Lane, opposite the bakery. Open Monday to Sunday, 10am\u20137pm.
|
|
205
|
+
Or sign up to receive one handpicked book each month.
|
|
206
|
+
</p>
|
|
207
|
+
<div class="bw-cta-actions">
|
|
208
|
+
${button({ label: 'Plan a visit', href: '#visit', variant: 'primary' })}
|
|
209
|
+
${button({ label: 'Reader subscription', href: '#visit', variant: 'secondary' })}
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
</section>
|
|
213
|
+
</main>
|
|
214
|
+
|
|
215
|
+
${footer({
|
|
216
|
+
logo: 'BookWorms',
|
|
217
|
+
links: [
|
|
218
|
+
{ label: 'Browse', href: '#browse' },
|
|
219
|
+
{ label: 'Events', href: '#events' },
|
|
220
|
+
{ label: 'Visit', href: '#visit' },
|
|
221
|
+
{ label: 'Contact', href: '#visit' },
|
|
222
|
+
],
|
|
223
|
+
legal: '\u00a9 2026 BookWorms \u00b7 14 Old Market Lane',
|
|
224
|
+
})}
|
|
225
|
+
<script src="${asset('/pulse-ui.js')}" defer></script>
|
|
226
|
+
`,
|
|
227
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
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)
|
|
5
|
+
//
|
|
6
|
+
// Substituted fonts: Bebas Neue (→ Plaak 3), Cormorant Garamond (→ Exposure Regular)
|
|
7
|
+
// All photography: placeholder images (originals are proprietary Coutumes photos)
|
|
8
|
+
|
|
9
|
+
import { nav, banner, editorialHero, pullquote, grid, productCard, imagePair, brandStatement, uiImage, footer, input, button } from '@invisibleloop/pulse/ui'
|
|
10
|
+
import { asset } from '../../lib/layout.js'
|
|
11
|
+
|
|
12
|
+
export const spec = {
|
|
13
|
+
route: '/templates/coutumes',
|
|
14
|
+
meta: {
|
|
15
|
+
title: 'Coutumes — Recreation Test',
|
|
16
|
+
description: 'Pulse recreation of coutumes.com for capability testing.',
|
|
17
|
+
styles: [
|
|
18
|
+
'https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Cormorant+Garamond:ital,wght@0,400;0,500;1,400&family=Barlow:wght@400;500&display=optional',
|
|
19
|
+
'/pulse-ui.css',
|
|
20
|
+
'/coutumes.css',
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
state: { bannerVisible: true },
|
|
25
|
+
|
|
26
|
+
mutations: {
|
|
27
|
+
dismissBanner: (state) => ({ bannerVisible: false }),
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
view: (state) => `
|
|
31
|
+
<main id="main-content">
|
|
32
|
+
|
|
33
|
+
<!-- Announcement bar — fixed to bottom, dismissible -->
|
|
34
|
+
${state.bannerVisible ? banner({
|
|
35
|
+
content: `<span>SILVER LANDS • 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 -->
|
|
45
|
+
${nav({
|
|
46
|
+
logo: 'COUTUMES',
|
|
47
|
+
logoHref: '/',
|
|
48
|
+
links: [
|
|
49
|
+
{ label: 'Jewelry', href: '#' },
|
|
50
|
+
{ label: 'Accessories', href: '#' },
|
|
51
|
+
{ label: 'Souvenirs Club', href: '#' },
|
|
52
|
+
{ label: 'About', href: '#' },
|
|
53
|
+
],
|
|
54
|
+
utilityLinks: [
|
|
55
|
+
{ label: 'Search', href: '#' },
|
|
56
|
+
{ label: 'Magazine', href: '#' },
|
|
57
|
+
{ label: 'FAQ', href: '#' },
|
|
58
|
+
{ label: 'Cart (0)', href: '#' },
|
|
59
|
+
],
|
|
60
|
+
sticky: true,
|
|
61
|
+
background: 'var(--coutumes-white)',
|
|
62
|
+
color: 'var(--coutumes-black)',
|
|
63
|
+
})}
|
|
64
|
+
|
|
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
|
+
})}
|
|
78
|
+
|
|
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. -->
|
|
82
|
+
<section id="collection">
|
|
83
|
+
${pullquote({
|
|
84
|
+
quote: 'Open roads with no precise destination, passing landscapes, a few days spent outside with friends. Pieces designed to travel for a long time.',
|
|
85
|
+
cite: 'Silver Lands Capsule Collection',
|
|
86
|
+
variant: 'editorial',
|
|
87
|
+
size: 'lg',
|
|
88
|
+
})}
|
|
89
|
+
</section>
|
|
90
|
+
|
|
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. -->
|
|
94
|
+
${grid({
|
|
95
|
+
cols: 3,
|
|
96
|
+
gap: 'sm',
|
|
97
|
+
content: [
|
|
98
|
+
{
|
|
99
|
+
img: 'https://images.unsplash.com/photo-1611085583191-a3b181a88401?w=600&q=80',
|
|
100
|
+
alt: 'White beaded bracelet on dark wrist',
|
|
101
|
+
name: 'Silver Tube Bracelet',
|
|
102
|
+
price: '£67.00',
|
|
103
|
+
badge: 'NEW',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
img: 'https://images.unsplash.com/photo-1535632066927-ab7c9ab60908?w=600&q=80',
|
|
107
|
+
alt: 'Close portrait of young man with curly hair wearing gold earring',
|
|
108
|
+
name: 'Kali Hoop Earring',
|
|
109
|
+
price: '£31.00',
|
|
110
|
+
badge: 'NEW',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
img: 'https://picsum.photos/seed/cuff/600/750',
|
|
114
|
+
alt: 'Silver cuff bracelet on wrist',
|
|
115
|
+
name: 'Land Cuff',
|
|
116
|
+
price: '£62.00',
|
|
117
|
+
badge: 'NEW',
|
|
118
|
+
},
|
|
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(''),
|
|
128
|
+
class: 'coutumes-product-grid',
|
|
129
|
+
})}
|
|
130
|
+
|
|
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’s wardrobe.',
|
|
153
|
+
font: 'serif',
|
|
154
|
+
size: 'md',
|
|
155
|
+
})}
|
|
156
|
+
|
|
157
|
+
<!-- Portrait — uiImage() works well here with maxWidth constraint -->
|
|
158
|
+
${uiImage({
|
|
159
|
+
src: 'https://images.unsplash.com/photo-1506794778202-cad84cf45f1d?w=600&q=80',
|
|
160
|
+
alt: 'Young man holding a Kodak camera, wearing a silver ring',
|
|
161
|
+
ratio: '3/4',
|
|
162
|
+
maxWidth: '600px',
|
|
163
|
+
class: 'coutumes-portrait',
|
|
164
|
+
})}
|
|
165
|
+
|
|
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. -->
|
|
169
|
+
${footer({
|
|
170
|
+
background: 'var(--coutumes-amber)',
|
|
171
|
+
color: 'var(--coutumes-black)',
|
|
172
|
+
subscribe: `
|
|
173
|
+
<p class="coutumes-footer-subscribe-label">JOIN THE COUTUMES CLUB</p>
|
|
174
|
+
<form class="coutumes-footer-form" aria-label="Newsletter subscribe">
|
|
175
|
+
<label for="footer-email" class="coutumes-sr-only">Email address</label>
|
|
176
|
+
<input id="footer-email" type="email" name="email" placeholder="Email" autocomplete="email">
|
|
177
|
+
<button type="submit">■ SUBSCRIBE</button>
|
|
178
|
+
</form>
|
|
179
|
+
<p class="coutumes-footer-legal">By clicking Subscribe, I accept the <a href="#">Privacy Policy</a>.</p>
|
|
180
|
+
`,
|
|
181
|
+
columns: [
|
|
182
|
+
{
|
|
183
|
+
title: 'Our Brand',
|
|
184
|
+
links: [
|
|
185
|
+
{ label: 'Craftmanship', href: '#' },
|
|
186
|
+
{ label: 'About Us', href: '#' },
|
|
187
|
+
{ label: 'Our Store', href: '#' },
|
|
188
|
+
{ label: 'Magazine', href: '#' },
|
|
189
|
+
{ label: 'Instagram', href: '#' },
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
title: 'Our Collections',
|
|
194
|
+
links: [
|
|
195
|
+
{ label: 'Best sellers', href: '#' },
|
|
196
|
+
{ label: 'Silver Lands', href: '#' },
|
|
197
|
+
{ label: 'Classics', href: '#' },
|
|
198
|
+
{ label: 'Stones', href: '#' },
|
|
199
|
+
{ label: 'Denim Collection',href: '#' },
|
|
200
|
+
{ label: 'Souvenirs Club', href: '#' },
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
title: 'Help',
|
|
205
|
+
links: [
|
|
206
|
+
{ label: 'Shipping Policy', href: '#' },
|
|
207
|
+
{ label: 'Return', href: '#' },
|
|
208
|
+
{ label: 'FAQ', href: '#' },
|
|
209
|
+
{ label: 'Contact', href: '#' },
|
|
210
|
+
],
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
wordmark: 'COUTUMES',
|
|
214
|
+
links: [
|
|
215
|
+
{ label: 'Privacy Policy', href: '#' },
|
|
216
|
+
{ label: 'Terms of service', href: '#' },
|
|
217
|
+
],
|
|
218
|
+
legal: 'Site by Coutumes',
|
|
219
|
+
})}
|
|
220
|
+
|
|
221
|
+
</main>
|
|
222
|
+
<script src="${asset('/pulse-ui.js')}" defer></script>
|
|
223
|
+
`,
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export default spec
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse — Journey example
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates:
|
|
5
|
+
* - Mode B creative override — raw HTML throughout, no structural components
|
|
6
|
+
* - Split-screen layout: sticky hero left / scrollable timeline right (100svh grid)
|
|
7
|
+
* - appBadge component from @invisibleloop/pulse/ui
|
|
8
|
+
* - Deep-night palette (#0A0A0F) with gold (#C9A96E) accent
|
|
9
|
+
* - Alternating left/right timeline cards with centred thread + connector dots
|
|
10
|
+
* - calc(50% - var(--ui-space-8)) pattern for half-width timeline cards
|
|
11
|
+
* - Sticky positioned timeline column with overflow-y: auto + scrollbar-width: none
|
|
12
|
+
* - Frosted glass nav: rgba translucency + backdrop-filter blur
|
|
13
|
+
* - Skip link, visually-hidden headings, section aria-labelledby — full a11y
|
|
14
|
+
* - Mobile responsive: stacks vertically, thread moves left, full-width cards
|
|
15
|
+
* - iconTag, iconPlus, iconSend, iconArrowDown, iconFeather from icons.js
|
|
16
|
+
* - picsum.photos/id/* for stable prototype images
|
|
17
|
+
*
|
|
18
|
+
* Run: node examples/dev.server.js → http://localhost:3001/journey
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { appBadge } from '../../../../src/ui/index.js'
|
|
22
|
+
import { iconTag, iconPlus, iconSend, iconArrowDown, iconFeather } from '../../../../src/ui/icons.js'
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Timeline data
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
const timelineEvents = [
|
|
29
|
+
{
|
|
30
|
+
year: '2019',
|
|
31
|
+
title: 'First Day in the City',
|
|
32
|
+
body: 'Packed everything into one suitcase. The apartment was smaller than expected. The feeling was not.',
|
|
33
|
+
tag: 'Milestone',
|
|
34
|
+
image: { src: '/images/journey/29.jpg', alt: 'City street view' },
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
year: '2020',
|
|
38
|
+
title: 'Learning to Stay Still',
|
|
39
|
+
body: 'The world paused. Finished three books. Started painting. Discovered what quiet actually sounds like.',
|
|
40
|
+
tag: 'Reflection',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
year: '2021',
|
|
44
|
+
title: 'The Road Trip',
|
|
45
|
+
body: '4,200 miles across eleven states. Every diner looks the same after midnight. Every sunrise does not.',
|
|
46
|
+
tag: 'Adventure',
|
|
47
|
+
image: { src: '/images/journey/178.jpg', alt: 'Open road through a landscape' },
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
year: '2022',
|
|
51
|
+
title: 'Said Yes',
|
|
52
|
+
body: 'On a Tuesday. In the kitchen. No ring yet, just the question and the answer and the rest of our lives.',
|
|
53
|
+
tag: 'Love',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
year: '2024',
|
|
57
|
+
title: 'This Is Home Now',
|
|
58
|
+
body: 'The boxes are gone. The walls have pictures. The cat has a name. The journey continues.',
|
|
59
|
+
tag: 'Present',
|
|
60
|
+
},
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Timeline card helper
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
const tlCard = ({ year, title, body, tag, image }) => `
|
|
68
|
+
<div class="j-tl-card${image ? ' j-tl-card--image' : ''}">
|
|
69
|
+
${image ? `<img class="j-tl-image" src="${image.src}" alt="${image.alt}" width="600" height="340" loading="lazy" decoding="async">` : ''}
|
|
70
|
+
<div class="j-tl-content">
|
|
71
|
+
<p class="j-tl-year">${year}</p>
|
|
72
|
+
<h3 class="j-tl-title">${title}</h3>
|
|
73
|
+
<p class="j-tl-body">${body}</p>
|
|
74
|
+
<span class="j-tl-tag">${iconTag({ size: 10 })}${tag}</span>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
`
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Spec
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
export default {
|
|
84
|
+
route: '/templates/journey',
|
|
85
|
+
|
|
86
|
+
meta: {
|
|
87
|
+
title: 'Journey \u2014 Your story, in order',
|
|
88
|
+
description: 'Plot your life\u2019s journey. Map events, milestones, and memories on a beautiful timeline.',
|
|
89
|
+
styles: ['/pulse-ui.css', '/journey.css'],
|
|
90
|
+
theme: 'dark',
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
view: () => `
|
|
94
|
+
<a href="#main-content" class="j-skip-link">Skip to main content</a>
|
|
95
|
+
|
|
96
|
+
<header role="banner">
|
|
97
|
+
<nav class="j-nav" aria-label="Site navigation">
|
|
98
|
+
<a href="/journey" class="j-nav-wordmark" aria-label="Journey \u2014 home">Journey</a>
|
|
99
|
+
${appBadge({ store: 'apple', href: '#download' })}
|
|
100
|
+
</nav>
|
|
101
|
+
</header>
|
|
102
|
+
|
|
103
|
+
<main id="main-content">
|
|
104
|
+
|
|
105
|
+
<!-- Hero + Timeline split -->
|
|
106
|
+
<div class="j-hero-split">
|
|
107
|
+
|
|
108
|
+
<!-- Hero -->
|
|
109
|
+
<section class="j-hero" aria-labelledby="journey-hero-title">
|
|
110
|
+
<p class="j-hero-eyebrow" aria-hidden="true">Your story \u00b7 In order</p>
|
|
111
|
+
<h1 class="j-hero-title" id="journey-hero-title">
|
|
112
|
+
Every<br>moment<br><em>matters.</em>
|
|
113
|
+
</h1>
|
|
114
|
+
<p class="j-hero-sub">
|
|
115
|
+
Journey is a beautiful timeline for your life. Plot events, mark milestones,
|
|
116
|
+
and explore the story only you can tell.
|
|
117
|
+
</p>
|
|
118
|
+
<div class="j-hero-cta">
|
|
119
|
+
<a href="#download" class="j-btn-primary">
|
|
120
|
+
Download free
|
|
121
|
+
</a>
|
|
122
|
+
<a href="#journey-timeline" class="j-btn-ghost">
|
|
123
|
+
See how it works ${iconArrowDown({ size: 14 })}
|
|
124
|
+
</a>
|
|
125
|
+
</div>
|
|
126
|
+
<div class="j-hero-scroll" aria-hidden="true">
|
|
127
|
+
<span class="j-hero-line"></span>
|
|
128
|
+
<span>Scroll to explore</span>
|
|
129
|
+
</div>
|
|
130
|
+
</section>
|
|
131
|
+
|
|
132
|
+
<!-- Timeline demo -->
|
|
133
|
+
<section class="j-timeline-section" id="journey-timeline" aria-labelledby="journey-timeline-heading">
|
|
134
|
+
<p class="j-section-label" aria-hidden="true">01 \u2014 Your timeline</p>
|
|
135
|
+
<h2 class="j-visually-hidden" id="journey-timeline-heading">Example journey timeline</h2>
|
|
136
|
+
<div class="j-timeline" role="list">
|
|
137
|
+
${timelineEvents.map(ev => `
|
|
138
|
+
<div class="j-tl-item" role="listitem">
|
|
139
|
+
${tlCard(ev)}
|
|
140
|
+
</div>
|
|
141
|
+
`).join('')}
|
|
142
|
+
</div>
|
|
143
|
+
</section>
|
|
144
|
+
|
|
145
|
+
</div><!-- end j-hero-split -->
|
|
146
|
+
|
|
147
|
+
<!-- Features -->
|
|
148
|
+
<section class="j-features" aria-labelledby="journey-features-heading">
|
|
149
|
+
<h2 class="j-visually-hidden" id="journey-features-heading">Features</h2>
|
|
150
|
+
<div>
|
|
151
|
+
<p class="j-feature-label">${iconPlus({ size: 12 })}Plot your path</p>
|
|
152
|
+
<p class="j-feature-body">Add any event \u2014 big or small \u2014 and watch your story take shape along a living timeline.</p>
|
|
153
|
+
</div>
|
|
154
|
+
<div>
|
|
155
|
+
<p class="j-feature-label">${iconTag({ size: 12 })}Tag & explore</p>
|
|
156
|
+
<p class="j-feature-body">Organise moments by theme, mood, or chapter. Filter your timeline to find any memory instantly.</p>
|
|
157
|
+
</div>
|
|
158
|
+
<div>
|
|
159
|
+
<p class="j-feature-label">${iconSend({ size: 12 })}Share your journey</p>
|
|
160
|
+
<p class="j-feature-body">Export a beautiful visual timeline to share with the people who made it worth living.</p>
|
|
161
|
+
</div>
|
|
162
|
+
</section>
|
|
163
|
+
|
|
164
|
+
<!-- CTA / Download -->
|
|
165
|
+
<section class="j-cta" id="download" aria-labelledby="journey-cta-heading">
|
|
166
|
+
<p class="j-cta-eyebrow">Available now \u00b7 iOS</p>
|
|
167
|
+
<h2 class="j-cta-title" id="journey-cta-heading">
|
|
168
|
+
Begin your<br><em>journey</em> today.
|
|
169
|
+
</h2>
|
|
170
|
+
<div class="j-cta-badges">
|
|
171
|
+
${appBadge({ store: 'apple', href: '#' })}
|
|
172
|
+
</div>
|
|
173
|
+
</section>
|
|
174
|
+
|
|
175
|
+
</main>
|
|
176
|
+
|
|
177
|
+
<footer class="j-footer" role="contentinfo">
|
|
178
|
+
<span class="j-footer-word">${iconFeather({ size: 12 })} Journey © 2025</span>
|
|
179
|
+
<nav class="j-footer-links" aria-label="Footer navigation">
|
|
180
|
+
<a href="#privacy">Privacy</a>
|
|
181
|
+
<a href="#terms">Terms</a>
|
|
182
|
+
<a href="#contact">Contact</a>
|
|
183
|
+
</nav>
|
|
184
|
+
</footer>
|
|
185
|
+
`,
|
|
186
|
+
}
|