@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
package/examples/pricing.js
DELETED
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse — Pricing page example
|
|
3
|
-
*
|
|
4
|
-
* Demonstrates:
|
|
5
|
-
* - Landing page components (nav, accordion, cta, footer)
|
|
6
|
-
* - Pricing tiers rendered with card() — the former pricing() component
|
|
7
|
-
* was removed from the library as a narrow single-purpose wrapper
|
|
8
|
-
* - Billing-period toggle (monthly / annual) via mutation
|
|
9
|
-
* - Layout components (section, container, stack, cluster, grid)
|
|
10
|
-
* - No server data — fully static, still gets streaming SSR
|
|
11
|
-
*
|
|
12
|
-
* Run: node examples/dev.server.js → http://localhost:3001/pricing
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import {
|
|
16
|
-
nav as uiNav, card, badge, accordion, cta, footer as uiFooter,
|
|
17
|
-
section as uiSection, container, stack, cluster,
|
|
18
|
-
button, heading,
|
|
19
|
-
} from '../src/ui/index.js'
|
|
20
|
-
|
|
21
|
-
// Pricing tile — replaces the removed pricing() component with card()
|
|
22
|
-
const pricingCard = ({ name, price, period, description, features, badge: badgeLabel, highlighted, action, level = 2 }) => card({
|
|
23
|
-
variant: highlighted ? 'elevated' : 'bordered',
|
|
24
|
-
class: 'pr-plan',
|
|
25
|
-
content: `
|
|
26
|
-
${badgeLabel ? badge({ label: badgeLabel }) : ''}
|
|
27
|
-
<h${level} class="u-text-xl u-font-bold u-mt-2">${name}</h${level}>
|
|
28
|
-
<p class="u-text-3xl u-font-bold u-mt-2"><span class="pr-plan-amount">${price}</span><span class="u-text-sm u-text-muted">${period}</span></p>
|
|
29
|
-
<p class="u-text-sm u-text-muted u-mt-2">${description}</p>
|
|
30
|
-
<ul class="u-mt-4 u-flex u-flex-col u-gap-2">
|
|
31
|
-
${features.map(f => `<li>${f}</li>`).join('')}
|
|
32
|
-
</ul>
|
|
33
|
-
`,
|
|
34
|
-
footer: action,
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
const PLANS = {
|
|
38
|
-
monthly: [
|
|
39
|
-
{
|
|
40
|
-
name: 'Starter',
|
|
41
|
-
price: '£0',
|
|
42
|
-
period: 'forever',
|
|
43
|
-
description: 'For personal projects and tinkering.',
|
|
44
|
-
features: [
|
|
45
|
-
'1 project',
|
|
46
|
-
'5,000 page views / month',
|
|
47
|
-
'Pulse framework',
|
|
48
|
-
'Community support',
|
|
49
|
-
],
|
|
50
|
-
action: button({ label: 'Get started free', variant: 'secondary', href: '/getting-started', fullWidth: true }),
|
|
51
|
-
highlighted: false,
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
name: 'Pro',
|
|
55
|
-
price: '£12',
|
|
56
|
-
period: '/ month',
|
|
57
|
-
description: 'For indie developers and small teams.',
|
|
58
|
-
badge: 'Most popular',
|
|
59
|
-
features: [
|
|
60
|
-
'10 projects',
|
|
61
|
-
'500,000 page views / month',
|
|
62
|
-
'Everything in Starter',
|
|
63
|
-
'Streaming SSR',
|
|
64
|
-
'Global CDN',
|
|
65
|
-
'Email support',
|
|
66
|
-
],
|
|
67
|
-
action: button({ label: 'Start free trial', variant: 'primary', href: '/getting-started', fullWidth: true }),
|
|
68
|
-
highlighted: true,
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
name: 'Team',
|
|
72
|
-
price: '£49',
|
|
73
|
-
period: '/ month',
|
|
74
|
-
description: 'For growing teams shipping fast.',
|
|
75
|
-
features: [
|
|
76
|
-
'Unlimited projects',
|
|
77
|
-
'5M page views / month',
|
|
78
|
-
'Everything in Pro',
|
|
79
|
-
'Team collaboration',
|
|
80
|
-
'Custom domains',
|
|
81
|
-
'Priority support',
|
|
82
|
-
'SLA guarantee',
|
|
83
|
-
],
|
|
84
|
-
action: button({ label: 'Contact sales', variant: 'secondary', href: '/contact', fullWidth: true }),
|
|
85
|
-
highlighted: false,
|
|
86
|
-
},
|
|
87
|
-
],
|
|
88
|
-
annual: [
|
|
89
|
-
{
|
|
90
|
-
name: 'Starter',
|
|
91
|
-
price: '£0',
|
|
92
|
-
period: 'forever',
|
|
93
|
-
description: 'For personal projects and tinkering.',
|
|
94
|
-
features: [
|
|
95
|
-
'1 project',
|
|
96
|
-
'5,000 page views / month',
|
|
97
|
-
'Pulse framework',
|
|
98
|
-
'Community support',
|
|
99
|
-
],
|
|
100
|
-
action: button({ label: 'Get started free', variant: 'secondary', href: '/getting-started', fullWidth: true }),
|
|
101
|
-
highlighted: false,
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
name: 'Pro',
|
|
105
|
-
price: '£9',
|
|
106
|
-
period: '/ month',
|
|
107
|
-
description: 'For indie developers and small teams.',
|
|
108
|
-
badge: 'Most popular',
|
|
109
|
-
features: [
|
|
110
|
-
'10 projects',
|
|
111
|
-
'500,000 page views / month',
|
|
112
|
-
'Everything in Starter',
|
|
113
|
-
'Streaming SSR',
|
|
114
|
-
'Global CDN',
|
|
115
|
-
'Email support',
|
|
116
|
-
],
|
|
117
|
-
action: button({ label: 'Start free trial', variant: 'primary', href: '/getting-started', fullWidth: true }),
|
|
118
|
-
highlighted: true,
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
name: 'Team',
|
|
122
|
-
price: '£39',
|
|
123
|
-
period: '/ month',
|
|
124
|
-
description: 'For growing teams shipping fast.',
|
|
125
|
-
features: [
|
|
126
|
-
'Unlimited projects',
|
|
127
|
-
'5M page views / month',
|
|
128
|
-
'Everything in Pro',
|
|
129
|
-
'Team collaboration',
|
|
130
|
-
'Custom domains',
|
|
131
|
-
'Priority support',
|
|
132
|
-
'SLA guarantee',
|
|
133
|
-
],
|
|
134
|
-
action: button({ label: 'Contact sales', variant: 'secondary', href: '/contact', fullWidth: true }),
|
|
135
|
-
highlighted: false,
|
|
136
|
-
},
|
|
137
|
-
],
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export const FAQ = [
|
|
141
|
-
{ question: 'Can I change plans later?', answer: 'Yes — upgrade or downgrade at any time. Changes take effect immediately; you\'ll be charged or credited on a pro-rated basis.' },
|
|
142
|
-
{ question: 'What counts as a page view?', answer: 'A page view is a single server-rendered HTML response. Static assets, API calls, and navigations that reuse cached HTML are not counted.' },
|
|
143
|
-
{ question: 'Do you offer a free trial?', answer: 'Pro and Team plans include a 14-day free trial — no credit card required. You\'ll get a reminder before the trial ends.' },
|
|
144
|
-
{ question: 'What happens if I exceed my page views?', answer: 'We\'ll notify you when you reach 80% of your limit. Your site stays live — we won\'t cut you off mid-month. Additional usage is billed at £1 per 100k views.' },
|
|
145
|
-
{ question: 'Is there a discount for non-profits?', answer: 'Yes — registered non-profits and open source projects qualify for 50% off any paid plan. Email us with your details.' },
|
|
146
|
-
]
|
|
147
|
-
|
|
148
|
-
export default {
|
|
149
|
-
route: '/pricing',
|
|
150
|
-
|
|
151
|
-
meta: {
|
|
152
|
-
title: 'Pricing — Pulse',
|
|
153
|
-
description: 'Simple, transparent pricing for every stage. Starter is free forever. Pro and Team plans include a 14-day trial.',
|
|
154
|
-
styles: ['/pulse-ui.css', '/examples/pricing.css'],
|
|
155
|
-
theme: 'dark',
|
|
156
|
-
},
|
|
157
|
-
|
|
158
|
-
state: {
|
|
159
|
-
billing: 'monthly', // monthly | annual
|
|
160
|
-
},
|
|
161
|
-
|
|
162
|
-
mutations: {
|
|
163
|
-
setBilling: (_state, e) => ({
|
|
164
|
-
billing: e.target.closest('[data-billing]')?.dataset.billing || 'monthly',
|
|
165
|
-
}),
|
|
166
|
-
},
|
|
167
|
-
|
|
168
|
-
view: (state) => {
|
|
169
|
-
const plans = PLANS[state.billing]
|
|
170
|
-
|
|
171
|
-
return `
|
|
172
|
-
${uiNav({
|
|
173
|
-
logo: `<span class="pr-logo">⚡ Pulse</span>`,
|
|
174
|
-
logoHref: '/pricing',
|
|
175
|
-
links: [
|
|
176
|
-
{ label: 'Counter', href: '/counter' },
|
|
177
|
-
{ label: 'Todos', href: '/todos' },
|
|
178
|
-
{ label: 'Contact', href: '/contact' },
|
|
179
|
-
{ label: 'Quiz', href: '/quiz' },
|
|
180
|
-
{ label: 'Products', href: '/products' },
|
|
181
|
-
{ label: 'Pricing', href: '/pricing' },
|
|
182
|
-
],
|
|
183
|
-
sticky: true,
|
|
184
|
-
})}
|
|
185
|
-
|
|
186
|
-
<main id="main-content">
|
|
187
|
-
|
|
188
|
-
${uiSection({ content: container({ size: 'lg', content: stack({ gap: 'lg', align: 'center', content: `
|
|
189
|
-
|
|
190
|
-
<div class="pr-eyebrow">Pricing</div>
|
|
191
|
-
<h1 class="pr-hero-title">Simple, honest pricing.</h1>
|
|
192
|
-
<p class="pr-hero-sub">Starter is free forever. Paid plans include a 14-day trial — no credit card required.</p>
|
|
193
|
-
|
|
194
|
-
<div class="pr-toggle" role="group" aria-label="Billing period">
|
|
195
|
-
${button({ label: 'Monthly', variant: state.billing === 'monthly' ? 'primary' : 'ghost', size: 'sm', attrs: { 'data-event': 'setBilling', 'data-billing': 'monthly', 'aria-pressed': String(state.billing === 'monthly') } })}
|
|
196
|
-
${button({ label: 'Annual', variant: state.billing === 'annual' ? 'primary' : 'ghost', size: 'sm', iconAfter: '<span class="pr-save-label">Save 25%</span>', attrs: { 'data-event': 'setBilling', 'data-billing': 'annual', 'aria-pressed': String(state.billing === 'annual') } })}
|
|
197
|
-
</div>
|
|
198
|
-
|
|
199
|
-
${state.billing === 'annual' ? `<p class="pr-annual-note">Annual plans are billed yearly. Prices shown are monthly equivalents.</p>` : ''}
|
|
200
|
-
|
|
201
|
-
<div class="pr-plans">
|
|
202
|
-
${plans.map(p => pricingCard({ ...p, level: 2 })).join('')}
|
|
203
|
-
</div>
|
|
204
|
-
|
|
205
|
-
` }) }) })}
|
|
206
|
-
|
|
207
|
-
${uiSection({ variant: 'alt', content: container({ size: 'md', content: stack({ gap: 'xl', align: 'center', content: `
|
|
208
|
-
|
|
209
|
-
${heading({ level: 2, text: 'Everything you need to ship', size: '3xl', class: 'u-text-center' })}
|
|
210
|
-
|
|
211
|
-
<div class="pr-feature-table" role="region" aria-label="Feature comparison">
|
|
212
|
-
<div class="pr-feature-header">
|
|
213
|
-
<span>Feature</span>
|
|
214
|
-
<span>Starter</span>
|
|
215
|
-
<span>Pro</span>
|
|
216
|
-
<span>Team</span>
|
|
217
|
-
</div>
|
|
218
|
-
${[
|
|
219
|
-
['Streaming SSR', '✓', '✓', '✓'],
|
|
220
|
-
['Security headers', '✓', '✓', '✓'],
|
|
221
|
-
['Immutable asset caching','✓', '✓', '✓'],
|
|
222
|
-
['Custom domains', '—', '—', '✓'],
|
|
223
|
-
['Global CDN', '—', '✓', '✓'],
|
|
224
|
-
['Team seats', '1', '3', 'Unlimited'],
|
|
225
|
-
['Support', 'Community', 'Email', 'Priority + SLA'],
|
|
226
|
-
].map(([feat, ...vals]) => `
|
|
227
|
-
<div class="pr-feature-row">
|
|
228
|
-
<span class="pr-feature-name">${feat}</span>
|
|
229
|
-
${vals.map(v => `<span class="pr-feature-val${v === '—' ? ' pr-feature-val--no' : v === '✓' ? ' pr-feature-val--yes' : ''}">${v}</span>`).join('')}
|
|
230
|
-
</div>`).join('')}
|
|
231
|
-
</div>
|
|
232
|
-
|
|
233
|
-
` }) }) })}
|
|
234
|
-
|
|
235
|
-
${uiSection({ content: container({ size: 'md', content: stack({ gap: 'lg', align: 'center', content: `
|
|
236
|
-
${heading({ level: 2, text: 'Frequently asked questions', size: '3xl', class: 'u-text-center' })}
|
|
237
|
-
${accordion({ items: FAQ })}
|
|
238
|
-
` }) }) })}
|
|
239
|
-
|
|
240
|
-
${cta({
|
|
241
|
-
title: 'Ready to build?',
|
|
242
|
-
content: 'Starter is free forever. No credit card. No catch.',
|
|
243
|
-
actions: cluster({ justify: 'center', content:
|
|
244
|
-
button({ label: 'Get started free', href: '/getting-started', size: 'lg' }) + ' ' +
|
|
245
|
-
button({ label: 'Read the docs', href: '/getting-started', variant: 'secondary', size: 'lg' })
|
|
246
|
-
}),
|
|
247
|
-
})}
|
|
248
|
-
|
|
249
|
-
</main>
|
|
250
|
-
|
|
251
|
-
${uiFooter({
|
|
252
|
-
logo: '<span class="pr-logo">⚡ Pulse</span>',
|
|
253
|
-
tagline: 'The spec-first web framework.',
|
|
254
|
-
columns: [
|
|
255
|
-
{ heading: 'Product', links: [{ label: 'Counter', href: '/counter' }, { label: 'Todos', href: '/todos' }, { label: 'Contact', href: '/contact' }] },
|
|
256
|
-
{ heading: 'Examples', links: [{ label: 'Quiz', href: '/quiz' }, { label: 'Products', href: '/products' }] },
|
|
257
|
-
{ heading: 'Docs', links: [{ label: 'Getting Started', href: '/getting-started' }, { label: 'Spec Reference', href: '/spec' }] },
|
|
258
|
-
],
|
|
259
|
-
copy: `© ${new Date().getFullYear()} Pulse. Built with Pulse.`,
|
|
260
|
-
})}`
|
|
261
|
-
},
|
|
262
|
-
}
|
package/examples/products.js
DELETED
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse — Product catalog example
|
|
3
|
-
*
|
|
4
|
-
* Demonstrates:
|
|
5
|
-
* - Server data (20 mock products fetched before render)
|
|
6
|
-
* - Client-side search, category filter, and sort — all via mutations
|
|
7
|
-
* - Streaming SSR (shell instant, product grid deferred)
|
|
8
|
-
* - Empty state when no results match
|
|
9
|
-
*
|
|
10
|
-
* Run: node examples/dev.server.js → http://localhost:3001/products
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { badge, button, empty, card, heading, search, select, uiImage } from '../src/ui/index.js'
|
|
14
|
-
import { examplesNav } from './shared.js'
|
|
15
|
-
|
|
16
|
-
function esc(s) {
|
|
17
|
-
return String(s)
|
|
18
|
-
.replace(/&/g, '&').replace(/</g, '<')
|
|
19
|
-
.replace(/>/g, '>').replace(/"/g, '"')
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// ─── Mock data ───────────────────────────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
const PRODUCTS = [
|
|
25
|
-
{ id: 1, name: 'Arc Desk Lamp', category: 'Lighting', price: 89, rating: 4.8, reviews: 312, img: 'https://picsum.photos/seed/desklamp/400/280', featured: true },
|
|
26
|
-
{ id: 2, name: 'Walnut Desk Tray', category: 'Storage', price: 45, rating: 4.7, reviews: 189, img: 'https://picsum.photos/seed/desktray/400/280', featured: false },
|
|
27
|
-
{ id: 3, name: 'Linen Desk Pad', category: 'Accessories', price: 32, rating: 4.6, reviews: 445, img: 'https://picsum.photos/seed/deskpad/400/280', featured: false },
|
|
28
|
-
{ id: 4, name: 'Cable Management Kit', category: 'Accessories', price: 24, rating: 4.5, reviews: 678, img: 'https://picsum.photos/seed/cables/400/280', featured: false },
|
|
29
|
-
{ id: 5, name: 'Monitor Riser', category: 'Furniture', price: 65, rating: 4.9, reviews: 521, img: 'https://picsum.photos/seed/monitorriser/400/280', featured: true },
|
|
30
|
-
{ id: 6, name: 'Ergonomic Chair Cushion', category: 'Furniture', price: 79, rating: 4.4, reviews: 234, img: 'https://picsum.photos/seed/cushion/400/280', featured: false },
|
|
31
|
-
{ id: 7, name: 'Wireless Charger Pad', category: 'Accessories', price: 38, rating: 4.3, reviews: 390, img: 'https://picsum.photos/seed/charger/400/280', featured: false },
|
|
32
|
-
{ id: 8, name: 'Bamboo Shelf Organiser', category: 'Storage', price: 55, rating: 4.6, reviews: 167, img: 'https://picsum.photos/seed/bamboo/400/280', featured: false },
|
|
33
|
-
{ id: 9, name: 'Mechanical Keyboard', category: 'Peripherals', price: 129, rating: 4.9, reviews: 891, img: 'https://picsum.photos/seed/keyboard/400/280', featured: true },
|
|
34
|
-
{ id: 10, name: 'Wrist Rest Pad', category: 'Accessories', price: 29, rating: 4.2, reviews: 276, img: 'https://picsum.photos/seed/wristrest/400/280', featured: false },
|
|
35
|
-
{ id: 11, name: 'Desk Plant Pot', category: 'Decor', price: 22, rating: 4.7, reviews: 152, img: 'https://picsum.photos/seed/plantpot/400/280', featured: false },
|
|
36
|
-
{ id: 12, name: 'Anti-Fatigue Mat', category: 'Furniture', price: 94, rating: 4.5, reviews: 308, img: 'https://picsum.photos/seed/floormat/400/280', featured: false },
|
|
37
|
-
{ id: 13, name: 'USB-C Hub 7-in-1', category: 'Peripherals', price: 69, rating: 4.8, reviews: 622, img: 'https://picsum.photos/seed/usbhub/400/280', featured: false },
|
|
38
|
-
{ id: 14, name: 'Sticky Note Dispenser', category: 'Storage', price: 18, rating: 4.1, reviews: 99, img: 'https://picsum.photos/seed/stickynote/400/280', featured: false },
|
|
39
|
-
{ id: 15, name: 'LED Bias Light Strip', category: 'Lighting', price: 42, rating: 4.6, reviews: 287, img: 'https://picsum.photos/seed/ledstrip/400/280', featured: false },
|
|
40
|
-
{ id: 16, name: 'Laptop Stand', category: 'Furniture', price: 58, rating: 4.7, reviews: 734, img: 'https://picsum.photos/seed/laptopstand/400/280', featured: true },
|
|
41
|
-
{ id: 17, name: 'Noise Cancelling Muffs', category: 'Peripherals', price: 49, rating: 4.4, reviews: 213, img: 'https://picsum.photos/seed/headphones/400/280', featured: false },
|
|
42
|
-
{ id: 18, name: 'Desktop Calendar', category: 'Decor', price: 16, rating: 4.3, reviews: 88, img: 'https://picsum.photos/seed/calendar/400/280', featured: false },
|
|
43
|
-
{ id: 19, name: 'Document Scanner', category: 'Peripherals', price: 149, rating: 4.8, reviews: 445, img: 'https://picsum.photos/seed/scanner/400/280', featured: false },
|
|
44
|
-
{ id: 20, name: 'Velvet Pen Cup', category: 'Decor', price: 19, rating: 4.5, reviews: 131, img: 'https://picsum.photos/seed/pencup/400/280', featured: false },
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
export const CATEGORIES = ['All', ...new Set(PRODUCTS.map(p => p.category))].sort((a, b) =>
|
|
48
|
-
a === 'All' ? -1 : b === 'All' ? 1 : a.localeCompare(b)
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
export function applyFilters(products, search, category, sort) {
|
|
52
|
-
let out = products
|
|
53
|
-
|
|
54
|
-
if (category && category !== 'All') {
|
|
55
|
-
out = out.filter(p => p.category === category)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (search) {
|
|
59
|
-
const q = search.toLowerCase()
|
|
60
|
-
out = out.filter(p => p.name.toLowerCase().includes(q) || p.category.toLowerCase().includes(q))
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (sort === 'price-asc') out = [...out].sort((a, b) => a.price - b.price)
|
|
64
|
-
if (sort === 'price-desc') out = [...out].sort((a, b) => b.price - a.price)
|
|
65
|
-
if (sort === 'rating') out = [...out].sort((a, b) => b.rating - a.rating)
|
|
66
|
-
if (sort === 'reviews') out = [...out].sort((a, b) => b.reviews - a.reviews)
|
|
67
|
-
|
|
68
|
-
return out
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ─── Spec ────────────────────────────────────────────────────────────────────
|
|
72
|
-
|
|
73
|
-
export default {
|
|
74
|
-
route: '/products',
|
|
75
|
-
|
|
76
|
-
meta: {
|
|
77
|
-
title: 'Products — Pulse',
|
|
78
|
-
description: 'A product catalog built with Pulse. Demonstrates server data, client-side filtering, search, and sort.',
|
|
79
|
-
styles: ['/pulse-ui.css', '/examples/products.css'],
|
|
80
|
-
theme: 'light',
|
|
81
|
-
},
|
|
82
|
-
|
|
83
|
-
server: {
|
|
84
|
-
products: async () => PRODUCTS,
|
|
85
|
-
},
|
|
86
|
-
|
|
87
|
-
stream: {
|
|
88
|
-
shell: ['header', 'filters'],
|
|
89
|
-
deferred: ['grid'],
|
|
90
|
-
},
|
|
91
|
-
|
|
92
|
-
state: {
|
|
93
|
-
search: '',
|
|
94
|
-
category: 'All',
|
|
95
|
-
sort: 'featured',
|
|
96
|
-
},
|
|
97
|
-
|
|
98
|
-
mutations: {
|
|
99
|
-
setSearch: (_state, e) => ({ search: e.target.value }),
|
|
100
|
-
setCategory: (_state, e) => ({ category: e.target.closest('[data-cat]')?.dataset.cat || 'All' }),
|
|
101
|
-
setSort: (_state, e) => ({ sort: e.target.value }),
|
|
102
|
-
clearSearch: () => ({ search: '' }),
|
|
103
|
-
},
|
|
104
|
-
|
|
105
|
-
view: {
|
|
106
|
-
header: (state, server) => {
|
|
107
|
-
const total = server.products?.length ?? 0
|
|
108
|
-
const filtered = applyFilters(server.products ?? [], state.search, state.category, state.sort)
|
|
109
|
-
|
|
110
|
-
return `<div class="pd-root">
|
|
111
|
-
${examplesNav('<span class="pd-logo-text">🛍 Products</span>', '/products')}
|
|
112
|
-
|
|
113
|
-
<main id="main-content" class="pd-main">
|
|
114
|
-
|
|
115
|
-
<div class="pd-page-header">
|
|
116
|
-
<div>
|
|
117
|
-
${heading({ level: 1, text: 'Home Office', size: '2xl' })}
|
|
118
|
-
<p class="pd-subtitle">Showing <strong>${filtered.length}</strong> of <strong>${total}</strong> products</p>
|
|
119
|
-
</div>
|
|
120
|
-
<div class="pd-sort-wrap">
|
|
121
|
-
${select({ name: 'sort', label: 'Sort by', event: 'change:setSort', value: state.sort, options: [
|
|
122
|
-
{ value: 'featured', label: 'Featured' },
|
|
123
|
-
{ value: 'price-asc', label: 'Price: low to high' },
|
|
124
|
-
{ value: 'price-desc', label: 'Price: high to low' },
|
|
125
|
-
{ value: 'rating', label: 'Top rated' },
|
|
126
|
-
{ value: 'reviews', label: 'Most reviewed' },
|
|
127
|
-
]})}
|
|
128
|
-
</div>
|
|
129
|
-
</div>`
|
|
130
|
-
},
|
|
131
|
-
|
|
132
|
-
filters: (state) => {
|
|
133
|
-
return `
|
|
134
|
-
<div class="pd-sidebar">
|
|
135
|
-
|
|
136
|
-
${search({ name: 'search', label: 'Search', placeholder: 'Search products…', value: state.search, event: 'input:setSearch', debounce: 200, clearEvent: 'clearSearch' })}
|
|
137
|
-
|
|
138
|
-
<div class="pd-category-wrap">
|
|
139
|
-
<p class="pd-sidebar-label">Category</p>
|
|
140
|
-
<ul class="pd-categories" role="list">
|
|
141
|
-
${CATEGORIES.map(cat => `<li>
|
|
142
|
-
${button({ label: cat, variant: state.category === cat ? 'primary' : 'secondary', size: 'sm', class: 'pd-cat-pill', attrs: { 'data-event': 'setCategory', 'data-cat': cat, 'aria-pressed': String(state.category === cat) } })}
|
|
143
|
-
</li>`).join('')}
|
|
144
|
-
</ul>
|
|
145
|
-
</div>
|
|
146
|
-
|
|
147
|
-
</div>`
|
|
148
|
-
},
|
|
149
|
-
|
|
150
|
-
grid: (state, server) => {
|
|
151
|
-
const filtered = applyFilters(server.products ?? [], state.search, state.category, state.sort)
|
|
152
|
-
|
|
153
|
-
const gridContent = filtered.length === 0
|
|
154
|
-
? empty({
|
|
155
|
-
title: 'No products found',
|
|
156
|
-
description: state.search ? `No results for "${esc(state.search)}".` : 'Try a different category.',
|
|
157
|
-
})
|
|
158
|
-
: `<ul class="pd-grid" aria-label="Products">
|
|
159
|
-
${filtered.map(p => `<li>
|
|
160
|
-
${card({
|
|
161
|
-
flush: true,
|
|
162
|
-
content: `
|
|
163
|
-
${uiImage({ src: p.img, alt: p.name, ratio: '16/9' })}
|
|
164
|
-
<div class="pd-product-body">
|
|
165
|
-
<div class="pd-product-meta">
|
|
166
|
-
${badge({ label: p.category, variant: 'default' })}
|
|
167
|
-
${p.featured ? badge({ label: 'Featured', variant: 'info' }) : ''}
|
|
168
|
-
</div>
|
|
169
|
-
${heading({ level: 2, text: p.name, size: 'sm' })}
|
|
170
|
-
<div class="pd-product-stats">
|
|
171
|
-
<span class="pd-rating" aria-label="${p.rating} out of 5 stars">★ ${p.rating}</span>
|
|
172
|
-
<span class="pd-reviews">(${p.reviews.toLocaleString()})</span>
|
|
173
|
-
</div>
|
|
174
|
-
<p class="pd-price">£${p.price}</p>
|
|
175
|
-
</div>
|
|
176
|
-
`,
|
|
177
|
-
})}
|
|
178
|
-
</li>`).join('')}
|
|
179
|
-
</ul>`
|
|
180
|
-
|
|
181
|
-
return `
|
|
182
|
-
<div class="pd-content">
|
|
183
|
-
${gridContent}
|
|
184
|
-
</div>
|
|
185
|
-
|
|
186
|
-
</main>
|
|
187
|
-
</div>`
|
|
188
|
-
},
|
|
189
|
-
},
|
|
190
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.14.1
|