@invisibleloop/pulse 0.1.28 → 0.1.29

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.
Files changed (117) hide show
  1. package/.claude/settings.local.json +113 -0
  2. package/.github/workflows/publish.yml +11 -21
  3. package/docs/public/.pulse-ui-version +1 -1
  4. package/docs/public/docs.css +19 -1
  5. package/docs/public/pulse-ui.css +1 -0
  6. package/docs/server.js +5 -2
  7. package/docs/src/lib/highlight.js +57 -13
  8. package/docs/src/lib/layout.js +5 -2
  9. package/docs/src/pages/faq.js +5 -2
  10. package/docs/src/pages/home.js +9 -5
  11. package/docs/src/pages/meta.js +21 -0
  12. package/docs/src/pages/routing.js +12 -1
  13. package/package.json +1 -1
  14. package/src/agent/guide-routing.md +20 -0
  15. package/src/agent/guide-spec.md +9 -1
  16. package/src/cli/scaffold.js +63 -2
  17. package/src/server/index.js +21 -6
  18. package/src/server/server.test.js +47 -0
  19. package/docs/public/dist/accessibility.boot-5DVTARJU.js +0 -115
  20. package/docs/public/dist/actions.boot-P66HKQEM.js +0 -164
  21. package/docs/public/dist/auth.boot-IMAJAUPH.js +0 -140
  22. package/docs/public/dist/caching.boot-DVR6KDE7.js +0 -53
  23. package/docs/public/dist/components--accordion.boot-3HVKMNWC.js +0 -11
  24. package/docs/public/dist/components--alert.boot-GCEXOZAC.js +0 -6
  25. package/docs/public/dist/components--app-badge.boot-DVT3GCHJ.js +0 -6
  26. package/docs/public/dist/components--avatar.boot-PSW24EVA.js +0 -5
  27. package/docs/public/dist/components--badge.boot-TYDY2RMK.js +0 -7
  28. package/docs/public/dist/components--banner.boot-EI5PZSZK.js +0 -7
  29. package/docs/public/dist/components--breadcrumbs.boot-SMA2E2GO.js +0 -34
  30. package/docs/public/dist/components--button.boot-J54BQM2E.js +0 -23
  31. package/docs/public/dist/components--card.boot-PZGNDIB6.js +0 -138
  32. package/docs/public/dist/components--carousel.boot-TP6LPFZZ.js +0 -12
  33. package/docs/public/dist/components--charts.boot-2EOYQWKL.js +0 -108
  34. package/docs/public/dist/components--checkbox.boot-DS5BSL6T.js +0 -54
  35. package/docs/public/dist/components--cluster.boot-HHVIBBJG.js +0 -9
  36. package/docs/public/dist/components--code-window.boot-2GR2DV33.js +0 -20
  37. package/docs/public/dist/components--container.boot-7LOOGK2K.js +0 -5
  38. package/docs/public/dist/components--cta.boot-FSNZ5YRT.js +0 -11
  39. package/docs/public/dist/components--divider.boot-3NI2C3QG.js +0 -6
  40. package/docs/public/dist/components--empty.boot-YX2UR3PV.js +0 -7
  41. package/docs/public/dist/components--feature.boot-MUD7NSUO.js +0 -13
  42. package/docs/public/dist/components--fieldset.boot-J7BYHMKF.js +0 -19
  43. package/docs/public/dist/components--fileupload.boot-NIKVTTPD.js +0 -52
  44. package/docs/public/dist/components--footer.boot-EYUK5FRG.js +0 -14
  45. package/docs/public/dist/components--grid.boot-URDQVDDR.js +0 -59
  46. package/docs/public/dist/components--heading.boot-BPQKU43E.js +0 -44
  47. package/docs/public/dist/components--hero.boot-4RAPRGAB.js +0 -17
  48. package/docs/public/dist/components--icons.boot-ZITNU5JP.js +0 -68
  49. package/docs/public/dist/components--image.boot-XEEGHQZF.js +0 -19
  50. package/docs/public/dist/components--input.boot-SGASZG5K.js +0 -7
  51. package/docs/public/dist/components--list.boot-W3XC5MHD.js +0 -55
  52. package/docs/public/dist/components--media.boot-5VFIETZO.js +0 -13
  53. package/docs/public/dist/components--modal.boot-RZUYXBN2.js +0 -47
  54. package/docs/public/dist/components--nav.boot-ODBOHU7O.js +0 -33
  55. package/docs/public/dist/components--pricing.boot-4AQ4ZVBY.js +0 -21
  56. package/docs/public/dist/components--progress.boot-GHAGYZOK.js +0 -30
  57. package/docs/public/dist/components--prose.boot-QANJL6JI.js +0 -67
  58. package/docs/public/dist/components--pullquote.boot-Q2WMNAZU.js +0 -22
  59. package/docs/public/dist/components--radio.boot-TJRDQ2OL.js +0 -75
  60. package/docs/public/dist/components--rating.boot-QBAN6DEL.js +0 -38
  61. package/docs/public/dist/components--search.boot-PXH5O5AG.js +0 -17
  62. package/docs/public/dist/components--section.boot-AQGIYHWW.js +0 -12
  63. package/docs/public/dist/components--segmented.boot-BEVTKEJO.js +0 -33
  64. package/docs/public/dist/components--select.boot-47X5RHOC.js +0 -10
  65. package/docs/public/dist/components--slider.boot-PSRRX7XL.js +0 -47
  66. package/docs/public/dist/components--spinner.boot-MZ5MO2OH.js +0 -22
  67. package/docs/public/dist/components--stack.boot-DI4NJXBF.js +0 -9
  68. package/docs/public/dist/components--stat.boot-QMFUWBQT.js +0 -9
  69. package/docs/public/dist/components--stepper.boot-34PP2NEV.js +0 -22
  70. package/docs/public/dist/components--table.boot-FCQGSFIQ.js +0 -11
  71. package/docs/public/dist/components--testimonial.boot-DWQPDKYG.js +0 -11
  72. package/docs/public/dist/components--textarea.boot-QVXLBOJ5.js +0 -4
  73. package/docs/public/dist/components--timeline.boot-26LN52P2.js +0 -95
  74. package/docs/public/dist/components--toggle.boot-IQQEI76S.js +0 -29
  75. package/docs/public/dist/components--tooltip.boot-LGHCO6NN.js +0 -9
  76. package/docs/public/dist/components.boot-SE6PQ4P7.js +0 -103
  77. package/docs/public/dist/config.boot-DTRRWUE6.js +0 -126
  78. package/docs/public/dist/constraints.boot-DUHDZBMC.js +0 -71
  79. package/docs/public/dist/deploy.boot-SLAD3NI2.js +0 -163
  80. package/docs/public/dist/docs-8e3d4b5c.css +0 -1
  81. package/docs/public/dist/extending.boot-UA3CN243.js +0 -159
  82. package/docs/public/dist/faq.boot-6EQAWLQR.js +0 -43
  83. package/docs/public/dist/getting-started.boot-TDKIFL5U.js +0 -86
  84. package/docs/public/dist/guard.boot-AUHAWTG4.js +0 -80
  85. package/docs/public/dist/home.boot-BVQXRH32.js +0 -383
  86. package/docs/public/dist/how-it-works.boot-LTWAKWKW.js +0 -104
  87. package/docs/public/dist/hydration.boot-JRM6IPJL.js +0 -78
  88. package/docs/public/dist/images.boot-M6ZVKTZS.js +0 -80
  89. package/docs/public/dist/manifest.json +0 -94
  90. package/docs/public/dist/meta.boot-7NXGPHR4.js +0 -79
  91. package/docs/public/dist/mutations.boot-F6F43UDX.js +0 -79
  92. package/docs/public/dist/navigation.boot-AOXWS3ZF.js +0 -57
  93. package/docs/public/dist/performance.boot-C3UPCOBK.js +0 -98
  94. package/docs/public/dist/persist.boot-WT32PQOQ.js +0 -61
  95. package/docs/public/dist/project-structure.boot-FB3LRVJ4.js +0 -63
  96. package/docs/public/dist/prompt-examples.boot-YKR4VDK4.js +0 -31
  97. package/docs/public/dist/pulse-ui-81a85c03.css +0 -1
  98. package/docs/public/dist/raw-responses.boot-M4KA5YXL.js +0 -104
  99. package/docs/public/dist/routing.boot-FNX5FDGH.js +0 -70
  100. package/docs/public/dist/runtime-B73WLANC.js +0 -1
  101. package/docs/public/dist/runtime-KO4BHUQ3.js +0 -49
  102. package/docs/public/dist/runtime-L2HNXIHW.js +0 -59
  103. package/docs/public/dist/runtime-QFURDKA2.js +0 -5
  104. package/docs/public/dist/runtime-UVPXO4IR.js +0 -375
  105. package/docs/public/dist/runtime-VMJA3Z4N.js +0 -10
  106. package/docs/public/dist/runtime-ZJ4FXT5O.js +0 -11
  107. package/docs/public/dist/server-api.boot-K7X3LCFB.js +0 -219
  108. package/docs/public/dist/server-data.boot-Y7HQYC4R.js +0 -157
  109. package/docs/public/dist/slash-commands.boot-V2UV7OW2.js +0 -26
  110. package/docs/public/dist/spec.boot-2WU7ZHCV.js +0 -159
  111. package/docs/public/dist/state.boot-B24GUE3R.js +0 -73
  112. package/docs/public/dist/store.boot-TLIB4XHH.js +0 -150
  113. package/docs/public/dist/streaming.boot-W2DZSMW4.js +0 -80
  114. package/docs/public/dist/stripe.boot-QN3C2GEL.js +0 -164
  115. package/docs/public/dist/supabase.boot-BG4XXLZE.js +0 -303
  116. package/docs/public/dist/testing.boot-6U4WKMTE.js +0 -130
  117. package/docs/public/dist/validation.boot-PQHYGW5B.js +0 -100
