@invisibleloop/pulse 0.1.28 → 0.1.30
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/.github/workflows/publish.yml +11 -20
- package/README.md +1 -1
- package/docs/public/.pulse-ui-version +1 -1
- package/docs/public/docs.css +19 -1
- package/docs/public/pulse-ui.css +1 -0
- package/docs/server.js +5 -2
- package/docs/src/lib/highlight.js +57 -13
- package/docs/src/lib/layout.js +5 -2
- package/docs/src/pages/faq.js +5 -2
- package/docs/src/pages/home.js +9 -5
- package/docs/src/pages/meta.js +21 -0
- package/docs/src/pages/routing.js +12 -1
- package/package.json +1 -1
- package/public/pulse-ui.css +2 -2
- package/src/agent/guide-components.md +1 -1
- package/src/agent/guide-routing.md +20 -0
- package/src/agent/guide-spec.md +10 -1
- package/src/agent/guide-styles.md +16 -1
- package/src/agent/workflow.md +1 -1
- package/src/cli/scaffold.js +63 -2
- package/src/mcp/server.js +34 -18
- package/src/server/index.js +26 -7
- package/src/server/server.test.js +47 -0
- package/src/ui/stat.js +1 -1
- package/src/ui/ui.test.js +6 -0
- package/docs/public/dist/accessibility.boot-5DVTARJU.js +0 -115
- package/docs/public/dist/actions.boot-P66HKQEM.js +0 -164
- package/docs/public/dist/auth.boot-IMAJAUPH.js +0 -140
- package/docs/public/dist/caching.boot-DVR6KDE7.js +0 -53
- package/docs/public/dist/components--accordion.boot-3HVKMNWC.js +0 -11
- package/docs/public/dist/components--alert.boot-GCEXOZAC.js +0 -6
- package/docs/public/dist/components--app-badge.boot-DVT3GCHJ.js +0 -6
- package/docs/public/dist/components--avatar.boot-PSW24EVA.js +0 -5
- package/docs/public/dist/components--badge.boot-TYDY2RMK.js +0 -7
- package/docs/public/dist/components--banner.boot-EI5PZSZK.js +0 -7
- package/docs/public/dist/components--breadcrumbs.boot-SMA2E2GO.js +0 -34
- package/docs/public/dist/components--button.boot-J54BQM2E.js +0 -23
- package/docs/public/dist/components--card.boot-PZGNDIB6.js +0 -138
- package/docs/public/dist/components--carousel.boot-TP6LPFZZ.js +0 -12
- package/docs/public/dist/components--charts.boot-2EOYQWKL.js +0 -108
- package/docs/public/dist/components--checkbox.boot-DS5BSL6T.js +0 -54
- package/docs/public/dist/components--cluster.boot-HHVIBBJG.js +0 -9
- package/docs/public/dist/components--code-window.boot-2GR2DV33.js +0 -20
- package/docs/public/dist/components--container.boot-7LOOGK2K.js +0 -5
- package/docs/public/dist/components--cta.boot-FSNZ5YRT.js +0 -11
- package/docs/public/dist/components--divider.boot-3NI2C3QG.js +0 -6
- package/docs/public/dist/components--empty.boot-YX2UR3PV.js +0 -7
- package/docs/public/dist/components--feature.boot-MUD7NSUO.js +0 -13
- package/docs/public/dist/components--fieldset.boot-J7BYHMKF.js +0 -19
- package/docs/public/dist/components--fileupload.boot-NIKVTTPD.js +0 -52
- package/docs/public/dist/components--footer.boot-EYUK5FRG.js +0 -14
- package/docs/public/dist/components--grid.boot-URDQVDDR.js +0 -59
- package/docs/public/dist/components--heading.boot-BPQKU43E.js +0 -44
- package/docs/public/dist/components--hero.boot-4RAPRGAB.js +0 -17
- package/docs/public/dist/components--icons.boot-ZITNU5JP.js +0 -68
- package/docs/public/dist/components--image.boot-XEEGHQZF.js +0 -19
- package/docs/public/dist/components--input.boot-SGASZG5K.js +0 -7
- package/docs/public/dist/components--list.boot-W3XC5MHD.js +0 -55
- package/docs/public/dist/components--media.boot-5VFIETZO.js +0 -13
- package/docs/public/dist/components--modal.boot-RZUYXBN2.js +0 -47
- package/docs/public/dist/components--nav.boot-ODBOHU7O.js +0 -33
- package/docs/public/dist/components--pricing.boot-4AQ4ZVBY.js +0 -21
- package/docs/public/dist/components--progress.boot-GHAGYZOK.js +0 -30
- package/docs/public/dist/components--prose.boot-QANJL6JI.js +0 -67
- package/docs/public/dist/components--pullquote.boot-Q2WMNAZU.js +0 -22
- package/docs/public/dist/components--radio.boot-TJRDQ2OL.js +0 -75
- package/docs/public/dist/components--rating.boot-QBAN6DEL.js +0 -38
- package/docs/public/dist/components--search.boot-PXH5O5AG.js +0 -17
- package/docs/public/dist/components--section.boot-AQGIYHWW.js +0 -12
- package/docs/public/dist/components--segmented.boot-BEVTKEJO.js +0 -33
- package/docs/public/dist/components--select.boot-47X5RHOC.js +0 -10
- package/docs/public/dist/components--slider.boot-PSRRX7XL.js +0 -47
- package/docs/public/dist/components--spinner.boot-MZ5MO2OH.js +0 -22
- package/docs/public/dist/components--stack.boot-DI4NJXBF.js +0 -9
- package/docs/public/dist/components--stat.boot-QMFUWBQT.js +0 -9
- package/docs/public/dist/components--stepper.boot-34PP2NEV.js +0 -22
- package/docs/public/dist/components--table.boot-FCQGSFIQ.js +0 -11
- package/docs/public/dist/components--testimonial.boot-DWQPDKYG.js +0 -11
- package/docs/public/dist/components--textarea.boot-QVXLBOJ5.js +0 -4
- package/docs/public/dist/components--timeline.boot-26LN52P2.js +0 -95
- package/docs/public/dist/components--toggle.boot-IQQEI76S.js +0 -29
- package/docs/public/dist/components--tooltip.boot-LGHCO6NN.js +0 -9
- package/docs/public/dist/components.boot-SE6PQ4P7.js +0 -103
- package/docs/public/dist/config.boot-DTRRWUE6.js +0 -126
- package/docs/public/dist/constraints.boot-DUHDZBMC.js +0 -71
- package/docs/public/dist/deploy.boot-SLAD3NI2.js +0 -163
- package/docs/public/dist/docs-8e3d4b5c.css +0 -1
- package/docs/public/dist/extending.boot-UA3CN243.js +0 -159
- package/docs/public/dist/faq.boot-6EQAWLQR.js +0 -43
- package/docs/public/dist/getting-started.boot-TDKIFL5U.js +0 -86
- package/docs/public/dist/guard.boot-AUHAWTG4.js +0 -80
- package/docs/public/dist/home.boot-BVQXRH32.js +0 -383
- package/docs/public/dist/how-it-works.boot-LTWAKWKW.js +0 -104
- package/docs/public/dist/hydration.boot-JRM6IPJL.js +0 -78
- package/docs/public/dist/images.boot-M6ZVKTZS.js +0 -80
- package/docs/public/dist/manifest.json +0 -94
- package/docs/public/dist/meta.boot-7NXGPHR4.js +0 -79
- package/docs/public/dist/mutations.boot-F6F43UDX.js +0 -79
- package/docs/public/dist/navigation.boot-AOXWS3ZF.js +0 -57
- package/docs/public/dist/performance.boot-C3UPCOBK.js +0 -98
- package/docs/public/dist/persist.boot-WT32PQOQ.js +0 -61
- package/docs/public/dist/project-structure.boot-FB3LRVJ4.js +0 -63
- package/docs/public/dist/prompt-examples.boot-YKR4VDK4.js +0 -31
- package/docs/public/dist/pulse-ui-81a85c03.css +0 -1
- package/docs/public/dist/raw-responses.boot-M4KA5YXL.js +0 -104
- package/docs/public/dist/routing.boot-FNX5FDGH.js +0 -70
- package/docs/public/dist/runtime-B73WLANC.js +0 -1
- package/docs/public/dist/runtime-KO4BHUQ3.js +0 -49
- package/docs/public/dist/runtime-L2HNXIHW.js +0 -59
- package/docs/public/dist/runtime-QFURDKA2.js +0 -5
- package/docs/public/dist/runtime-UVPXO4IR.js +0 -375
- package/docs/public/dist/runtime-VMJA3Z4N.js +0 -10
- package/docs/public/dist/runtime-ZJ4FXT5O.js +0 -11
- package/docs/public/dist/server-api.boot-K7X3LCFB.js +0 -219
- package/docs/public/dist/server-data.boot-Y7HQYC4R.js +0 -157
- package/docs/public/dist/slash-commands.boot-V2UV7OW2.js +0 -26
- package/docs/public/dist/spec.boot-2WU7ZHCV.js +0 -159
- package/docs/public/dist/state.boot-B24GUE3R.js +0 -73
- package/docs/public/dist/store.boot-TLIB4XHH.js +0 -150
- package/docs/public/dist/streaming.boot-W2DZSMW4.js +0 -80
- package/docs/public/dist/stripe.boot-QN3C2GEL.js +0 -164
- package/docs/public/dist/supabase.boot-BG4XXLZE.js +0 -303
- package/docs/public/dist/testing.boot-6U4WKMTE.js +0 -130
- package/docs/public/dist/validation.boot-PQHYGW5B.js +0 -100
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import{a as o}from"./runtime-QFURDKA2.js";import{a,b as d,c,d as p,e,g as t,i as r}from"./runtime-L2HNXIHW.js";import{a as i,b as u}from"./runtime-B73WLANC.js";var{prev:l,next:h}=a("/hydration"),s={route:"/hydration",meta:{title:"Hydration \u2014 Pulse Docs",description:"How Pulse hydrates server-rendered HTML on the client \u2014 the hydrate field, mount(), and production bundles.",styles:["/docs.css"]},state:{},view:()=>d({currentHref:"/hydration",prev:l,next:h,content:`
|
|
2
|
-
${c("Hydration")}
|
|
3
|
-
${p("Hydration in Pulse is opt-in and minimal. Omit <code>hydrate</code> and zero JavaScript is sent to the browser. Add it and Pulse binds events to the server-rendered HTML without re-rendering it \u2014 the SSR-painted content is preserved exactly as the server sent it.")}
|
|
4
|
-
|
|
5
|
-
${e("enabling","Enabling hydration")}
|
|
6
|
-
<p>Set the <code>hydrate</code> field to a browser-importable path to your spec file. Pulse uses this to generate the bootstrap script that mounts the client runtime:</p>
|
|
7
|
-
${t(o(`export default {
|
|
8
|
-
route: '/counter',
|
|
9
|
-
hydrate: '/src/pages/counter.js', // browser path to this file
|
|
10
|
-
state: { count: 0 },
|
|
11
|
-
mutations: {
|
|
12
|
-
increment: (state) => ({ count: state.count + 1 }),
|
|
13
|
-
decrement: (state) => ({ count: state.count - 1 }),
|
|
14
|
-
},
|
|
15
|
-
view: (state) => \`
|
|
16
|
-
<div>
|
|
17
|
-
<button data-event="decrement">-</button>
|
|
18
|
-
<span>\${state.count}</span>
|
|
19
|
-
<button data-event="increment">+</button>
|
|
20
|
-
</div>
|
|
21
|
-
\`,
|
|
22
|
-
}`,"js"))}
|
|
23
|
-
|
|
24
|
-
${e("dev-bootstrap","Development bootstrap")}
|
|
25
|
-
<p>In development (when <code>hydrate</code> is a source file path, not a <code>/dist/</code> bundle), Pulse emits an inline bootstrap script:</p>
|
|
26
|
-
${t(o(`<script type="module">
|
|
27
|
-
import spec from '/src/pages/counter.js'
|
|
28
|
-
import { mount } from '/src/runtime/index.js'
|
|
29
|
-
import { initNavigation } from '/src/runtime/navigate.js'
|
|
30
|
-
mount(spec, root, window.__PULSE_SERVER__ || {}, { ssr: true })
|
|
31
|
-
initNavigation(root, mount)
|
|
32
|
-
<\/script>`,"html"))}
|
|
33
|
-
<p>This imports the spec and runtime source files directly \u2014 no build step required for development.</p>
|
|
34
|
-
|
|
35
|
-
${e("production","Production bundles")}
|
|
36
|
-
<p>Run <code>npm run build</code> to generate production bundles. This creates content-hashed files in <code>public/dist/</code> and a <code>manifest.json</code> mapping spec hydrate paths to bundle paths.</p>
|
|
37
|
-
${t(o(`# Generated by npm run build
|
|
38
|
-
public/dist/
|
|
39
|
-
runtime-abc123.js # shared runtime (~2.1 kB brotli)
|
|
40
|
-
counter.boot-def456.js # per-page spec bundle (~0.5 kB brotli)
|
|
41
|
-
manifest.json # { '/src/pages/counter.js': '/dist/counter.boot-def456.js' }`,"bash"))}
|
|
42
|
-
<p>When Pulse detects a manifest (via <code>staticDir</code> auto-detection or explicit <code>manifest</code> option), it resolves the <code>hydrate</code> path to the bundle path and emits a single <code><script src></code> tag instead of the inline bootstrap:</p>
|
|
43
|
-
${t(o('<script type="module" src="/dist/counter.boot-def456.js"><\/script>',"html"))}
|
|
44
|
-
|
|
45
|
-
${e("ssr-true","The { ssr: true } option")}
|
|
46
|
-
<p>The bootstrap script calls <code>mount(spec, root, serverState, { ssr: true })</code>. This tells the runtime to skip the initial re-render and bind event listeners to the existing DOM only.</p>
|
|
47
|
-
<p>This is what keeps LCP fast. The server-painted HTML is the LCP element. The JavaScript binds events without touching the DOM \u2014 no flash, no layout shift, no JS-rendered replacement.</p>
|
|
48
|
-
${r("warning","Never set <code>{ ssr: false }</code> on a server-rendered page. It re-renders the entire DOM on mount, replacing the server-painted HTML \u2014 causing a visible flash and pushing LCP to 400\u2013600ms.")}
|
|
49
|
-
|
|
50
|
-
${e("mount","mount()")}
|
|
51
|
-
<p>The <code>mount</code> function attaches the Pulse runtime to a DOM element:</p>
|
|
52
|
-
${t(o(`import { mount } from '@invisibleloop/pulse/runtime'
|
|
53
|
-
|
|
54
|
-
mount(
|
|
55
|
-
spec, // the page spec
|
|
56
|
-
rootEl, // the DOM element to mount into
|
|
57
|
-
serverState, // window.__PULSE_SERVER__ (server data from SSR)
|
|
58
|
-
{ ssr: true } // skip re-render on first mount
|
|
59
|
-
)`,"js"))}
|
|
60
|
-
<p>After mount, all <code>data-event</code> and <code>data-action</code> attributes in the DOM are wired to the spec's mutations and actions. State updates trigger a full view re-render via <code>innerHTML</code> replacement.</p>
|
|
61
|
-
|
|
62
|
-
${e("no-hydrate","Pages without hydration")}
|
|
63
|
-
<p>Omit <code>hydrate</code> and Pulse sends zero JavaScript to the browser \u2014 no runtime overhead, no hydration cost. This is the correct default for:</p>
|
|
64
|
-
<ul>
|
|
65
|
-
<li>Documentation pages</li>
|
|
66
|
-
<li>Marketing/landing pages</li>
|
|
67
|
-
<li>Blog posts and articles</li>
|
|
68
|
-
<li>Any page with no client-side interactivity</li>
|
|
69
|
-
</ul>
|
|
70
|
-
${r("tip",'Start without <code>hydrate</code> and only add it when actual client interactivity is needed. Many pages that appear to need JavaScript can be handled server-side with <a href="/routing">routing</a> and <a href="/server-data">server data</a>.')}
|
|
71
|
-
|
|
72
|
-
${e("server-state","Passing server state to the client")}
|
|
73
|
-
<p>Server data fetched via <code>server.data()</code> is serialised into the page HTML as <code>window.__PULSE_SERVER__</code>. The client runtime reads this on mount, making server data available to the view during client re-renders without an additional network request.</p>
|
|
74
|
-
${t(o(`// Emitted in the page HTML
|
|
75
|
-
<script id="__PULSE_SERVER__" type="application/json">
|
|
76
|
-
{"product":{"id":1,"name":"Widget","price":9.99}}
|
|
77
|
-
<\/script>`,"html"))}
|
|
78
|
-
`})};var n=document.getElementById("pulse-root");n&&!n.dataset.pulseMounted&&(n.dataset.pulseMounted="1",i(s,n,window.__PULSE_SERVER__||{},{ssr:!0}),u(n,i));var P=s;export{P as default};
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import{a as o}from"./runtime-QFURDKA2.js";import{a as d,b as p,c as g,d as n,e,g as t,h as r,i as s}from"./runtime-L2HNXIHW.js";import{a as c,b as h}from"./runtime-B73WLANC.js";var{prev:m,next:l}=d("/images"),a={route:"/images",meta:{title:"Images \u2014 Pulse Docs",description:"The img() and picture() helpers for optimised, CLS-free image markup.",styles:["/docs.css"]},state:{},view:()=>p({currentHref:"/images",prev:m,next:l,content:`
|
|
2
|
-
${g("Images")}
|
|
3
|
-
${n("The <code>img()</code> and <code>picture()</code> helpers generate image markup that prevents CLS by requiring dimensions and handles loading priority correctly. Pulse targets 0.00 CLS \u2014 these helpers enforce the attributes that make that possible.")}
|
|
4
|
-
|
|
5
|
-
${e("import","Importing")}
|
|
6
|
-
${t(o(`// In your page spec or component
|
|
7
|
-
import { img, picture } from '@invisibleloop/pulse/image'`,"js"))}
|
|
8
|
-
|
|
9
|
-
${e("img","img(options)")}
|
|
10
|
-
<p>Generates an optimised <code><img></code> element:</p>
|
|
11
|
-
${t(o(`img({
|
|
12
|
-
src: '/images/hero.jpg',
|
|
13
|
-
alt: 'A hero image showing our product',
|
|
14
|
-
width: 1200,
|
|
15
|
-
height: 630,
|
|
16
|
-
priority: true, // \u2192 eager loading + high fetchpriority
|
|
17
|
-
})`,"js"))}
|
|
18
|
-
<p>Output:</p>
|
|
19
|
-
${t(o('<img src="/images/hero.jpg" alt="A hero image showing our product" width="1200" height="630" loading="eager" decoding="async" fetchpriority="high">',"html"))}
|
|
20
|
-
|
|
21
|
-
${e("img-options","img() options")}
|
|
22
|
-
${r(["Option","Type","Required","Description"],[["<code>src</code>","<code>string</code>","Yes","Image URL."],["<code>alt</code>","<code>string</code>","Yes","Alt text. Required for accessibility. Use an empty string for decorative images."],["<code>width</code>","<code>number</code>","Recommended","Intrinsic width in pixels. Prevents CLS by reserving layout space."],["<code>height</code>","<code>number</code>","Recommended","Intrinsic height in pixels. Prevents CLS."],["<code>priority</code>","<code>boolean</code>","No",'If true: <code>loading="eager"</code> + <code>fetchpriority="high"</code>. Use for LCP images. Default: <code>false</code>.'],["<code>class</code>","<code>string</code>","No","CSS class applied to the <code><img></code> element."]])}
|
|
23
|
-
|
|
24
|
-
${s("warning","<code>width</code> and <code>height</code> are required to prevent Cumulative Layout Shift. Without them the browser cannot reserve layout space before the image loads. Pulse targets 0.00 CLS \u2014 omitting these attributes breaks that guarantee.")}
|
|
25
|
-
|
|
26
|
-
${e("picture","picture(options)")}
|
|
27
|
-
<p>Generates a <code><picture></code> element with modern format sources and a fallback <code><img></code>. Use this to serve AVIF or WebP to browsers that support them, with JPEG/PNG as the fallback:</p>
|
|
28
|
-
${t(o(`picture({
|
|
29
|
-
src: '/images/hero.jpg', // fallback
|
|
30
|
-
alt: 'Hero image',
|
|
31
|
-
width: 1200,
|
|
32
|
-
height: 630,
|
|
33
|
-
priority: true,
|
|
34
|
-
sources: [
|
|
35
|
-
{ src: '/images/hero.avif', type: 'image/avif' },
|
|
36
|
-
{ src: '/images/hero.webp', type: 'image/webp' },
|
|
37
|
-
],
|
|
38
|
-
})`,"js"))}
|
|
39
|
-
<p>Output:</p>
|
|
40
|
-
${t(o(`<picture>
|
|
41
|
-
<source srcset="/images/hero.avif" type="image/avif">
|
|
42
|
-
<source srcset="/images/hero.webp" type="image/webp">
|
|
43
|
-
<img src="/images/hero.jpg" alt="Hero image" width="1200" height="630" loading="eager" decoding="async" fetchpriority="high">
|
|
44
|
-
</picture>`,"html"))}
|
|
45
|
-
|
|
46
|
-
${e("picture-options","picture() options")}
|
|
47
|
-
${r(["Option","Type","Description"],[["<code>src</code>","<code>string</code>","Fallback image URL (JPEG/PNG for universal compatibility)."],["<code>alt</code>","<code>string</code>","Alt text \u2014 shared by the inner <code><img></code>."],["<code>width</code>","<code>number</code>","Intrinsic width \u2014 applied to inner <code><img></code>."],["<code>height</code>","<code>number</code>","Intrinsic height \u2014 applied to inner <code><img></code>."],["<code>priority</code>","<code>boolean</code>","If true: eager loading + high priority."],["<code>class</code>","<code>string</code>","CSS class on the inner <code><img></code>."],["<code>sources</code>","<code>{src, type}[]</code>","Modern format sources in preference order (AVIF first, WebP second)."]])}
|
|
48
|
-
|
|
49
|
-
${e("priority","When to use priority")}
|
|
50
|
-
<p>Set <code>priority: true</code> on the <strong>Largest Contentful Paint (LCP) element</strong> \u2014 typically the hero image above the fold. This tells the browser to load it with high priority, improving LCP.</p>
|
|
51
|
-
<p>Every other image should omit <code>priority</code> (defaults to lazy loading with <code>loading="lazy"</code>). Lazy images are not fetched until they approach the viewport \u2014 reducing initial page weight and speeding up load time.</p>
|
|
52
|
-
${s("tip",'Only one image per page should have <code>priority: true</code>. Using it on multiple images defeats the purpose \u2014 every image becomes "high priority" which is the same as no image being prioritised.')}
|
|
53
|
-
|
|
54
|
-
${e("in-view","Using in a view")}
|
|
55
|
-
${t(o(`import { img, picture } from '@invisibleloop/pulse/image'
|
|
56
|
-
|
|
57
|
-
export default {
|
|
58
|
-
route: '/blog/:slug',
|
|
59
|
-
state: {},
|
|
60
|
-
server: {
|
|
61
|
-
data: async (ctx) => ({ post: await db.posts.findBySlug(ctx.params.slug) }),
|
|
62
|
-
},
|
|
63
|
-
view: (state, server) => \`
|
|
64
|
-
<article>
|
|
65
|
-
\${picture({
|
|
66
|
-
src: server.post.heroImage,
|
|
67
|
-
alt: server.post.heroAlt,
|
|
68
|
-
width: 1200,
|
|
69
|
-
height: 630,
|
|
70
|
-
priority: true,
|
|
71
|
-
sources: [
|
|
72
|
-
{ src: server.post.heroImage.replace('.jpg', '.avif'), type: 'image/avif' },
|
|
73
|
-
{ src: server.post.heroImage.replace('.jpg', '.webp'), type: 'image/webp' },
|
|
74
|
-
],
|
|
75
|
-
})}
|
|
76
|
-
<h1>\${server.post.title}</h1>
|
|
77
|
-
</article>
|
|
78
|
-
\`,
|
|
79
|
-
}`,"js"))}
|
|
80
|
-
`})};var i=document.getElementById("pulse-root");i&&!i.dataset.pulseMounted&&(i.dataset.pulseMounted="1",c(a,i,window.__PULSE_SERVER__||{},{ssr:!0}),h(i,c));var L=a;export{L as default};
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"/src/pages/validation.js": "/../docs/public/dist/validation.boot-PQHYGW5B.js",
|
|
3
|
-
"/src/pages/slash-commands.js": "/../docs/public/dist/slash-commands.boot-V2UV7OW2.js",
|
|
4
|
-
"/src/pages/spec.js": "/../docs/public/dist/spec.boot-2WU7ZHCV.js",
|
|
5
|
-
"/src/pages/state.js": "/../docs/public/dist/state.boot-B24GUE3R.js",
|
|
6
|
-
"/src/pages/store.js": "/../docs/public/dist/store.boot-TLIB4XHH.js",
|
|
7
|
-
"/src/pages/streaming.js": "/../docs/public/dist/streaming.boot-W2DZSMW4.js",
|
|
8
|
-
"/src/pages/stripe.js": "/../docs/public/dist/stripe.boot-QN3C2GEL.js",
|
|
9
|
-
"/src/pages/supabase.js": "/../docs/public/dist/supabase.boot-BG4XXLZE.js",
|
|
10
|
-
"/src/pages/testing.js": "/../docs/public/dist/testing.boot-6U4WKMTE.js",
|
|
11
|
-
"/src/pages/performance.js": "/../docs/public/dist/performance.boot-C3UPCOBK.js",
|
|
12
|
-
"/src/pages/persist.js": "/../docs/public/dist/persist.boot-WT32PQOQ.js",
|
|
13
|
-
"/src/pages/project-structure.js": "/../docs/public/dist/project-structure.boot-FB3LRVJ4.js",
|
|
14
|
-
"/src/pages/prompt-examples.js": "/../docs/public/dist/prompt-examples.boot-YKR4VDK4.js",
|
|
15
|
-
"/src/pages/raw-responses.js": "/../docs/public/dist/raw-responses.boot-M4KA5YXL.js",
|
|
16
|
-
"/src/pages/routing.js": "/../docs/public/dist/routing.boot-FNX5FDGH.js",
|
|
17
|
-
"/src/pages/server-api.js": "/../docs/public/dist/server-api.boot-K7X3LCFB.js",
|
|
18
|
-
"/src/pages/server-data.js": "/../docs/public/dist/server-data.boot-Y7HQYC4R.js",
|
|
19
|
-
"/src/pages/guard.js": "/../docs/public/dist/guard.boot-AUHAWTG4.js",
|
|
20
|
-
"/src/pages/home.js": "/../docs/public/dist/home.boot-BVQXRH32.js",
|
|
21
|
-
"/src/pages/how-it-works.js": "/../docs/public/dist/how-it-works.boot-LTWAKWKW.js",
|
|
22
|
-
"/src/pages/hydration.js": "/../docs/public/dist/hydration.boot-JRM6IPJL.js",
|
|
23
|
-
"/src/pages/images.js": "/../docs/public/dist/images.boot-M6ZVKTZS.js",
|
|
24
|
-
"/src/pages/meta.js": "/../docs/public/dist/meta.boot-7NXGPHR4.js",
|
|
25
|
-
"/src/pages/mutations.js": "/../docs/public/dist/mutations.boot-F6F43UDX.js",
|
|
26
|
-
"/src/pages/navigation.js": "/../docs/public/dist/navigation.boot-AOXWS3ZF.js",
|
|
27
|
-
"/src/pages/components/tooltip.js": "/../docs/public/dist/components--tooltip.boot-LGHCO6NN.js",
|
|
28
|
-
"/src/pages/components.js": "/../docs/public/dist/components.boot-SE6PQ4P7.js",
|
|
29
|
-
"/src/pages/config.js": "/../docs/public/dist/config.boot-DTRRWUE6.js",
|
|
30
|
-
"/src/pages/constraints.js": "/../docs/public/dist/constraints.boot-DUHDZBMC.js",
|
|
31
|
-
"/src/pages/deploy.js": "/../docs/public/dist/deploy.boot-SLAD3NI2.js",
|
|
32
|
-
"/src/pages/extending.js": "/../docs/public/dist/extending.boot-UA3CN243.js",
|
|
33
|
-
"/src/pages/faq.js": "/../docs/public/dist/faq.boot-6EQAWLQR.js",
|
|
34
|
-
"/src/pages/getting-started.js": "/../docs/public/dist/getting-started.boot-TDKIFL5U.js",
|
|
35
|
-
"/src/pages/components/stack.js": "/../docs/public/dist/components--stack.boot-DI4NJXBF.js",
|
|
36
|
-
"/src/pages/components/stat.js": "/../docs/public/dist/components--stat.boot-QMFUWBQT.js",
|
|
37
|
-
"/src/pages/components/stepper.js": "/../docs/public/dist/components--stepper.boot-34PP2NEV.js",
|
|
38
|
-
"/src/pages/components/table.js": "/../docs/public/dist/components--table.boot-FCQGSFIQ.js",
|
|
39
|
-
"/src/pages/components/testimonial.js": "/../docs/public/dist/components--testimonial.boot-DWQPDKYG.js",
|
|
40
|
-
"/src/pages/components/textarea.js": "/../docs/public/dist/components--textarea.boot-QVXLBOJ5.js",
|
|
41
|
-
"/src/pages/components/timeline.js": "/../docs/public/dist/components--timeline.boot-26LN52P2.js",
|
|
42
|
-
"/src/pages/components/toggle.js": "/../docs/public/dist/components--toggle.boot-IQQEI76S.js",
|
|
43
|
-
"/src/pages/components/radio.js": "/../docs/public/dist/components--radio.boot-TJRDQ2OL.js",
|
|
44
|
-
"/src/pages/components/rating.js": "/../docs/public/dist/components--rating.boot-QBAN6DEL.js",
|
|
45
|
-
"/src/pages/components/search.js": "/../docs/public/dist/components--search.boot-PXH5O5AG.js",
|
|
46
|
-
"/src/pages/components/section.js": "/../docs/public/dist/components--section.boot-AQGIYHWW.js",
|
|
47
|
-
"/src/pages/components/segmented.js": "/../docs/public/dist/components--segmented.boot-BEVTKEJO.js",
|
|
48
|
-
"/src/pages/components/select.js": "/../docs/public/dist/components--select.boot-47X5RHOC.js",
|
|
49
|
-
"/src/pages/components/slider.js": "/../docs/public/dist/components--slider.boot-PSRRX7XL.js",
|
|
50
|
-
"/src/pages/components/spinner.js": "/../docs/public/dist/components--spinner.boot-MZ5MO2OH.js",
|
|
51
|
-
"/src/pages/components/list.js": "/../docs/public/dist/components--list.boot-W3XC5MHD.js",
|
|
52
|
-
"/src/pages/components/media.js": "/../docs/public/dist/components--media.boot-5VFIETZO.js",
|
|
53
|
-
"/src/pages/components/modal.js": "/../docs/public/dist/components--modal.boot-RZUYXBN2.js",
|
|
54
|
-
"/src/pages/components/nav.js": "/../docs/public/dist/components--nav.boot-ODBOHU7O.js",
|
|
55
|
-
"/src/pages/components/pricing.js": "/../docs/public/dist/components--pricing.boot-4AQ4ZVBY.js",
|
|
56
|
-
"/src/pages/components/progress.js": "/../docs/public/dist/components--progress.boot-GHAGYZOK.js",
|
|
57
|
-
"/src/pages/components/prose.js": "/../docs/public/dist/components--prose.boot-QANJL6JI.js",
|
|
58
|
-
"/src/pages/components/pullquote.js": "/../docs/public/dist/components--pullquote.boot-Q2WMNAZU.js",
|
|
59
|
-
"/src/pages/components/fileupload.js": "/../docs/public/dist/components--fileupload.boot-NIKVTTPD.js",
|
|
60
|
-
"/src/pages/components/footer.js": "/../docs/public/dist/components--footer.boot-EYUK5FRG.js",
|
|
61
|
-
"/src/pages/components/grid.js": "/../docs/public/dist/components--grid.boot-URDQVDDR.js",
|
|
62
|
-
"/src/pages/components/heading.js": "/../docs/public/dist/components--heading.boot-BPQKU43E.js",
|
|
63
|
-
"/src/pages/components/hero.js": "/../docs/public/dist/components--hero.boot-4RAPRGAB.js",
|
|
64
|
-
"/src/pages/components/icons.js": "/../docs/public/dist/components--icons.boot-ZITNU5JP.js",
|
|
65
|
-
"/src/pages/components/image.js": "/../docs/public/dist/components--image.boot-XEEGHQZF.js",
|
|
66
|
-
"/src/pages/components/input.js": "/../docs/public/dist/components--input.boot-SGASZG5K.js",
|
|
67
|
-
"/src/pages/components/cluster.js": "/../docs/public/dist/components--cluster.boot-HHVIBBJG.js",
|
|
68
|
-
"/src/pages/components/code-window.js": "/../docs/public/dist/components--code-window.boot-2GR2DV33.js",
|
|
69
|
-
"/src/pages/components/container.js": "/../docs/public/dist/components--container.boot-7LOOGK2K.js",
|
|
70
|
-
"/src/pages/components/cta.js": "/../docs/public/dist/components--cta.boot-FSNZ5YRT.js",
|
|
71
|
-
"/src/pages/components/divider.js": "/../docs/public/dist/components--divider.boot-3NI2C3QG.js",
|
|
72
|
-
"/src/pages/components/empty.js": "/../docs/public/dist/components--empty.boot-YX2UR3PV.js",
|
|
73
|
-
"/src/pages/components/feature.js": "/../docs/public/dist/components--feature.boot-MUD7NSUO.js",
|
|
74
|
-
"/src/pages/components/fieldset.js": "/../docs/public/dist/components--fieldset.boot-J7BYHMKF.js",
|
|
75
|
-
"/src/pages/components/badge.js": "/../docs/public/dist/components--badge.boot-TYDY2RMK.js",
|
|
76
|
-
"/src/pages/components/banner.js": "/../docs/public/dist/components--banner.boot-EI5PZSZK.js",
|
|
77
|
-
"/src/pages/components/breadcrumbs.js": "/../docs/public/dist/components--breadcrumbs.boot-SMA2E2GO.js",
|
|
78
|
-
"/src/pages/components/button.js": "/../docs/public/dist/components--button.boot-J54BQM2E.js",
|
|
79
|
-
"/src/pages/components/card.js": "/../docs/public/dist/components--card.boot-PZGNDIB6.js",
|
|
80
|
-
"/src/pages/components/carousel.js": "/../docs/public/dist/components--carousel.boot-TP6LPFZZ.js",
|
|
81
|
-
"/src/pages/components/charts.js": "/../docs/public/dist/components--charts.boot-2EOYQWKL.js",
|
|
82
|
-
"/src/pages/components/checkbox.js": "/../docs/public/dist/components--checkbox.boot-DS5BSL6T.js",
|
|
83
|
-
"/src/pages/accessibility.js": "/../docs/public/dist/accessibility.boot-5DVTARJU.js",
|
|
84
|
-
"/src/pages/actions.js": "/../docs/public/dist/actions.boot-P66HKQEM.js",
|
|
85
|
-
"/src/pages/auth.js": "/../docs/public/dist/auth.boot-IMAJAUPH.js",
|
|
86
|
-
"/src/pages/caching.js": "/../docs/public/dist/caching.boot-DVR6KDE7.js",
|
|
87
|
-
"/src/pages/components/accordion.js": "/../docs/public/dist/components--accordion.boot-3HVKMNWC.js",
|
|
88
|
-
"/src/pages/components/alert.js": "/../docs/public/dist/components--alert.boot-GCEXOZAC.js",
|
|
89
|
-
"/src/pages/components/app-badge.js": "/../docs/public/dist/components--app-badge.boot-DVT3GCHJ.js",
|
|
90
|
-
"/src/pages/components/avatar.js": "/../docs/public/dist/components--avatar.boot-PSW24EVA.js",
|
|
91
|
-
"_runtime": "/../docs/public/dist/runtime-B73WLANC.js",
|
|
92
|
-
"/docs.css": "/dist/docs-8e3d4b5c.css",
|
|
93
|
-
"/pulse-ui.css": "/dist/pulse-ui-81a85c03.css"
|
|
94
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import{a as o}from"./runtime-QFURDKA2.js";import{a as i,b as n,c as l,d as p,e,g as t,h as s,i as r}from"./runtime-L2HNXIHW.js";import{a as c,b as m}from"./runtime-B73WLANC.js";var{prev:h,next:g}=i("/meta"),d={route:"/meta",meta:{title:"Metadata & SEO \u2014 Pulse Docs",description:"How to configure page metadata, Open Graph tags, and structured data in Pulse.",styles:["/docs.css"]},state:{},view:()=>n({currentHref:"/meta",prev:h,next:g,content:`
|
|
2
|
-
${l("Metadata & SEO")}
|
|
3
|
-
${p("The <code>meta</code> field declares everything that appears in the <code><head></code> \u2014 title, description, stylesheets, Open Graph tags, and structured data. All metadata is rendered server-side, so crawlers and social media scrapers see the final HTML without executing JavaScript.")}
|
|
4
|
-
|
|
5
|
-
${e("basics","Basic metadata")}
|
|
6
|
-
${t(o(`export default {
|
|
7
|
-
route: '/about',
|
|
8
|
-
meta: {
|
|
9
|
-
title: 'About Us \u2014 Acme Corp',
|
|
10
|
-
description: 'Learn about the team behind Acme Corp.',
|
|
11
|
-
styles: ['/app.css'],
|
|
12
|
-
},
|
|
13
|
-
state: {},
|
|
14
|
-
view: () => \`<h1>About Us</h1>\`,
|
|
15
|
-
}`,"js"))}
|
|
16
|
-
<p>This generates:</p>
|
|
17
|
-
${t(o(`<title>About Us \u2014 Acme Corp</title>
|
|
18
|
-
<meta name="description" content="Learn about the team behind Acme Corp.">
|
|
19
|
-
<link rel="stylesheet" href="/app.css">`,"html"))}
|
|
20
|
-
|
|
21
|
-
${e("all-fields","All meta fields")}
|
|
22
|
-
${s(["Field","Type","Description"],[["<code>title</code>","<code>string</code>","Page title \u2014 appears in the browser tab and search results."],["<code>description</code>","<code>string</code>","Meta description \u2014 appears in search engine snippets. Keep under 160 characters."],["<code>styles</code>","<code>string[]</code>",'Array of stylesheet URLs \u2014 each emits a <code><link rel="stylesheet"></code> tag.'],["<code>ogTitle</code>","<code>string</code>","Open Graph title. If omitted, falls back to <code>title</code>."],["<code>ogImage</code>","<code>string</code>","Open Graph image URL \u2014 shown when the page is shared on social media."],["<code>schema</code>","<code>object</code>",'JSON-LD structured data object \u2014 emitted as a <code><script type="application/ld+json"></code> tag.']])}
|
|
23
|
-
|
|
24
|
-
${e("open-graph","Open Graph")}
|
|
25
|
-
<p>Open Graph tags control how the page appears when shared on social media (Twitter/X, Facebook, LinkedIn, Slack, etc.):</p>
|
|
26
|
-
${t(o(`meta: {
|
|
27
|
-
title: 'My Product \u2014 Acme Corp',
|
|
28
|
-
description: 'The best product ever made.',
|
|
29
|
-
ogTitle: 'My Product', // shorter for social
|
|
30
|
-
ogImage: 'https://acme.com/og/my-product.jpg', // 1200\xD7630 recommended
|
|
31
|
-
}`,"js"))}
|
|
32
|
-
<p>Generated tags:</p>
|
|
33
|
-
${t(o(`<meta property="og:title" content="My Product">
|
|
34
|
-
<meta property="og:description" content="The best product ever made.">
|
|
35
|
-
<meta property="og:image" content="https://acme.com/og/my-product.jpg">
|
|
36
|
-
<meta name="twitter:card" content="summary_large_image">
|
|
37
|
-
<meta name="twitter:title" content="My Product">
|
|
38
|
-
<meta name="twitter:image" content="https://acme.com/og/my-product.jpg">`,"html"))}
|
|
39
|
-
${r("tip","Use an absolute URL for <code>ogImage</code> \u2014 social media crawlers need the full URL to fetch the image. Recommended size: 1200\xD7630 pixels.")}
|
|
40
|
-
|
|
41
|
-
${e("structured-data","Structured data (ld+json)")}
|
|
42
|
-
<p>The <code>schema</code> field accepts a plain object conforming to <a href="https://schema.org" target="_blank" rel="noopener">schema.org</a> vocabulary. Pulse serialises it as a <code><script type="application/ld+json"></code> tag in the head:</p>
|
|
43
|
-
${t(o(`meta: {
|
|
44
|
-
title: 'How to make sourdough \u2014 My Blog',
|
|
45
|
-
schema: {
|
|
46
|
-
'@context': 'https://schema.org',
|
|
47
|
-
'@type': 'Article',
|
|
48
|
-
headline: 'How to make sourdough',
|
|
49
|
-
author: {
|
|
50
|
-
'@type': 'Person',
|
|
51
|
-
name: 'Jane Smith',
|
|
52
|
-
},
|
|
53
|
-
datePublished: '2025-01-15',
|
|
54
|
-
image: 'https://myblog.com/sourdough.jpg',
|
|
55
|
-
},
|
|
56
|
-
}`,"js"))}
|
|
57
|
-
<p>Common schema types:</p>
|
|
58
|
-
${s(["@type","Use for"],[["<code>WebSite</code>","The homepage or site root"],["<code>WebPage</code>","General pages"],["<code>Article</code>","Blog posts, news articles"],["<code>Product</code>","E-commerce product pages"],["<code>FAQPage</code>","FAQ pages (enables rich results in Google)"],["<code>BreadcrumbList</code>","Breadcrumb navigation"],["<code>Organization</code>","Company/brand information"]])}
|
|
59
|
-
|
|
60
|
-
${e("styles","Stylesheets")}
|
|
61
|
-
<p>The <code>styles</code> array accepts any number of stylesheet URLs. They are emitted as <code><link rel="stylesheet"></code> tags in the <code><head></code> in the order declared:</p>
|
|
62
|
-
${t(o(`meta: {
|
|
63
|
-
styles: [
|
|
64
|
-
'/app.css',
|
|
65
|
-
'/fonts.css',
|
|
66
|
-
'https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap',
|
|
67
|
-
],
|
|
68
|
-
}`,"js"))}
|
|
69
|
-
${r("note","For maximum performance, host your own fonts rather than using Google Fonts. External stylesheet requests add render-blocking latency and a DNS lookup.")}
|
|
70
|
-
|
|
71
|
-
${e("seo-tips","SEO tips")}
|
|
72
|
-
<ul>
|
|
73
|
-
<li>Write a unique <code>title</code> and <code>description</code> for every page \u2014 duplicate metadata prevents pages from competing in search results.</li>
|
|
74
|
-
<li>Keep descriptions under 160 characters \u2014 longer values are truncated.</li>
|
|
75
|
-
<li>Use structured data to qualify for Google rich results.</li>
|
|
76
|
-
<li>All metadata is in the server-rendered HTML \u2014 search engines and social scrapers do not need to execute JavaScript to read it.</li>
|
|
77
|
-
<li>Pulse targets 100/100 Lighthouse SEO out of the box. Run <code>/pulse-report</code> after new pages to confirm.</li>
|
|
78
|
-
</ul>
|
|
79
|
-
`})};var a=document.getElementById("pulse-root");a&&!a.dataset.pulseMounted&&(a.dataset.pulseMounted="1",c(d,a,window.__PULSE_SERVER__||{},{ssr:!0}),m(a,c));var L=d;export{L as default};
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import{a}from"./runtime-QFURDKA2.js";import{a as i,b as c,c as u,d,e,g as t,h as l,i as o}from"./runtime-L2HNXIHW.js";import{a as s,b as m}from"./runtime-B73WLANC.js";var{prev:p,next:h}=i("/mutations"),r={route:"/mutations",meta:{title:"Mutations \u2014 Pulse Docs",description:"Synchronous state changes in Pulse \u2014 how to declare and trigger mutations.",styles:["/docs.css"]},state:{},view:()=>c({currentHref:"/mutations",prev:p,next:h,content:`
|
|
2
|
-
${u("Mutations")}
|
|
3
|
-
${d("Mutations are the only permitted way to change client state. They are synchronous pure functions \u2014 network requests, DOM manipulation, and timers are structurally excluded. After every mutation, Pulse automatically applies constraints and re-renders the view.")}
|
|
4
|
-
|
|
5
|
-
${e("what","What is a mutation?")}
|
|
6
|
-
<p>A mutation is a function with the signature <code>(state, event) => partialState</code>. It receives the current state and the DOM event that triggered it, and returns a plain object to merge into state.</p>
|
|
7
|
-
${t(a(`mutations: {
|
|
8
|
-
increment: (state) => ({ count: state.count + 1 }),
|
|
9
|
-
decrement: (state) => ({ count: state.count - 1 }),
|
|
10
|
-
reset: () => ({ count: 0 }),
|
|
11
|
-
}`,"js"))}
|
|
12
|
-
<p>The returned partial is shallow-merged. Only the returned keys are changed \u2014 everything else in state is preserved.</p>
|
|
13
|
-
|
|
14
|
-
${e("binding","Binding mutations to DOM events")}
|
|
15
|
-
<p>Mutations are bound to DOM events using the <code>data-event</code> attribute in the view HTML:</p>
|
|
16
|
-
${t(a(`<button data-event="increment">+</button> <!-- click \u2192 increment -->
|
|
17
|
-
<button data-event="click:decrement">-</button> <!-- explicit click -->
|
|
18
|
-
<input data-event="change:setName"> <!-- change event -->
|
|
19
|
-
<input data-event="input:setQuery"> <!-- input event (every keystroke) -->`,"html"))}
|
|
20
|
-
|
|
21
|
-
${l(["Event type","Shorthand","Typical use"],[["<code>click</code>",'<code>data-event="mutName"</code>',"Buttons, links"],["<code>change</code>",'<code>data-event="change:mutName"</code>',"Select dropdowns, checkboxes"],["<code>input</code>",'<code>data-event="input:mutName"</code>','Search/filter fields \u2014 add <code>data-debounce="300"</code> to rate-limit']])}
|
|
22
|
-
|
|
23
|
-
${e("debounce","Debounce and throttle")}
|
|
24
|
-
<p>Add <code>data-debounce="300"</code> alongside <code>data-event</code> to delay the mutation until typing stops. The mutation fires once, 300ms after the last keystroke \u2014 not on every character. Use this for live search and filter inputs.</p>
|
|
25
|
-
${t(a(`<input data-event="input:search" data-debounce="300">
|
|
26
|
-
<input data-event="input:filter" data-throttle="100">`,"html"))}
|
|
27
|
-
<p><code>data-throttle="100"</code> fires at most once per 100ms \u2014 useful when you want frequent updates but need to limit the rate. Both attributes accept a value in milliseconds and apply to <code>input</code> and <code>change</code> events. No per-spec timer code needed.</p>
|
|
28
|
-
|
|
29
|
-
${e("event-arg","The event argument")}
|
|
30
|
-
<p>The second argument to a mutation is the native DOM <code>Event</code> object, giving access to the element and its value:</p>
|
|
31
|
-
${t(a(`mutations: {
|
|
32
|
-
setName: (state, e) => ({ name: e.target.value }),
|
|
33
|
-
setCountry: (state, e) => ({ country: e.target.value }),
|
|
34
|
-
toggle: (state, e) => ({ checked: e.target.checked }),
|
|
35
|
-
}`,"js"))}
|
|
36
|
-
|
|
37
|
-
${e("partial-merge","Partial state merge")}
|
|
38
|
-
<p>Only the keys that need to change are returned. The runtime merges the returned object into the existing state at the top level:</p>
|
|
39
|
-
${t(a(`// state = { step: 2, name: 'Alice', email: 'a@b.com' }
|
|
40
|
-
|
|
41
|
-
mutations: {
|
|
42
|
-
nextStep: (state) => ({ step: state.step + 1 }),
|
|
43
|
-
// After: { step: 3, name: 'Alice', email: 'a@b.com' }
|
|
44
|
-
// name and email are untouched
|
|
45
|
-
}`,"js"))}
|
|
46
|
-
${o("warning","The merge is <strong>shallow</strong>. When state has nested objects, returning a new version of a nested key replaces the entire sub-object \u2014 not a deep merge. Use the spread operator to preserve nested fields:")}
|
|
47
|
-
${t(a(`mutations: {
|
|
48
|
-
setEmail: (state, e) => ({
|
|
49
|
-
// Spread the nested object to preserve sibling keys
|
|
50
|
-
user: { ...state.user, email: e.target.value }
|
|
51
|
-
}),
|
|
52
|
-
}`,"js"))}
|
|
53
|
-
|
|
54
|
-
${e("no-side-effects","No side effects")}
|
|
55
|
-
<p>Mutations are pure functions. Network requests, DOM manipulation, and timers belong in <a href="/actions">actions</a> \u2014 not here. The structural separation is what lets Pulse re-render predictably, apply constraints safely, and make state changes auditable.</p>
|
|
56
|
-
|
|
57
|
-
${e("constraints","Mutations and constraints")}
|
|
58
|
-
<p>After every mutation, Pulse automatically applies any <a href="/constraints">constraints</a> declared in the spec. State bounds are never checked inside mutations \u2014 they are declared once and enforced by the framework regardless of what a mutation returns:</p>
|
|
59
|
-
${t(a(`{
|
|
60
|
-
state: { count: 0 },
|
|
61
|
-
constraints: { count: { min: 0, max: 10 } },
|
|
62
|
-
mutations: {
|
|
63
|
-
increment: (state) => ({ count: state.count + 1 }),
|
|
64
|
-
// count is automatically clamped to 10 \u2014 no need to check here
|
|
65
|
-
}
|
|
66
|
-
}`,"js"))}
|
|
67
|
-
|
|
68
|
-
${e("forms","Mutations and forms")}
|
|
69
|
-
<p>Mirroring every keystroke into state via <code>data-event="input:..."</code> causes <code>innerHTML</code> replacement on each keypress, which destroys input focus. Pulse prevents this by keeping form inputs uncontrolled \u2014 values are captured via <code>FormData</code> in an action's <code>onStart</code>, before the view re-renders:</p>
|
|
70
|
-
${t(a(`<!-- mirroring every keystroke causes focus loss -->
|
|
71
|
-
<input data-event="input:setEmail">
|
|
72
|
-
|
|
73
|
-
<!-- uncontrolled: values captured at submit via FormData -->
|
|
74
|
-
<form data-action="submit">
|
|
75
|
-
<input name="email" type="email">
|
|
76
|
-
<button>Submit</button>
|
|
77
|
-
</form>`,"html"))}
|
|
78
|
-
${o("tip","Mutations are the right tool for binary UI state (toggles, step counters, tab selections) and controls where a re-render on each change is acceptable \u2014 such as live character counters or filtered lists. For form submission, use actions.")}
|
|
79
|
-
`})};var n=document.getElementById("pulse-root");n&&!n.dataset.pulseMounted&&(n.dataset.pulseMounted="1",s(r,n,window.__PULSE_SERVER__||{},{ssr:!0}),m(n,s));var $=r;export{$ as default};
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import{a}from"./runtime-QFURDKA2.js";import{a as s,b as d,c as l,d as c,e,g as o,h as p,i}from"./runtime-L2HNXIHW.js";import{a as n,b as h}from"./runtime-B73WLANC.js";var{prev:u,next:g}=s("/navigation"),r={route:"/navigation",meta:{title:"Navigation \u2014 Pulse Docs",description:"Client-side navigation in Pulse \u2014 how link interception, JSON responses, and spec re-mounting work.",styles:["/docs.css"]},state:{},view:()=>d({currentHref:"/navigation",prev:u,next:g,content:`
|
|
2
|
-
${l("Navigation")}
|
|
3
|
-
${c("Client-side navigation in Pulse requires no configuration. When hydration is active, same-origin link clicks are intercepted automatically \u2014 the server renders the new page and returns JSON, and Pulse swaps the content without a full reload. If anything fails, it falls back to standard browser navigation.")}
|
|
4
|
-
|
|
5
|
-
${e("how-it-works","How it works")}
|
|
6
|
-
<p>When <code>initNavigation</code> is called (part of the hydration bootstrap), Pulse attaches a click listener to the document. When a same-origin <code><a></code> is clicked:</p>
|
|
7
|
-
<ol>
|
|
8
|
-
<li>The default navigation is prevented.</li>
|
|
9
|
-
<li>A fetch request is sent to the new URL with the header <code>X-Pulse-Navigate: true</code>.</li>
|
|
10
|
-
<li>The server renders the page and returns a JSON response instead of full HTML.</li>
|
|
11
|
-
<li>Pulse replaces the current page's root <code>innerHTML</code> with the new content and updates <code>document.title</code>.</li>
|
|
12
|
-
<li>The new page's spec bundle is dynamically imported.</li>
|
|
13
|
-
<li><code>mount()</code> is called to bind the new spec's events.</li>
|
|
14
|
-
<li>The browser history is updated with <code>history.pushState</code>.</li>
|
|
15
|
-
</ol>
|
|
16
|
-
${i("note","If any step fails \u2014 network error, missing bundle, unexpected response \u2014 Pulse falls back to <code>location.href = url</code> for a standard full-page navigation.")}
|
|
17
|
-
|
|
18
|
-
${e("init-navigation","initNavigation")}
|
|
19
|
-
<p><code>initNavigation</code> is called automatically by the hydration bootstrap script. You do not call it manually in application code. It is exported from the runtime for use in custom bootstrap scenarios:</p>
|
|
20
|
-
${o(a(`import { mount } from '@invisibleloop/pulse/runtime'
|
|
21
|
-
import { initNavigation } from '@invisibleloop/pulse/navigate'
|
|
22
|
-
|
|
23
|
-
mount(spec, root, window.__PULSE_SERVER__ || {}, { ssr: true })
|
|
24
|
-
initNavigation(root, mount)`,"js"))}
|
|
25
|
-
|
|
26
|
-
${e("json-response","JSON response shape")}
|
|
27
|
-
<p>When Pulse receives a request with <code>X-Pulse-Navigate: true</code>, it renders the page and returns:</p>
|
|
28
|
-
${o(a(`{
|
|
29
|
-
"html": "<main>...the page content...</main>",
|
|
30
|
-
"title": "New Page Title \u2014 Site",
|
|
31
|
-
"hydrate": "/dist/new-page.boot-abc123.js",
|
|
32
|
-
"serverState": { "key": "value" }
|
|
33
|
-
}`,"js"))}
|
|
34
|
-
${p(["Field","Description"],[["<code>html</code>","The rendered page content (the output of the view function, without the full document wrapper)."],["<code>title</code>","The new page title, set via <code>document.title</code>."],["<code>hydrate</code>","The bundle path for the new spec. <code>null</code> if the page has no hydration."],["<code>serverState</code>","The server data for the new page, used when mounting the new spec."]])}
|
|
35
|
-
|
|
36
|
-
${e("link-interception","Which links are intercepted")}
|
|
37
|
-
<p>Only same-origin links are intercepted. Links with <code>target="_blank"</code>, <code>download</code>, <code>rel="external"</code>, or any cross-origin <code>href</code> are ignored and behave normally:</p>
|
|
38
|
-
${o(a(`<!-- Intercepted by Pulse client navigation -->
|
|
39
|
-
<a href="/about">About</a>
|
|
40
|
-
<a href="/products/42">Product</a>
|
|
41
|
-
|
|
42
|
-
<!-- NOT intercepted \u2014 standard browser navigation -->
|
|
43
|
-
<a href="https://example.com">External</a>
|
|
44
|
-
<a href="/report.pdf" download>Download</a>
|
|
45
|
-
<a href="/admin" target="_blank">Open in new tab</a>`,"html"))}
|
|
46
|
-
|
|
47
|
-
${e("history","Browser history")}
|
|
48
|
-
<p>Pulse uses the History API (<code>history.pushState</code>) to update the URL after each navigation. The back and forward buttons work as expected \u2014 each history entry corresponds to a page navigation.</p>
|
|
49
|
-
<p>When the user navigates back, Pulse receives a <code>popstate</code> event and performs the same fetch-and-swap process for the previous URL.</p>
|
|
50
|
-
|
|
51
|
-
${e("no-hydration","Navigation without hydration")}
|
|
52
|
-
<p>Client-side navigation only works on pages that have loaded the Pulse client runtime (i.e. pages with a <code>hydrate</code> path). If a user navigates from a hydrated page to a non-hydrated page, Pulse falls back to a full page load.</p>
|
|
53
|
-
${i("tip","For documentation sites or mostly-static apps, omitting <code>hydrate</code> entirely and relying on standard browser navigation is simpler and keeps the JS payload at zero.")}
|
|
54
|
-
|
|
55
|
-
${e("scroll","Scroll behaviour")}
|
|
56
|
-
<p>After each client-side navigation, Pulse scrolls to the top of the page \u2014 matching the behaviour of a full page load. If the URL includes a hash (e.g. <code>/docs#section</code>), Pulse scrolls to the target element.</p>
|
|
57
|
-
`})};var t=document.getElementById("pulse-root");t&&!t.dataset.pulseMounted&&(t.dataset.pulseMounted="1",n(r,t,window.__PULSE_SERVER__||{},{ssr:!0}),h(t,n));var N=r;export{N as default};
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import{a as r}from"./runtime-QFURDKA2.js";import{a as c,b as d,c as l,d as h,e,g as i,h as t,i as o}from"./runtime-L2HNXIHW.js";import{a,b as p}from"./runtime-B73WLANC.js";var{prev:u,next:m}=c("/performance"),n={route:"/performance",meta:{title:"Performance \u2014 Pulse Docs",description:"Performance targets, techniques, and built-in optimisations in Pulse.",styles:["/docs.css"]},state:{},view:()=>d({currentHref:"/performance",prev:u,next:m,content:`
|
|
2
|
-
${l("Performance")}
|
|
3
|
-
${h("Performance in Pulse is structural, not optional. Streaming SSR, immutable asset caching, brotli compression, and zero client JS by default are the baseline \u2014 not optimisations applied after the fact. A page that uses the framework correctly cannot score poorly.")}
|
|
4
|
-
|
|
5
|
-
${e("targets","Performance targets")}
|
|
6
|
-
<p>Every page served by Pulse should meet these targets on localhost with no throttling:</p>
|
|
7
|
-
${t(["Metric","Target","How"],[["LCP","Fast","Streaming SSR sends HTML before server data resolves. Actual LCP depends on server location, CDN, and network conditions."],["CLS","0.00","Always set <code>width</code> and <code>height</code> on images; framework never shifts layout"],["Lighthouse Performance","100","Compression, immutable caching, no render-blocking resources"],["Lighthouse Accessibility","100","Semantic HTML, proper alt text, sufficient contrast"],["Lighthouse SEO","100","Meta tags, structured data, canonical links"],["Lighthouse Best Practices","100","HTTPS, security headers, no deprecated APIs"]])}
|
|
8
|
-
${o("note","Run <code>mcp__chrome-devtools__lighthouse_audit</code> after every new page to verify all four scores are 100. Fix any failures before considering the task done.")}
|
|
9
|
-
|
|
10
|
-
${e("streaming-ssr","Streaming SSR")}
|
|
11
|
-
<p>Pulse uses Node.js streams for SSR. The server sends the <code><head></code> and page shell immediately, before any async data resolves. Browsers start downloading CSS and fonts while the server fetches data \u2014 so the user sees a styled shell within milliseconds.</p>
|
|
12
|
-
${i(r(`export default {
|
|
13
|
-
route: '/feed',
|
|
14
|
-
|
|
15
|
-
// Shell renders instantly \u2014 hero, nav, layout
|
|
16
|
-
// Deferred segments wait for data then stream in
|
|
17
|
-
stream: {
|
|
18
|
-
shell: ['header', 'hero'],
|
|
19
|
-
deferred: ['feed', 'sidebar'],
|
|
20
|
-
},
|
|
21
|
-
|
|
22
|
-
server: {
|
|
23
|
-
feed: async () => db.posts.getLatest(20), // slow
|
|
24
|
-
sidebar: async () => db.tags.getPopular(), // slow
|
|
25
|
-
},
|
|
26
|
-
|
|
27
|
-
// view is a keyed object matching stream segments
|
|
28
|
-
view: {
|
|
29
|
-
header: (state) => \`<header>...</header>\`,
|
|
30
|
-
hero: (state) => \`<section class="hero">...</section>\`,
|
|
31
|
-
feed: (state, server) => server.feed.map(renderPost).join(''),
|
|
32
|
-
sidebar: (state, server) => renderSidebar(server.sidebar),
|
|
33
|
-
},
|
|
34
|
-
}`,"js"))}
|
|
35
|
-
|
|
36
|
-
${e("compression","Automatic compression")}
|
|
37
|
-
<p>All responses are compressed automatically. Pulse negotiates the best available encoding from the <code>Accept-Encoding</code> header:</p>
|
|
38
|
-
${t(["Encoding","Priority","Typical savings"],[["Brotli (<code>br</code>)","First choice","20\u201330% smaller than gzip for text content"],["gzip","Fallback","Widely supported, good compression"],["None","Last resort","No compression (rare)"]])}
|
|
39
|
-
<p>HTML, CSS, JavaScript, JSON, XML, and SVG responses are all compressed. Binary formats (images, fonts) are served as-is.</p>
|
|
40
|
-
|
|
41
|
-
${e("asset-caching","Immutable asset caching")}
|
|
42
|
-
<p>Production JS bundles include a content hash in their filename (<code>/dist/counter.boot-a1b2c3d4.js</code>). The server sends <code>Cache-Control: public, max-age=31536000, immutable</code> for these files \u2014 browsers cache them forever and never re-request them unless the hash changes.</p>
|
|
43
|
-
<p>When you deploy a new version, the hash changes, and users automatically get the new file. No cache-busting tricks needed.</p>
|
|
44
|
-
${o("tip","Static assets in <code>public/</code> get <code>max-age=3600</code>. For rarely-updated images, consider versioning them by filename.")}
|
|
45
|
-
|
|
46
|
-
${e("zero-client-js","Zero client JS by default")}
|
|
47
|
-
<p>Pages with no <code>mutations</code>, <code>actions</code>, or <code>persist</code> send no JavaScript at all \u2014 the HTML is entirely self-contained. The Pulse CLI detects this automatically; you never need to opt out. This is appropriate for static content pages: marketing pages, blog posts, documentation.</p>
|
|
48
|
-
${i(r(`// No mutations, actions, or persist \u2192 zero JS sent to browser
|
|
49
|
-
export default {
|
|
50
|
-
route: '/about',
|
|
51
|
-
meta: { title: 'About', styles: ['/app.css'] },
|
|
52
|
-
state: {},
|
|
53
|
-
view: () => \`<main><h1>About us</h1></main>\`,
|
|
54
|
-
}`,"js"))}
|
|
55
|
-
|
|
56
|
-
${e("js-bundle-splitting","JS bundle splitting and caching")}
|
|
57
|
-
<p>When you open the network tab you will see a single <code>[page].boot-[hash].js</code> file loading. What is inside that file depends on how many pages your app has:</p>
|
|
58
|
-
${t(["App size","What the boot file contains","Size (brotli)"],[["Single page","Your spec + the full Pulse runtime bundled together","~3.5 kB"],["Multiple pages","Your spec only \u2014 runtime is in a separate <code>runtime-[hash].js</code> chunk","~0.35\u20130.5 kB"]])}
|
|
59
|
-
<p>With multiple pages, esbuild's code splitting extracts the Pulse runtime into a shared chunk because every page imports it. The browser downloads it once and caches it \u2014 subsequent page navigations only fetch the small per-page boot file.</p>
|
|
60
|
-
<p><strong>What you see in the network tab across navigations:</strong></p>
|
|
61
|
-
<ul>
|
|
62
|
-
<li><strong>First page visit</strong> \u2014 <code>runtime-[hash].js</code> (~3.1 kB) + <code>home.boot-[hash].js</code> (~0.35 kB)</li>
|
|
63
|
-
<li><strong>Navigate to another page</strong> \u2014 <code>contact.boot-[hash].js</code> (~0.47 kB) only. Runtime already cached.</li>
|
|
64
|
-
<li><strong>Return visit</strong> \u2014 nothing. Both files served from cache with <code>immutable</code> headers.</li>
|
|
65
|
-
</ul>
|
|
66
|
-
${o("tip","The runtime hash only changes when the Pulse runtime itself is updated \u2014 not when your app changes. Deploying new pages or mutations does not bust the runtime cache for returning visitors.")}
|
|
67
|
-
|
|
68
|
-
${e("security-headers","Security headers")}
|
|
69
|
-
<p>Every response \u2014 including 404 and 500 errors \u2014 carries a full set of security headers automatically. There is no configuration step and no way to accidentally omit them:</p>
|
|
70
|
-
${t(["Header","Value"],[["<code>X-Content-Type-Options</code>","<code>nosniff</code>"],["<code>X-Frame-Options</code>","<code>DENY</code>"],["<code>Referrer-Policy</code>","<code>strict-origin-when-cross-origin</code>"],["<code>Permissions-Policy</code>","<code>camera=(), microphone=(), geolocation=()</code>"],["<code>Cross-Origin-Opener-Policy</code>","<code>same-origin</code>"],["<code>Cross-Origin-Resource-Policy</code>","<code>same-origin</code>"]])}
|
|
71
|
-
<p>These headers are applied automatically \u2014 no configuration needed.</p>
|
|
72
|
-
|
|
73
|
-
${e("browser-support","Browser support")}
|
|
74
|
-
<p>Pulse ships modern JavaScript and CSS without transpilation or polyfills. The effective minimum is set by two features:</p>
|
|
75
|
-
${t(["Constraint","Chrome","Firefox","Safari","Edge","Since"],[["<code>?.</code> optional chaining (JS)","80","74","13.1","80","Feb \u2013 Mar 2020"],["<code>gap</code> on flexbox (CSS)","84","63","14.1","84","Aug 2020 \u2013 Apr 2021"]])}
|
|
76
|
-
<p>In practice, <strong>Safari 14.1 (April 2021) is the combined floor</strong> \u2014 browsers released after that date support everything Pulse uses. This covers roughly 95%+ of global traffic.</p>
|
|
77
|
-
${o("note","No explicit <code>target</code> is set in the esbuild config, so syntax ships as written. If you need to support older browsers, set <code>target</code> in <code>scripts/build.js</code> and esbuild will downcompile optional chaining and other modern syntax automatically.")}
|
|
78
|
-
|
|
79
|
-
${e("cls","Preventing layout shift (CLS)")}
|
|
80
|
-
<p>Pulse targets 0.00 CLS. Layout shift is caused by elements that change size or position after the initial paint. These rules prevent it:</p>
|
|
81
|
-
<ul>
|
|
82
|
-
<li>Always set <code>width</code> and <code>height</code> on images (use the <code>img()</code> helper)</li>
|
|
83
|
-
<li>Never inject content above existing content after page load</li>
|
|
84
|
-
<li>Use <code>aspect-ratio</code> CSS for embeds (videos, iframes)</li>
|
|
85
|
-
<li>Avoid loading web fonts that cause FOUT \u2014 use <code>font-display: swap</code> or system fonts</li>
|
|
86
|
-
</ul>
|
|
87
|
-
|
|
88
|
-
${e("lcp","Optimising LCP")}
|
|
89
|
-
<p>LCP (Largest Contentful Paint) is typically your hero image or largest heading. Tips:</p>
|
|
90
|
-
<ul>
|
|
91
|
-
<li>Use <code>priority: true</code> on the LCP image \u2014 sets <code>fetchpriority="high"</code> and <code>loading="eager"</code></li>
|
|
92
|
-
<li>Avoid blocking server fetches \u2014 use <code>stream</code> so the shell renders without waiting for data</li>
|
|
93
|
-
<li>Keep your hero HTML inline (SSR) \u2014 never rely on client JS to render the LCP element</li>
|
|
94
|
-
<li>Use modern image formats (AVIF, WebP) via the <code>picture()</code> helper</li>
|
|
95
|
-
</ul>
|
|
96
|
-
|
|
97
|
-
${o("warning","Never render your LCP element (hero image, main heading) client-side. If it requires a client JS import or a dynamic import, it will not paint until JS executes \u2014 pushing LCP from <100ms to >500ms.")}
|
|
98
|
-
`})};var s=document.getElementById("pulse-root");s&&!s.dataset.pulseMounted&&(s.dataset.pulseMounted="1",a(n,s,window.__PULSE_SERVER__||{},{ssr:!0}),p(s,a));var P=n;export{P as default};
|