@invisibleloop/pulse 0.5.0 → 0.6.0

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 (99) hide show
  1. package/CLAUDE.md +36 -0
  2. package/docs/.claude/pulse-checklist.md +7 -5
  3. package/docs/public/.pulse-ui-version +1 -1
  4. package/docs/public/pulse-ui.css +27 -0
  5. package/docs/server.js +1 -0
  6. package/docs/src/lib/highlight.js +2 -299
  7. package/docs/src/lib/nav.js +1 -0
  8. package/docs/src/pages/accessibility.js +0 -1
  9. package/docs/src/pages/auth.js +0 -2
  10. package/docs/src/pages/caching.js +0 -1
  11. package/docs/src/pages/components/accordion.js +0 -1
  12. package/docs/src/pages/components/alert.js +0 -1
  13. package/docs/src/pages/components/app-badge.js +0 -1
  14. package/docs/src/pages/components/avatar.js +0 -1
  15. package/docs/src/pages/components/badge.js +0 -1
  16. package/docs/src/pages/components/banner.js +0 -1
  17. package/docs/src/pages/components/breadcrumbs.js +0 -1
  18. package/docs/src/pages/components/button.js +0 -1
  19. package/docs/src/pages/components/card.js +0 -1
  20. package/docs/src/pages/components/carousel.js +0 -1
  21. package/docs/src/pages/components/charts.js +0 -1
  22. package/docs/src/pages/components/checkbox.js +0 -1
  23. package/docs/src/pages/components/cluster.js +0 -1
  24. package/docs/src/pages/components/code-window.js +0 -1
  25. package/docs/src/pages/components/container.js +0 -1
  26. package/docs/src/pages/components/divider.js +0 -1
  27. package/docs/src/pages/components/empty.js +0 -1
  28. package/docs/src/pages/components/feature.js +0 -1
  29. package/docs/src/pages/components/fieldset.js +0 -1
  30. package/docs/src/pages/components/footer.js +0 -1
  31. package/docs/src/pages/components/grid.js +0 -1
  32. package/docs/src/pages/components/heading.js +0 -1
  33. package/docs/src/pages/components/icons.js +0 -1
  34. package/docs/src/pages/components/image.js +0 -1
  35. package/docs/src/pages/components/input.js +0 -1
  36. package/docs/src/pages/components/list.js +0 -1
  37. package/docs/src/pages/components/media.js +0 -1
  38. package/docs/src/pages/components/modal.js +0 -1
  39. package/docs/src/pages/components/nav.js +0 -1
  40. package/docs/src/pages/components/pricing.js +0 -1
  41. package/docs/src/pages/components/progress.js +0 -1
  42. package/docs/src/pages/components/prose.js +2 -3
  43. package/docs/src/pages/components/pullquote.js +0 -1
  44. package/docs/src/pages/components/radio.js +0 -1
  45. package/docs/src/pages/components/rating.js +0 -1
  46. package/docs/src/pages/components/search.js +0 -1
  47. package/docs/src/pages/components/section.js +0 -1
  48. package/docs/src/pages/components/segmented.js +0 -1
  49. package/docs/src/pages/components/select.js +0 -1
  50. package/docs/src/pages/components/spinner.js +0 -1
  51. package/docs/src/pages/components/stack.js +0 -1
  52. package/docs/src/pages/components/stat.js +0 -1
  53. package/docs/src/pages/components/stepper.js +0 -1
  54. package/docs/src/pages/components/table.js +0 -1
  55. package/docs/src/pages/components/testimonial.js +0 -1
  56. package/docs/src/pages/components/textarea.js +0 -1
  57. package/docs/src/pages/components/timeline.js +0 -1
  58. package/docs/src/pages/components/toggle.js +0 -1
  59. package/docs/src/pages/components/tooltip.js +0 -1
  60. package/docs/src/pages/components.js +0 -1
  61. package/docs/src/pages/config.js +0 -1
  62. package/docs/src/pages/deploy.js +0 -1
  63. package/docs/src/pages/extending.js +0 -1
  64. package/docs/src/pages/faq.js +0 -1
  65. package/docs/src/pages/getting-started.js +0 -1
  66. package/docs/src/pages/guard.js +0 -3
  67. package/docs/src/pages/how-it-works.js +0 -1
  68. package/docs/src/pages/images.js +0 -2
  69. package/docs/src/pages/markdown.js +155 -0
  70. package/docs/src/pages/meta.js +0 -2
  71. package/docs/src/pages/navigation.js +0 -1
  72. package/docs/src/pages/performance.js +0 -2
  73. package/docs/src/pages/project-structure.js +0 -2
  74. package/docs/src/pages/prompt-examples.js +0 -1
  75. package/docs/src/pages/raw-responses.js +0 -4
  76. package/docs/src/pages/routing.js +0 -2
  77. package/docs/src/pages/server-api.js +0 -2
  78. package/docs/src/pages/server-data.js +18 -1
  79. package/docs/src/pages/slash-commands.js +0 -1
  80. package/docs/src/pages/spec.js +2 -1
  81. package/docs/src/pages/streaming.js +0 -2
  82. package/docs/src/pages/testing.js +0 -1
  83. package/docs/src/pages/validation.js +0 -1
  84. package/package.json +5 -1
  85. package/public/.pulse-ui-version +1 -1
  86. package/public/pulse-ui.css +27 -0
  87. package/scripts/build.js +53 -2
  88. package/scripts/strip-server.test.js +74 -2
  89. package/src/agent/checklist.md +20 -0
  90. package/src/agent/guide-server.md +62 -0
  91. package/src/agent/guide-spec.md +5 -3
  92. package/src/agent/guide.md +105 -2
  93. package/src/cli/dev.js +4 -0
  94. package/src/cli/scaffold.js +0 -2
  95. package/src/cli/start.js +1 -0
  96. package/src/md/highlight.js +369 -0
  97. package/src/md/index.js +337 -0
  98. package/src/spec/schema.js +8 -1
  99. package/types/md.d.ts +66 -0