@@ -1,163 +0,0 @@
1
- import{a as t}from"./runtime-QFURDKA2.js";import{a as d,b as c,c as p,d as l,e,g as o,h as r,i as s}from"./runtime-L2HNXIHW.js";import{a as n,b as u}from"./runtime-B73WLANC.js";var{prev:m,next:h}=d("/deploy"),i={route:"/deploy",meta:{title:"Deployment \u2014 Pulse Docs",description:"Deploy a Pulse app to a VPS, Docker, Fly.io, Railway, or Render.",styles:["/docs.css"]},state:{},view:()=>c({currentHref:"/deploy",prev:m,next:h,content:`
2
- ${p("Deployment")}
3
- ${l("Pulse deploys as a single Node.js process. No adapters, no serverless wrapping, no separate static file server required. Build once, run anywhere Node 22+ runs. All guarantees \u2014 security headers, brotli compression, immutable asset caching \u2014 are active in production automatically.")}
4
-
5
- ${e("build","Build")}
6
- <p>Run the production build before deploying:</p>
7
- ${o(t("npm run build","bash"))}
8
- <p>This generates content-hashed bundles in <code>public/dist/</code> and writes <code>public/dist/manifest.json</code>. The server reads the manifest at startup to resolve hydration script paths.</p>
9
- ${s("warning","Without a manifest, the server falls back to serving source files directly \u2014 no compression, no content-hashed filenames, and no <code>immutable</code> cache headers. Always run <code>npm run build</code> before deploying to production.")}
10
-
11
- ${e("files","What to deploy")}
12
- ${r(["Include","Reason"],[["<code>src/</code>","Page specs \u2014 imported by the server at runtime"],["<code>public/</code>","Static assets and built bundles (<code>public/dist/</code>)"],["<code>server.js</code>","Entry point"],["<code>pulse.config.js</code>","Server config"],["<code>package.json</code> + <code>node_modules/</code>","Runtime dependencies"]])}
13
- ${r(["Exclude","Reason"],[["<code>.claude/</code>","AI agent config \u2014 not needed at runtime"],["<code>.pulse/</code>","Local report data \u2014 not needed at runtime"]])}
14
-
15
- ${e("env-vars","Environment variables")}
16
- ${r(["Variable","Default","Description"],[["<code>NODE_ENV</code>","<code>development</code>","Set to <code>production</code> to enable HSTS headers and production cache behaviour."],["<code>PORT</code>","Value in <code>pulse.config.js</code> (default <code>3000</code>)","Override the listening port. Most PaaS platforms set this automatically."]])}
17
- ${o(t("NODE_ENV=production pulse start","bash"))}
18
-
19
- ${e("pm2","VPS with PM2")}
20
- <p>PM2 keeps the process alive, restarts it on crash, and manages logs.</p>
21
- ${o(t(`# Install PM2 globally
22
- npm install -g pm2
23
-
24
- # Start the app
25
- NODE_ENV=production pm2 start server.js --name myapp
26
-
27
- # Persist across reboots
28
- pm2 save
29
- pm2 startup
30
-
31
- # Zero-downtime reload after a deploy
32
- pm2 reload myapp`,"bash"))}
33
- <p>For repeatable deployments, check an <code>ecosystem.config.cjs</code> into version control:</p>
34
- ${o(t(`// ecosystem.config.cjs
35
- module.exports = {
36
- apps: [{
37
- name: 'myapp',
38
- script: 'server.js',
39
- env_production: {
40
- NODE_ENV: 'production',
41
- PORT: 3000,
42
- },
43
- }],
44
- }`,"js"))}
45
- ${o(t("pm2 start ecosystem.config.cjs --env production","bash"))}
46
-
47
- ${e("docker","Docker")}
48
- <p>A two-stage build keeps the image small \u2014 build tools stay in the first stage.</p>
49
- ${o(t(`# ---- build stage ----
50
- FROM node:22-alpine AS build
51
- WORKDIR /app
52
- COPY package*.json ./
53
- RUN npm ci
54
- COPY . .
55
- RUN npx pulse build
56
-
57
- # ---- runtime stage ----
58
- FROM node:22-alpine
59
- WORKDIR /app
60
- ENV NODE_ENV=production
61
- COPY package*.json ./
62
- RUN npm ci --omit=dev
63
- COPY --from=build /app/src ./src
64
- COPY --from=build /app/public ./public
65
- COPY --from=build /app/server.js ./server.js
66
- COPY pulse.config.js ./
67
- EXPOSE 3000
68
- CMD ["node", "server.js"]`,"bash"))}
69
- ${o(t(`docker build -t myapp .
70
- docker run -p 3000:3000 --env NODE_ENV=production myapp`,"bash"))}
71
-
72
- ${e("fly","Fly.io")}
73
- ${o(t(`# fly.toml
74
- app = 'myapp'
75
- primary_region = 'lhr'
76
-
77
- [env]
78
- NODE_ENV = 'production'
79
-
80
- [build]
81
- [build.args]
82
- NODE_VERSION = '22'
83
-
84
- [deploy]
85
- release_command = 'npx pulse build'
86
-
87
- [http_service]
88
- internal_port = 3000
89
- force_https = true
90
- auto_stop_machines = 'stop'
91
- auto_start_machines = true
92
-
93
- [[vm]]
94
- memory = '256mb'
95
- cpu_kind = 'shared'
96
- cpus = 1`,"bash"))}
97
- ${o(t(`# First deploy
98
- fly launch
99
-
100
- # Subsequent deploys
101
- fly deploy`,"bash"))}
102
-
103
- ${e("railway","Railway")}
104
- <p>Railway auto-detects Node apps. Add a <code>railway.json</code> to set the build and start commands:</p>
105
- ${o(t(`{
106
- "$schema": "https://railway.app/railway.schema.json",
107
- "build": {
108
- "builder": "NIXPACKS",
109
- "buildCommand": "npm run build"
110
- },
111
- "deploy": {
112
- "startCommand": "NODE_ENV=production node server.js",
113
- "healthcheckPath": "/",
114
- "restartPolicyType": "ON_FAILURE"
115
- }
116
- }`,"js"))}
117
-
118
- ${e("render","Render")}
119
- ${r(["Setting","Value"],[["Environment","Node"],["Build command","<code>npm install &amp;&amp; npm run build</code>"],["Start command","<code>NODE_ENV=production node server.js</code>"],["Node version","<code>22.x</code>"]])}
120
- <p>Set <code>NODE_ENV=production</code> in the Render environment variables dashboard.</p>
121
-
122
- ${e("serverless","Vercel, Cloudflare, and edge platforms")}
123
- <p>These platforms each have multiple products with very different runtimes \u2014 the compatibility story varies significantly between them.</p>
124
-
125
- ${e("vercel","Vercel")}
126
- <p>Vercel has two distinct runtimes:</p>
127
- ${r(["Product","Runtime","Pulse compatible?"],[["<strong>Functions</strong> (Node.js)","Full Node.js \u2014 same built-ins as a VPS","Partially \u2014 see below"],["<strong>Edge Functions</strong>","V8 isolates (no Node built-ins)","No"]])}
128
- <p><strong>Vercel Functions (Node.js)</strong> can run Pulse with some differences in behaviour:</p>
129
- ${r(["Feature","Behaviour on Vercel Functions"],[["<code>serverTtl</code> cache","Works within a warm instance, but cold starts reset it. Not reliable for expensive queries."],["Streaming SSR","Vercel Functions support streaming responses, but require explicit configuration via <code>supportsResponseStreaming</code>."],["Static files","Vercel serves <code>public/</code> automatically via its CDN \u2014 Pulse's static file serving is bypassed."],["Security headers","Work as normal \u2014 Pulse adds them to every response."]])}
130
- ${s("warning","Vercel Functions are not a tested or officially supported deployment target for Pulse. The adapter pattern (exporting a request handler rather than starting a server) is not yet documented. Railway, Render, or Fly.io are simpler choices with no adaptation required.")}
131
-
132
- ${e("cloudflare","Cloudflare")}
133
- ${r(["Product","Runtime","Pulse compatible?"],[["<strong>Workers</strong>","V8 isolates \u2014 no <code>node:http</code>, <code>node:fs</code>, <code>node:zlib</code>","No"],["<strong>Pages Functions</strong>","Same V8 isolate runtime as Workers","No"],["<strong>CDN / proxy</strong>","Sits in front of your origin server","Yes \u2014 works great with Fly.io or a VPS behind it"]])}
134
- ${s("tip","The recommended pattern for edge performance: deploy Pulse to <strong>Fly.io</strong> (which runs real VMs in many regions) and put <strong>Cloudflare as a CDN/proxy</strong> in front of it. Static assets and cached HTML are served from Cloudflare's edge; dynamic requests are proxied to the nearest Fly VM.")}
135
-
136
- ${e("https","HTTPS and reverse proxy")}
137
- <p>Pulse detects TLS automatically. When a request arrives with an <code>x-forwarded-proto: https</code> header or over a direct TLS socket, <code>Strict-Transport-Security: max-age=31536000; includeSubDomains</code> is added to the response. All four platforms above forward this header \u2014 no Pulse config is needed.</p>
138
- <p>If running behind nginx for TLS termination:</p>
139
- ${o(t(`# nginx \u2014 TLS termination, proxy to Pulse
140
- server {
141
- listen 443 ssl;
142
- server_name myapp.com;
143
-
144
- ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem;
145
- ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem;
146
-
147
- location / {
148
- proxy_pass http://localhost:3000;
149
- proxy_http_version 1.1;
150
- proxy_set_header Host $host;
151
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
152
- proxy_set_header X-Forwarded-Proto $scheme;
153
- }
154
- }
155
-
156
- # Redirect HTTP to HTTPS
157
- server {
158
- listen 80;
159
- server_name myapp.com;
160
- return 301 https://$host$request_uri;
161
- }`,"bash"))}
162
- ${s("tip",`Use <a href="https://certbot.eff.org">Certbot</a> to obtain and auto-renew a free Let's Encrypt certificate: <code>certbot --nginx -d myapp.com</code>.`)}
163
- `})};var a=document.getElementById("pulse-root");a&&!a.dataset.pulseMounted&&(a.dataset.pulseMounted="1",n(i,a,window.__PULSE_SERVER__||{},{ssr:!0}),u(a,n));var P=i;export{P as default};
@@ -1 +0,0 @@
1
- *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}:root{--bg:#0d0d10;--surface:#111116;--surface-2:#18181f;--border:#38383f;--border-subtle:#1a1a20;--text:#e2e2ea;--muted:#9090a0;--muted-2:#7e7e92;--accent:#c9b800;--accent-text:#0a0a0a;--accent-hover:#e0ce00;--accent-dim:rgba(201,184,0,0.12);--green:#3dd68c;--red:#ff6b6b;--tok-kw:#c792ea;--tok-str:#c3e88d;--tok-cmt:#7a8a9a;--tok-num:#f78c6c;--tok-fn:#82aaff;--tok-prop:#ffcb6b;--tok-op:#89ddff;--tok-punct:#7e8899;--sidebar-w:260px;--header-h:52px;--content-w:740px;--font:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;--mono:"Fira Code","Cascadia Code","JetBrains Mono","Menlo","Monaco",monospace;--radius:8px}html{font-size:16px}body{font-family:var(--font);background:var(--bg);color:var(--text);line-height:1.6;min-height:100vh;-webkit-font-smoothing:antialiased}a{color:var(--accent);text-decoration:none}a:hover{color:var(--accent-hover)}.home{min-height:100vh;background:#f0e642;background-image:radial-gradient( circle,rgba(0,0,0,0.12) 1px,transparent 1px );background-size:22px 22px;color:#0a0a0a;--accent:#0a0a0a;--accent-hover:#2a2a2a;--accent-dim:rgba(0,0,0,0.07);--text:#0a0a0a;--muted:#52524a;--muted-2:#888880;--surface:#ffffff;--surface-2:#f0ede0;--border:#c8c4aa;--border-subtle:rgba(0,0,0,0.1);--green:#1a7a4a;--red:#c0392b}.home-nav{display:flex;align-items:center;justify-content:space-between;padding:1.25rem 2rem;border-bottom:none;position:sticky;top:0;background:#0a0a0a;backdrop-filter:none;z-index:10;--accent:#f0e642;--text:#ffffff}.home-nav-links{display:flex;align-items:center;gap:1.5rem}.home-nav-links a{color:rgba(255,255,255,0.85);font-size:0.9rem;transition:color 0.15s}.home-nav-links a:hover{color:#ffffff}.home-nav-links .btn-primary{background:#f0e642;color:#0a0a0a;font-weight:600;padding:0.4rem 1rem;border-radius:6px;font-size:0.875rem}.home-nav-links .btn-primary:hover{background:#f7f040;color:#0a0a0a}.hero{max-width:860px;margin:0 auto;padding:5rem 2rem;text-align:center;background:none}.hero-icon{margin-bottom:1.25rem}.hero-badge{display:inline-flex;align-items:center;gap:0.4rem;font-size:0.78rem;font-weight:600;color:#0a0a0a;background:rgba(0,0,0,0.08);border:1px solid rgba(0,0,0,0.18);border-radius:100px;padding:0.3rem 0.85rem;margin-bottom:2rem;letter-spacing:0.06em;text-transform:uppercase}.hero h1{font-size:clamp(2.5rem,6vw,4.5rem);font-weight:800;line-height:1.1;letter-spacing:-0.03em;text-wrap:balance;margin-bottom:1.5rem;color:#0a0a0a;background:none;-webkit-background-clip:unset;-webkit-text-fill-color:#0a0a0a;background-clip:unset}.hero-subtitle{font-size:1.1rem;color:var(--muted);max-width:580px;margin:0 auto 2.5rem;line-height:1.65}.hero-ctas{display:flex;gap:1rem;justify-content:center;flex-wrap:wrap}.btn-primary{display:inline-flex;align-items:center;padding:0.7rem 1.5rem;border-radius:var(--radius);font-size:0.95rem;font-weight:600;background:#0a0a0a;color:#f0e642;text-decoration:none;border:2px solid #0a0a0a;transition:background 0.15s,color 0.15s,transform 0.15s}.btn-primary:hover{background:#f0e642;color:#0a0a0a;border-color:#0a0a0a;transform:translateY(-1px)}.btn-secondary{display:inline-flex;align-items:center;padding:0.7rem 1.5rem;border-radius:var(--radius);font-size:0.95rem;font-weight:600;background:transparent;color:#0a0a0a;border:1px solid rgba(0,0,0,0.25);text-decoration:none;transition:background 0.15s,border-color 0.15s}.btn-secondary:hover{background:rgba(0,0,0,0.06);border-color:#0a0a0a;color:#0a0a0a}.home-cta .btn-primary{background:#f0e642;color:#0a0a0a;border-color:#f0e642}.home-cta .btn-primary:hover{background:#0a0a0a;color:#f0e642;border-color:#f0e642;transform:translateY(-1px)}.home-cta .btn-secondary{background:transparent;border-color:rgba(255,255,255,0.25);color:rgba(255,255,255,0.75)}.home-cta .btn-secondary:hover{background:rgba(255,255,255,0.08);border-color:rgba(255,255,255,0.5);color:#ffffff}.home-code{background:#111114;padding:5rem 2rem 6rem}.home-code-inner{max-width:1000px;margin:0 auto}.home-code-header{text-align:center;margin-bottom:2.5rem}.home-code-header h2{font-size:clamp(1.6rem,3vw,2.2rem);font-weight:700;letter-spacing:-0.02em;color:#ffffff;margin-bottom:0.6rem}.home-code-header p{color:rgba(255,255,255,0.5);font-size:1.1rem;max-width:560px;margin:0 auto;line-height:1.65}.faq-item{padding:2rem 0;border-bottom:1px solid var(--border-subtle)}.faq-item:last-child{border-bottom:none}.faq-q{font-size:1.1rem;font-weight:600;color:var(--text);margin-bottom:0.875rem;letter-spacing:-0.01em;line-height:1.4}.faq-a p{font-size:0.925rem;color:var(--muted);line-height:1.75;max-width:68ch;margin-bottom:0.75rem}.faq-a p:last-child{margin-bottom:0}.faq-a .code-block{margin:0.75rem 0}.home-footer{border-top:1px solid rgba(255,255,255,0.08);text-align:center;padding:2rem;background:#0a0a0a;color:rgba(255,255,255,0.6);font-size:0.82rem}.home-footer a{color:rgba(255,255,255,0.75)}.home-footer a:hover{color:#f0e642}.section-label{font-size:1rem;font-weight:700;text-transform:uppercase;letter-spacing:0.1em;color:#0a0a0a;text-align:center;margin-bottom:1rem;opacity:0.5}.home-stats{display:flex;align-items:center;justify-content:center;gap:0;padding:2.5rem 2rem;border-top:none;border-bottom:1px solid rgba(255,255,255,0.08);background:#0a0a0a;flex-wrap:wrap}.home-stat{display:flex;flex-direction:column;align-items:center;gap:0.35rem;padding:0.75rem 3rem}.home-stat-value{font-size:2rem;font-weight:700;color:#f0e642;letter-spacing:-0.02em;line-height:1}.home-stat-label{font-size:1.1rem;color:rgba(255,255,255,0.7);text-align:center}.home-stat-divider{width:1px;height:2.5rem;background:#f0e642;flex-shrink:0}.how{background:#0a0a0a;background-image:radial-gradient( circle,rgba(255,255,255,0.08) 1px,transparent 1px );background-size:22px 22px;padding:5rem 2rem;margin:0;max-width:none;--accent:#f0e642;--text:#ffffff;--muted:47473e;--border:rgba(255,255,255,0.12)}.how-inner{max-width:960px;margin:0 auto;text-align:center}.how .section-label{color:#f0e642;opacity:1}.how-steps{display:flex;align-items:flex-start;justify-content:center;gap:0;margin-top:3rem}.how-step{flex:1;max-width:260px;display:flex;flex-direction:column;align-items:center;gap:0.75rem;padding:0 1rem}.how-step-num{width:2.5rem;height:2.5rem;border-radius:50%;background:#f0e642;border:1px solid #f0e642;display:flex;align-items:center;justify-content:center;font-size:0.9rem;font-weight:700;color:#0a0a0a;flex-shrink:0}.how-step h3{font-size:1.1rem;font-weight:600;color:var(--text)}.how-step p{font-size:1.1rem;color:rgba(255,255,255,0.5);line-height:1.6}.how-connector{width:3rem;height:1px;background:#f0e642;margin-top:1.25rem;flex-shrink:0}.ai-first{padding:5rem 2rem;max-width:900px;margin:0 auto}.ai-first-title{font-size:clamp(1.6rem,4vw,2.2rem);font-weight:700;letter-spacing:-0.02em;margin:0.75rem 0 1.25rem}.ai-first-lead{color:var(--muted);font-size:1.1rem;line-height:1.75;max-width:72ch;margin-bottom:3rem}.ai-cols{display:grid;grid-template-columns:1fr 1fr;gap:1.5rem}@media (max-width:640px){.ai-cols{grid-template-columns:1fr}.ai-col--pulse{order:-1}}.ai-col{padding:1.75rem;border-radius:10px;border:1px solid var(--border);background:var(--surface)}.ai-col--pulse{border-color:#0a0a0a;background:#0a0a0a}.ai-col-title{font-size:0.8rem;font-weight:600;letter-spacing:0.07em;text-transform:uppercase;margin-bottom:1.25rem}.ai-col-title--bad{color:#5f5f5a}.ai-col-title--good{color:#f0e642}.ai-col-list{list-style:none;display:flex;flex-direction:column;gap:0.9rem}.ai-col-list li{font-size:1.1rem;color:var(--muted);line-height:1.6;padding-left:1.1rem;position:relative}.ai-col-list li::before{content:"–";position:absolute;left:0;color:var(--muted-2)}.ai-col--pulse .ai-col-list li{color:rgba(255,255,255,0.75)}.ai-col--pulse .ai-col-list li::before{content:"✓";color:#f0e642}.versus{padding:5rem 2rem;background:#0a0a0a;border-top:none;border-bottom:none;text-align:center;--accent:#f0e642}.versus-title{font-size:1.75rem;font-weight:700;margin-bottom:0.75rem;color:#ffffff}.versus-sub{color:rgba(255,255,255,0.5);max-width:560px;margin:0 auto 2.5rem;font-size:1.1rem;line-height:1.7}.versus .section-label{color:#f0e642;opacity:1}.table-sticky-col{overflow-x:auto}.table-sticky-col table th:first-child,.table-sticky-col table td:first-child{position:sticky;left:0;z-index:1;border-right:1px solid rgba(255,255,255,0.15)}.versus-table-wrap{overflow-x:auto;max-width:900px;margin:0 auto}.versus-table{width:100%;min-width:640px;border-collapse:collapse;font-size:1rem;text-align:left}.versus-table thead{background:rgba(255,255,255,0.06)}.versus-table thead th{padding:0.75rem 1rem;color:rgba(255,255,255,0.45);font-size:0.78rem;font-weight:600;text-transform:uppercase;letter-spacing:0.07em;border-bottom:1px solid rgba(255,255,255,0.1)}.versus-table thead th:first-child{width:22%;background:#111314}.versus-table th[scope="row"]{background:#0a0a0a}.versus-table tbody tr:nth-child(odd) th[scope="row"]{background:#111214}.versus-table tbody tr:nth-child(odd){background:rgba(255,255,255,0.04)}.versus-table tr:hover td{background:rgba(255,255,255,0.08)}.versus-table td{padding:0.85rem 1rem;border-bottom:1px solid rgba(255,255,255,0.06);color:rgba(255,255,255,0.45);vertical-align:top;line-height:1.5}.versus-table th[scope="row"]{padding:0.85rem 1rem;border-bottom:1px solid rgba(255,255,255,0.06);color:rgba(255,255,255,0.75);font-weight:500;font-size:0.87rem;text-align:left}.versus-table .v-yes{color:#f0e642}.versus-table .v-partial{color:rgba(255,255,255,0.6)}.versus-table .v-no{color:rgba(255,255,255,0.6)}.usp-blocks{padding:4rem 2rem;max-width:1000px;margin:0 auto;display:flex;flex-direction:column;gap:0}.usp-block{display:grid;grid-template-columns:280px 1fr;gap:3rem;padding:3.5rem 0;border-bottom:1px solid var(--border-subtle);align-items:start}.usp-block:last-child{border-bottom:none}.usp-block-alt{direction:rtl}.usp-block-alt>*{direction:ltr}.usp-block-aside{display:flex;flex-direction:column;gap:0.75rem}.usp-icon{width:3rem;height:3rem;background:#0a0a0a;border:none;border-radius:var(--radius);display:flex;align-items:center;justify-content:center;margin-bottom:0.25rem;--accent:#f0e642}.usp-block-aside h2{font-size:1.2rem;font-weight:700}.usp-block-aside p{font-size:1.1rem;color:var(--muted);line-height:1.7}.usp-points{list-style:none;display:flex;flex-direction:column;gap:1.25rem;padding-top:0.25rem}.usp-points li{padding-left:1.25rem;border-left:2px solid #0a0a0a;font-size:1.1rem;color:var(--muted);line-height:1.7}.usp-points li strong{display:block;color:var(--text);font-weight:600;margin-bottom:0.15rem}.metrics-report{padding:5rem 2rem;border-top:1px solid var(--border-subtle);max-width:1100px;margin:0 auto}.metrics-header{text-align:center;margin-bottom:3rem}.metrics-title{font-size:clamp(1.6rem,3.5vw,2.2rem);font-weight:700;letter-spacing:-0.02em;margin-bottom:0.6rem}.metrics-generated{font-size:0.8rem;color:var(--muted);letter-spacing:0.01em}.metrics-groups{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:1rem}.metrics-group{background:var(--surface);border:1px solid var(--border-subtle);border-radius:var(--radius);padding:1.25rem 1.5rem 1.5rem}.metrics-group-label{font-size:0.7rem;font-weight:600;letter-spacing:0.1em;text-transform:uppercase;color:var(--muted);margin-bottom:1rem;padding-bottom:0.6rem;border-bottom:1px solid var(--border-subtle)}.metrics-items{display:flex;flex-direction:column;gap:1rem}.metric-item{display:flex;flex-direction:column;gap:0.15rem}.metric-val{font-size:1.5rem;font-weight:700;letter-spacing:-0.02em;color:var(--text);line-height:1}.metric-val--green{color:var(--green)}.metric-val--accent{color:var(--accent)}.metric-label{font-size:0.78rem;color:var(--muted);line-height:1.3}.home-cta{text-align:center;padding:5rem 2rem;background:#0a0a0a;background-image:radial-gradient( circle,rgba(255,255,255,0.07) 1px,transparent 1px );background-size:22px 22px;border-top:none}.home-cta h2{font-size:2rem;font-weight:700;margin-bottom:0.75rem;color:#ffffff}.home-cta p{color:rgba(255,255,255,0.5);margin-bottom:2rem;font-size:1.1rem}.home-cta-checks{list-style:none;display:flex;flex-wrap:wrap;justify-content:center;gap:0.6rem 1.5rem;margin-bottom:2rem}.home-cta-checks li{font-size:1.1rem;color:rgba(255,255,255,0.8);display:flex;align-items:center;gap:0.4rem}.home-cta-checks li::before{content:"✓";color:#f0e642;font-weight:700;font-size:1rem}.home-cta-actions{display:flex;gap:1rem;justify-content:center;flex-wrap:wrap}.sidebar-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.55);z-index:99;opacity:0;transition:opacity 0.25s ease}@media (max-width:768px){.sidebar-overlay{display:block;pointer-events:none}.sidebar-overlay.visible{opacity:1;pointer-events:auto}}.component-demo{border:1px solid var(--border);border-radius:var(--radius);margin:1.5rem 0}.demo-preview{padding:3.5rem 1.5rem 1.5rem;background:#0d0d10;border-radius:var(--radius) var(--radius) 0 0;display:flex;flex-direction:column;gap:0.75rem;position:relative}.demo-preview-inner{display:flex;flex-wrap:wrap;gap:0.75rem;align-items:flex-start;min-width:0;width:100%}.demo-preview--col .demo-preview-inner{flex-direction:column;align-items:stretch}.demo-preview--scroll .demo-preview-inner{flex-wrap:nowrap;overflow-x:auto}.demo-mobile-nav .ui-nav-links{display:none}.demo-mobile-nav .ui-nav-burger{display:flex}.demo-phone{width:320px;border-radius:32px;overflow:hidden;background:var(--bg,#0d0d10);box-shadow:0 0 0 7px var(--surface-2,#18181f),0 0 0 8px var(--border,#222228),0 24px 48px rgba(0,0,0,0.5)}.demo-phone-statusbar{height:28px;background:var(--bg,#0d0d10);display:flex;align-items:center;justify-content:center}.demo-phone-pill{width:64px;height:10px;background:var(--surface-2,#18181f);border-radius:99px}.demo-phone-content{min-height:160px;display:flex;align-items:center;justify-content:center;padding:1.5rem}.demo-phone-content p{margin:0;color:var(--muted,#6b6b80);font-size:0.8rem;text-align:center}.demo-preview.is-light{background:#f0f0f8}.demo-theme-toggle{position:absolute;top:0.6rem;right:0.6rem;display:flex;align-items:center;justify-content:center;width:28px;height:28px;border:1px solid rgba(255,255,255,0.15);border-radius:6px;background:rgba(255,255,255,0.07);color:rgba(255,255,255,0.5);cursor:pointer;transition:background 0.15s,color 0.15s;z-index:1}.demo-theme-toggle:hover{background:rgba(255,255,255,0.15);color:rgba(255,255,255,0.9)}.demo-preview.is-light .demo-theme-toggle{border-color:rgba(0,0,0,0.15);background:rgba(0,0,0,0.05);color:rgba(0,0,0,0.4)}.demo-preview.is-light .demo-theme-toggle:hover{background:rgba(0,0,0,0.1);color:rgba(0,0,0,0.8)}.demo-theme-toggle__light{display:none}.demo-preview.is-light .demo-theme-toggle__dark{display:none}.demo-preview.is-light .demo-theme-toggle__light{display:block}.demo-code pre.code-block{margin:0;border-radius:0;border:none;border-top:1px solid var(--border)}.demo-code .code-filename{border-radius:0;border:none;border-top:1px solid var(--border)}.prompt-group{margin-bottom:3rem}.prompt-group-title{font-size:0.78rem;font-weight:600;text-transform:uppercase;letter-spacing:0.1em;color:var(--muted);margin-bottom:1rem;padding-bottom:0.5rem;border-bottom:1px solid var(--border-subtle)}.prompt-grid{display:flex;flex-direction:column;gap:0.75rem}.prompt-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:1.25rem 1.5rem}.prompt-tag{font-size:0.72rem;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;color:var(--accent);margin-bottom:0.65rem}.prompt-text{font-size:0.93rem;color:var(--text);line-height:1.65;border-left:3px solid var(--accent);padding-left:1rem;margin:0 0 0.85rem;font-style:italic}.prompt-produces{font-size:0.82rem;color:var(--muted);line-height:1.6}.prompt-produces code{font-family:var(--mono);font-size:0.8rem;background:var(--surface-2);border:1px solid var(--border);border-radius:3px;padding:0.1em 0.35em;color:var(--tok-str);font-style:normal}.docs-sidebar{position:fixed;top:0;left:0;width:var(--sidebar-w);height:100vh;overflow-y:auto;background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;z-index:100;scrollbar-width:thin;scrollbar-color:var(--border) transparent}.docs-sidebar::-webkit-scrollbar{width:4px}.docs-sidebar::-webkit-scrollbar-track{background:transparent}.docs-sidebar::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}.sidebar-logo{display:flex;align-items:center;justify-content:space-between;padding:1.25rem 1.25rem 1rem;border-bottom:1px solid var(--border);flex-shrink:0}.logo-link{display:flex;align-items:center;gap:0.5rem;color:var(--text);font-weight:700;font-size:1rem;text-decoration:none}.logo-link:hover{color:var(--accent)}.logo-name{color:var(--text)}.version-badge{font-size:0.7rem;font-weight:600;color:var(--accent);background:var(--accent-dim);border:1px solid rgba(155,141,255,0.2);border-radius:4px;padding:0.15rem 0.45rem;font-family:var(--mono)}.sidebar-nav{padding:0.75rem 0 2rem;flex:1}.nav-section{padding:0.5rem 0}.nav-section+.nav-section{border-top:1px solid var(--border-subtle);margin-top:0.25rem;padding-top:0.75rem}.nav-section-title{font-size:0.68rem;font-weight:700;text-transform:uppercase;letter-spacing:0.1em;color:var(--muted-2);padding:0.25rem 1.25rem 0.5rem}.nav-link{display:block;padding:0.35rem 1.25rem;font-size:0.875rem;color:var(--muted);border-radius:0;transition:color 0.1s,background 0.1s;position:relative;border-left:2px solid transparent}.nav-link:hover{color:var(--text);background:var(--surface-2)}.nav-link.active{color:var(--accent);border-left-color:var(--accent);background:var(--accent-dim)}.docs-main{margin-left:var(--sidebar-w);flex:1;min-width:0;display:flex;flex-direction:column}.docs-header{height:var(--header-h);border-bottom:1px solid var(--border-subtle);display:flex;align-items:center;justify-content:space-between;padding:0 2rem;position:sticky;top:0;background:rgba(13,13,16,0.9);backdrop-filter:blur(8px);z-index:10}.mobile-menu-btn{display:none;background:none;border:none;color:var(--muted);cursor:pointer;padding:0.25rem}.mobile-menu-btn:hover{color:var(--text)}.header-logo-mobile{display:none;color:var(--text)}.header-github{display:flex;align-items:center;gap:0.4rem;font-size:0.82rem;color:var(--muted);transition:color 0.1s;margin-left:auto}.header-github:hover{color:var(--text)}.docs-content{flex:1;padding:3rem 3.5rem 5rem;max-width:calc(var(--content-w)+7rem)}.doc-h1{font-size:2.25rem;font-weight:800;letter-spacing:-0.03em;line-height:1.15;margin-bottom:1rem;color:var(--text)}.doc-lead{font-size:1.1rem;color:var(--muted);line-height:1.65;max-width:600px;margin-bottom:2.5rem}.doc-h2{font-size:1.35rem;font-weight:700;letter-spacing:-0.02em;margin:2.5rem 0 0.75rem;color:var(--text);padding-top:1rem;border-top:1px solid var(--border-subtle)}.doc-h2:first-of-type{border-top:none;margin-top:0}.doc-h3{font-size:1.05rem;font-weight:600;margin:1.75rem 0 0.6rem;color:var(--text)}.definition-list{display:flex;flex-direction:column;gap:0.75rem;margin:1rem 0}.definition-list dt{margin:0;font-weight:600}.definition-list dd{margin:0.2rem 0 0 0;color:var(--muted);font-size:0.9rem;line-height:1.55;padding-bottom:0.75rem;border-bottom:1px solid var(--border-subtle)}.definition-list dd:last-of-type{border-bottom:none;padding-bottom:0}.docs-content p:not([class*="ui-"]){margin-bottom:0.9rem;line-height:1.7;color:var(--text)}.docs-content p+p{margin-top:-0.1rem}.docs-content ul:not([class*="ui-"]),.docs-content ol{padding-left:1.5rem;margin-bottom:1rem}.docs-content li:not([class*="ui-"]){margin-bottom:0.35rem;line-height:1.65;color:var(--text)}.docs-content strong:not([class*="ui-"]){font-weight:600;color:#fff}.docs-content a:not(.ui-btn):not(.ui-nav-link):not(.ui-nav-logo):not(.ui-app-badge){color:var(--accent);text-decoration:underline;text-underline-offset:2px}.docs-content a:not(.ui-btn):not(.ui-nav-link):not(.ui-nav-logo):not(.ui-app-badge):hover{color:var(--accent-hover)}.docs-content .ui-theme-light p,.docs-content .ui-theme-light li,.docs-content .ui-theme-light strong{color:var(--ui-text)}.docs-content code:not(pre code){font-family:var(--mono);font-size:0.82em;background:var(--surface-2);border:1px solid var(--border);border-radius:4px;padding:0.1em 0.35em;color:var(--accent-hover)}.heading-anchor{color:inherit;text-decoration:none}.heading-anchor:hover{color:var(--accent)}.code-filename{background:var(--surface-2);border:1px solid var(--border);border-bottom:none;border-radius:var(--radius) var(--radius) 0 0;padding:0.4rem 1rem;font-family:var(--mono);font-size:0.75rem;color:var(--muted)}.code-filename+.code-block{border-radius:0 0 var(--radius) var(--radius);margin-top:0}.code-block{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:1.25rem 1.5rem;overflow-x:auto;font-family:var(--mono);font-size:0.9rem;line-height:1.75;margin:1.25rem 0;tab-size:2;white-space:pre;scrollbar-width:thin;scrollbar-color:var(--border) transparent}.code-block::-webkit-scrollbar{height:4px}.code-block::-webkit-scrollbar-track{background:transparent}.code-block::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}.tok-kw{color:var(--tok-kw)}.tok-str{color:var(--tok-str)}.tok-cmt{color:var(--tok-cmt);font-style:italic}.tok-num{color:var(--tok-num)}.tok-fn{color:var(--tok-fn)}.tok-op{color:var(--tok-op)}.tok-punct{color:var(--tok-punct)}.table-wrap{overflow-x:auto;margin:1.25rem 0;border:1px solid var(--border);border-radius:var(--radius)}table{width:100%;border-collapse:collapse;font-size:0.875rem}thead{background:var(--surface-2)}th{font-weight:600;font-size:0.78rem;text-transform:uppercase;letter-spacing:0.06em;color:var(--muted);padding:0.65rem 1rem;text-align:left;border-bottom:1px solid var(--border)}td{padding:0.65rem 1rem;border-bottom:1px solid var(--border-subtle);vertical-align:top;line-height:1.5}tr:last-child td{border-bottom:none}tr:hover td{background:var(--surface-2)}.callout{display:flex;gap:0.75rem;padding:1rem 1.25rem;border-radius:var(--radius);margin:1.25rem 0;border-left:3px solid;font-size:0.9rem;line-height:1.6}.callout-note{background:rgba(130,170,255,0.07);border-color:rgba(130,170,255,0.5)}.callout-warning{background:rgba(255,107,107,0.07);border-color:rgba(255,107,107,0.5)}.callout-tip{background:rgba(61,214,140,0.07);border-color:rgba(61,214,140,0.5)}.callout-note .callout-icon{color:#82aaff}.callout-warning .callout-icon{color:var(--red)}.callout-tip .callout-icon{color:var(--green)}.callout-icon{font-size:1rem;flex-shrink:0;margin-top:0.1rem}.callout-body p:last-child{margin-bottom:0}.doc-prev-next{margin-top:4rem;padding-top:2rem;border-top:1px solid var(--border)}.prev-next-grid{display:grid;grid-template-columns:1fr 1fr;gap:1rem}.prev-next-link{display:flex;flex-direction:column;gap:0.2rem;padding:1rem 1.25rem;border:1px solid var(--border);border-radius:var(--radius);color:inherit;text-decoration:none;transition:border-color 0.15s,background 0.15s}.prev-next-link:hover{border-color:var(--accent);background:var(--accent-dim);color:inherit}.next-link{text-align:right}.prev-next-label{font-size:0.75rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.06em}.prev-next-title{font-size:0.95rem;font-weight:600;color:var(--accent)}@media (max-width:768px){.docs-sidebar{transform:translateX(-100%);transition:transform 0.25s ease}.docs-sidebar.open{transform:translateX(0);box-shadow:4px 0 24px rgba(0,0,0,0.4)}.docs-main{margin-left:0}.mobile-menu-btn,.header-logo-mobile{display:flex}.docs-content{padding:2rem 1.25rem 4rem}.doc-h1{font-size:1.75rem}.prev-next-grid{grid-template-columns:1fr}.hero{padding:4rem 1.25rem 3rem}.home-nav{padding:1rem 1.25rem}.home-nav-links{gap:1rem}.home-stats{padding:2rem 1rem;gap:0}.home-stat{padding:0.75rem 1.5rem}.home-stat-value{font-size:1.5rem}.home-stat-divider{display:none}.how{padding:3.5rem 1.25rem}.how-steps{flex-direction:column;align-items:center;gap:1.5rem}.how-connector{width:1px;height:2rem}.how-step{max-width:100%}.versus{padding:3.5rem 1.25rem}.versus-title{font-size:1.4rem}.usp-blocks{padding:2.5rem 1.25rem}.usp-block{grid-template-columns:1fr;gap:1.5rem}.usp-block-alt{direction:ltr}.home-cta{padding:3.5rem 1.25rem}.home-cta h2{font-size:1.5rem}}.icon-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:0.5rem;margin-bottom:1rem}.icon-grid-item{display:flex;flex-direction:column;align-items:center;gap:0.5rem;padding:1rem 0.5rem;border:1px solid var(--border-subtle);border-radius:8px;background:var(--surface);cursor:default;transition:border-color 0.15s,background 0.15s}.icon-grid-item:hover{border-color:var(--border);background:var(--surface-2)}.icon-grid-preview{color:var(--text);line-height:1}.icon-grid-name{font-family:var(--mono,monospace);font-size:0.6rem;color:var(--muted);text-align:center;word-break:break-all;line-height:1.4}
@@ -1,159 +0,0 @@
1
- import{a as t}from"./runtime-QFURDKA2.js";import{a,b as d,c as l,d as h,e as r,g as e,h as n,i as s}from"./runtime-L2HNXIHW.js";import{a as c,b as p}from"./runtime-B73WLANC.js";var{prev:u,next:m}=a("/extending"),i={route:"/extending",meta:{title:"Extending Pulse \u2014 Pulse Docs",description:"Escape hatches for features outside the Pulse spec \u2014 onRequest middleware, raw server access, WebSockets, SSE, and custom error handling.",styles:["/docs.css"]},state:{},view:()=>d({currentHref:"/extending",prev:u,next:m,content:`
2
- ${l("Extending Pulse")}
3
- ${h("Pulse handles the standard request-response lifecycle through specs. For requirements outside that model \u2014 middleware, WebSockets, SSE, custom error pages \u2014 Pulse exposes deliberate integration points directly on the underlying Node.js server. There is no abstraction layer to fight through.")}
4
-
5
- ${r("onrequest","Request interception with onRequest")}
6
- <p><code>onRequest</code> is a hook on <code>createServer</code> that fires on every incoming request before Pulse handles it. Return <code>false</code> to short-circuit Pulse entirely and handle the response yourself.</p>
7
- ${e(t(`// server.js
8
- import { createServer } from '@invisibleloop/pulse'
9
- import home from './src/pages/home.js'
10
-
11
- createServer([home], {
12
- port: 3000,
13
- onRequest(req, res) {
14
- // Logging
15
- console.log(req.method, req.url)
16
-
17
- // Block a path entirely
18
- if (req.url === '/internal') {
19
- res.writeHead(403)
20
- res.end('Forbidden')
21
- return false // stops Pulse processing this request
22
- }
23
-
24
- // Custom header on every response
25
- res.setHeader('X-App-Version', '1.0.0')
26
- // returning nothing (or undefined) lets Pulse continue normally
27
- },
28
- })`,"js"))}
29
- ${s("note","<code>onRequest</code> runs before routing, guard, and all server fetchers. Returning <code>false</code> gives full control of the response \u2014 Pulse steps aside entirely for that request.")}
30
- <p>Common uses:</p>
31
- ${n(["Use case","Approach"],[["Request logging","Log <code>req.method</code>, <code>req.url</code>, timing"],["IP allowlisting","Check <code>req.socket.remoteAddress</code>, return <code>false</code> with 403 if blocked"],["Rate limiting","Track request counts in a <code>Map</code>, return <code>false</code> with 429 when exceeded"],["Custom response headers","Call <code>res.setHeader()</code> before returning"],["Health check endpoint","Match <code>/healthz</code>, write <code>200 ok</code>, return <code>false</code>"]])}
32
-
33
- ${r("raw-server","Accessing the raw server")}
34
- <p><code>createServer</code> returns a <code>{ server }</code> object where <code>server</code> is a plain Node.js <code>http.Server</code>. You can attach any listener to it directly.</p>
35
- ${e(t(`const { server } = createServer([home], { port: 3000 })
36
-
37
- // The server instance is available immediately after createServer() returns.
38
- // It starts listening automatically \u2014 no need to call server.listen().
39
- server.on('listening', () => {
40
- console.log('ready')
41
- })`,"js"))}
42
-
43
- ${r("websockets","WebSockets")}
44
- <p>Use the <code>upgrade</code> event on the server instance. The <code>ws</code> package handles the WebSocket handshake and framing.</p>
45
- ${e(t("npm install ws","bash"))}
46
- ${e(t(`// server.js
47
- import { WebSocketServer } from 'ws'
48
- import { createServer } from '@invisibleloop/pulse'
49
- import home from './src/pages/home.js'
50
-
51
- const { server } = createServer([home], { port: 3000 })
52
-
53
- const wss = new WebSocketServer({ noServer: true })
54
-
55
- server.on('upgrade', (req, socket, head) => {
56
- if (req.url === '/ws') {
57
- wss.handleUpgrade(req, socket, head, (ws) => {
58
- wss.emit('connection', ws, req)
59
- })
60
- } else {
61
- socket.destroy()
62
- }
63
- })
64
-
65
- wss.on('connection', (ws) => {
66
- ws.send('connected')
67
- ws.on('message', (msg) => ws.send(\`echo: \${msg}\`))
68
- })`,"js"))}
69
- <p>The Pulse-rendered page can connect to the WebSocket using an inline script. Pass <code>ctx.nonce</code> through a server fetcher so the script is allowed by the CSP:</p>
70
- ${e(t(`server: {
71
- meta: async (ctx) => ({ nonce: ctx.nonce }),
72
- },
73
-
74
- view: (_state, server) => \`
75
- <main id="main-content">
76
- <p id="status">Connecting\u2026</p>
77
- <script nonce="\${server.meta.nonce}">
78
- const ws = new WebSocket('ws://' + location.host + '/ws')
79
- ws.onmessage = (e) => {
80
- document.getElementById('status').textContent = e.data
81
- }
82
- <\/script>
83
- </main>
84
- \`,`,"js"))}
85
-
86
- ${r("sse","Server-Sent Events")}
87
- <p>SSE keeps an HTTP connection open and streams events to the browser. Use <code>onRequest</code> to intercept the SSE path and write to the response directly.</p>
88
- ${e(t(`onRequest(req, res) {
89
- if (req.url !== '/events') return // let Pulse handle everything else
90
-
91
- res.writeHead(200, {
92
- 'Content-Type': 'text/event-stream',
93
- 'Cache-Control': 'no-cache',
94
- 'Connection': 'keep-alive',
95
- })
96
-
97
- // Send a keepalive comment every 15 seconds
98
- const keepalive = setInterval(() => res.write(': keepalive\\n\\n'), 15000)
99
-
100
- // Send an event
101
- function send(event, data) {
102
- res.write(\`event: \${event}\\ndata: \${JSON.stringify(data)}\\n\\n\`)
103
- }
104
-
105
- send('connected', { time: Date.now() })
106
-
107
- req.on('close', () => clearInterval(keepalive))
108
-
109
- return false
110
- },`,"js"))}
111
-
112
- ${r("error","Custom error handling")}
113
- <p><code>onError</code> in <code>createServer</code> receives unhandled errors from server fetchers and guard functions. Use it to log errors to an external service or render a custom error page.</p>
114
- ${e(t(`createServer([home], {
115
- port: 3000,
116
-
117
- onError(err, req, res) {
118
- // Log to your error tracking service
119
- console.error(err)
120
-
121
- // Respond with a custom error page
122
- res.writeHead(500, { 'Content-Type': 'text/html' })
123
- res.end(\`
124
- <!doctype html>
125
- <html lang="en">
126
- <head><title>Error</title></head>
127
- <body>
128
- <main id="main-content">
129
- <h1>Something went wrong</h1>
130
- <p>We've been notified and are looking into it.</p>
131
- </main>
132
- </body>
133
- </html>
134
- \`)
135
- },
136
- })`,"js"))}
137
-
138
- ${r("client-js","Custom client-side JavaScript")}
139
- <p>Pulse has no client-side JS of its own beyond the hydration runtime. For behaviour that genuinely needs to run in the browser \u2014 third-party widgets, analytics, canvas \u2014 use an inline script in the view with the request nonce. The nonce is unique per request and is required for the script to pass the CSP.</p>
140
- ${e(t(`server: {
141
- meta: async (ctx) => ({ nonce: ctx.nonce }),
142
- },
143
-
144
- view: (_state, server) => \`
145
- <main id="main-content">
146
- <canvas id="chart" width="600" height="300"></canvas>
147
- <script nonce="\${server.meta.nonce}">
148
- const ctx = document.getElementById('chart').getContext('2d')
149
- // draw directly with Canvas API \u2014 no library needed for simple charts
150
- ctx.fillStyle = '#9b8dff'
151
- ctx.fillRect(10, 10, 100, 80)
152
- <\/script>
153
- </main>
154
- \`,`,"js"))}
155
- ${s("note","External scripts loaded via <code>src</code> also need the nonce attribute, or their domain must be added to the <code>script-src</code> directive in the CSP. The nonce approach is simpler and does not require config changes.")}
156
-
157
- ${r("when","Choosing the right approach")}
158
- ${n(["What you need","Reach for"],[["Middleware \u2014 logging, rate limiting, custom headers","<code>onRequest</code>"],["Non-HTML responses \u2014 JSON APIs, webhooks, RSS, sitemaps","Raw response spec (<code>contentType</code> + <code>render</code>)"],["Real-time bidirectional communication","WebSockets via <code>server.on('upgrade')</code>"],["Server-pushed updates (read-only stream)","SSE via <code>onRequest</code>"],["Custom error pages","<code>onError</code>"],["Browser-only behaviour","Inline <code>&lt;script nonce&gt;</code> in the view"]])}
159
- `})};var o=document.getElementById("pulse-root");o&&!o.dataset.pulseMounted&&(o.dataset.pulseMounted="1",c(i,o,window.__PULSE_SERVER__||{},{ssr:!0}),p(o,c));var q=i;export{q as default};
@@ -1,43 +0,0 @@
1
- import{a as r,b as i,c as o,d as n}from"./runtime-L2HNXIHW.js";import{a,b as c}from"./runtime-B73WLANC.js";var{prev:l,next:u}=r("/faq");function e(d,p){return`
2
- <div class="faq-item">
3
- <h2 class="faq-q">${d}</h2>
4
- <div class="faq-a">${p}</div>
5
- </div>`}var s={route:"/faq",meta:{title:"FAQ \u2014 Pulse Docs",description:"Frequently asked questions about the Pulse framework.",styles:["/pulse-ui.css","/docs.css"]},state:{},view:()=>i({currentHref:"/faq",prev:l,next:u,content:`
6
- ${o("FAQ")}
7
- ${n("Common questions about Pulse \u2014 what it is, what it isn't, and whether it's right for your project.")}
8
-
9
- ${e("Is Pulse ready for production?",`<p>The architecture is production-quality \u2014 streaming SSR, security headers, immutable caching, and zero runtime dependencies are built in and have been running reliably in real deployments. The framework itself targets Lighthouse 100 on every scaffolded page.</p>
10
- <p>That said, Pulse is v0.1 early access. The core spec format is stable, but some APIs may change before v1. It is best suited to new projects where you control the stack, and to teams who are comfortable building on something that is still evolving. If you need a framework with a five-year stability guarantee, wait for v1.</p>`)}
11
-
12
- ${e("Why plain JS objects instead of JSX or components?",`<p>JSX and component trees are designed for incremental DOM updates \u2014 the virtual DOM needs a tree to diff. Pulse does not have a virtual DOM. Views are pure functions that return an HTML string; the framework re-renders the whole page segment on state change.</p>
13
- <p>Plain JS objects are also unambiguous for AI agents. There is no syntax to learn, no component abstraction to navigate \u2014 just a JS object with well-defined keys. An agent can read, write, and validate a spec without understanding any framework-specific idioms.</p>`)}
14
-
15
- ${e("Why no virtual DOM?",`<p>A virtual DOM solves the problem of efficient incremental updates to a large, complex component tree. Pulse pages are server-rendered HTML strings \u2014 the client runtime re-renders a bounded section of the page when state changes, which is fast enough for the kinds of interactions Pulse is designed for.</p>
16
- <p>Eliminating the virtual DOM means eliminating the ~40\u2013100 kB runtime that comes with it. Pulse ships ~2 kB brotli to the browser on first visit. That is not a compression trick \u2014 there is genuinely no framework runtime on the client.</p>`)}
17
-
18
- ${e("Can I use npm packages in my specs?",`<p>Yes, on the server side. Server fetchers run in Node.js and can import any npm package \u2014 database clients, API SDKs, utility libraries. These never reach the browser.</p>
19
- <p>Client-side code (mutations and actions) runs in the browser and is bundled by esbuild. You can import pure JS utilities here, but avoid packages that depend on Node.js built-ins or that are large \u2014 the goal is to keep the client bundle small. UI components come from the built-in component library, which covers most cases without external dependencies.</p>`)}
20
-
21
- ${e("Does Pulse work with TypeScript?",`<p>Type definitions are included for all public APIs \u2014 <code>createServer</code>, the spec shape, UI components, and the testing utilities. You can write specs in TypeScript and import the <code>Spec</code> type to get full autocompletion and type checking.</p>
22
- <p>The examples and docs are in plain JS for readability, but TypeScript is a first-class option.</p>`)}
23
-
24
- ${e("Can I use a database directly in Pulse?",`<p>Yes. Server fetchers are plain async functions \u2014 you can query a database, call an ORM, or use any server-side library directly. There is no data layer abstraction to work around.</p>
25
- <pre class="code-block"><code>server: {
26
- posts: async (ctx) => {
27
- return db.posts.findMany({ where: { published: true } })
28
- },
29
- }</code></pre>
30
- <p>The result is passed to your view as <code>server.posts</code>. See the <a href="/server-data">Server Data</a> and <a href="/supabase">Supabase</a> docs for worked examples.</p>`)}
31
-
32
- ${e("How does Pulse compare to Next.js?",`<p>Next.js is a full-featured React framework designed for large teams building complex applications. It ships React to the browser on every page, supports many rendering strategies, and has a large ecosystem.</p>
33
- <p>Pulse is opinionated in the other direction: one spec format, one rendering model, no client framework, no configuration. It is smaller, simpler, and faster to get a Lighthouse 100 result \u2014 but it does not have the ecosystem breadth or the component model that React provides. If your project needs React, use Next.js. If you want to ship fast, simple, server-rendered pages with minimal JS, Pulse is worth trying.</p>`)}
34
-
35
- ${e("Can I build a multi-page app with client-side navigation?",`<p>Yes. <code>initNavigation</code> intercepts same-origin link clicks, fetches the new page, swaps the DOM, and re-mounts the spec \u2014 all without a full page reload. From the user's perspective it feels like an SPA; from the server's perspective every navigation is a standard HTTP request.</p>
36
- <p>See the <a href="/navigation">Navigation</a> docs for details.</p>`)}
37
-
38
- ${e("Does Pulse handle authentication?",`<p>Pulse has a <code>guard</code> function that runs before any server fetcher executes. It receives the request context (cookies, headers, params) and can redirect or return an error if the user is not authenticated. This makes it impossible to accidentally skip an auth check \u2014 the guard runs before data is fetched.</p>
39
- <p>For a complete auth integration, see the <a href="/auth">Auth0 guide</a>. The pattern works the same with any auth provider.</p>`)}
40
-
41
- ${e("What is the AI-native claim actually about?",`<p>Two things. First, the spec format was designed to be easy for an AI agent to generate correctly \u2014 one JS object per page, a validated schema, no ambiguous patterns. An agent that writes a Pulse spec either produces a valid spec or gets a schema error it can fix. There is no grey area.</p>
42
- <p>Second, the CLI starts an MCP server alongside the dev server, giving the agent tools to create pages, validate specs, screenshot routes, and run Lighthouse audits \u2014 without leaving the editor. The agent can build and verify a page end-to-end without human intervention on the tooling side.</p>`)}
43
- `})};var t=document.getElementById("pulse-root");t&&!t.dataset.pulseMounted&&(t.dataset.pulseMounted="1",a(s,t,window.__PULSE_SERVER__||{},{ssr:!0}),c(t,a));var b=s;export{b as default};
@@ -1,86 +0,0 @@
1
- import{a as s}from"./runtime-QFURDKA2.js";import{a as n,b as l,c as d,d as c,e,g as t,i as a}from"./runtime-L2HNXIHW.js";import{a as r,b as u}from"./runtime-B73WLANC.js";var{prev:p,next:h}=n("/getting-started"),i={route:"/getting-started",meta:{title:"Getting Started \u2014 Pulse Docs",description:"Install Pulse and build your first page with an AI agent in minutes.",styles:["/docs.css"]},state:{},view:()=>l({currentHref:"/getting-started",prev:p,next:h,content:`
2
- ${d("Getting Started")}
3
- ${c("Install Pulse, run one command, and have Claude building your first page \u2014 with streaming SSR, security headers, and a 100 Lighthouse score already in place.")}
4
-
5
- ${e("requirements","Requirements")}
6
- <ul>
7
- <li><strong>Node.js 22 or later</strong> \u2014 <a href="https://nodejs.org" target="_blank" rel="noopener">nodejs.org</a></li>
8
- <li><strong>Claude Code</strong> \u2014 the CLI for Claude, installed and authenticated \u2014 <a href="https://docs.anthropic.com/en/docs/claude-code/getting-started" target="_blank" rel="noopener">installation guide</a></li>
9
- </ul>
10
- <p>Claude Code provides the <code>claude</code> command. Pulse launches it automatically with the Pulse MCP server wired in \u2014 so the agent has instant access to the framework reference, your project structure, and all Pulse tools without any manual configuration.</p>
11
- ${a("note","GitHub Copilot integration is coming soon. For now, Pulse works exclusively with Claude Code.")}
12
-
13
- ${e("install","Install Pulse")}
14
- <p>Install the Pulse CLI globally:</p>
15
- ${t(s("npm install -g @invisibleloop/pulse","bash"))}
16
-
17
- ${e("create","Create your project")}
18
- <p>Run <code>pulse</code> in any empty directory:</p>
19
- ${t(s(`mkdir my-app
20
- cd my-app
21
- pulse`,"bash"))}
22
- <p>Pulse detects the directory is empty and scaffolds a project there. It creates the project files, installs dependencies, and exits with:</p>
23
- ${t(s("\u2713 Project ready. Run `pulse` again to start your AI session.","bash"))}
24
- ${a("tip","Running <code>pulse</code> in a non-empty directory prompts for a project name, then creates and scaffolds a subdirectory with that name \u2014 so you can run it from anywhere.")}
25
-
26
- ${e("session","Start a session")}
27
- <p>Run <code>pulse</code> again from inside your project directory:</p>
28
- ${t(s("pulse","bash"))}
29
- <p>This time, Pulse detects the existing project and launches Claude Code with the Pulse MCP server already connected. Claude opens with the complete framework guide loaded, your project structure visible, and all Pulse tools available \u2014 ready to build immediately.</p>
30
- ${a("note","Run <code>pulse</code> every time you open a working session. It handles starting Claude and wiring up the MCP server. Once Claude is open, use <code>/pulse-dev</code> to start the dev server.")}
31
-
32
- ${e("first-build","Build your first page")}
33
- <p>Once Claude opens, start the dev server and ask for something:</p>
34
- ${t(s(`/pulse-dev
35
-
36
- "Create a contact form with name, email, and message fields.
37
- Validate the email format before submitting."`,"bash"))}
38
- <p>The agent will:</p>
39
- <ol>
40
- <li>Fetch the Pulse guide from the MCP server \u2014 spec format, component library, quality rules</li>
41
- <li>Check what pages already exist in your project</li>
42
- <li>Write the spec \u2014 route, state, validation, action lifecycle, view</li>
43
- <li>Validate it against the schema and fix every error and warning</li>
44
- <li>Open the page in the browser and confirm it looks right</li>
45
- </ol>
46
- <p>You do not need to explain Pulse to the agent. The MCP server supplies the reference. Just describe what you want.</p>
47
-
48
- ${e("what-was-created","What got created")}
49
- <p>When you ran <code>pulse</code> in step 2, these files were written to your directory:</p>
50
- ${t(s(`my-app/
51
- \u251C\u2500\u2500 src/
52
- \u2502 \u251C\u2500\u2500 pages/
53
- \u2502 \u2502 \u2514\u2500\u2500 home.js \u2190 your first page spec (a working counter)
54
- \u2502 \u2514\u2500\u2500 components/ \u2190 shared view components go here
55
- \u251C\u2500\u2500 public/
56
- \u2502 \u251C\u2500\u2500 app.css \u2190 global stylesheet
57
- \u2502 \u251C\u2500\u2500 pulse-ui.css \u2190 Pulse component library styles
58
- \u2502 \u2514\u2500\u2500 pulse-ui.js \u2190 Pulse component library behaviour
59
- \u251C\u2500\u2500 .claude/
60
- \u2502 \u251C\u2500\u2500 CLAUDE.md \u2190 session instructions Claude reads on startup
61
- \u2502 \u251C\u2500\u2500 settings.json \u2190 hooks: syntax checks, colour guards, package blocklist
62
- \u2502 \u2514\u2500\u2500 pulse-checklist.md \u2190 spec review checklist, kept in sync by Pulse
63
- \u251C\u2500\u2500 package.json
64
- \u2514\u2500\u2500 pulse.config.js \u2190 port and project settings`,"bash"))}
65
- <p><code>src/pages/home.js</code> is a complete working spec \u2014 a counter with increment and decrement buttons. Open <a href="http://localhost:3000">localhost:3000</a> after running <code>/pulse-dev</code> to see it. Every new page you create goes into <code>src/pages/</code> and is discovered automatically.</p>
66
- <p>The <code>.claude/</code> directory contains the agent's operating context. <code>CLAUDE.md</code> tells Claude how the project is structured, <code>settings.json</code> configures hooks that catch common mistakes before they reach you \u2014 hardcoded hex colours, emoji in UI output, and installing client-side rendering libraries are all flagged or blocked automatically.</p>
67
-
68
- ${e("commands","Agent commands")}
69
- <p>These slash commands are available once Claude is open:</p>
70
- ${t(s(`/pulse-dev # start (or restart) the dev server
71
- /pulse-stop # stop the dev server
72
- /pulse-build # production build \u2192 public/dist/
73
- /pulse-start # run the production server
74
- /pulse-report # Lighthouse audit + performance report`,"bash"))}
75
- <p>You can also skip the commands entirely \u2014 just describe what you want and the agent handles the rest, including starting the dev server when needed.</p>
76
-
77
- ${e("next-steps","Next steps")}
78
- <ul>
79
- <li><a href="/how-it-works">How It Works</a> \u2014 the MCP server, what the agent knows, and the full build cycle</li>
80
- <li><a href="/project-structure">Project Structure</a> \u2014 where files live and how pages are discovered</li>
81
- <li><a href="/spec">Spec Reference</a> \u2014 every field the spec supports</li>
82
- <li><a href="/state">State</a> \u2014 client state and mutations</li>
83
- <li><a href="/server-data">Server Data</a> \u2014 fetch data on the server before the page renders</li>
84
- <li><a href="/prompt-examples">Prompt Examples</a> \u2014 real prompts with the output they produce</li>
85
- </ul>
86
- `})};var o=document.getElementById("pulse-root");o&&!o.dataset.pulseMounted&&(o.dataset.pulseMounted="1",r(i,o,window.__PULSE_SERVER__||{},{ssr:!0}),u(o,r));var C=i;export{C as default};
@@ -1,80 +0,0 @@
1
- import{a as r}from"./runtime-QFURDKA2.js";import{a as i,b as u,c as h,d as l,e,f as s,g as t,h as a,i as d}from"./runtime-L2HNXIHW.js";import{a as c,b as p}from"./runtime-B73WLANC.js";var{prev:f,next:g}=i("/guard"),n={route:"/guard",meta:{title:"Guard \u2014 Pulse Docs",description:"Per-route authorization in Pulse. Guard functions run before server data fetchers and redirect unauthorized requests.",styles:["/docs.css"]},state:{},view:()=>u({currentHref:"/guard",prev:f,next:g,content:`
2
- ${h("Guard")}
3
- ${l("A <code>guard</code> function runs on every request to a route, before any server data is fetched. It is the enforced access control point \u2014 unauthorized requests are redirected before any database queries or data fetchers execute.")}
4
-
5
- ${e("basics","Basic usage")}
6
- <p>A <code>guard</code> function on any spec receives the same <code>ctx</code> object as server data fetchers \u2014 params, query, headers, and cookies.</p>
7
- ${t(r(`export default {
8
- route: '/dashboard',
9
-
10
- guard: async (ctx) => {
11
- if (!ctx.cookies.session) return { redirect: '/login' }
12
- },
13
-
14
- server: {
15
- user: async (ctx) => getCurrentUser(ctx.cookies.session),
16
- },
17
-
18
- state: {},
19
- view: (state, server) => \`
20
- <main id="main-content">
21
- <h1>Welcome, \${server.user.name}</h1>
22
- </main>
23
- \`,
24
- }`,"js"))}
25
-
26
- <p>When the guard returns <code>{ redirect }</code>, the server responds with a <strong>302</strong> and all server data fetchers are skipped \u2014 no data is fetched for unauthorized requests. When the guard returns nothing, the request proceeds normally.</p>
27
-
28
- ${e("ctx","What ctx contains")}
29
- ${a(["Property / Method","Type","Description"],[["<code>ctx.cookies</code>","object","Parsed cookies from the <code>Cookie</code> header"],["<code>ctx.headers</code>","object","Raw request headers"],["<code>ctx.params</code>","object",'Route params e.g. <code>{ id: "42" }</code>'],["<code>ctx.query</code>","object","Parsed query string"],["<code>ctx.pathname</code>","string","URL path e.g. <code>/dashboard</code>"],["<code>ctx.method</code>","string","HTTP method e.g. <code>GET</code>, <code>POST</code>"],["<code>ctx.store</code>","object","Resolved global store state (if a store is registered)"],["<code>ctx.nonce</code>","string","CSP nonce for the current request"],["<code>await ctx.json()</code>","object | null","Parse a JSON request body"],["<code>await ctx.text()</code>","string","Read the body as a plain string"],["<code>await ctx.formData()</code>","object | null","Parse a URL-encoded body into a plain object"],["<code>await ctx.buffer()</code>","Buffer","Read the raw body as a Node.js Buffer"]])}
30
-
31
- ${e("examples","Common patterns")}
32
-
33
- ${s("Session check")}
34
- <p>Redirect to login when no session cookie is present.</p>
35
- ${t(r(`guard: async (ctx) => {
36
- if (!ctx.cookies.session) return { redirect: '/login' }
37
- }`,"js"))}
38
-
39
- ${s("Role-based access")}
40
- <p>Fetch the user from the session and check their role. Keep the lookup fast \u2014 guard runs on every request to the route.</p>
41
- ${t(r(`guard: async (ctx) => {
42
- const user = await getUserFromSession(ctx.cookies.session)
43
- if (!user) return { redirect: '/login' }
44
- if (!user.isAdmin) return { redirect: '/403' }
45
- }`,"js"))}
46
-
47
- ${s("Redirect authenticated users away from login")}
48
- <p>Useful for login and signup pages \u2014 send already-authenticated users somewhere useful.</p>
49
- ${t(r(`export default {
50
- route: '/login',
51
-
52
- guard: async (ctx) => {
53
- if (ctx.cookies.session) return { redirect: '/dashboard' }
54
- },
55
-
56
- state: {},
57
- view: () => \`<main id="main-content">...</main>\`,
58
- }`,"js"))}
59
-
60
- ${d("info","Guard runs server-side on every request, including client-side navigation requests \u2014 those go through the same server pipeline. There is no way to bypass guard from the browser.")}
61
-
62
- ${e("custom-responses","Custom status responses")}
63
- <p>Guard can return a custom HTTP response instead of (or alongside) a redirect. Return <code>{ status, json?, body?, headers? }</code> to send any status code with an optional JSON or text body. This is useful for POST handlers that need to signal validation errors or API-style rejections:</p>
64
- ${t(r(`guard: async (ctx) => {
65
- const token = ctx.headers.authorization
66
- if (!token) return { status: 401, json: { error: 'Unauthorized' } }
67
-
68
- if (ctx.method === 'POST') {
69
- const data = await ctx.formData()
70
- if (!data.email) return { status: 422, json: { error: 'Email required' } }
71
- await db.leads.create(data)
72
- return { redirect: '/contact?sent=1' }
73
- }
74
- // return nothing to let a GET request proceed to the view
75
- }`,"js"))}
76
- ${d("note","To use <code>guard</code> as a POST handler, the spec must declare <code>methods: ['GET', 'POST']</code>. Without it, POST requests are rejected with 405 before guard runs.")}
77
-
78
- ${e("reference","Reference")}
79
- ${a(["Property","Type","Required"],[["<code>guard</code>","<code>async (ctx) =&gt; { redirect?: string } | { status, json?, body?, headers? } | void</code>","No"]])}
80
- `})};var o=document.getElementById("pulse-root");o&&!o.dataset.pulseMounted&&(o.dataset.pulseMounted="1",c(n,o,window.__PULSE_SERVER__||{},{ssr:!0}),p(o,c));var P=n;export{P as default};