@invisibleloop/pulse 0.1.21
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/build-page.md +59 -0
- package/.claude/commands/new-doc-page.md +45 -0
- package/.claude/commands/verify.md +52 -0
- package/.claude/pulse-checklist.md +111 -0
- package/.claude/settings.local.json +102 -0
- package/.github/workflows/ci.yml +22 -0
- package/.github/workflows/publish.yml +41 -0
- package/.pulse/load-reports/home/1773432711417.json +22 -0
- package/CLAUDE.md +383 -0
- package/README.md +95 -0
- package/docs/.claude/pulse-checklist.md +111 -0
- package/docs/public/.pulse-ui-version +1 -0
- package/docs/public/dist/accessibility.boot-5DVTARJU.js +115 -0
- package/docs/public/dist/actions.boot-P66HKQEM.js +164 -0
- package/docs/public/dist/auth.boot-IMAJAUPH.js +140 -0
- package/docs/public/dist/caching.boot-DVR6KDE7.js +53 -0
- package/docs/public/dist/components--accordion.boot-3HVKMNWC.js +11 -0
- package/docs/public/dist/components--alert.boot-GCEXOZAC.js +6 -0
- package/docs/public/dist/components--app-badge.boot-DVT3GCHJ.js +6 -0
- package/docs/public/dist/components--avatar.boot-PSW24EVA.js +5 -0
- package/docs/public/dist/components--badge.boot-TYDY2RMK.js +7 -0
- package/docs/public/dist/components--banner.boot-EI5PZSZK.js +7 -0
- package/docs/public/dist/components--breadcrumbs.boot-SMA2E2GO.js +34 -0
- package/docs/public/dist/components--button.boot-J54BQM2E.js +23 -0
- package/docs/public/dist/components--card.boot-PZGNDIB6.js +138 -0
- package/docs/public/dist/components--carousel.boot-TP6LPFZZ.js +12 -0
- package/docs/public/dist/components--charts.boot-2EOYQWKL.js +108 -0
- package/docs/public/dist/components--checkbox.boot-DS5BSL6T.js +54 -0
- package/docs/public/dist/components--cluster.boot-HHVIBBJG.js +9 -0
- package/docs/public/dist/components--code-window.boot-2GR2DV33.js +20 -0
- package/docs/public/dist/components--container.boot-7LOOGK2K.js +5 -0
- package/docs/public/dist/components--cta.boot-FSNZ5YRT.js +11 -0
- package/docs/public/dist/components--divider.boot-3NI2C3QG.js +6 -0
- package/docs/public/dist/components--empty.boot-YX2UR3PV.js +7 -0
- package/docs/public/dist/components--feature.boot-MUD7NSUO.js +13 -0
- package/docs/public/dist/components--fieldset.boot-J7BYHMKF.js +19 -0
- package/docs/public/dist/components--fileupload.boot-NIKVTTPD.js +52 -0
- package/docs/public/dist/components--footer.boot-EYUK5FRG.js +14 -0
- package/docs/public/dist/components--grid.boot-URDQVDDR.js +59 -0
- package/docs/public/dist/components--heading.boot-BPQKU43E.js +44 -0
- package/docs/public/dist/components--hero.boot-4RAPRGAB.js +17 -0
- package/docs/public/dist/components--icons.boot-ZITNU5JP.js +68 -0
- package/docs/public/dist/components--image.boot-XEEGHQZF.js +19 -0
- package/docs/public/dist/components--input.boot-SGASZG5K.js +7 -0
- package/docs/public/dist/components--list.boot-W3XC5MHD.js +55 -0
- package/docs/public/dist/components--media.boot-5VFIETZO.js +13 -0
- package/docs/public/dist/components--modal.boot-RZUYXBN2.js +47 -0
- package/docs/public/dist/components--nav.boot-ODBOHU7O.js +33 -0
- package/docs/public/dist/components--pricing.boot-4AQ4ZVBY.js +21 -0
- package/docs/public/dist/components--progress.boot-GHAGYZOK.js +30 -0
- package/docs/public/dist/components--prose.boot-QANJL6JI.js +67 -0
- package/docs/public/dist/components--pullquote.boot-Q2WMNAZU.js +22 -0
- package/docs/public/dist/components--radio.boot-TJRDQ2OL.js +75 -0
- package/docs/public/dist/components--rating.boot-QBAN6DEL.js +38 -0
- package/docs/public/dist/components--search.boot-PXH5O5AG.js +17 -0
- package/docs/public/dist/components--section.boot-AQGIYHWW.js +12 -0
- package/docs/public/dist/components--segmented.boot-BEVTKEJO.js +33 -0
- package/docs/public/dist/components--select.boot-47X5RHOC.js +10 -0
- package/docs/public/dist/components--slider.boot-PSRRX7XL.js +47 -0
- package/docs/public/dist/components--spinner.boot-MZ5MO2OH.js +22 -0
- package/docs/public/dist/components--stack.boot-DI4NJXBF.js +9 -0
- package/docs/public/dist/components--stat.boot-QMFUWBQT.js +9 -0
- package/docs/public/dist/components--stepper.boot-34PP2NEV.js +22 -0
- package/docs/public/dist/components--table.boot-FCQGSFIQ.js +11 -0
- package/docs/public/dist/components--testimonial.boot-DWQPDKYG.js +11 -0
- package/docs/public/dist/components--textarea.boot-QVXLBOJ5.js +4 -0
- package/docs/public/dist/components--timeline.boot-26LN52P2.js +95 -0
- package/docs/public/dist/components--toggle.boot-IQQEI76S.js +29 -0
- package/docs/public/dist/components--tooltip.boot-LGHCO6NN.js +9 -0
- package/docs/public/dist/components.boot-SE6PQ4P7.js +103 -0
- package/docs/public/dist/config.boot-DTRRWUE6.js +126 -0
- package/docs/public/dist/constraints.boot-DUHDZBMC.js +71 -0
- package/docs/public/dist/deploy.boot-SLAD3NI2.js +163 -0
- package/docs/public/dist/docs-8e3d4b5c.css +1 -0
- package/docs/public/dist/extending.boot-UA3CN243.js +159 -0
- package/docs/public/dist/faq.boot-6EQAWLQR.js +43 -0
- package/docs/public/dist/getting-started.boot-TDKIFL5U.js +86 -0
- package/docs/public/dist/guard.boot-AUHAWTG4.js +80 -0
- package/docs/public/dist/home.boot-BVQXRH32.js +383 -0
- package/docs/public/dist/how-it-works.boot-LTWAKWKW.js +104 -0
- package/docs/public/dist/hydration.boot-JRM6IPJL.js +78 -0
- package/docs/public/dist/images.boot-M6ZVKTZS.js +80 -0
- package/docs/public/dist/manifest.json +94 -0
- package/docs/public/dist/meta.boot-7NXGPHR4.js +79 -0
- package/docs/public/dist/mutations.boot-F6F43UDX.js +79 -0
- package/docs/public/dist/navigation.boot-AOXWS3ZF.js +57 -0
- package/docs/public/dist/performance.boot-C3UPCOBK.js +98 -0
- package/docs/public/dist/persist.boot-WT32PQOQ.js +61 -0
- package/docs/public/dist/project-structure.boot-FB3LRVJ4.js +63 -0
- package/docs/public/dist/prompt-examples.boot-YKR4VDK4.js +31 -0
- package/docs/public/dist/pulse-ui-81a85c03.css +1 -0
- package/docs/public/dist/raw-responses.boot-M4KA5YXL.js +104 -0
- package/docs/public/dist/routing.boot-FNX5FDGH.js +70 -0
- package/docs/public/dist/runtime-B73WLANC.js +1 -0
- package/docs/public/dist/runtime-KO4BHUQ3.js +49 -0
- package/docs/public/dist/runtime-L2HNXIHW.js +59 -0
- package/docs/public/dist/runtime-QFURDKA2.js +5 -0
- package/docs/public/dist/runtime-UVPXO4IR.js +375 -0
- package/docs/public/dist/runtime-VMJA3Z4N.js +10 -0
- package/docs/public/dist/runtime-ZJ4FXT5O.js +11 -0
- package/docs/public/dist/server-api.boot-K7X3LCFB.js +219 -0
- package/docs/public/dist/server-data.boot-Y7HQYC4R.js +157 -0
- package/docs/public/dist/slash-commands.boot-V2UV7OW2.js +26 -0
- package/docs/public/dist/spec.boot-2WU7ZHCV.js +159 -0
- package/docs/public/dist/state.boot-B24GUE3R.js +73 -0
- package/docs/public/dist/store.boot-TLIB4XHH.js +150 -0
- package/docs/public/dist/streaming.boot-W2DZSMW4.js +80 -0
- package/docs/public/dist/stripe.boot-QN3C2GEL.js +164 -0
- package/docs/public/dist/supabase.boot-BG4XXLZE.js +303 -0
- package/docs/public/dist/testing.boot-6U4WKMTE.js +130 -0
- package/docs/public/dist/validation.boot-PQHYGW5B.js +100 -0
- package/docs/public/docs.css +2020 -0
- package/docs/public/menu.js +83 -0
- package/docs/public/pulse-ui.css +2739 -0
- package/docs/public/pulse-ui.js +236 -0
- package/docs/server.js +192 -0
- package/docs/src/lib/component-page.js +47 -0
- package/docs/src/lib/highlight.js +255 -0
- package/docs/src/lib/layout.js +131 -0
- package/docs/src/lib/metrics-store.js +6 -0
- package/docs/src/lib/nav.js +159 -0
- package/docs/src/lib/stats.js +81 -0
- package/docs/src/pages/accessibility.js +157 -0
- package/docs/src/pages/actions.js +191 -0
- package/docs/src/pages/auth.js +177 -0
- package/docs/src/pages/caching.js +95 -0
- package/docs/src/pages/components/accordion.js +48 -0
- package/docs/src/pages/components/alert.js +35 -0
- package/docs/src/pages/components/app-badge.js +41 -0
- package/docs/src/pages/components/avatar.js +35 -0
- package/docs/src/pages/components/badge.js +36 -0
- package/docs/src/pages/components/banner.js +45 -0
- package/docs/src/pages/components/breadcrumbs.js +94 -0
- package/docs/src/pages/components/button.js +84 -0
- package/docs/src/pages/components/card.js +225 -0
- package/docs/src/pages/components/carousel.js +72 -0
- package/docs/src/pages/components/charts.js +278 -0
- package/docs/src/pages/components/checkbox.js +129 -0
- package/docs/src/pages/components/cluster.js +47 -0
- package/docs/src/pages/components/code-window.js +57 -0
- package/docs/src/pages/components/container.js +40 -0
- package/docs/src/pages/components/cta.js +53 -0
- package/docs/src/pages/components/divider.js +37 -0
- package/docs/src/pages/components/empty.js +36 -0
- package/docs/src/pages/components/feature.js +60 -0
- package/docs/src/pages/components/fieldset.js +65 -0
- package/docs/src/pages/components/fileupload.js +127 -0
- package/docs/src/pages/components/footer.js +58 -0
- package/docs/src/pages/components/grid.js +165 -0
- package/docs/src/pages/components/heading.js +107 -0
- package/docs/src/pages/components/hero.js +65 -0
- package/docs/src/pages/components/icons.js +285 -0
- package/docs/src/pages/components/image.js +71 -0
- package/docs/src/pages/components/input.js +51 -0
- package/docs/src/pages/components/list.js +112 -0
- package/docs/src/pages/components/media.js +51 -0
- package/docs/src/pages/components/modal.js +111 -0
- package/docs/src/pages/components/nav.js +86 -0
- package/docs/src/pages/components/pricing.js +68 -0
- package/docs/src/pages/components/progress.js +102 -0
- package/docs/src/pages/components/prose.js +111 -0
- package/docs/src/pages/components/pullquote.js +71 -0
- package/docs/src/pages/components/radio.js +194 -0
- package/docs/src/pages/components/rating.js +106 -0
- package/docs/src/pages/components/search.js +61 -0
- package/docs/src/pages/components/section.js +59 -0
- package/docs/src/pages/components/segmented.js +121 -0
- package/docs/src/pages/components/select.js +45 -0
- package/docs/src/pages/components/slider.js +114 -0
- package/docs/src/pages/components/spinner.js +73 -0
- package/docs/src/pages/components/stack.js +48 -0
- package/docs/src/pages/components/stat.js +55 -0
- package/docs/src/pages/components/stepper.js +66 -0
- package/docs/src/pages/components/table.js +45 -0
- package/docs/src/pages/components/testimonial.js +49 -0
- package/docs/src/pages/components/textarea.js +31 -0
- package/docs/src/pages/components/timeline.js +227 -0
- package/docs/src/pages/components/toggle.js +84 -0
- package/docs/src/pages/components/tooltip.js +48 -0
- package/docs/src/pages/components.js +204 -0
- package/docs/src/pages/config.js +193 -0
- package/docs/src/pages/constraints.js +99 -0
- package/docs/src/pages/deploy.js +233 -0
- package/docs/src/pages/extending.js +198 -0
- package/docs/src/pages/faq.js +96 -0
- package/docs/src/pages/getting-started.js +106 -0
- package/docs/src/pages/guard.js +121 -0
- package/docs/src/pages/home.js +401 -0
- package/docs/src/pages/how-it-works.js +183 -0
- package/docs/src/pages/hydration.js +98 -0
- package/docs/src/pages/images.js +121 -0
- package/docs/src/pages/meta.js +120 -0
- package/docs/src/pages/mutations.js +106 -0
- package/docs/src/pages/navigation.js +85 -0
- package/docs/src/pages/performance.js +157 -0
- package/docs/src/pages/persist.js +88 -0
- package/docs/src/pages/project-structure.js +90 -0
- package/docs/src/pages/prompt-examples.js +186 -0
- package/docs/src/pages/raw-responses.js +124 -0
- package/docs/src/pages/routing.js +99 -0
- package/docs/src/pages/server-api.js +281 -0
- package/docs/src/pages/server-data.js +185 -0
- package/docs/src/pages/slash-commands.js +55 -0
- package/docs/src/pages/spec.js +207 -0
- package/docs/src/pages/state.js +101 -0
- package/docs/src/pages/store.js +181 -0
- package/docs/src/pages/streaming.js +108 -0
- package/docs/src/pages/stripe.js +193 -0
- package/docs/src/pages/supabase.js +323 -0
- package/docs/src/pages/testing.js +198 -0
- package/docs/src/pages/validation.js +138 -0
- package/examples/contact.js +166 -0
- package/examples/counter.js +94 -0
- package/examples/dev.server.js +91 -0
- package/examples/examples.test.js +394 -0
- package/examples/pricing.js +244 -0
- package/examples/products.js +191 -0
- package/examples/quiz.js +208 -0
- package/examples/shared.js +78 -0
- package/examples/todos.js +162 -0
- package/package.json +75 -0
- package/public/.pulse-ui-version +1 -0
- package/public/chippy-bird.css +246 -0
- package/public/examples/contact.css +119 -0
- package/public/examples/counter.css +79 -0
- package/public/examples/pricing.css +132 -0
- package/public/examples/products.css +100 -0
- package/public/examples/quiz.css +200 -0
- package/public/examples/todos.css +137 -0
- package/public/favicon.ico +0 -0
- package/public/log-dashboard.css +383 -0
- package/public/pulse-ui.css +2740 -0
- package/public/pulse-ui.js +236 -0
- package/public/pulse.css +149 -0
- package/scripts/build.js +411 -0
- package/src/agent/checklist.md +111 -0
- package/src/agent/coverage-check.js +66 -0
- package/src/agent/guide-components.md +274 -0
- package/src/agent/guide-examples.md +54 -0
- package/src/agent/guide-routing.md +36 -0
- package/src/agent/guide-server.md +258 -0
- package/src/agent/guide-spec.md +103 -0
- package/src/agent/guide-styles.md +191 -0
- package/src/agent/guide.md +979 -0
- package/src/agent/identity.md +106 -0
- package/src/agent/workflow.md +108 -0
- package/src/cli/cli.test.js +82 -0
- package/src/cli/dev.js +195 -0
- package/src/cli/discover.js +113 -0
- package/src/cli/index.js +361 -0
- package/src/cli/load-report.js +91 -0
- package/src/cli/load-runner.js +121 -0
- package/src/cli/report-server.js +723 -0
- package/src/cli/report.js +116 -0
- package/src/cli/scaffold.archive.js +1371 -0
- package/src/cli/scaffold.js +349 -0
- package/src/cli/start.js +74 -0
- package/src/html.js +19 -0
- package/src/mcp/server.js +884 -0
- package/src/mcp/validate-worker.js +110 -0
- package/src/runtime/image.js +74 -0
- package/src/runtime/image.test.js +111 -0
- package/src/runtime/index.js +621 -0
- package/src/runtime/navigate.js +146 -0
- package/src/runtime/runtime.test.js +773 -0
- package/src/runtime/ssr.js +464 -0
- package/src/runtime/ssr.test.js +421 -0
- package/src/runtime/store.js +92 -0
- package/src/runtime/toast.js +163 -0
- package/src/server/index.js +1386 -0
- package/src/server/server.test.js +1248 -0
- package/src/spec/schema.js +428 -0
- package/src/spec/schema.test.js +291 -0
- package/src/store/index.js +102 -0
- package/src/store/store.test.js +210 -0
- package/src/testing/html.js +283 -0
- package/src/testing/index.js +249 -0
- package/src/testing/testing.test.js +450 -0
- package/src/ui/accordion.js +28 -0
- package/src/ui/alert.js +43 -0
- package/src/ui/app-badge.js +48 -0
- package/src/ui/avatar.js +47 -0
- package/src/ui/badge.js +24 -0
- package/src/ui/banner.js +26 -0
- package/src/ui/breadcrumbs.js +38 -0
- package/src/ui/button.js +66 -0
- package/src/ui/card.js +34 -0
- package/src/ui/carousel.js +59 -0
- package/src/ui/charts.js +321 -0
- package/src/ui/checkbox.js +65 -0
- package/src/ui/cluster.js +44 -0
- package/src/ui/code-window.js +39 -0
- package/src/ui/container.js +24 -0
- package/src/ui/cta.js +37 -0
- package/src/ui/divider.js +29 -0
- package/src/ui/empty.js +33 -0
- package/src/ui/feature.js +33 -0
- package/src/ui/fieldset.js +37 -0
- package/src/ui/fileupload.js +89 -0
- package/src/ui/footer.js +38 -0
- package/src/ui/grid.js +36 -0
- package/src/ui/heading.js +45 -0
- package/src/ui/hero.js +37 -0
- package/src/ui/icons.js +161 -0
- package/src/ui/index.js +89 -0
- package/src/ui/input.js +74 -0
- package/src/ui/list.js +36 -0
- package/src/ui/media.js +44 -0
- package/src/ui/modal.js +80 -0
- package/src/ui/nav.js +61 -0
- package/src/ui/pricing.js +56 -0
- package/src/ui/progress.js +62 -0
- package/src/ui/prose.js +29 -0
- package/src/ui/pullquote.js +34 -0
- package/src/ui/radio.js +102 -0
- package/src/ui/rating.js +93 -0
- package/src/ui/search.js +77 -0
- package/src/ui/section.js +69 -0
- package/src/ui/segmented.js +50 -0
- package/src/ui/select.js +77 -0
- package/src/ui/slider.js +84 -0
- package/src/ui/spinner.js +34 -0
- package/src/ui/stack.js +36 -0
- package/src/ui/stat.js +52 -0
- package/src/ui/stepper.js +46 -0
- package/src/ui/switch.js +57 -0
- package/src/ui/table.js +45 -0
- package/src/ui/testimonial.js +48 -0
- package/src/ui/textarea.js +72 -0
- package/src/ui/timeline.js +72 -0
- package/src/ui/tooltip.js +28 -0
- package/src/ui/ui.test.js +1241 -0
- package/src/ui/uiimage.js +65 -0
- package/tsconfig.json +13 -0
- package/types/html.d.ts +17 -0
- package/types/image.d.ts +70 -0
- package/types/index.d.ts +7 -0
- package/types/navigate.d.ts +38 -0
- package/types/runtime.d.ts +63 -0
- package/types/schema.d.ts +243 -0
- package/types/server.d.ts +145 -0
- package/types/ssr.d.ts +110 -0
- package/types/testing.d.ts +154 -0
- package/types/ui.d.ts +704 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse — Pricing page example
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates:
|
|
5
|
+
* - Landing page components (nav, pricing, accordion, cta, footer)
|
|
6
|
+
* - Billing-period toggle (monthly / annual) via mutation
|
|
7
|
+
* - Layout components (section, container, stack, cluster, grid)
|
|
8
|
+
* - No server data — fully static, still gets streaming SSR
|
|
9
|
+
*
|
|
10
|
+
* Run: node examples/dev.server.js → http://localhost:3001/pricing
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
nav as uiNav, pricing, accordion, cta, footer as uiFooter,
|
|
15
|
+
section as uiSection, container, stack, cluster,
|
|
16
|
+
button, heading,
|
|
17
|
+
} from '../src/ui/index.js'
|
|
18
|
+
|
|
19
|
+
const PLANS = {
|
|
20
|
+
monthly: [
|
|
21
|
+
{
|
|
22
|
+
name: 'Starter',
|
|
23
|
+
price: '£0',
|
|
24
|
+
period: 'forever',
|
|
25
|
+
description: 'For personal projects and tinkering.',
|
|
26
|
+
features: [
|
|
27
|
+
'1 project',
|
|
28
|
+
'5,000 page views / month',
|
|
29
|
+
'Pulse framework',
|
|
30
|
+
'Community support',
|
|
31
|
+
],
|
|
32
|
+
action: button({ label: 'Get started free', variant: 'secondary', href: '/getting-started', fullWidth: true }),
|
|
33
|
+
highlighted: false,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'Pro',
|
|
37
|
+
price: '£12',
|
|
38
|
+
period: '/ month',
|
|
39
|
+
description: 'For indie developers and small teams.',
|
|
40
|
+
badge: 'Most popular',
|
|
41
|
+
features: [
|
|
42
|
+
'10 projects',
|
|
43
|
+
'500,000 page views / month',
|
|
44
|
+
'Everything in Starter',
|
|
45
|
+
'Streaming SSR',
|
|
46
|
+
'Global CDN',
|
|
47
|
+
'Email support',
|
|
48
|
+
],
|
|
49
|
+
action: button({ label: 'Start free trial', variant: 'primary', href: '/getting-started', fullWidth: true }),
|
|
50
|
+
highlighted: true,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'Team',
|
|
54
|
+
price: '£49',
|
|
55
|
+
period: '/ month',
|
|
56
|
+
description: 'For growing teams shipping fast.',
|
|
57
|
+
features: [
|
|
58
|
+
'Unlimited projects',
|
|
59
|
+
'5M page views / month',
|
|
60
|
+
'Everything in Pro',
|
|
61
|
+
'Team collaboration',
|
|
62
|
+
'Custom domains',
|
|
63
|
+
'Priority support',
|
|
64
|
+
'SLA guarantee',
|
|
65
|
+
],
|
|
66
|
+
action: button({ label: 'Contact sales', variant: 'secondary', href: '/contact', fullWidth: true }),
|
|
67
|
+
highlighted: false,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
annual: [
|
|
71
|
+
{
|
|
72
|
+
name: 'Starter',
|
|
73
|
+
price: '£0',
|
|
74
|
+
period: 'forever',
|
|
75
|
+
description: 'For personal projects and tinkering.',
|
|
76
|
+
features: [
|
|
77
|
+
'1 project',
|
|
78
|
+
'5,000 page views / month',
|
|
79
|
+
'Pulse framework',
|
|
80
|
+
'Community support',
|
|
81
|
+
],
|
|
82
|
+
action: button({ label: 'Get started free', variant: 'secondary', href: '/getting-started', fullWidth: true }),
|
|
83
|
+
highlighted: false,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'Pro',
|
|
87
|
+
price: '£9',
|
|
88
|
+
period: '/ month',
|
|
89
|
+
description: 'For indie developers and small teams.',
|
|
90
|
+
badge: 'Most popular',
|
|
91
|
+
features: [
|
|
92
|
+
'10 projects',
|
|
93
|
+
'500,000 page views / month',
|
|
94
|
+
'Everything in Starter',
|
|
95
|
+
'Streaming SSR',
|
|
96
|
+
'Global CDN',
|
|
97
|
+
'Email support',
|
|
98
|
+
],
|
|
99
|
+
action: button({ label: 'Start free trial', variant: 'primary', href: '/getting-started', fullWidth: true }),
|
|
100
|
+
highlighted: true,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'Team',
|
|
104
|
+
price: '£39',
|
|
105
|
+
period: '/ month',
|
|
106
|
+
description: 'For growing teams shipping fast.',
|
|
107
|
+
features: [
|
|
108
|
+
'Unlimited projects',
|
|
109
|
+
'5M page views / month',
|
|
110
|
+
'Everything in Pro',
|
|
111
|
+
'Team collaboration',
|
|
112
|
+
'Custom domains',
|
|
113
|
+
'Priority support',
|
|
114
|
+
'SLA guarantee',
|
|
115
|
+
],
|
|
116
|
+
action: button({ label: 'Contact sales', variant: 'secondary', href: '/contact', fullWidth: true }),
|
|
117
|
+
highlighted: false,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const FAQ = [
|
|
123
|
+
{ 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.' },
|
|
124
|
+
{ 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.' },
|
|
125
|
+
{ 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.' },
|
|
126
|
+
{ 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.' },
|
|
127
|
+
{ 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.' },
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
export default {
|
|
131
|
+
route: '/pricing',
|
|
132
|
+
hydrate: '/examples/pricing.js',
|
|
133
|
+
|
|
134
|
+
meta: {
|
|
135
|
+
title: 'Pricing — Pulse',
|
|
136
|
+
description: 'Simple, transparent pricing for every stage. Starter is free forever. Pro and Team plans include a 14-day trial.',
|
|
137
|
+
styles: ['/pulse-ui.css', '/examples/pricing.css'],
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
state: {
|
|
141
|
+
billing: 'monthly', // monthly | annual
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
mutations: {
|
|
145
|
+
setBilling: (_state, e) => ({
|
|
146
|
+
billing: e.target.closest('[data-billing]')?.dataset.billing || 'monthly',
|
|
147
|
+
}),
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
view: (state) => {
|
|
151
|
+
const plans = PLANS[state.billing]
|
|
152
|
+
|
|
153
|
+
return `
|
|
154
|
+
${uiNav({
|
|
155
|
+
logo: `<span class="pr-logo">⚡ Pulse</span>`,
|
|
156
|
+
logoHref: '/pricing',
|
|
157
|
+
links: [
|
|
158
|
+
{ label: 'Counter', href: '/counter' },
|
|
159
|
+
{ label: 'Todos', href: '/todos' },
|
|
160
|
+
{ label: 'Contact', href: '/contact' },
|
|
161
|
+
{ label: 'Quiz', href: '/quiz' },
|
|
162
|
+
{ label: 'Products', href: '/products' },
|
|
163
|
+
{ label: 'Pricing', href: '/pricing' },
|
|
164
|
+
],
|
|
165
|
+
sticky: true,
|
|
166
|
+
})}
|
|
167
|
+
|
|
168
|
+
<main id="main-content">
|
|
169
|
+
|
|
170
|
+
${uiSection({ content: container({ size: 'lg', content: stack({ gap: 'lg', align: 'center', content: `
|
|
171
|
+
|
|
172
|
+
<div class="pr-eyebrow">Pricing</div>
|
|
173
|
+
<h1 class="pr-hero-title">Simple, honest pricing.</h1>
|
|
174
|
+
<p class="pr-hero-sub">Starter is free forever. Paid plans include a 14-day trial — no credit card required.</p>
|
|
175
|
+
|
|
176
|
+
<div class="pr-toggle" role="group" aria-label="Billing period">
|
|
177
|
+
${button({ label: 'Monthly', variant: state.billing === 'monthly' ? 'primary' : 'ghost', size: 'sm', attrs: { 'data-event': 'setBilling', 'data-billing': 'monthly', 'aria-pressed': String(state.billing === 'monthly') } })}
|
|
178
|
+
${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') } })}
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
${state.billing === 'annual' ? `<p class="pr-annual-note">Annual plans are billed yearly. Prices shown are monthly equivalents.</p>` : ''}
|
|
182
|
+
|
|
183
|
+
<div class="pr-plans">
|
|
184
|
+
${plans.map(p => pricing({ ...p, level: 2 })).join('')}
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
` }) }) })}
|
|
188
|
+
|
|
189
|
+
${uiSection({ variant: 'alt', content: container({ size: 'md', content: stack({ gap: 'xl', align: 'center', content: `
|
|
190
|
+
|
|
191
|
+
${heading({ level: 2, text: 'Everything you need to ship', size: '3xl', class: 'u-text-center' })}
|
|
192
|
+
|
|
193
|
+
<div class="pr-feature-table" role="region" aria-label="Feature comparison">
|
|
194
|
+
<div class="pr-feature-header">
|
|
195
|
+
<span>Feature</span>
|
|
196
|
+
<span>Starter</span>
|
|
197
|
+
<span>Pro</span>
|
|
198
|
+
<span>Team</span>
|
|
199
|
+
</div>
|
|
200
|
+
${[
|
|
201
|
+
['Streaming SSR', '✓', '✓', '✓'],
|
|
202
|
+
['Security headers', '✓', '✓', '✓'],
|
|
203
|
+
['Immutable asset caching','✓', '✓', '✓'],
|
|
204
|
+
['Custom domains', '—', '—', '✓'],
|
|
205
|
+
['Global CDN', '—', '✓', '✓'],
|
|
206
|
+
['Team seats', '1', '3', 'Unlimited'],
|
|
207
|
+
['Support', 'Community', 'Email', 'Priority + SLA'],
|
|
208
|
+
].map(([feat, ...vals]) => `
|
|
209
|
+
<div class="pr-feature-row">
|
|
210
|
+
<span class="pr-feature-name">${feat}</span>
|
|
211
|
+
${vals.map(v => `<span class="pr-feature-val${v === '—' ? ' pr-feature-val--no' : v === '✓' ? ' pr-feature-val--yes' : ''}">${v}</span>`).join('')}
|
|
212
|
+
</div>`).join('')}
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
` }) }) })}
|
|
216
|
+
|
|
217
|
+
${uiSection({ content: container({ size: 'md', content: stack({ gap: 'lg', align: 'center', content: `
|
|
218
|
+
${heading({ level: 2, text: 'Frequently asked questions', size: '3xl', class: 'u-text-center' })}
|
|
219
|
+
${accordion({ items: FAQ })}
|
|
220
|
+
` }) }) })}
|
|
221
|
+
|
|
222
|
+
${cta({
|
|
223
|
+
title: 'Ready to build?',
|
|
224
|
+
content: 'Starter is free forever. No credit card. No catch.',
|
|
225
|
+
actions: cluster({ justify: 'center', content:
|
|
226
|
+
button({ label: 'Get started free', href: '/getting-started', size: 'lg' }) + ' ' +
|
|
227
|
+
button({ label: 'Read the docs', href: '/getting-started', variant: 'secondary', size: 'lg' })
|
|
228
|
+
}),
|
|
229
|
+
})}
|
|
230
|
+
|
|
231
|
+
</main>
|
|
232
|
+
|
|
233
|
+
${uiFooter({
|
|
234
|
+
logo: '<span class="pr-logo">⚡ Pulse</span>',
|
|
235
|
+
tagline: 'The spec-first web framework.',
|
|
236
|
+
columns: [
|
|
237
|
+
{ heading: 'Product', links: [{ label: 'Counter', href: '/counter' }, { label: 'Todos', href: '/todos' }, { label: 'Contact', href: '/contact' }] },
|
|
238
|
+
{ heading: 'Examples', links: [{ label: 'Quiz', href: '/quiz' }, { label: 'Products', href: '/products' }] },
|
|
239
|
+
{ heading: 'Docs', links: [{ label: 'Getting Started', href: '/getting-started' }, { label: 'Spec Reference', href: '/spec' }] },
|
|
240
|
+
],
|
|
241
|
+
copy: `© ${new Date().getFullYear()} Pulse. Built with Pulse.`,
|
|
242
|
+
})}`
|
|
243
|
+
},
|
|
244
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
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
|
+
hydrate: '/examples/products.js',
|
|
76
|
+
|
|
77
|
+
meta: {
|
|
78
|
+
title: 'Products — Pulse',
|
|
79
|
+
description: 'A product catalog built with Pulse. Demonstrates server data, client-side filtering, search, and sort.',
|
|
80
|
+
styles: ['/pulse-ui.css', '/examples/products.css'],
|
|
81
|
+
theme: 'light',
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
server: {
|
|
85
|
+
products: async () => PRODUCTS,
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
stream: {
|
|
89
|
+
shell: ['header', 'filters'],
|
|
90
|
+
deferred: ['grid'],
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
state: {
|
|
94
|
+
search: '',
|
|
95
|
+
category: 'All',
|
|
96
|
+
sort: 'featured',
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
mutations: {
|
|
100
|
+
setSearch: (_state, e) => ({ search: e.target.value }),
|
|
101
|
+
setCategory: (_state, e) => ({ category: e.target.closest('[data-cat]')?.dataset.cat || 'All' }),
|
|
102
|
+
setSort: (_state, e) => ({ sort: e.target.value }),
|
|
103
|
+
clearSearch: () => ({ search: '' }),
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
view: {
|
|
107
|
+
header: (state, server) => {
|
|
108
|
+
const total = server.products?.length ?? 0
|
|
109
|
+
const filtered = applyFilters(server.products ?? [], state.search, state.category, state.sort)
|
|
110
|
+
|
|
111
|
+
return `<div class="pd-root">
|
|
112
|
+
${examplesNav('<span class="pd-logo-text">🛍 Products</span>', '/products')}
|
|
113
|
+
|
|
114
|
+
<main id="main-content" class="pd-main">
|
|
115
|
+
|
|
116
|
+
<div class="pd-page-header">
|
|
117
|
+
<div>
|
|
118
|
+
${heading({ level: 1, text: 'Home Office', size: '2xl' })}
|
|
119
|
+
<p class="pd-subtitle">Showing <strong>${filtered.length}</strong> of <strong>${total}</strong> products</p>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="pd-sort-wrap">
|
|
122
|
+
${select({ name: 'sort', label: 'Sort by', event: 'change:setSort', value: state.sort, options: [
|
|
123
|
+
{ value: 'featured', label: 'Featured' },
|
|
124
|
+
{ value: 'price-asc', label: 'Price: low to high' },
|
|
125
|
+
{ value: 'price-desc', label: 'Price: high to low' },
|
|
126
|
+
{ value: 'rating', label: 'Top rated' },
|
|
127
|
+
{ value: 'reviews', label: 'Most reviewed' },
|
|
128
|
+
]})}
|
|
129
|
+
</div>
|
|
130
|
+
</div>`
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
filters: (state) => {
|
|
134
|
+
return `
|
|
135
|
+
<div class="pd-sidebar">
|
|
136
|
+
|
|
137
|
+
${search({ name: 'search', label: 'Search', placeholder: 'Search products…', value: state.search, event: 'input:setSearch', debounce: 200, clearEvent: 'clearSearch' })}
|
|
138
|
+
|
|
139
|
+
<div class="pd-category-wrap">
|
|
140
|
+
<p class="pd-sidebar-label">Category</p>
|
|
141
|
+
<ul class="pd-categories" role="list">
|
|
142
|
+
${CATEGORIES.map(cat => `<li>
|
|
143
|
+
${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) } })}
|
|
144
|
+
</li>`).join('')}
|
|
145
|
+
</ul>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
</div>`
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
grid: (state, server) => {
|
|
152
|
+
const filtered = applyFilters(server.products ?? [], state.search, state.category, state.sort)
|
|
153
|
+
|
|
154
|
+
const gridContent = filtered.length === 0
|
|
155
|
+
? empty({
|
|
156
|
+
title: 'No products found',
|
|
157
|
+
description: state.search ? `No results for "${esc(state.search)}".` : 'Try a different category.',
|
|
158
|
+
})
|
|
159
|
+
: `<ul class="pd-grid" aria-label="Products">
|
|
160
|
+
${filtered.map(p => `<li>
|
|
161
|
+
${card({
|
|
162
|
+
flush: true,
|
|
163
|
+
content: `
|
|
164
|
+
${uiImage({ src: p.img, alt: p.name, ratio: '16/9' })}
|
|
165
|
+
<div class="pd-product-body">
|
|
166
|
+
<div class="pd-product-meta">
|
|
167
|
+
${badge({ label: p.category, variant: 'default' })}
|
|
168
|
+
${p.featured ? badge({ label: 'Featured', variant: 'info' }) : ''}
|
|
169
|
+
</div>
|
|
170
|
+
${heading({ level: 2, text: p.name, size: 'sm' })}
|
|
171
|
+
<div class="pd-product-stats">
|
|
172
|
+
<span class="pd-rating" aria-label="${p.rating} out of 5 stars">★ ${p.rating}</span>
|
|
173
|
+
<span class="pd-reviews">(${p.reviews.toLocaleString()})</span>
|
|
174
|
+
</div>
|
|
175
|
+
<p class="pd-price">£${p.price}</p>
|
|
176
|
+
</div>
|
|
177
|
+
`,
|
|
178
|
+
})}
|
|
179
|
+
</li>`).join('')}
|
|
180
|
+
</ul>`
|
|
181
|
+
|
|
182
|
+
return `
|
|
183
|
+
<div class="pd-content">
|
|
184
|
+
${gridContent}
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
</main>
|
|
188
|
+
</div>`
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
}
|
package/examples/quiz.js
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse — Quiz example
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates:
|
|
5
|
+
* - Multi-step state machine (intro → question → results)
|
|
6
|
+
* - Pure mutation logic with no server data
|
|
7
|
+
* - Computed view output from state
|
|
8
|
+
* - Restart flow
|
|
9
|
+
*
|
|
10
|
+
* Run: node examples/dev.server.js → http://localhost:3001/quiz
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { button, badge, card, container, section, stack, cluster, progress, iconCheck, iconX } from '../src/ui/index.js'
|
|
14
|
+
import { examplesNav } from './shared.js'
|
|
15
|
+
|
|
16
|
+
export const QUESTIONS = [
|
|
17
|
+
{
|
|
18
|
+
id: 1,
|
|
19
|
+
text: 'What does SSR stand for in web development?',
|
|
20
|
+
options: ['Static Site Rendering', 'Server-Side Rendering', 'Stylesheet Rendering', 'Synchronous Script Runtime'],
|
|
21
|
+
answer: 1,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 2,
|
|
25
|
+
text: 'Which HTTP method is typically used to submit a form?',
|
|
26
|
+
options: ['GET', 'PUT', 'POST', 'PATCH'],
|
|
27
|
+
answer: 2,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 3,
|
|
31
|
+
text: 'What does CLS measure in Core Web Vitals?',
|
|
32
|
+
options: ['Cumulative Layout Shift', 'Content Loading Speed', 'CSS Load Score', 'Cached Layout State'],
|
|
33
|
+
answer: 0,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 4,
|
|
37
|
+
text: 'Which of these is NOT a valid HTTP status code?',
|
|
38
|
+
options: ['200 OK', '304 Not Modified', '418 I\'m a teapot', '512 Server Timeout'],
|
|
39
|
+
answer: 3,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 5,
|
|
43
|
+
text: 'What does the "hydrate" property in a Pulse spec enable?',
|
|
44
|
+
options: [
|
|
45
|
+
'Server-side data fetching',
|
|
46
|
+
'Client-side interactivity by importing the spec in the browser',
|
|
47
|
+
'Automatic CSS injection',
|
|
48
|
+
'Database connection pooling',
|
|
49
|
+
],
|
|
50
|
+
answer: 1,
|
|
51
|
+
},
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
export function scoreLabel(score, total) {
|
|
55
|
+
const pct = score / total
|
|
56
|
+
if (pct === 1) return { label: 'Perfect score!', variant: 'success' }
|
|
57
|
+
if (pct >= 0.8) return { label: 'Excellent!', variant: 'success' }
|
|
58
|
+
if (pct >= 0.6) return { label: 'Good effort!', variant: 'info' }
|
|
59
|
+
if (pct >= 0.4) return { label: 'Keep practising!', variant: 'warning' }
|
|
60
|
+
return { label: 'Better luck next time.', variant: 'error' }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default {
|
|
64
|
+
route: '/quiz',
|
|
65
|
+
hydrate: '/examples/quiz.js',
|
|
66
|
+
|
|
67
|
+
meta: {
|
|
68
|
+
title: 'Quiz — Pulse',
|
|
69
|
+
description: 'A multiple-choice quiz built with Pulse. Demonstrates multi-step state machine and computed views.',
|
|
70
|
+
styles: ['/pulse-ui.css'],
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
state: {
|
|
74
|
+
phase: 'intro', // intro | question | results
|
|
75
|
+
current: 0, // index into QUESTIONS
|
|
76
|
+
answers: [], // [number] — chosen option index per question
|
|
77
|
+
score: 0,
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
mutations: {
|
|
81
|
+
start: () => ({
|
|
82
|
+
phase: 'question',
|
|
83
|
+
current: 0,
|
|
84
|
+
answers: [],
|
|
85
|
+
score: 0,
|
|
86
|
+
}),
|
|
87
|
+
|
|
88
|
+
answer: (state, e) => {
|
|
89
|
+
const chosen = parseInt(e.target.closest('[data-option]')?.dataset.option, 10)
|
|
90
|
+
if (isNaN(chosen)) return state
|
|
91
|
+
|
|
92
|
+
const q = QUESTIONS[state.current]
|
|
93
|
+
const correct = chosen === q.answer
|
|
94
|
+
const answers = [...state.answers, chosen]
|
|
95
|
+
const score = state.score + (correct ? 1 : 0)
|
|
96
|
+
const last = state.current === QUESTIONS.length - 1
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
answers,
|
|
100
|
+
score,
|
|
101
|
+
current: last ? state.current : state.current + 1,
|
|
102
|
+
phase: last ? 'results' : 'question',
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
restart: () => ({
|
|
107
|
+
phase: 'intro',
|
|
108
|
+
current: 0,
|
|
109
|
+
answers: [],
|
|
110
|
+
score: 0,
|
|
111
|
+
}),
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
view: (state) => {
|
|
115
|
+
const nav = examplesNav('<span>🧠 Quiz</span>', '/quiz')
|
|
116
|
+
|
|
117
|
+
if (state.phase === 'intro') {
|
|
118
|
+
return `
|
|
119
|
+
${nav}
|
|
120
|
+
<main id="main-content">
|
|
121
|
+
${section({ eyebrow: `${QUESTIONS.length} questions · multiple choice`, title: 'Web Dev Quiz', level: 1, subtitle: 'Test your knowledge of web development fundamentals — HTTP, performance, and Pulse.', align: 'center', content:
|
|
122
|
+
container({ size: 'sm', content:
|
|
123
|
+
cluster({ justify: 'center', content:
|
|
124
|
+
button({ label: 'Start quiz', size: 'lg', attrs: { 'data-event': 'start' } })
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
})}
|
|
128
|
+
</main>`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (state.phase === 'question') {
|
|
132
|
+
const q = QUESTIONS[state.current]
|
|
133
|
+
const num = state.current + 1
|
|
134
|
+
const pct = Math.round((num / QUESTIONS.length) * 100)
|
|
135
|
+
|
|
136
|
+
return `
|
|
137
|
+
${nav}
|
|
138
|
+
<main id="main-content">
|
|
139
|
+
${section({ content:
|
|
140
|
+
container({ size: 'sm', content:
|
|
141
|
+
card({ content: stack({ gap: 'lg', content: `
|
|
142
|
+
|
|
143
|
+
${cluster({ justify: 'between', content:
|
|
144
|
+
`<span class="u-text-sm u-text-muted">Question ${num} of ${QUESTIONS.length}</span>` +
|
|
145
|
+
badge({ label: `${state.score} correct`, variant: 'success' })
|
|
146
|
+
})}
|
|
147
|
+
|
|
148
|
+
${progress({ value: pct, max: 100, label: `Question ${num} of ${QUESTIONS.length}` })}
|
|
149
|
+
|
|
150
|
+
${stack({ gap: 'md', content: `
|
|
151
|
+
<p class="u-text-lg u-font-semibold" id="question-text">${q.text}</p>
|
|
152
|
+
<ul role="list" aria-labelledby="question-text">
|
|
153
|
+
${q.options.map((opt, i) => `<li>${
|
|
154
|
+
button({
|
|
155
|
+
label: `${String.fromCharCode(65 + i)}. ${opt}`,
|
|
156
|
+
variant: 'secondary',
|
|
157
|
+
fullWidth: true,
|
|
158
|
+
attrs: { 'data-event': 'answer', 'data-option': String(i) },
|
|
159
|
+
})
|
|
160
|
+
}</li>`).join('')}
|
|
161
|
+
</ul>
|
|
162
|
+
` })}
|
|
163
|
+
|
|
164
|
+
` }) })
|
|
165
|
+
})
|
|
166
|
+
})}
|
|
167
|
+
</main>`
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Results
|
|
171
|
+
const { label, variant } = scoreLabel(state.score, QUESTIONS.length)
|
|
172
|
+
|
|
173
|
+
const resultRows = QUESTIONS.map((q, i) => {
|
|
174
|
+
const chosen = state.answers[i]
|
|
175
|
+
const correct = chosen === q.answer
|
|
176
|
+
return `<div class="u-flex u-items-start u-gap-3 u-p-3 u-border-b">
|
|
177
|
+
<span class="${correct ? 'u-text-green' : 'u-text-red'}">${correct ? iconCheck({ size: 16 }) : iconX({ size: 16 })}</span>
|
|
178
|
+
<div>
|
|
179
|
+
<p class="u-text-sm">${q.text}</p>
|
|
180
|
+
${!correct ? `<p class="u-text-sm u-text-green u-mt-1">Correct: <strong>${q.options[q.answer]}</strong></p>` : ''}
|
|
181
|
+
</div>
|
|
182
|
+
</div>`
|
|
183
|
+
}).join('')
|
|
184
|
+
|
|
185
|
+
return `
|
|
186
|
+
${nav}
|
|
187
|
+
<main id="main-content">
|
|
188
|
+
${section({ content:
|
|
189
|
+
container({ size: 'sm', content:
|
|
190
|
+
card({ content: stack({ gap: 'lg', align: 'center', content: `
|
|
191
|
+
|
|
192
|
+
<div class="u-flex u-items-end u-justify-center u-gap-1" aria-label="Your score">
|
|
193
|
+
<span class="u-text-4xl u-font-bold u-text-accent">${state.score}</span>
|
|
194
|
+
<span class="u-text-xl u-text-muted">/ ${QUESTIONS.length}</span>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
${badge({ label, variant })}
|
|
198
|
+
|
|
199
|
+
<div class="u-w-full u-border u-rounded-md u-overflow-hidden">${resultRows}</div>
|
|
200
|
+
|
|
201
|
+
${button({ label: 'Try again', variant: 'secondary', attrs: { 'data-event': 'restart' } })}
|
|
202
|
+
|
|
203
|
+
` }) })
|
|
204
|
+
})
|
|
205
|
+
})}
|
|
206
|
+
</main>`
|
|
207
|
+
},
|
|
208
|
+
}
|