@@ -145,7 +145,6 @@ export default {
145
145
  description: 'Built-in icon set for Pulse UI.',
146
146
  styles: ['/pulse-ui.css', '/docs.css'],
147
147
  },
148
- state: {},
149
148
  view: () => renderComponentPage({
150
149
  currentHref: '/components/icons',
151
150
  prev,
@@ -12,7 +12,6 @@ export default {
12
12
  description: 'Responsive image component with optional aspect-ratio crop and caption for Pulse UI.',
13
13
  styles: ['/pulse-ui.css', '/docs.css'],
14
14
  },
15
- state: {},
16
15
  view: () => renderComponentPage({
17
16
  currentHref: '/components/image',
18
17
  prev,
@@ -12,7 +12,6 @@ export default {
12
12
  description: 'Input component for Pulse UI.',
13
13
  styles: ['/pulse-ui.css', '/docs.css'],
14
14
  },
15
- state: {},
16
15
  view: () => renderComponentPage({
17
16
  currentHref: '/components/input',
18
17
  prev,
@@ -12,7 +12,6 @@ export default {
12
12
  description: 'Styled ordered and unordered list component for Pulse UI.',
13
13
  styles: ['/pulse-ui.css', '/docs.css'],
14
14
  },
15
- state: {},
16
15
  view: () => renderComponentPage({
17
16
  currentHref: '/components/list',
18
17
  prev,
@@ -12,7 +12,6 @@ export default {
12
12
  description: 'Media component for Pulse UI.',
13
13
  styles: ['/pulse-ui.css', '/docs.css'],
14
14
  },
15
- state: {},
16
15
  view: () => renderComponentPage({
17
16
  currentHref: '/components/media',
18
17
  prev,
@@ -12,7 +12,6 @@ export default {
12
12
  description: 'Modal dialog component for Pulse UI.',
13
13
  styles: ['/pulse-ui.css', '/docs.css'],
14
14
  },
15
- state: {},
16
15
  view: () => renderComponentPage({
17
16
  currentHref: '/components/modal',
18
17
  prev,
@@ -12,7 +12,6 @@ export default {
12
12
  description: 'Nav component for Pulse UI.',
13
13
  styles: ['/pulse-ui.css', '/docs.css'],
14
14
  },
15
- state: {},
16
15
  view: () => renderComponentPage({
17
16
  currentHref: '/components/nav',
18
17
  prev,
@@ -12,7 +12,6 @@ export default {
12
12
  description: 'Pricing component for Pulse UI.',
13
13
  styles: ['/pulse-ui.css', '/docs.css'],
14
14
  },
15
- state: {},
16
15
  view: () => renderComponentPage({
17
16
  currentHref: '/components/pricing',
18
17
  prev,
@@ -12,7 +12,6 @@ export default {
12
12
  description: 'Progress bar component for Pulse UI.',
13
13
  styles: ['/pulse-ui.css', '/docs.css'],
14
14
  },
15
- state: {},
16
15
  view: () => renderComponentPage({
17
16
  currentHref: '/components/progress',
18
17
  prev,
@@ -25,7 +25,6 @@ export default {
25
25
  description: 'Typography wrapper for CMS output and rich text content in Pulse UI.',
26
26
  styles: ['/pulse-ui.css', '/docs.css'],
27
27
  },
28
- state: {},
29
28
  view: () => renderComponentPage({
30
29
  currentHref: '/components/prose',
31
30
  prev,
@@ -43,8 +42,8 @@ export default {
43
42
  // CMS rich text field — output directly, fully styled
44
43
  prose({ content: server.article.bodyHtml })
45
44
 
46
- // Markdown rendered to HTML
47
- prose({ content: renderMarkdown(server.post.body) })`,
45
+ // Markdown file via md() helper
46
+ prose({ content: server.post.html }) // see /markdown`,
48
47
  { col: true }
49
48
  )}
50
49
 
@@ -12,7 +12,6 @@ export default {
12
12
  description: 'Styled blockquote component for Pulse UI.',
13
13
  styles: ['/pulse-ui.css', '/docs.css'],
14
14
  },
15
- state: {},
16
15
  view: () => renderComponentPage({
17
16
  currentHref: '/components/pullquote',
18
17
  prev,
@@ -12,7 +12,6 @@ export default {
12
12
  description: 'Radio button and radio group components for Pulse UI.',
13
13
  styles: ['/pulse-ui.css', '/docs.css'],
14
14
  },
15
- state: {},
16
15
  view: () => renderComponentPage({
17
16
  currentHref: '/components/radio',
18
17
  prev,
@@ -12,7 +12,6 @@ export default {
12
12
  description: 'Star rating display and interactive input component for Pulse UI.',
13
13
  styles: ['/pulse-ui.css', '/docs.css'],
14
14
  },
15
- state: {},
16
15
  view: () => renderComponentPage({
17
16
  currentHref: '/components/rating',
18
17
  prev,
@@ -12,7 +12,6 @@ export default {
12
12
  description: 'Search input component with icon, debounce binding, and clear button.',
13
13
  styles: ['/pulse-ui.css', '/docs.css'],
14
14
  },
15
- state: {},
16
15
  view: () => renderComponentPage({
17
16
  currentHref: '/components/search',
18
17
  prev,
@@ -12,7 +12,6 @@ export default {
12
12
  description: 'Vertical padding wrapper with optional background. Compose with container() for full-width background with constrained content.',
13
13
  styles: ['/pulse-ui.css', '/docs.css'],
14
14
  },
15
- state: {},
16
15
  view: () => renderComponentPage({
17
16
  currentHref: '/components/section',
18
17
  prev,
@@ -12,7 +12,6 @@ export default {
12
12
  description: 'iOS-style segmented control component for Pulse UI.',
13
13
  styles: ['/pulse-ui.css', '/docs.css'],
14
14
  },
15
- state: {},
16
15
  view: () => renderComponentPage({
17
16
  currentHref: '/components/segmented',
18
17
  prev,
@@ -11,7 +11,6 @@ export default {
11
11
  description: 'Select component for Pulse UI.',
12
12
  styles: ['/pulse-ui.css', '/docs.css'],
13
13
  },
14
- state: {},
15
14
  view: () => renderComponentPage({
16
15
  currentHref: '/components/select',
17
16
  prev,
@@ -12,7 +12,6 @@ export default {
12
12
  description: 'Loading spinner component for Pulse UI.',
13
13
  styles: ['/pulse-ui.css', '/docs.css'],
14
14
  },
15
- state: {},
16
15
  view: () => renderComponentPage({
17
16
  currentHref: '/components/spinner',
18
17
  prev,
@@ -12,7 +12,6 @@ export default {
12
12
  description: 'Stack component for Pulse UI.',
13
13
  styles: ['/pulse-ui.css', '/docs.css'],
14
14
  },
15
- state: {},
16
15
  view: () => renderComponentPage({
17
16
  currentHref: '/components/stack',
18
17
  prev,
@@ -12,7 +12,6 @@ export default {
12
12
  description: 'Stat component for Pulse UI.',
13
13
  styles: ['/pulse-ui.css', '/docs.css'],
14
14
  },
15
- state: {},
16
15
  view: () => renderComponentPage({
17
16
  currentHref: '/components/stat',
18
17
  prev,
@@ -14,7 +14,6 @@ export default {
14
14
  description: 'Horizontal step progress indicator component for Pulse UI.',
15
15
  styles: ['/pulse-ui.css', '/docs.css'],
16
16
  },
17
- state: {},
18
17
  view: () => renderComponentPage({
19
18
  currentHref: '/components/stepper',
20
19
  prev,
@@ -12,7 +12,6 @@ export default {
12
12
  description: 'Table component for Pulse UI.',
13
13
  styles: ['/pulse-ui.css', '/docs.css'],
14
14
  },
15
- state: {},
16
15
  view: () => renderComponentPage({
17
16
  currentHref: '/components/table',
18
17
  prev,
@@ -12,7 +12,6 @@ export default {
12
12
  description: 'Testimonial component for Pulse UI.',
13
13
  styles: ['/pulse-ui.css', '/docs.css'],
14
14
  },
15
- state: {},
16
15
  view: () => renderComponentPage({
17
16
  currentHref: '/components/testimonial',
18
17
  prev,
@@ -11,7 +11,6 @@ export default {
11
11
  description: 'Textarea component for Pulse UI.',
12
12
  styles: ['/pulse-ui.css', '/docs.css'],
13
13
  },
14
- state: {},
15
14
  view: () => renderComponentPage({
16
15
  currentHref: '/components/textarea',
17
16
  prev,
@@ -17,7 +17,6 @@ export default {
17
17
  description: 'Timeline component for Pulse UI.',
18
18
  styles: ['/pulse-ui.css', '/docs.css'],
19
19
  },
20
- state: {},
21
20
  view: () => renderComponentPage({
22
21
  currentHref: '/components/timeline',
23
22
  prev,
@@ -12,7 +12,6 @@ export default {
12
12
  description: 'iOS-style switch toggle component for Pulse UI.',
13
13
  styles: ['/pulse-ui.css', '/docs.css'],
14
14
  },
15
- state: {},
16
15
  view: () => renderComponentPage({
17
16
  currentHref: '/components/toggle',
18
17
  prev,
@@ -12,7 +12,6 @@ export default {
12
12
  description: 'Tooltip component for Pulse UI.',
13
13
  styles: ['/pulse-ui.css', '/docs.css'],
14
14
  },
15
- state: {},
16
15
  view: () => renderComponentPage({
17
16
  currentHref: '/components/tooltip',
18
17
  prev,
@@ -11,7 +11,6 @@ export default {
11
11
  description: 'Server-rendered UI components for Pulse — button, card, input, alert, stat, avatar, table and more. Fully accessible, mobile-ready, zero client JS.',
12
12
  styles: ['/pulse-ui.css', '/docs.css'],
13
13
  },
14
- state: {},
15
14
  view: () => renderLayout({
16
15
  currentHref: '/components',
17
16
  prev,
@@ -11,7 +11,6 @@ export default {
11
11
  description: 'Full reference for pulse.config.js — port, Lighthouse thresholds, load test config, environments, and per-route overrides.',
12
12
  styles: ['/docs.css'],
13
13
  },
14
- state: {},
15
14
  view: () => renderLayout({
16
15
  currentHref: '/config',
17
16
  prev,
@@ -11,7 +11,6 @@ export default {
11
11
  description: 'Deploy a Pulse app to a VPS, Docker, Fly.io, Railway, or Render.',
12
12
  styles: ['/docs.css'],
13
13
  },
14
- state: {},
15
14
  view: () => renderLayout({
16
15
  currentHref: '/deploy',
17
16
  prev,
@@ -11,7 +11,6 @@ export default {
11
11
  description: 'Escape hatches for features outside the Pulse spec — onRequest middleware, raw server access, WebSockets, SSE, and custom error handling.',
12
12
  styles: ['/docs.css'],
13
13
  },
14
- state: {},
15
14
  view: () => renderLayout({
16
15
  currentHref: '/extending',
17
16
  prev,
@@ -21,7 +21,6 @@ export default {
21
21
  description: 'Frequently asked questions about the Pulse framework.',
22
22
  styles: ['/pulse-ui.css', '/docs.css'],
23
23
  },
24
- state: {},
25
24
  view: () => renderLayout({
26
25
  currentHref: '/faq',
27
26
  prev,
@@ -11,7 +11,6 @@ export default {
11
11
  description: 'Install Pulse and build your first page with an AI agent in minutes.',
12
12
  styles: ['/docs.css'],
13
13
  },
14
- state: {},
15
14
  view: () => renderLayout({
16
15
  currentHref: '/getting-started',
17
16
  prev,
@@ -11,7 +11,6 @@ export default {
11
11
  description: 'Per-route authorization in Pulse. Guard functions run before server data fetchers and redirect unauthorized requests.',
12
12
  styles: ['/docs.css'],
13
13
  },
14
- state: {},
15
14
  view: () => renderLayout({
16
15
  currentHref: '/guard',
17
16
  prev,
@@ -33,7 +32,6 @@ export default {
33
32
  user: async (ctx) => getCurrentUser(ctx.cookies.session),
34
33
  },
35
34
 
36
- state: {},
37
35
  view: (state, server) => \`
38
36
  <main id="main-content">
39
37
  <h1>Welcome, \${server.user.name}</h1>
@@ -87,7 +85,6 @@ export default {
87
85
  if (ctx.cookies.session) return { redirect: '/dashboard' }
88
86
  },
89
87
 
90
- state: {},
91
88
  view: () => \`<main id="main-content">...</main>\`,
92
89
  }`, 'js'))}
93
90
 
@@ -70,7 +70,6 @@ export default {
70
70
  description: 'How the Pulse MCP server gives an AI agent the knowledge and tools to build Pulse apps correctly.',
71
71
  styles: ['/pulse-ui.css', '/docs.css'],
72
72
  },
73
- state: {},
74
73
  view: () => renderLayout({
75
74
  currentHref: '/how-it-works',
76
75
  prev,
@@ -11,7 +11,6 @@ export default {
11
11
  description: 'The img() and picture() helpers for optimised, CLS-free image markup.',
12
12
  styles: ['/docs.css'],
13
13
  },
14
- state: {},
15
14
  view: () => renderLayout({
16
15
  currentHref: '/images',
17
16
  prev,
@@ -95,7 +94,6 @@ import { img, picture } from '@invisibleloop/pulse/image'`, 'js'))}
95
94
 
96
95
  export default {
97
96
  route: '/blog/:slug',
98
- state: {},
99
97
  server: {
100
98
  data: async (ctx) => ({ post: await db.posts.findBySlug(ctx.params.slug) }),
101
99
  },
@@ -0,0 +1,155 @@
1
+ import { renderLayout, h1, lead, section, codeBlock, callout, table } from '../lib/layout.js'
2
+ import { prevNext } from '../lib/nav.js'
3
+ import { highlight } from '../lib/highlight.js'
4
+
5
+ const { prev, next } = prevNext('/markdown')
6
+
7
+ export default {
8
+ route: '/markdown',
9
+ meta: {
10
+ title: 'Markdown — Pulse Docs',
11
+ description: 'Render markdown files as pages with syntax-highlighted code blocks, frontmatter, and the prose component.',
12
+ styles: ['/docs.css'],
13
+ },
14
+ view: () => renderLayout({
15
+ currentHref: '/markdown',
16
+ prev,
17
+ next,
18
+ content: `
19
+ ${h1('Markdown')}
20
+ ${lead('Pulse includes a built-in markdown parser. Write content in <code>.md</code> files, load them with the <code>md()</code> helper, and render with the <code>prose</code> component. No external dependencies — zero browser bytes.')}
21
+
22
+ ${section('basic', 'Basic usage')}
23
+ <p>Import <code>md</code> from <code>@invisibleloop/pulse/md</code> and call it with a file path. It returns an async server fetcher — use it in <code>server</code> and optionally in <code>meta</code>:</p>
24
+ ${codeBlock(highlight(`import { md } from '@invisibleloop/pulse/md'
25
+ import { prose } from '@invisibleloop/pulse/ui'
26
+
27
+ const page = md('content/about.md')
28
+
29
+ export default {
30
+ route: '/about',
31
+ server: { page },
32
+ view: (state, { page }) => prose({ content: page.html }),
33
+ }`, 'js'))}
34
+ <p>The fetcher returns <code>{ html, frontmatter }</code>. The <code>html</code> is ready to pass directly to <code>prose()</code>. The <code>frontmatter</code> object contains the parsed key/value pairs from the <code>---</code> block at the top of the file.</p>
35
+
36
+ ${section('frontmatter', 'Frontmatter')}
37
+ <p>Add a <code>---</code> block at the top of any <code>.md</code> file to define metadata. Use the values in <code>meta</code> for SEO:</p>
38
+ ${codeBlock(highlight(`---
39
+ title: About Us
40
+ description: Learn about our company
41
+ date: 2024-01-15
42
+ ---
43
+
44
+ # About Us
45
+
46
+ Welcome to our company…`, 'bash'))}
47
+ ${codeBlock(highlight(`const page = md('content/about.md')
48
+
49
+ export default {
50
+ route: '/about',
51
+ meta: {
52
+ title: async (ctx) => (await page(ctx)).frontmatter.title,
53
+ description: async (ctx) => (await page(ctx)).frontmatter.description,
54
+ },
55
+ server: { page },
56
+ view: (state, { page }) => prose({ content: page.html }),
57
+ }`, 'js'))}
58
+ ${callout('tip', 'Calling <code>page(ctx)</code> in both <code>meta</code> and <code>server</code> reads the file only once per request — the result is cached on <code>ctx._mdCache</code> automatically.')}
59
+
60
+ ${section('dynamic', 'Dynamic routes')}
61
+ <p>Use <code>:param</code> placeholders in the path — they are resolved from <code>ctx.params</code> at request time:</p>
62
+ ${codeBlock(highlight(`import { md } from '@invisibleloop/pulse/md'
63
+ import { prose } from '@invisibleloop/pulse/ui'
64
+
65
+ const post = md('content/blog/:slug.md')
66
+
67
+ export default {
68
+ route: '/blog/:slug',
69
+ meta: {
70
+ title: async (ctx) => (await post(ctx)).frontmatter.title,
71
+ description: async (ctx) => (await post(ctx)).frontmatter.description,
72
+ },
73
+ server: { post },
74
+ view: (state, { post }) => \`
75
+ <main id="main-content">
76
+ \${prose({ content: post.html })}
77
+ </main>
78
+ \`,
79
+ onViewError: (err) => \`
80
+ <main id="main-content">
81
+ <p>Post not found.</p>
82
+ </main>
83
+ \`,
84
+ }`, 'js'))}
85
+ ${callout('note', 'If the file does not exist, the fetcher throws with <code>{ status: 404 }</code>. Define <code>onViewError</code> on dynamic-route pages to render a friendly not-found message instead of a 500.')}
86
+
87
+ ${section('parseMd', 'parseMd()')}
88
+ <p>For cases where you already have a markdown string (from a database, API, or generated content), use <code>parseMd</code> directly instead of the file helper:</p>
89
+ ${codeBlock(highlight(`import { parseMd } from '@invisibleloop/pulse/md'
90
+ import { prose } from '@invisibleloop/pulse/ui'
91
+
92
+ export default {
93
+ route: '/post/:id',
94
+ server: {
95
+ post: async (ctx) => {
96
+ const record = await db.posts.find(ctx.params.id)
97
+ const { html, frontmatter } = parseMd(record.body)
98
+ return { html, title: frontmatter.title ?? record.title }
99
+ }
100
+ },
101
+ view: (state, { post }) => prose({ content: post.html }),
102
+ }`, 'js'))}
103
+
104
+ ${section('what-renders', 'What the parser renders')}
105
+ ${table(
106
+ ['Markdown', 'Output'],
107
+ [
108
+ ['<code># Heading</code> through <code>###### Heading</code>', '<code>&lt;h1&gt;</code>–<code>&lt;h6&gt;</code> with auto <code>id</code> anchors'],
109
+ ['<code>**bold**</code>, <code>*italic*</code>, <code>~~strike~~</code>', '<code>&lt;strong&gt;</code>, <code>&lt;em&gt;</code>, <code>&lt;del&gt;</code>'],
110
+ ['<code>`inline code`</code>', '<code>&lt;code&gt;</code>'],
111
+ ['Fenced code block with language tag', '<code>&lt;pre&gt;&lt;code class="language-X"&gt;</code> with syntax highlighting'],
112
+ ['<code>[text](url)</code>, <code>![alt](url)</code>', '<code>&lt;a&gt;</code>, <code>&lt;img&gt;</code>'],
113
+ ['<code>- item</code>, <code>1. item</code>', '<code>&lt;ul&gt;</code>, <code>&lt;ol&gt;</code> (nested lists supported)'],
114
+ ['<code>&gt; quote</code>', '<code>&lt;blockquote&gt;</code>'],
115
+ ['GFM table (<code>| col | col |</code>)', '<code>&lt;table&gt;</code> with <code>&lt;thead&gt;</code> and <code>&lt;tbody&gt;</code>'],
116
+ ['<code>---</code> (three or more)', '<code>&lt;hr&gt;</code>'],
117
+ ]
118
+ )}
119
+
120
+ ${section('syntax-highlighting', 'Syntax highlighting')}
121
+ <p>Fenced code blocks with a language tag are syntax-highlighted automatically — no client JS required. The highlighting runs on the server and emits <code>&lt;span class="tok-*"&gt;</code> elements. The colour tokens are defined in <code>pulse-ui.css</code> and respect the active theme.</p>
122
+ ${codeBlock(highlight(`\`\`\`js
123
+ const greeting = 'Hello, world'
124
+ console.log(greeting)
125
+ \`\`\``, 'bash'))}
126
+ <p>Supported languages:</p>
127
+ ${table(
128
+ ['Tag(s)', 'Highlighter'],
129
+ [
130
+ ['<code>js</code>, <code>javascript</code>, <code>ts</code>, <code>typescript</code>, <code>jsx</code>, <code>tsx</code>', 'JavaScript / TypeScript'],
131
+ ['<code>html</code>, <code>xml</code>, <code>svg</code>, <code>vue</code>, <code>svelte</code>', 'HTML / XML'],
132
+ ['<code>css</code>, <code>scss</code>, <code>less</code>', 'CSS'],
133
+ ['<code>bash</code>, <code>sh</code>, <code>shell</code>, <code>zsh</code>', 'Bash / Shell'],
134
+ ['<code>json</code>, <code>jsonc</code>', 'JSON'],
135
+ ['Any other tag', 'Plain text (escaped, no colour tokens)'],
136
+ ]
137
+ )}
138
+
139
+ ${section('prose', 'The prose component')}
140
+ <p>The <code>prose</code> component is designed specifically for markdown output. It applies typographic styles to all descendant elements — headings, paragraphs, lists, blockquotes, code blocks, tables, images — without requiring any classes on the elements themselves:</p>
141
+ ${codeBlock(highlight(`import { prose } from '@invisibleloop/pulse/ui'
142
+
143
+ // Wrap the html from parseMd or md()
144
+ prose({ content: page.html })
145
+
146
+ // Optional size modifier
147
+ prose({ content: page.html, size: 'lg' }) // 'sm' | 'base' | 'lg'`, 'js'))}
148
+ ${callout('note', 'The <code>content</code> slot is injected as raw HTML — only pass server-side content that you trust. Never pass user-submitted HTML directly; sanitise it first.')}
149
+
150
+ ${section('url-paths', 'Using URL paths')}
151
+ <p>Pass a <code>URL</code> object to resolve paths relative to the spec file rather than the process working directory:</p>
152
+ ${codeBlock(highlight(`const page = md(new URL('./content/about.md', import.meta.url))`, 'js'))}
153
+ `,
154
+ }),
155
+ }
@@ -11,7 +11,6 @@ export default {
11
11
  description: 'How to configure page metadata, Open Graph tags, and structured data in Pulse.',
12
12
  styles: ['/docs.css'],
13
13
  },
14
- state: {},
15
14
  view: () => renderLayout({
16
15
  currentHref: '/meta',
17
16
  prev,
@@ -28,7 +27,6 @@ export default {
28
27
  description: 'Learn about the team behind Acme Corp.',
29
28
  styles: ['/app.css'],
30
29
  },
31
- state: {},
32
30
  view: () => \`<h1>About Us</h1>\`,
33
31
  }`, 'js'))}
34
32
  <p>This generates:</p>
@@ -11,7 +11,6 @@ export default {
11
11
  description: 'Client-side navigation in Pulse — how link interception, JSON responses, and spec re-mounting work.',
12
12
  styles: ['/docs.css'],
13
13
  },
14
- state: {},
15
14
  view: () => renderLayout({
16
15
  currentHref: '/navigation',
17
16
  prev,
@@ -11,7 +11,6 @@ export default {
11
11
  description: 'Performance targets, techniques, and built-in optimisations in Pulse.',
12
12
  styles: ['/docs.css'],
13
13
  },
14
- state: {},
15
14
  view: () => renderLayout({
16
15
  currentHref: '/performance',
17
16
  prev,
@@ -85,7 +84,6 @@ export default {
85
84
  export default {
86
85
  route: '/about',
87
86
  meta: { title: 'About', styles: ['/app.css'] },
88
- state: {},
89
87
  view: () => \`<main><h1>About us</h1></main>\`,
90
88
  }`, 'js'))}
91
89
 
@@ -11,7 +11,6 @@ export default {
11
11
  description: 'Recommended directory layout for a Pulse application.',
12
12
  styles: ['/docs.css'],
13
13
  },
14
- state: {},
15
14
  view: () => renderLayout({
16
15
  currentHref: '/project-structure',
17
16
  prev,
@@ -77,7 +76,6 @@ import { card } from '../components/card.js'
77
76
 
78
77
  export default {
79
78
  route: '/',
80
- state: {},
81
79
  view: () => card({ title: 'Hello', body: 'Welcome to Pulse' }),
82
80
  }`, 'js'), 'src/pages/home.js')}
83
81
 
@@ -29,7 +29,6 @@ export default {
29
29
  description: 'Real prompts for building Pulse pages with your AI agent — from creating pages to authentication, performance audits, and integrations.',
30
30
  styles: ['/docs.css'],
31
31
  },
32
- state: {},
33
32
  view: () => renderLayout({
34
33
  currentHref: '/prompt-examples',
35
34
  prev,
@@ -11,7 +11,6 @@ export default {
11
11
  description: 'Return non-HTML responses from Pulse specs — RSS, XML, JSON, and other content types.',
12
12
  styles: ['/docs.css'],
13
13
  },
14
- state: {},
15
14
  view: () => renderLayout({
16
15
  currentHref: '/raw-responses',
17
16
  prev,
@@ -25,7 +24,6 @@ export default {
25
24
  ${codeBlock(highlight(`export default {
26
25
  route: '/feed.xml',
27
26
  contentType: 'application/rss+xml',
28
- state: {},
29
27
  server: {
30
28
  data: async () => ({
31
29
  posts: await db.posts.latest(20),
@@ -53,7 +51,6 @@ export default {
53
51
  ${codeBlock(highlight(`export default {
54
52
  route: '/api/products',
55
53
  contentType: 'application/json',
56
- state: {},
57
54
  server: {
58
55
  data: async (ctx) => ({
59
56
  products: await db.products.list({
@@ -73,7 +70,6 @@ export default {
73
70
  route: '/sitemap.xml',
74
71
  contentType: 'application/xml',
75
72
  serverTtl: 3600,
76
- state: {},
77
73
  server: {
78
74
  data: async () => ({
79
75
  pages: await db.pages.allPublished(),
@@ -11,7 +11,6 @@ export default {
11
11
  description: 'How Pulse routes requests to page specs — static and dynamic routes, params, and conventions.',
12
12
  styles: ['/docs.css'],
13
13
  },
14
- state: {},
15
14
  view: () => renderLayout({
16
15
  currentHref: '/routing',
17
16
  prev,
@@ -24,7 +23,6 @@ export default {
24
23
  <p>Every spec has a <code>route</code> field. This is the URL pattern the spec handles:</p>
25
24
  ${codeBlock(highlight(`export default {
26
25
  route: '/about',
27
- state: {},
28
26
  view: () => \`<h1>About</h1>\`,
29
27
  }`, 'js'))}
30
28
  <p>Pulse matches the exact path. By default, trailing slashes are removed — <code>/about/</code> redirects to <code>/about</code> with a 301. This is controlled by the <code>trailingSlash</code> option in <code>createServer</code>:</p>