@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,103 @@
|
|
|
1
|
+
Pulse is a spec-first frontend framework. Pages are JS files that export a default spec object.
|
|
2
|
+
|
|
3
|
+
## Spec structure
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
export default {
|
|
7
|
+
meta: { title, description, styles: ['/app.css'] },
|
|
8
|
+
state: { /* initial state */ },
|
|
9
|
+
mutations: {
|
|
10
|
+
// Synchronous. Return partial state. First arg is state, second is the DOM event.
|
|
11
|
+
setName: (state, e) => ({ name: e.target.value }),
|
|
12
|
+
},
|
|
13
|
+
actions: {
|
|
14
|
+
// Async lifecycle. <form data-action="actionName"> passes FormData as payload.
|
|
15
|
+
submit: {
|
|
16
|
+
onStart: (state, formData) => ({ status: 'loading' }), // optional — runs before run()
|
|
17
|
+
run: async (state, serverState, formData) => { // required — return value passed to onSuccess
|
|
18
|
+
const name = formData.get('name')
|
|
19
|
+
return await fetch('/api', { method: 'POST', body: formData }).then(r => r.json())
|
|
20
|
+
},
|
|
21
|
+
onSuccess: (state, result) => ({ status: 'success' }), // required — result = return value of run()
|
|
22
|
+
onError: (state, error) => ({ status: 'error' }), // required — called if run() throws
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
server: {
|
|
26
|
+
// Resolved server-side before render. Passed to view() as second arg.
|
|
27
|
+
posts: async () => fetchPostsFromDb(),
|
|
28
|
+
},
|
|
29
|
+
view: (state, serverState) => `<h1>${state.title}</h1>`,
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Streaming SSR — shell + deferred segments
|
|
34
|
+
|
|
35
|
+
Use streaming when a page has slow data and you want the shell to paint instantly while slower content streams in.
|
|
36
|
+
|
|
37
|
+
To use streaming, the `view` must be an **object of named segment functions** (not a single function), and the spec must declare a `stream` field:
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
export default {
|
|
41
|
+
route: '/',
|
|
42
|
+
state: {},
|
|
43
|
+
server: {
|
|
44
|
+
user: async (ctx) => getUser(ctx.cookies.session), // fast
|
|
45
|
+
items: async () => {
|
|
46
|
+
await new Promise(r => setTimeout(r, 1000)) // slow
|
|
47
|
+
return fetchItems()
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
stream: {
|
|
51
|
+
shell: ['shell'], // rendered and sent immediately
|
|
52
|
+
deferred: ['content'], // streamed in when server data resolves
|
|
53
|
+
},
|
|
54
|
+
view: {
|
|
55
|
+
shell: (state, server) => `<nav>My App</nav><h1>Welcome ${server.user?.name}</h1>`,
|
|
56
|
+
content: (state, server) => `<ul>${server.items.map(i => `<li>${i.title}</li>`).join('')}</ul>`,
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Key rules:**
|
|
62
|
+
- `view` must be an object of named functions — not a single function — when using `stream`
|
|
63
|
+
- Streaming splits the **view**, not data fetching. All `server.*` fetchers resolve before any segment renders. Use `Promise.all` inside a fetcher to parallelise slow calls
|
|
64
|
+
- Deferred segments render a `<pulse-deferred>` placeholder while loading — no client JS required
|
|
65
|
+
- Only specs with a `stream` field use chunked responses; all others use standard buffered SSR
|
|
66
|
+
|
|
67
|
+
## Key rules
|
|
68
|
+
|
|
69
|
+
- view() returns an HTML string using template literals
|
|
70
|
+
- data-event="mutationName" on buttons/elements — passes the DOM event to the mutation
|
|
71
|
+
- data-event="change:mutationName" to fire on input change
|
|
72
|
+
- data-action="actionName" on <form> elements only — submits pass FormData to the action
|
|
73
|
+
- Do NOT use data-event on text inputs to mirror value into state — innerHTML re-renders destroy focus. Capture values from FormData in onStart or run instead.
|
|
74
|
+
- **`data-debounce="300"`** alongside `data-event="input:search"` — fires the mutation 300ms after the last keystroke. Use for live search, filtering. No per-spec timer code needed.
|
|
75
|
+
- **`data-throttle="100"`** alongside `data-event` — fires at most once per 100ms. Use for scroll or resize-like inputs. Both attributes apply to `input` and `change` events. Value is milliseconds.
|
|
76
|
+
- onSuccess AND onError are both required in every action (missing either will cause a runtime error)
|
|
77
|
+
- **`_toast`** — return `_toast: { message, variant, duration }` from any action hook (`onStart`, `onSuccess`, `onError`) or mutation to show a toast notification. It is stripped from spec state automatically — never stored. `variant`: `success` | `error` | `warning` | `info` (default). `duration`: ms before auto-dismiss (default 4000, 0 = sticky). The toast container is injected into `document.body` once and survives page navigations. Works from mutations too: `inc: (state) => ({ count: state.count + 1, _toast: { message: 'Done!' } })`.
|
|
78
|
+
- **`onViewError(err, state, serverState) → string`** — optional spec field. Called client-side when `view()` throws; return an HTML string to display instead of crashing. Without it the runtime renders a default inline error message. On the server, a throwing view propagates to the server error handler (500) unless `onViewError` is defined, in which case it returns a 200 with the fallback HTML.
|
|
79
|
+
- Always export default spec
|
|
80
|
+
|
|
81
|
+
## Form layout pattern
|
|
82
|
+
|
|
83
|
+
Use a `<form data-action="...">` element with class `u-flex u-flex-col u-gap-4` for vertical field stacking. For side-by-side fields (e.g. name + email), use `grid({ cols: 2, gap: 'md' })` inside the form — it collapses to one column on mobile automatically. Never use raw `<div class="...">` grids or custom CSS when `grid()` covers it.
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
card({ content: `
|
|
87
|
+
<form data-action="submit" class="u-flex u-flex-col u-gap-4">
|
|
88
|
+
${fieldset({ legend: 'Your details', content: `
|
|
89
|
+
${grid({ cols: 2, gap: 'md', content: `
|
|
90
|
+
${input({ name: 'firstName', label: 'First name', required: true })}
|
|
91
|
+
${input({ name: 'lastName', label: 'Last name', required: true })}
|
|
92
|
+
` })}
|
|
93
|
+
${input({ name: 'email', label: 'Email', type: 'email', required: true })}
|
|
94
|
+
` })}
|
|
95
|
+
${fieldset({ legend: 'Message', content: `
|
|
96
|
+
${textarea({ name: 'message', label: 'Tell us about your project', rows: 5, required: true })}
|
|
97
|
+
` })}
|
|
98
|
+
${button({ label: 'Send', type: 'submit', variant: 'primary', fullWidth: true })}
|
|
99
|
+
</form>
|
|
100
|
+
` })
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
For simple forms without distinct groups, omit `fieldset` and use `u-flex u-flex-col u-gap-4` on the `<form>` directly.
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
## meta.styles and meta.scripts
|
|
2
|
+
|
|
3
|
+
- meta.styles — array of CSS paths loaded as <link rel="stylesheet">. Always include '/app.css'.
|
|
4
|
+
- meta.scripts — array of JS paths loaded as <script defer>. Required for interactive UI components.
|
|
5
|
+
|
|
6
|
+
Interactive Pulse UI components (carousel, modal, accordion, tooltip) require BOTH:
|
|
7
|
+
```js
|
|
8
|
+
meta: {
|
|
9
|
+
styles: ['/app.css', '/pulse-ui.css'],
|
|
10
|
+
scripts: ['/pulse-ui.js'],
|
|
11
|
+
}
|
|
12
|
+
```
|
|
13
|
+
Non-interactive components (nav, hero, button, card, etc.) only need '/pulse-ui.css' in styles.
|
|
14
|
+
|
|
15
|
+
## Theming — always use CSS custom properties
|
|
16
|
+
|
|
17
|
+
pulse-ui.css exposes CSS custom properties for every token. app.css MUST use these tokens — never hardcode colour hex values.
|
|
18
|
+
|
|
19
|
+
Override tokens in :root inside app.css to retheme all components at once:
|
|
20
|
+
```css
|
|
21
|
+
:root {
|
|
22
|
+
--bg: #0d0d10; /* page background */
|
|
23
|
+
--surface: #111116; /* card / panel background */
|
|
24
|
+
--surface-2: #18181f; /* inset / code background */
|
|
25
|
+
--border: #38383f;
|
|
26
|
+
--text: #e2e2ea;
|
|
27
|
+
--muted: #9090a0;
|
|
28
|
+
--accent: #9b8dff;
|
|
29
|
+
--accent-hover: #b5aaff;
|
|
30
|
+
--radius: 8px;
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Then use the computed --ui-* tokens everywhere in app.css:
|
|
35
|
+
```css
|
|
36
|
+
/* pulse-ui.css already sets background-color and color on html/body — do not repeat them */
|
|
37
|
+
h1 { color: var(--ui-text); }
|
|
38
|
+
p { color: var(--ui-muted); }
|
|
39
|
+
a { color: var(--ui-accent); }
|
|
40
|
+
code { background: var(--ui-surface-2); color: var(--ui-accent); border: 1px solid var(--ui-border); }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**The library is dark by default.** `pulse-ui.css` applies `background-color: var(--ui-bg)` and `color: var(--ui-text)` to `html, body` automatically — you do NOT need to add these to app.css.
|
|
44
|
+
|
|
45
|
+
To apply a **light theme**, set `meta.theme: 'light'` in the spec — this adds `data-theme="light"` to the `<body>` and activates the built-in light token set (accessible contrast for badges, alerts, and all semantic colours). Do NOT manually copy token values into `:root`.
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
meta: {
|
|
49
|
+
theme: 'light',
|
|
50
|
+
styles: ['/pulse-ui.css', '/app.css'],
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Token list: --ui-bg, --ui-surface, --ui-surface-2, --ui-border, --ui-text, --ui-muted, --ui-accent, --ui-accent-hover, --ui-green, --ui-red, --ui-yellow, --ui-blue, --ui-radius, --ui-radius-sm, --ui-font, --ui-mono. Never hardcode hex values — override the tokens.
|
|
55
|
+
|
|
56
|
+
## Custom fonts
|
|
57
|
+
|
|
58
|
+
All components use `--ui-font` (body) and `--ui-mono` (code). These resolve from `--font` and `--mono` respectively, so overriding those two variables in `:root` is all that is ever needed — no other CSS changes required.
|
|
59
|
+
|
|
60
|
+
**To display monospace/code text, use a `<code>` element — not a utility class.** There is no `u-font-mono` class. `<code>` automatically inherits `--ui-mono` and gets the correct background and colour from pulse-ui.css.
|
|
61
|
+
|
|
62
|
+
**Two rules that must never be broken:**
|
|
63
|
+
- **Never `@import url(...)` a font in CSS** — use `meta.styles` instead. CSS `@import` is render-blocking; a `<link>` tag is not.
|
|
64
|
+
- **Never set `font-family` directly on `body` or any element** — this bypasses `--ui-font` and breaks component inheritance. Always set `--font` in `:root`.
|
|
65
|
+
|
|
66
|
+
```css
|
|
67
|
+
/* app.css */
|
|
68
|
+
:root {
|
|
69
|
+
--font: 'Inter', system-ui, sans-serif;
|
|
70
|
+
--mono: 'JetBrains Mono', monospace;
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Google Fonts
|
|
75
|
+
|
|
76
|
+
Add the Google Fonts stylesheet URL **before** `pulse-ui.css` in `meta.styles`. Use `family=Name:wght@weights` and always include `&display=swap`.
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
meta: {
|
|
80
|
+
styles: [
|
|
81
|
+
'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap',
|
|
82
|
+
'/pulse-ui.css',
|
|
83
|
+
'/app.css',
|
|
84
|
+
],
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Then in app.css:
|
|
89
|
+
```css
|
|
90
|
+
:root { --font: 'Inter', system-ui, sans-serif; }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
For multiple weights or italic variants, separate them with a semicolon in the URL:
|
|
94
|
+
```
|
|
95
|
+
?family=Inter:ital,wght@0,400;0,700;1,400&display=swap
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Adobe Fonts (Typekit)
|
|
99
|
+
|
|
100
|
+
Adobe Fonts provides a per-project CSS URL from your kit settings. Add it before `pulse-ui.css` in `meta.styles` — the font-family name comes from your Adobe Fonts kit.
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
meta: {
|
|
104
|
+
styles: [
|
|
105
|
+
'https://use.typekit.net/YOURPROJECTID.css',
|
|
106
|
+
'/pulse-ui.css',
|
|
107
|
+
'/app.css',
|
|
108
|
+
],
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Then in app.css, use the exact font name shown in your Adobe Fonts kit:
|
|
113
|
+
```css
|
|
114
|
+
:root { --font: 'proxima-nova', system-ui, sans-serif; }
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Self-hosted fonts
|
|
118
|
+
|
|
119
|
+
Place font files in `public/fonts/` and declare them with `@font-face` in `app.css`. Always use `woff2` format and `font-display: swap`.
|
|
120
|
+
|
|
121
|
+
```css
|
|
122
|
+
/* app.css */
|
|
123
|
+
@font-face {
|
|
124
|
+
font-family: 'MyFont';
|
|
125
|
+
src: url('/fonts/myfont-regular.woff2') format('woff2');
|
|
126
|
+
font-weight: 400;
|
|
127
|
+
font-style: normal;
|
|
128
|
+
font-display: swap;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
:root { --font: 'MyFont', system-ui, sans-serif; }
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Multi-brand fonts
|
|
135
|
+
|
|
136
|
+
For multi-brand sites, keep `@font-face` declarations (or the font service URL) in the per-brand theme file and override `--font` there:
|
|
137
|
+
|
|
138
|
+
```css
|
|
139
|
+
/* themes/acme.css */
|
|
140
|
+
:root { --font: 'proxima-nova', system-ui, sans-serif; }
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## CSS rules — where to put styles and when to use utilities
|
|
144
|
+
|
|
145
|
+
RULE: Never use inline style attributes (style="...") in HTML. Always use classes.
|
|
146
|
+
|
|
147
|
+
RULE: For spacing, typography, layout, and colour, always prefer pulse-ui utility classes first. Only add to app.css if you need something the utilities cannot provide (e.g. a unique component style, a keyframe animation, a custom grid).
|
|
148
|
+
|
|
149
|
+
pulse-ui.css includes a utility layer (u- prefix). **Only use classes from this exact list — do not invent variations. `u-container` does not exist; use the `container()` component instead.**
|
|
150
|
+
|
|
151
|
+
Spacing — exact available values only:
|
|
152
|
+
Margin top: u-mt-0 u-mt-1 u-mt-2 u-mt-3 u-mt-4 u-mt-5 u-mt-6 u-mt-8 u-mt-10 u-mt-12 u-mt-16
|
|
153
|
+
Margin bottom: u-mb-0 u-mb-1 u-mb-2 u-mb-3 u-mb-4 u-mb-5 u-mb-6 u-mb-8 u-mb-10 u-mb-12 u-mb-16
|
|
154
|
+
Margin x/auto: u-mx-auto u-ml-auto u-mr-auto
|
|
155
|
+
Padding all: u-p-0 u-p-1 u-p-2 u-p-3 u-p-4 u-p-5 u-p-6 u-p-8
|
|
156
|
+
Padding x: u-px-0 u-px-2 u-px-3 u-px-4 u-px-5 u-px-6 u-px-8
|
|
157
|
+
Padding y: u-py-0 u-py-2 u-py-3 u-py-4 u-py-5 u-py-6 u-py-8 ← max is u-py-8, not u-py-16
|
|
158
|
+
|
|
159
|
+
Typography:
|
|
160
|
+
u-text-{xs,sm,base,lg,xl,2xl,3xl,4xl}
|
|
161
|
+
u-font-{normal,medium,semibold,bold} ← weight only; there is NO u-font-mono
|
|
162
|
+
u-text-{left,center,right}
|
|
163
|
+
u-text-{default,muted,accent,green,red,yellow,blue}
|
|
164
|
+
u-leading-{tight,snug,normal,relaxed,loose}
|
|
165
|
+
u-text-balance u-break-all
|
|
166
|
+
|
|
167
|
+
Layout:
|
|
168
|
+
u-flex u-flex-col u-flex-wrap u-flex-1 u-shrink-0
|
|
169
|
+
u-items-{start,center,end,stretch}
|
|
170
|
+
u-justify-{start,center,end,between}
|
|
171
|
+
u-gap-1 u-gap-2 u-gap-3 u-gap-4 u-gap-5 u-gap-6 u-gap-8
|
|
172
|
+
u-w-full u-w-auto
|
|
173
|
+
u-max-w-{xs,sm,md,lg,xl,prose}
|
|
174
|
+
u-block u-inline u-inline-block u-hidden
|
|
175
|
+
|
|
176
|
+
Visual:
|
|
177
|
+
u-rounded u-rounded-md u-rounded-lg u-rounded-xl u-rounded-full
|
|
178
|
+
u-border u-border-t u-border-b
|
|
179
|
+
u-bg-surface u-bg-surface2 u-bg-accent
|
|
180
|
+
u-overflow-hidden u-overflow-auto
|
|
181
|
+
u-relative u-absolute u-opacity-50 u-opacity-75
|
|
182
|
+
|
|
183
|
+
Example — a centred hero block using only utilities, no custom CSS:
|
|
184
|
+
```html
|
|
185
|
+
<div class="u-flex u-flex-col u-items-center u-text-center u-py-8 u-gap-4">
|
|
186
|
+
<h1 class="u-text-4xl u-font-bold">Hello</h1>
|
|
187
|
+
<p class="u-text-lg u-text-muted u-max-w-prose">Subtitle goes here.</p>
|
|
188
|
+
</div>
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
When you DO need to write CSS, add it to public/app.css — never inline.
|