@invisibleloop/pulse 0.20.11 → 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 +13 -6
- package/src/runtime/ssr.test.js +31 -0
- package/src/server/index.js +162 -9
- 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 -350
- 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
package/examples/bookworms.js
DELETED
|
@@ -1,225 +0,0 @@
|
|
|
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
|
-
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
// Static data
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
|
|
26
|
-
const picks = [
|
|
27
|
-
{
|
|
28
|
-
tag: 'Fiction · Staff pick',
|
|
29
|
-
title: 'The Lantern Keepers',
|
|
30
|
-
author: 'Iris Aldenmoor',
|
|
31
|
-
note: 'A quiet novel about three sisters, an unreliable lighthouse, and the long winter that follows a single decision.',
|
|
32
|
-
cover: '/images/bookworms/lantern-keepers.jpg',
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
tag: 'Non-fiction · This week',
|
|
36
|
-
title: 'How We Read Now',
|
|
37
|
-
author: 'Dr. Samuel Okafor',
|
|
38
|
-
note: 'A short, generous history of attention — from monastic libraries to the open browser tab.',
|
|
39
|
-
cover: '/images/bookworms/how-we-read-now.jpg',
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
tag: 'Rare find · Just arrived',
|
|
43
|
-
title: 'Letters from a Provincial Bookseller',
|
|
44
|
-
author: 'M. Halloran (1927)',
|
|
45
|
-
note: 'First edition, lightly foxed. A wry account of running a shop through a quiet decade.',
|
|
46
|
-
cover: '/images/bookworms/provincial-bookseller.jpg',
|
|
47
|
-
},
|
|
48
|
-
]
|
|
49
|
-
|
|
50
|
-
const features = [
|
|
51
|
-
{
|
|
52
|
-
title: 'Curated, not algorithmic',
|
|
53
|
-
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.',
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
title: 'Browse by mood',
|
|
57
|
-
body: 'Looking for something restorative? Something difficult? Our shelves are arranged the way readers actually think.',
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
title: 'Author evenings & book clubs',
|
|
61
|
-
body: 'Small, considered events with writers we admire. Wine, conversation, and signed copies — no panels of four.',
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
title: 'Free delivery over £25',
|
|
65
|
-
body: 'Wrapped by hand, delivered locally by bicycle. Posted further afield by the next morning.',
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
title: 'Gift cards & reader subscriptions',
|
|
69
|
-
body: 'A monthly book chosen for one specific reader, with a note from the bookseller who picked it.',
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
title: 'Open seven days',
|
|
73
|
-
body: 'Old Market Lane, opposite the bakery. Coffee from nine, books from ten.',
|
|
74
|
-
},
|
|
75
|
-
]
|
|
76
|
-
|
|
77
|
-
const events = [
|
|
78
|
-
{ date: '12 June', title: 'An evening with Iris Aldenmoor', host: 'In conversation with Priya Joshi' },
|
|
79
|
-
{ date: '21 June', title: 'Slow Reading Club — Middlemarch, part one', host: 'Hosted by the BookWorms team' },
|
|
80
|
-
{ date: '03 July', title: 'New voices in non-fiction', host: 'Three debut authors read and answer questions' },
|
|
81
|
-
]
|
|
82
|
-
|
|
83
|
-
const navLinks = [
|
|
84
|
-
{ label: 'Browse', href: '#browse' },
|
|
85
|
-
{ label: 'Staff picks', href: '#picks' },
|
|
86
|
-
{ label: 'Events', href: '#events' },
|
|
87
|
-
{ label: 'Visit', href: '#visit' },
|
|
88
|
-
]
|
|
89
|
-
|
|
90
|
-
// ---------------------------------------------------------------------------
|
|
91
|
-
// Spec
|
|
92
|
-
// ---------------------------------------------------------------------------
|
|
93
|
-
|
|
94
|
-
export default {
|
|
95
|
-
route: '/bookworms',
|
|
96
|
-
|
|
97
|
-
meta: {
|
|
98
|
-
title: 'BookWorms — an independent bookstore for curious readers',
|
|
99
|
-
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.',
|
|
100
|
-
theme: 'light',
|
|
101
|
-
styles: [
|
|
102
|
-
'https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,400;0,9..144,500;1,9..144,400&display=swap',
|
|
103
|
-
'/pulse-ui.css',
|
|
104
|
-
'/examples/bookworms.css',
|
|
105
|
-
],
|
|
106
|
-
},
|
|
107
|
-
|
|
108
|
-
view: () => `
|
|
109
|
-
${nav({
|
|
110
|
-
logo: 'BookWorms',
|
|
111
|
-
links: navLinks,
|
|
112
|
-
action: button({ label: 'Visit the shop', href: '#visit', variant: 'primary', size: 'sm' }),
|
|
113
|
-
sticky: true,
|
|
114
|
-
})}
|
|
115
|
-
|
|
116
|
-
<main id="main-content">
|
|
117
|
-
<section class="bw-hero">
|
|
118
|
-
<div class="bw-shell bw-hero__inner">
|
|
119
|
-
<div class="bw-hero__content">
|
|
120
|
-
<p class="bw-eyebrow">Independent bookstore · Old Market Lane</p>
|
|
121
|
-
<h1 class="bw-hero-title">Books, handpicked for the curious.</h1>
|
|
122
|
-
<p class="bw-hero-lede">
|
|
123
|
-
BookWorms is a small shop with a long memory. Every title on our shelves has been read by
|
|
124
|
-
someone who works here — and is waiting for the right reader to find it.
|
|
125
|
-
</p>
|
|
126
|
-
<div class="bw-cta-actions bw-cta-actions--start">
|
|
127
|
-
${button({ label: 'See this week\u2019s picks', href: '#picks', variant: 'primary' })}
|
|
128
|
-
${button({ label: 'Upcoming events', href: '#events', variant: 'secondary' })}
|
|
129
|
-
</div>
|
|
130
|
-
</div>
|
|
131
|
-
<div class="bw-hero__image" aria-hidden="true">
|
|
132
|
-
<img src="/images/bookworms/hero-books.jpg" alt="" width="820" height="1080" />
|
|
133
|
-
</div>
|
|
134
|
-
</div>
|
|
135
|
-
</section>
|
|
136
|
-
|
|
137
|
-
<section class="bw-section" id="picks">
|
|
138
|
-
<div class="bw-shell">
|
|
139
|
-
<p class="bw-section-label">This week</p>
|
|
140
|
-
<h2 class="bw-section-title">Three books we\u2019re pressing into people\u2019s hands.</h2>
|
|
141
|
-
<div class="bw-picks">
|
|
142
|
-
${picks.map((p, i) => `
|
|
143
|
-
<article>
|
|
144
|
-
<div class="bw-pick-cover-wrap">
|
|
145
|
-
<img class="bw-pick-cover" src="${p.cover}" alt="Cover of ${p.title} by ${p.author}" width="400" height="600" loading="lazy" />
|
|
146
|
-
<div class="bw-pick-overlay">
|
|
147
|
-
<h3 class="bw-pick-overlay-title">${p.title}</h3>
|
|
148
|
-
<p class="bw-pick-overlay-author">${i === 2 ? p.author.replace(/\s*\(\d{4}\)/, '') : 'by ' + p.author}</p>
|
|
149
|
-
</div>
|
|
150
|
-
</div>
|
|
151
|
-
<p class="bw-pick-meta">${p.tag}</p>
|
|
152
|
-
<h3 class="bw-pick-title">${p.title}</h3>
|
|
153
|
-
<p class="bw-pick-author">by ${p.author}</p>
|
|
154
|
-
<p class="bw-pick-note">${p.note}</p>
|
|
155
|
-
</article>
|
|
156
|
-
`).join('')}
|
|
157
|
-
</div>
|
|
158
|
-
</div>
|
|
159
|
-
</section>
|
|
160
|
-
|
|
161
|
-
<section class="bw-pull">
|
|
162
|
-
<div class="bw-shell">
|
|
163
|
-
<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>
|
|
164
|
-
<p class="bw-pull-cite">\u2014 The Quarterly Review</p>
|
|
165
|
-
</div>
|
|
166
|
-
</section>
|
|
167
|
-
|
|
168
|
-
<section class="bw-section" id="browse">
|
|
169
|
-
<div class="bw-shell">
|
|
170
|
-
<p class="bw-section-label">What we do</p>
|
|
171
|
-
<h2 class="bw-section-title">A bookstore that still reads.</h2>
|
|
172
|
-
<div class="bw-features">
|
|
173
|
-
${features.map(f => `
|
|
174
|
-
<div>
|
|
175
|
-
<h3 class="bw-feature-title">${f.title}</h3>
|
|
176
|
-
<p class="bw-feature-body">${f.body}</p>
|
|
177
|
-
</div>
|
|
178
|
-
`).join('')}
|
|
179
|
-
</div>
|
|
180
|
-
</div>
|
|
181
|
-
</section>
|
|
182
|
-
|
|
183
|
-
<section class="bw-section" id="events">
|
|
184
|
-
<div class="bw-shell">
|
|
185
|
-
<p class="bw-section-label">Upcoming</p>
|
|
186
|
-
<h2 class="bw-section-title">Evenings worth turning up for.</h2>
|
|
187
|
-
<div class="bw-events">
|
|
188
|
-
${events.map(e => `
|
|
189
|
-
<article class="bw-event">
|
|
190
|
-
<p class="bw-event-date">${e.date}</p>
|
|
191
|
-
<h3 class="bw-event-title">${e.title}</h3>
|
|
192
|
-
<p class="bw-event-host">${e.host}</p>
|
|
193
|
-
</article>
|
|
194
|
-
`).join('')}
|
|
195
|
-
</div>
|
|
196
|
-
</div>
|
|
197
|
-
</section>
|
|
198
|
-
|
|
199
|
-
<section class="bw-cta" id="visit">
|
|
200
|
-
<div class="bw-shell">
|
|
201
|
-
<h2 class="bw-cta-title">Come and browse.</h2>
|
|
202
|
-
<p class="bw-cta-sub">
|
|
203
|
-
14 Old Market Lane, opposite the bakery. Open Monday to Sunday, 10am\u20137pm.
|
|
204
|
-
Or sign up to receive one handpicked book each month.
|
|
205
|
-
</p>
|
|
206
|
-
<div class="bw-cta-actions">
|
|
207
|
-
${button({ label: 'Plan a visit', href: '#visit', variant: 'primary' })}
|
|
208
|
-
${button({ label: 'Reader subscription', href: '#visit', variant: 'secondary' })}
|
|
209
|
-
</div>
|
|
210
|
-
</div>
|
|
211
|
-
</section>
|
|
212
|
-
</main>
|
|
213
|
-
|
|
214
|
-
${footer({
|
|
215
|
-
logo: 'BookWorms',
|
|
216
|
-
links: [
|
|
217
|
-
{ label: 'Browse', href: '#browse' },
|
|
218
|
-
{ label: 'Events', href: '#events' },
|
|
219
|
-
{ label: 'Visit', href: '#visit' },
|
|
220
|
-
{ label: 'Contact', href: '#visit' },
|
|
221
|
-
],
|
|
222
|
-
legal: '\u00a9 2026 BookWorms \u00b7 14 Old Market Lane',
|
|
223
|
-
})}
|
|
224
|
-
`,
|
|
225
|
-
}
|
package/examples/contact.js
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse — Contact form example
|
|
3
|
-
*
|
|
4
|
-
* Demonstrates:
|
|
5
|
-
* - Server data (fetched before render, passed to view)
|
|
6
|
-
* - Validation (required, email format, length limits)
|
|
7
|
-
* - Async action with full lifecycle: onStart → validate → run → onSuccess/onError
|
|
8
|
-
* - Loading, success, and error states
|
|
9
|
-
*
|
|
10
|
-
* Run: node examples/dev.server.js → http://localhost:3001/contact
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { button, alert, heading, input, textarea, card, container, section, stack, grid, iconMail, iconPhone, iconMapPin, iconClock, iconCheckCircle } from '../src/ui/index.js'
|
|
14
|
-
import { examplesNav } from './shared.js'
|
|
15
|
-
|
|
16
|
-
function infoDetail(icon, label, value, href) {
|
|
17
|
-
const valueHtml = href
|
|
18
|
-
? `<a href="${href}" class="u-text-default">${value}</a>`
|
|
19
|
-
: `<span>${value}</span>`
|
|
20
|
-
return `<div class="u-flex u-items-start u-gap-3">
|
|
21
|
-
<span class="u-text-muted u-shrink-0" aria-hidden="true">${icon}</span>
|
|
22
|
-
<div>
|
|
23
|
-
<p class="u-text-xs u-text-muted u-font-semibold u-mb-1">${label}</p>
|
|
24
|
-
<p class="u-text-sm">${valueHtml}</p>
|
|
25
|
-
</div>
|
|
26
|
-
</div>`
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export default {
|
|
30
|
-
route: '/contact',
|
|
31
|
-
|
|
32
|
-
meta: {
|
|
33
|
-
title: 'Contact — Pulse',
|
|
34
|
-
description: 'A contact form built with Pulse. Demonstrates server data, validation, and async action lifecycle.',
|
|
35
|
-
styles: ['/pulse-ui.css'],
|
|
36
|
-
theme: 'dark',
|
|
37
|
-
},
|
|
38
|
-
|
|
39
|
-
server: {
|
|
40
|
-
info: async () => ({
|
|
41
|
-
email: 'hello@example.com',
|
|
42
|
-
phone: '+44 20 7946 0958',
|
|
43
|
-
address: '123 Shoreditch High St, London E1 6JE',
|
|
44
|
-
hours: 'Monday – Friday, 9 am – 5 pm GMT',
|
|
45
|
-
}),
|
|
46
|
-
},
|
|
47
|
-
|
|
48
|
-
state: {
|
|
49
|
-
status: 'idle', // idle | loading | success | error
|
|
50
|
-
errors: [],
|
|
51
|
-
fields: { name: '', email: '', subject: '', message: '' },
|
|
52
|
-
},
|
|
53
|
-
|
|
54
|
-
validation: {
|
|
55
|
-
'fields.name': { required: true, minLength: 2, maxLength: 80 },
|
|
56
|
-
'fields.email': { required: true, format: 'email' },
|
|
57
|
-
'fields.subject': { required: true, minLength: 2, maxLength: 120 },
|
|
58
|
-
'fields.message': { required: true, minLength: 10, maxLength: 2000 },
|
|
59
|
-
},
|
|
60
|
-
|
|
61
|
-
mutations: {
|
|
62
|
-
reset: () => ({ status: 'idle', errors: [], fields: { name: '', email: '', subject: '', message: '' } }),
|
|
63
|
-
},
|
|
64
|
-
|
|
65
|
-
actions: {
|
|
66
|
-
submit: {
|
|
67
|
-
onStart: (_state, formData) => ({
|
|
68
|
-
status: 'loading',
|
|
69
|
-
errors: [],
|
|
70
|
-
fields: {
|
|
71
|
-
name: formData.get('name') || '',
|
|
72
|
-
email: formData.get('email') || '',
|
|
73
|
-
subject: formData.get('subject') || '',
|
|
74
|
-
message: formData.get('message') || '',
|
|
75
|
-
},
|
|
76
|
-
}),
|
|
77
|
-
|
|
78
|
-
validate: true,
|
|
79
|
-
|
|
80
|
-
run: async (_state, _server, formData) => {
|
|
81
|
-
// Simulate sending — replace with a real fetch() to your API
|
|
82
|
-
await new Promise(resolve => setTimeout(resolve, 900))
|
|
83
|
-
// Demo: email addresses containing "fail" trigger the error path
|
|
84
|
-
if ((formData.get('email') || '').includes('fail')) {
|
|
85
|
-
throw new Error('Unable to deliver message. Please try another address.')
|
|
86
|
-
}
|
|
87
|
-
},
|
|
88
|
-
|
|
89
|
-
onSuccess: () => ({ status: 'success', errors: [] }),
|
|
90
|
-
|
|
91
|
-
onError: (_state, err) => ({
|
|
92
|
-
status: 'error',
|
|
93
|
-
errors: err?.validation
|
|
94
|
-
? err.validation.map(e => {
|
|
95
|
-
const field = e.path?.split('.').pop() || ''
|
|
96
|
-
const label = { name: 'Name', email: 'Email', subject: 'Subject', message: 'Message' }[field] || field
|
|
97
|
-
const msg = e.rule === 'required' ? `${label} is required`
|
|
98
|
-
: e.rule === 'minLength' ? `${label} is too short`
|
|
99
|
-
: e.rule === 'maxLength' ? `${label} is too long`
|
|
100
|
-
: e.rule === 'format' ? `${label} must be a valid email address`
|
|
101
|
-
: e.message
|
|
102
|
-
return { field, message: msg }
|
|
103
|
-
})
|
|
104
|
-
: [{ message: err?.message || 'Something went wrong. Please try again.' }],
|
|
105
|
-
}),
|
|
106
|
-
},
|
|
107
|
-
},
|
|
108
|
-
|
|
109
|
-
view: (state, server) => {
|
|
110
|
-
const { info } = server
|
|
111
|
-
const loading = state.status === 'loading'
|
|
112
|
-
const isSuccess = state.status === 'success'
|
|
113
|
-
|
|
114
|
-
const fieldError = (field) => state.errors.find(e => e.field === field)?.message || ''
|
|
115
|
-
|
|
116
|
-
const infoCol = stack({ gap: 'lg', content: `
|
|
117
|
-
${heading({ level: 2, text: 'Get in touch', size: 'xl' })}
|
|
118
|
-
<p class="u-text-sm u-text-muted">Fill in the form and we’ll get back to you within one business day.</p>
|
|
119
|
-
${stack({ gap: 'md', content: `
|
|
120
|
-
${infoDetail(iconMail({ size: 18 }), 'Email', info.email, 'mailto:' + info.email)}
|
|
121
|
-
${infoDetail(iconPhone({ size: 18 }), 'Phone', info.phone, 'tel:' + info.phone.replace(/\s/g, ''))}
|
|
122
|
-
${infoDetail(iconMapPin({ size: 18 }), 'Address', info.address, null)}
|
|
123
|
-
${infoDetail(iconClock({ size: 18 }), 'Hours', info.hours, null)}
|
|
124
|
-
` })}
|
|
125
|
-
` })
|
|
126
|
-
|
|
127
|
-
const cardContent = isSuccess
|
|
128
|
-
? stack({ gap: 'md', align: 'center', content:
|
|
129
|
-
iconCheckCircle({ size: 48, bg: 'circle', bgColor: 'success' }) +
|
|
130
|
-
heading({ level: 2, text: 'Message sent!', size: 'xl' }) +
|
|
131
|
-
`<p class="u-text-sm u-text-muted u-text-center">Thanks for reaching out. We’ll be in touch within one business day.</p>` +
|
|
132
|
-
button({ label: 'Send another message', variant: 'secondary', attrs: { 'data-event': 'reset' } })
|
|
133
|
-
})
|
|
134
|
-
: `<form data-action="submit" novalidate>
|
|
135
|
-
<fieldset style="border:none;padding:0;margin:0" ${loading ? 'disabled' : ''}>
|
|
136
|
-
<legend class="u-text-lg u-font-semibold u-mb-5">Send a message</legend>
|
|
137
|
-
${stack({ gap: 'md', content: `
|
|
138
|
-
${state.status === 'error' && state.errors.length > 0
|
|
139
|
-
? alert({ variant: 'error', content: state.errors[0]?.message || 'Please check the fields below.' })
|
|
140
|
-
: ''}
|
|
141
|
-
${grid({ cols: 2, gap: 'sm', content:
|
|
142
|
-
input({ name: 'name', label: 'Name', type: 'text', placeholder: 'Jane Smith', value: state.fields.name, required: true, error: fieldError('name') }) +
|
|
143
|
-
input({ name: 'email', label: 'Email address', type: 'email', placeholder: 'jane@example.com', value: state.fields.email, required: true, error: fieldError('email') })
|
|
144
|
-
})}
|
|
145
|
-
${input({ name: 'subject', label: 'Subject', placeholder: 'How can we help?', value: state.fields.subject, required: true, error: fieldError('subject') })}
|
|
146
|
-
${textarea({ name: 'message', label: 'Message', placeholder: "Tell us what's on your mind…", value: state.fields.message, rows: 5, required: true, error: fieldError('message') })}
|
|
147
|
-
${button({ label: loading ? 'Sending…' : 'Send message', type: 'submit', fullWidth: true, disabled: loading })}
|
|
148
|
-
` })}
|
|
149
|
-
</fieldset>
|
|
150
|
-
</form>`
|
|
151
|
-
|
|
152
|
-
return `
|
|
153
|
-
${examplesNav('<span>✉ Contact</span>', '/contact')}
|
|
154
|
-
|
|
155
|
-
<main id="main-content">
|
|
156
|
-
${section({ eyebrow: 'Contact', title: 'We\u2019d love to hear from you', level: 1, content:
|
|
157
|
-
container({ size: 'lg', content:
|
|
158
|
-
grid({ cols: 2, gap: 'lg', content: `
|
|
159
|
-
<div>${infoCol}</div>
|
|
160
|
-
<div>${card({ content: cardContent })}</div>
|
|
161
|
-
` })
|
|
162
|
-
})
|
|
163
|
-
})}
|
|
164
|
-
</main>`
|
|
165
|
-
},
|
|
166
|
-
}
|
package/examples/counter.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse — Counter example
|
|
3
|
-
*
|
|
4
|
-
* Demonstrates:
|
|
5
|
-
* - State and mutations
|
|
6
|
-
* - Constraints (min/max enforced after every mutation)
|
|
7
|
-
* - data-event binding
|
|
8
|
-
* - Hydration for client interactivity
|
|
9
|
-
*
|
|
10
|
-
* Run: node examples/dev.server.js → http://localhost:3001/counter
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { button, badge, card, container, section, stack, cluster, progress, segmented } from '../src/ui/index.js'
|
|
14
|
-
import { examplesNav } from './shared.js'
|
|
15
|
-
|
|
16
|
-
export default {
|
|
17
|
-
route: '/counter',
|
|
18
|
-
|
|
19
|
-
meta: {
|
|
20
|
-
title: 'Counter — Pulse',
|
|
21
|
-
description: 'An interactive counter built with Pulse. Demonstrates mutations, constraints, and data-event binding.',
|
|
22
|
-
styles: ['/pulse-ui.css'],
|
|
23
|
-
theme: 'dark',
|
|
24
|
-
},
|
|
25
|
-
|
|
26
|
-
state: {
|
|
27
|
-
count: 0,
|
|
28
|
-
step: 1,
|
|
29
|
-
},
|
|
30
|
-
|
|
31
|
-
constraints: {
|
|
32
|
-
count: { min: 0, max: 20 },
|
|
33
|
-
step: { min: 1, max: 5 },
|
|
34
|
-
},
|
|
35
|
-
|
|
36
|
-
mutations: {
|
|
37
|
-
increment: (state) => ({ count: state.count + state.step }),
|
|
38
|
-
decrement: (state) => ({ count: state.count - state.step }),
|
|
39
|
-
reset: () => ({ count: 0 }),
|
|
40
|
-
setStep: (_state, e) => ({ step: parseInt(e.target.value, 10) || 1 }),
|
|
41
|
-
},
|
|
42
|
-
|
|
43
|
-
view: (state) => {
|
|
44
|
-
const atMin = state.count <= 0
|
|
45
|
-
const atMax = state.count >= 20
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return `
|
|
49
|
-
${examplesNav('<span>⚡ Counter</span>', '/counter')}
|
|
50
|
-
|
|
51
|
-
<main id="main-content">
|
|
52
|
-
${section({ eyebrow: 'Mutations & Constraints', title: 'Counter', level: 1, align: 'center',
|
|
53
|
-
subtitle: 'Constraints are enforced after every mutation — the count can never go below 0 or above 20.',
|
|
54
|
-
content:
|
|
55
|
-
container({ size: 'sm', content: `
|
|
56
|
-
|
|
57
|
-
${card({ content: stack({ gap: 'lg', align: 'center', content: `
|
|
58
|
-
|
|
59
|
-
<div class="u-flex u-items-end u-justify-center u-gap-3" aria-live="polite" aria-atomic="true">
|
|
60
|
-
<span class="u-text-7xl u-font-bold u-text-accent">${state.count}</span>
|
|
61
|
-
<span class="u-text-3xl u-text-muted u-mb-2">/ 20</span>
|
|
62
|
-
</div>
|
|
63
|
-
|
|
64
|
-
${progress({ value: state.count, max: 20, label: 'Counter progress' })}
|
|
65
|
-
|
|
66
|
-
${cluster({ justify: 'center', gap: 'sm', content:
|
|
67
|
-
button({ label: '−', variant: 'secondary', disabled: atMin, attrs: { 'data-event': 'decrement', 'aria-label': 'Decrement' } }) +
|
|
68
|
-
button({ label: 'Reset', variant: 'ghost', attrs: { 'data-event': 'reset' } }) +
|
|
69
|
-
button({ label: '+', variant: 'primary', disabled: atMax, attrs: { 'data-event': 'increment', 'aria-label': 'Increment' } })
|
|
70
|
-
})}
|
|
71
|
-
|
|
72
|
-
${stack({ gap: 'xs', align: 'center', content: `
|
|
73
|
-
<p class="u-text-xs u-text-muted" id="step-label">Step size</p>
|
|
74
|
-
${segmented({
|
|
75
|
-
name: 'step',
|
|
76
|
-
value: String(state.step),
|
|
77
|
-
event: 'change:setStep',
|
|
78
|
-
options: [1, 2, 5].map(n => ({ value: n, label: String(n) })),
|
|
79
|
-
size: 'md',
|
|
80
|
-
})}
|
|
81
|
-
` })}
|
|
82
|
-
|
|
83
|
-
${cluster({ justify: 'center', gap: 'xs', content:
|
|
84
|
-
badge({ label: atMin ? 'At minimum' : 'Min: 0', variant: atMin ? 'warning' : 'default' }) +
|
|
85
|
-
badge({ label: atMax ? 'At maximum' : 'Max: 20', variant: atMax ? 'warning' : 'default' })
|
|
86
|
-
})}
|
|
87
|
-
|
|
88
|
-
` }) })}
|
|
89
|
-
|
|
90
|
-
`})
|
|
91
|
-
})}
|
|
92
|
-
</main>`
|
|
93
|
-
},
|
|
94
|
-
}
|
package/examples/dev.server.js
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse — Examples dev server
|
|
3
|
-
*
|
|
4
|
-
* Serves all six standalone example apps.
|
|
5
|
-
* Handles /src/, /examples/, and /public/ as static JS so dev hydration works.
|
|
6
|
-
*
|
|
7
|
-
* Run: node examples/dev.server.js
|
|
8
|
-
*
|
|
9
|
-
* http://localhost:3001/counter — mutations + constraints
|
|
10
|
-
* http://localhost:3001/todos — CRUD + persist + filter
|
|
11
|
-
* http://localhost:3001/contact — server data + validation + action
|
|
12
|
-
* http://localhost:3001/quiz — multi-step state machine
|
|
13
|
-
* http://localhost:3001/products — server data + search/filter/sort
|
|
14
|
-
* http://localhost:3001/pricing — landing page components + billing toggle
|
|
15
|
-
* http://localhost:3001/swish — editorial fashion branding + light theme
|
|
16
|
-
* http://localhost:3001/bookworms — independent bookshop, Fraunces font, picsum covers
|
|
17
|
-
* http://localhost:3001/retrowheels — vintage bicycle shop, slab-serif, booking form
|
|
18
|
-
* http://localhost:3001/suzukibento — artist portfolio, dark brutalist, asymmetric grid
|
|
19
|
-
* http://localhost:3001/andypixel — blog article, editorial light, newsletter action
|
|
20
|
-
* http://localhost:3001/journey — app landing page, dark split-screen, sticky timeline
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import fs from 'fs'
|
|
24
|
-
import path from 'path'
|
|
25
|
-
import { createServer } from '../src/server/index.js'
|
|
26
|
-
import { themeScript } from './shared.js'
|
|
27
|
-
|
|
28
|
-
const ROOT = path.resolve(import.meta.dirname, '..')
|
|
29
|
-
|
|
30
|
-
const MIME = {
|
|
31
|
-
'.js': 'application/javascript',
|
|
32
|
-
'.css': 'text/css',
|
|
33
|
-
'.json': 'application/json',
|
|
34
|
-
'.html': 'text/html; charset=utf-8',
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// /@pulse/runtime/* → src/runtime/*
|
|
38
|
-
// /@pulse/ui/* → src/ui/*
|
|
39
|
-
// Maps bare-specifier aliases used by the dev-mode bootstrap.
|
|
40
|
-
const PULSE_PREFIX_MAP = {
|
|
41
|
-
'/@pulse/runtime/': path.join(ROOT, 'src/runtime/'),
|
|
42
|
-
'/@pulse/ui/': path.join(ROOT, 'src/ui/'),
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Serve /src/, /examples/, and /@pulse/ alias paths so the browser can import
|
|
46
|
-
// spec and runtime files during dev hydration.
|
|
47
|
-
function staticHandler(req, res) {
|
|
48
|
-
const url = new URL(req.url, 'http://localhost')
|
|
49
|
-
const pathname = url.pathname
|
|
50
|
-
|
|
51
|
-
// /@pulse/* prefix aliases → src/
|
|
52
|
-
for (const [prefix, dir] of Object.entries(PULSE_PREFIX_MAP)) {
|
|
53
|
-
if (pathname.startsWith(prefix)) {
|
|
54
|
-
const filePath = path.join(dir, pathname.slice(prefix.length))
|
|
55
|
-
if (!filePath.startsWith(dir)) return
|
|
56
|
-
let stat
|
|
57
|
-
try { stat = fs.statSync(filePath) } catch { return }
|
|
58
|
-
if (!stat.isFile()) return
|
|
59
|
-
res.writeHead(200, { 'Content-Type': 'application/javascript' })
|
|
60
|
-
fs.createReadStream(filePath).pipe(res)
|
|
61
|
-
return false
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (!pathname.startsWith('/examples/') && !pathname.startsWith('/src/')) return
|
|
66
|
-
|
|
67
|
-
const filePath = path.join(ROOT, pathname)
|
|
68
|
-
if (!filePath.startsWith(ROOT + path.sep)) return
|
|
69
|
-
|
|
70
|
-
let stat
|
|
71
|
-
try { stat = fs.statSync(filePath) } catch { return }
|
|
72
|
-
if (!stat.isFile()) return
|
|
73
|
-
|
|
74
|
-
const ext = path.extname(filePath)
|
|
75
|
-
const mime = MIME[ext] || 'text/plain'
|
|
76
|
-
|
|
77
|
-
res.writeHead(200, { 'Content-Type': mime })
|
|
78
|
-
fs.createReadStream(filePath).pipe(res)
|
|
79
|
-
return false
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
await createServer(
|
|
83
|
-
[
|
|
84
|
-
new URL('./counter.js', import.meta.url),
|
|
85
|
-
new URL('./todos.js', import.meta.url),
|
|
86
|
-
new URL('./contact.js', import.meta.url),
|
|
87
|
-
new URL('./quiz.js', import.meta.url),
|
|
88
|
-
new URL('./products.js', import.meta.url),
|
|
89
|
-
new URL('./pricing.js', import.meta.url),
|
|
90
|
-
new URL('./swish.js', import.meta.url),
|
|
91
|
-
new URL('./bookworms.js', import.meta.url),
|
|
92
|
-
new URL('./retrowheels.js', import.meta.url),
|
|
93
|
-
new URL('./suzukibento.js', import.meta.url),
|
|
94
|
-
new URL('./andypixel.js', import.meta.url),
|
|
95
|
-
new URL('./journey.js', import.meta.url),
|
|
96
|
-
],
|
|
97
|
-
{
|
|
98
|
-
port: 3001,
|
|
99
|
-
stream: true,
|
|
100
|
-
staticDir: path.join(ROOT, 'public'),
|
|
101
|
-
root: new URL('..', import.meta.url), // pulse2/ — specs served at /examples/*
|
|
102
|
-
onRequest: staticHandler,
|
|
103
|
-
extraBody: themeScript,
|
|
104
|
-
csp: {
|
|
105
|
-
'style-src': ['https://fonts.googleapis.com'],
|
|
106
|
-
'font-src': ['https://fonts.gstatic.com'],
|
|
107
|
-
'img-src': ['https://images.unsplash.com', 'https://picsum.photos', 'https://fastly.picsum.photos'],
|
|
108
|
-
},
|
|
109
|
-
}
|
|
110
|
-
)
|