@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
@@ -0,0 +1,113 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "mcp__chrome-devtools__navigate_page",
5
+ "mcp__chrome-devtools__evaluate_script",
6
+ "Bash(grep:*)",
7
+ "mcp__chrome-devtools__list_console_messages",
8
+ "mcp__chrome-devtools__take_screenshot",
9
+ "Bash(pkill -f \"docs/server.js\")",
10
+ "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:4000/)",
11
+ "Bash(node:*)",
12
+ "Bash(for f:*)",
13
+ "Bash(do echo:*)",
14
+ "Read(//Users/andy.stubbs/Repos/AS/pulse2/src/ui/**)",
15
+ "Bash(/dev/null done:*)",
16
+ "Bash(mkdir -p /Users/andy.stubbs/Repos/AS/agent-test/.claude)",
17
+ "Bash(mv /Users/andy.stubbs/Repos/AS/agent-test/CLAUDE.md /Users/andy.stubbs/Repos/AS/agent-test/.claude/CLAUDE.md)",
18
+ "Read(//Users/andy.stubbs/Repos/AS/agent-test-3/public/**)",
19
+ "Bash(ls /Users/andy.stubbs/Repos/AS/pulse2/public/*.css /Users/andy.stubbs/Repos/AS/pulse2/public/*.js)",
20
+ "Bash(cp /Users/andy.stubbs/Repos/AS/pulse2/public/pulse-ui.css /Users/andy.stubbs/Repos/AS/agent-test-3/public/pulse-ui.css)",
21
+ "Bash(cp /Users/andy.stubbs/Repos/AS/pulse2/public/pulse-ui.js /Users/andy.stubbs/Repos/AS/agent-test-3/public/pulse-ui.js)",
22
+ "Bash(cp /Users/andy.stubbs/Repos/AS/pulse2/public/pulse-ui.css /Users/andy.stubbs/Repos/AS/agent-test/public/pulse-ui.css)",
23
+ "Bash(cp /Users/andy.stubbs/Repos/AS/pulse2/public/pulse-ui.css /Users/andy.stubbs/Repos/AS/my-amazing-pulse-project/public/pulse-ui.css)",
24
+ "Bash(npm test:*)",
25
+ "Bash(npm run:*)",
26
+ "Bash(git config:*)",
27
+ "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:4000)",
28
+ "Bash(curl:*)",
29
+ "Bash(xargs kill:*)",
30
+ "mcp__chrome-devtools__lighthouse_audit",
31
+ "Bash(git add:*)",
32
+ "Bash(git commit:*)",
33
+ "Bash(git push:*)",
34
+ "Bash(git pull:*)",
35
+ "Bash(git stash:*)",
36
+ "Bash(__NEW_LINE_9ac7abf2e7b711bb__ node -e \":*)",
37
+ "Bash(python3:*)",
38
+ "mcp__chrome-devtools__list_pages",
39
+ "mcp__chrome-devtools__emulate",
40
+ "Bash(npx tsc:*)",
41
+ "Bash(find /Users/andy.stubbs/.claude -name *.json)",
42
+ "Bash(ls /Users/andy.stubbs/Repos/AS/pulse2/src/**/*.test.js)",
43
+ "mcp__chrome-devtools__list_network_requests",
44
+ "mcp__chrome-devtools__get_network_request",
45
+ "Bash(find /Users/andy.stubbs/Repos/AS/forge -name *.md -o -name *.txt)",
46
+ "Bash(node_modules/.bin/tsc --noEmit)",
47
+ "Bash(/Users/andy.stubbs/Repos/AS/forge/node_modules/.bin/tsc --noEmit --project /Users/andy.stubbs/Repos/AS/forge/tsconfig.json)",
48
+ "WebFetch(domain:localhost)",
49
+ "Skill(update-config)",
50
+ "Bash(jq:*)",
51
+ "Bash(ls /Users/andy.stubbs/Repos/AS/tasty/.claude/ grep -r \"pulse-dev\\\\|pulse-stop\\\\|pulse-build\\\\|pulse-start\\\\|Pulse project\" /Users/andy.stubbs/Repos/AS/tasty/.claude/ 2)",
52
+ "Bash(/dev/null grep:*)",
53
+ "Bash(ls -1 /Users/andy.stubbs/Repos/AS/pulse2/src/ui/*.js)",
54
+ "Bash(ls /Users/andy.stubbs/Repos/AS/pulse2/docs/src/pages/*.js)",
55
+ "Bash(wc -c /Users/andy.stubbs/Repos/AS/pulse2/src/agent/guide-*.md)",
56
+ "Bash(npx:*)",
57
+ "Bash(npm install:*)",
58
+ "Bash(./node_modules/.bin/tsc --noEmit --strict --moduleResolution bundler --module esnext --target esnext --allowJs false --skipLibCheck false types/schema.d.ts types/server.d.ts types/runtime.d.ts types/navigate.d.ts types/ssr.d.ts types/html.d.ts types/image.d.ts types/ui.d.ts types/index.d.ts 2>&1)",
59
+ "Bash(./node_modules/.bin/tsc --noEmit --strict --moduleResolution bundler --module esnext --target esnext --allowJs false --lib esnext,dom types/schema.d.ts types/server.d.ts types/runtime.d.ts types/navigate.d.ts types/ssr.d.ts types/html.d.ts types/image.d.ts types/ui.d.ts types/index.d.ts 2>&1)",
60
+ "Bash(/Users/andy.stubbs/Repos/AS/pulse2/node_modules/.bin/tsc -p /tmp/tsconfig_test.json 2>&1)",
61
+ "Bash(find /Users/andy.stubbs/Repos/AS/pulse2 -name *.test.js -type f)",
62
+ "Bash(wc:*)",
63
+ "Bash(ls -1 *.js)",
64
+ "Bash(grep -l \"<h[2-6]\" /Users/andy.stubbs/Repos/AS/pulse2/src/ui/*.js)",
65
+ "Bash(node --input-type=module --eval \"import ''''/Users/andy.stubbs/Repos/AS/pulse2/src/cli/scaffold.js''''\")",
66
+ "Bash(head -10 grep -n \"level\\\\|heading\" /Users/andy.stubbs/Repos/AS/pulse2/docs/src/pages/components/section.js)",
67
+ "Bash(head -10 grep -n \"level\\\\|heading\" /Users/andy.stubbs/Repos/AS/pulse2/docs/src/pages/components/modal.js)",
68
+ "Bash(head -10 grep -n \"table\\(\\\\|''''Prop''''\\\\|flush\\\\|center\\\\|highlighted\\\\|eyebrow\\\\|align\\\\|size.*md\\\\|ui-modal\" /Users/andy.stubbs/Repos/AS/pulse2/docs/src/pages/components/modal.js)",
69
+ "Bash(head -10 grep -n \"table\\(\\\\|''''Prop''''\\\\|eyebrow\\\\|padding\\\\|variant\\\\|subtitle\" /Users/andy.stubbs/Repos/AS/pulse2/docs/src/pages/components/section.js)",
70
+ "Bash(head -10 grep -n \"table\\(\\\\|''''Prop''''\\\\|highlighted\\\\|badge\\\\|period\\\\|features\" /Users/andy.stubbs/Repos/AS/pulse2/docs/src/pages/components/pricing.js)",
71
+ "Bash(head -10 grep -n \"table\\(\\\\|''''Prop''''\\\\|eyebrow\\\\|subtitle\\\\|actions\\\\|align\" /Users/andy.stubbs/Repos/AS/pulse2/docs/src/pages/components/cta.js)",
72
+ "Bash(echo \"exit: $?\")",
73
+ "Bash(xargs node:*)",
74
+ "Bash(find /Users/andy.stubbs/Repos/AS/ultimate-demo/twizzer -name server.js -o -name *.config.*)",
75
+ "Bash(xargs ls:*)",
76
+ "Bash(xargs -I{} lsof -p {})",
77
+ "Bash(brotli --best -c /Users/andy.stubbs/Repos/AS/ultimate-demo/twizzer/public/dist/home.boot-TJOPUIR4.js)",
78
+ "WebSearch",
79
+ "WebFetch(domain:github.com)",
80
+ "Bash(ls:*)",
81
+ "Bash(git -C /Users/andy.stubbs/Repos/AS/pulse2 log --follow --diff-filter=A --format=\"%ar\" -- public/dist/runtime-YLZ4ZGK4.js)",
82
+ "Read(//Users/andy.stubbs/Repos/AS/pulse2/**)",
83
+ "Bash(GetFileInfo /Users/andy.stubbs/Repos/AS/pulse2/public/dist/runtime-YLZ4ZGK4.js)",
84
+ "mcp__chrome-devtools__performance_start_trace",
85
+ "Bash(pkill -f docs/server.js)",
86
+ "Bash(sleep 1 node -e \":*)",
87
+ "mcp__chrome-devtools__close_page",
88
+ "mcp__chrome-devtools__fill",
89
+ "mcp__chrome-devtools__click",
90
+ "mcp__chrome-devtools__take_snapshot",
91
+ "mcp__chrome-devtools__type_text",
92
+ "Bash(head -5 grep -n \"section\\\\|container\" /Users/andy.stubbs/Repos/AS/pulse2/src/ui/index.js)",
93
+ "Bash(head -35 grep \"ui-section-header\\\\|ui-section-title\\\\|ui-section-eyebrow\\\\|ui-section--\" /Users/andy.stubbs/Repos/AS/pulse2/public/pulse-ui.css)",
94
+ "mcp__chrome-devtools__performance_analyze_insight",
95
+ "Bash(git checkout:*)",
96
+ "Bash(git branch:*)",
97
+ "Bash(gh run:*)",
98
+ "Bash(xargs -I{} gh run delete {} --repo invisibleloop/pulse-framework)",
99
+ "Bash(gh api:*)",
100
+ "Bash(npm access:*)",
101
+ "Bash(gh pr:*)",
102
+ "Bash(git fetch:*)",
103
+ "Bash(git rebase:*)",
104
+ "Bash(git rm:*)",
105
+ "Bash(sed -i '' 's/Ship with it built in/Production quality, built in/' docs/src/pages/home.js)",
106
+ "Bash(3 \" -A2 docs/src/pages/home.js)",
107
+ "Bash(git merge:*)",
108
+ "Bash(node -p \"require\\(''./package.json''\\).version\")",
109
+ "Bash(npm view:*)",
110
+ "Bash(git cherry-pick:*)"
111
+ ]
112
+ }
113
+ }
@@ -1,25 +1,17 @@
1
1
  name: Release
2
2
 
3
3
  on:
4
- workflow_dispatch:
5
- inputs:
6
- bump:
7
- description: Version bump
8
- type: choice
9
- options: [patch, minor, major]
10
- default: patch
11
- required: true
4
+ push:
5
+ branches: [main]
12
6
 
13
7
  jobs:
14
8
  release:
15
9
  runs-on: ubuntu-latest
16
10
  permissions:
17
- contents: write
11
+ contents: read
18
12
  id-token: write
19
13
  steps:
20
14
  - uses: actions/checkout@v4
21
- with:
22
- token: ${{ secrets.GITHUB_TOKEN }}
23
15
 
24
16
  - uses: actions/setup-node@v4
25
17
  with:
@@ -31,14 +23,12 @@ jobs:
31
23
 
32
24
  - run: npm test
33
25
 
34
- - name: Bump version and tag
26
+ - name: Publish if version is new
35
27
  run: |
36
- git config user.name "github-actions[bot]"
37
- git config user.email "github-actions[bot]@users.noreply.github.com"
38
- git pull --rebase
39
- npm version ${{ inputs.bump }} -m "chore: release %s [skip ci]"
40
- git push --follow-tags
41
-
42
- - run: npm publish --provenance --access public
43
- env:
44
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
28
+ VERSION=$(node -p "require('./package.json').version")
29
+ EXISTS=$(npm view @invisibleloop/pulse@$VERSION version 2>/dev/null || echo "")
30
+ if [ -z "$EXISTS" ]; then
31
+ npm publish --provenance --access public
32
+ else
33
+ echo "Version $VERSION already published — skipping."
34
+ fi
@@ -1 +1 @@
1
- 0.1.20
1
+ 0.1.28
@@ -167,6 +167,15 @@ a:hover {
167
167
  margin-bottom: 1.25rem;
168
168
  }
169
169
 
170
+ .hero-kicker {
171
+ font-size: 0.95rem;
172
+ font-weight: 500;
173
+ color: #0a0a0a;
174
+ opacity: 0.55;
175
+ margin-bottom: 1rem;
176
+ letter-spacing: 0.01em;
177
+ }
178
+
170
179
  .hero-badge {
171
180
  display: inline-flex;
172
181
  align-items: center;
@@ -432,6 +441,7 @@ a:hover {
432
441
  .home-code {
433
442
  background: #111114;
434
443
  padding: 5rem 2rem 6rem;
444
+ --muted: #9090a0;
435
445
  }
436
446
 
437
447
  .home-code-inner {
@@ -1467,7 +1477,11 @@ a:hover {
1467
1477
  border: none;
1468
1478
  color: var(--muted);
1469
1479
  cursor: pointer;
1470
- padding: 0.25rem;
1480
+ min-width: 44px;
1481
+ min-height: 44px;
1482
+ align-items: center;
1483
+ justify-content: center;
1484
+ padding: 0;
1471
1485
  }
1472
1486
  .mobile-menu-btn:hover {
1473
1487
  color: var(--text);
@@ -1476,6 +1490,10 @@ a:hover {
1476
1490
  .header-logo-mobile {
1477
1491
  display: none;
1478
1492
  color: var(--text);
1493
+ min-width: 44px;
1494
+ min-height: 44px;
1495
+ align-items: center;
1496
+ justify-content: center;
1479
1497
  }
1480
1498
 
1481
1499
  .header-github {
@@ -1635,6 +1635,7 @@ hr.ui-divider {
1635
1635
  font-family: var(--ui-mono);
1636
1636
  color: var(--ui-muted);
1637
1637
  margin-left: auto;
1638
+ opacity: .7;
1638
1639
  }
1639
1640
 
1640
1641
  .ui-code-window-pre {
package/docs/server.js CHANGED
@@ -37,6 +37,7 @@ import meta from './src/pages/meta.js'
37
37
  import performance from './src/pages/performance.js'
38
38
  import accessibility from './src/pages/accessibility.js'
39
39
  import testing from './src/pages/testing.js'
40
+ import store from './src/pages/store.js'
40
41
 
41
42
  // Component pages
42
43
  import compButton from './src/pages/components/button.js'
@@ -130,6 +131,7 @@ createServer(
130
131
  performance,
131
132
  accessibility,
132
133
  testing,
134
+ store,
133
135
  // Component pages
134
136
  compButton,
135
137
  compBadge,
@@ -186,7 +188,8 @@ createServer(
186
188
  compIcons,
187
189
  ],
188
190
  {
189
- port: process.env.PORT ? Number(process.env.PORT) : 4000,
190
- staticDir: new URL('./public', import.meta.url).pathname,
191
+ port: process.env.PORT ? Number(process.env.PORT) : 4000,
192
+ staticDir: new URL('./public', import.meta.url).pathname,
193
+ defaultCache: true,
191
194
  }
192
195
  )
@@ -212,6 +212,62 @@ function highlightBash(code) {
212
212
  // HTML tokeniser (minimal)
213
213
  // ---------------------------------------------------------------------------
214
214
 
215
+ function highlightHtmlTag(tag) {
216
+ // Single-pass tag highlighter — avoids the chained .replace() trap where a
217
+ // later pass re-processes class="tok-fn" strings already injected by an
218
+ // earlier pass, producing broken HTML like <span class=<span …>"tok-fn"</span>>.
219
+ let out = ''
220
+ let i = 0
221
+
222
+ // Opening < or </
223
+ out += '&lt;'
224
+ i++ // skip <
225
+ if (tag[i] === '/') { out += '/'; i++ }
226
+
227
+ // Tag name
228
+ let name = ''
229
+ while (i < tag.length && /[\w-]/.test(tag[i])) name += tag[i++]
230
+ if (name) out += span('tok-kw', name)
231
+
232
+ // Attributes and remainder up to >
233
+ while (i < tag.length) {
234
+ const ch = tag[i]
235
+ if (ch === '>') { out += '&gt;'; i++; break }
236
+ if (ch === '/') { out += '/'; i++; continue }
237
+
238
+ if (/[\w-]/.test(ch)) {
239
+ // Attribute name
240
+ let attr = ''
241
+ while (i < tag.length && /[\w-]/.test(tag[i])) attr += tag[i++]
242
+
243
+ if (tag[i] === '=') {
244
+ out += span('tok-fn', attr) + esc('=')
245
+ i++ // skip =
246
+ // Attribute value
247
+ const q = tag[i]
248
+ if (q === '"' || q === "'") {
249
+ let val = q
250
+ i++
251
+ while (i < tag.length && tag[i] !== q) {
252
+ if (tag[i] === '\\') { val += tag[i] + (tag[i + 1] || ''); i += 2; continue }
253
+ val += tag[i++]
254
+ }
255
+ if (i < tag.length) val += tag[i++] // closing quote
256
+ out += span('tok-str', val)
257
+ }
258
+ } else {
259
+ out += esc(attr)
260
+ }
261
+ continue
262
+ }
263
+
264
+ out += esc(ch)
265
+ i++
266
+ }
267
+
268
+ return out
269
+ }
270
+
215
271
  function highlightHtml(code) {
216
272
  let out = ''
217
273
  let i = 0
@@ -231,19 +287,7 @@ function highlightHtml(code) {
231
287
  if (code[i] === '<') {
232
288
  const end = code.indexOf('>', i)
233
289
  if (end === -1) { out += esc(code[i++]); continue }
234
- const tag = code.slice(i, end + 1)
235
- // Simple: highlight tag name + attributes
236
- const highlighted = tag.replace(
237
- /^(<\/?)([\w-]+)/,
238
- (_, lt, name) => esc(lt) + span('tok-kw', name)
239
- ).replace(
240
- /([\w-]+)(=)/g,
241
- (_, attr, eq) => span('tok-fn', attr) + esc(eq)
242
- ).replace(
243
- /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/g,
244
- (_, str) => span('tok-str', str)
245
- )
246
- out += highlighted
290
+ out += highlightHtmlTag(code.slice(i, end + 1))
247
291
  i = end + 1
248
292
  continue
249
293
  }
@@ -6,6 +6,9 @@
6
6
  */
7
7
 
8
8
  import { NAV } from './nav.js'
9
+ import pkg from '../../../package.json' with { type: 'json' }
10
+
11
+ const { version } = pkg
9
12
 
10
13
  function esc(s) {
11
14
  return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
@@ -33,7 +36,7 @@ function sidebar(currentHref) {
33
36
  </svg>
34
37
  <span class="logo-name">Pulse</span>
35
38
  </a>
36
- <span class="version-badge">v0.1</span>
39
+ <span class="version-badge">v${version}</span>
37
40
  </div>
38
41
  <nav class="sidebar-nav">
39
42
  ${sections}
@@ -74,7 +77,7 @@ export function renderLayout({ currentHref, content, prev = null, next = null })
74
77
  <path d="M13 2L4.5 13.5H11L10 22L19.5 10.5H13L13 2Z" fill="var(--accent)" stroke="var(--accent)" stroke-width="1" stroke-linejoin="round"/>
75
78
  </svg>
76
79
  </a>
77
- <a href="https://github.com/invisibleloop/pulse" class="header-github" aria-label="View on GitHub" target="_blank" rel="noopener noreferrer">
80
+ <a href="https://github.com/invisibleloop/pulse-framework" class="header-github" aria-label="View on GitHub" target="_blank" rel="noopener noreferrer">
78
81
  <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
79
82
  <path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/>
80
83
  </svg>
@@ -1,5 +1,8 @@
1
1
  import { renderLayout, h1, lead } from '../lib/layout.js'
2
2
  import { prevNext } from '../lib/nav.js'
3
+ import pkg from '../../../package.json' with { type: 'json' }
4
+
5
+ const { version } = pkg
3
6
 
4
7
  const { prev, next } = prevNext('/faq')
5
8
 
@@ -30,7 +33,7 @@ export default {
30
33
  ${q(
31
34
  'Is Pulse ready for production?',
32
35
  `<p>The architecture is production-quality — 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>
33
- <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>`
36
+ <p>That said, Pulse is v${version} 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>`
34
37
  )}
35
38
 
36
39
  ${q(
@@ -42,7 +45,7 @@ export default {
42
45
  ${q(
43
46
  'Why no virtual DOM?',
44
47
  `<p>A virtual DOM solves the problem of efficient incremental updates to a large, complex component tree. Pulse pages are server-rendered HTML strings — 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>
45
- <p>Eliminating the virtual DOM means eliminating the ~40–100 kB runtime that comes with it. Pulse ships ~2 kB brotli to the browser on first visit. That is not a compression trick — there is genuinely no framework runtime on the client.</p>`
48
+ <p>Eliminating the virtual DOM means eliminating the ~40–100 kB runtime that comes with it. Pulse ships ~3.5 kB brotli to the browser on first visit (shared runtime + page bundle). That is not a compression trick — there is genuinely no framework runtime on the client.</p>`
46
49
  )}
47
50
 
48
51
  ${q(
@@ -1,6 +1,9 @@
1
1
  import { highlight } from '../lib/highlight.js'
2
2
  import { metricsStore } from '../lib/metrics-store.js'
3
3
  import { codeWindow } from '../../../src/ui/code-window.js'
4
+ import pkg from '../../../package.json' with { type: 'json' }
5
+
6
+ const { version } = pkg
4
7
 
5
8
  const exampleSpec = highlight(`export default {
6
9
  route: '/dashboard',
@@ -52,7 +55,7 @@ export default {
52
55
  </a>
53
56
  <div class="home-nav-links">
54
57
  <a href="/getting-started">Docs</a>
55
- <a href="https://github.com/invisibleloop/pulse" target="_blank" rel="noopener">GitHub</a>
58
+ <a href="https://github.com/invisibleloop/pulse-framework" target="_blank" rel="noopener">GitHub</a>
56
59
  </div>
57
60
  </nav>
58
61
 
@@ -63,9 +66,10 @@ export default {
63
66
  <path d="M13 2L4.5 13.5H11L10 22L19.5 10.5H13L13 2Z" fill="var(--accent)" stroke="var(--accent)" stroke-width="1" stroke-linejoin="round"/>
64
67
  </svg>
65
68
  </div>
66
- <div class="hero-badge">v0.1 — EARLY ACCESS</div>
69
+ <div class="hero-badge">v${version} — EARLY ACCESS</div>
70
+ <p class="hero-kicker">A Node.js framework for building server-rendered web apps</p>
67
71
  <h1 class="hero-title">Describe the outcome. Pulse guarantees it.</h1>
68
- <p class="hero-subtitle">One spec object per page — server data, state, mutations, and view, co-located in plain JS. Streaming SSR, security headers, and production caching are enforced by the framework, not left to configuration.<br><br>Designed for AI agents. Production-quality architecture.</p>
72
+ <p class="hero-subtitle">One spec object per page — server data, state, mutations, and view in plain JS. Streaming SSR, security headers, and production caching are enforced by the framework, not left to configuration. Designed for AI agents.</p>
69
73
  <div class="hero-ctas">
70
74
  <a href="/getting-started" class="btn-primary">Get Started</a>
71
75
  <a href="/spec" class="btn-secondary">Read the Spec</a>
@@ -112,7 +116,7 @@ export default {
112
116
  <div class="how-connector" aria-hidden="true"></div>
113
117
  <div class="how-step">
114
118
  <div class="how-step-num">3</div>
115
- <h3>Ship with it built in</h3>
119
+ <h3>Production quality, built in</h3>
116
120
  <p>Streaming SSR, security headers, and immutable caching come from the framework — not your config. Follow the spec, and the results follow.</p>
117
121
  </div>
118
122
  </div>
@@ -394,7 +398,7 @@ export default {
394
398
 
395
399
  </main>
396
400
  <footer class="home-footer">
397
- <p>MIT License · <a href="https://github.com/invisibleloop/pulse" target="_blank" rel="noopener">GitHub</a> · <a href="/getting-started">Get started in 2 minutes</a></p>
401
+ <p>MIT License · <a href="https://github.com/invisibleloop/pulse-framework" target="_blank" rel="noopener">GitHub</a> · <a href="/getting-started">Get started in 2 minutes</a></p>
398
402
  </footer>
399
403
  </div>
400
404
  `,
@@ -46,6 +46,7 @@ export default {
46
46
  ['<code>ogTitle</code>', '<code>string</code>', 'Open Graph title. If omitted, falls back to <code>title</code>.'],
47
47
  ['<code>ogImage</code>', '<code>string</code>', 'Open Graph image URL — shown when the page is shared on social media.'],
48
48
  ['<code>schema</code>', '<code>object</code>', 'JSON-LD structured data object — emitted as a <code>&lt;script type="application/ld+json"&gt;</code> tag.'],
49
+ ['<code>canonical</code>', '<code>string | (ctx, serverState) => string</code>', 'Canonical URL — overrides the auto-derived canonical. Accepts a function for dynamic values.'],
49
50
  ]
50
51
  )}
51
52
 
@@ -96,6 +97,26 @@ export default {
96
97
  ]
97
98
  )}
98
99
 
100
+ ${section('canonical', 'Canonical URLs')}
101
+ <p>Pulse automatically derives a canonical URL from the request and emits it as a <code>&lt;link rel="canonical"&gt;</code> tag on every page. In most cases no configuration is needed.</p>
102
+ <p>Use <code>meta.canonical</code> to override — for example, on paginated pages or when content is accessible at more than one URL. A plain string is resolved once at startup:</p>
103
+ ${codeBlock(highlight(`// Paginated blog — pages 2, 3, … all canonicalise to the first page
104
+ meta: {
105
+ title: 'Blog — Page 2',
106
+ canonical: 'https://mysite.com/blog',
107
+ }`, 'js'))}
108
+ <p>Pass a function to derive the canonical from the request context or server data. The function receives <code>(ctx, serverState)</code>:</p>
109
+ ${codeBlock(highlight(`// Canonical from a URL param
110
+ meta: {
111
+ canonical: (ctx) => \`https://mysite.com/products/\${ctx.params.slug}\`,
112
+ }
113
+
114
+ // Canonical from a server fetcher result (e.g. canonical slug from a database lookup)
115
+ meta: {
116
+ canonical: (ctx, serverState) => \`https://mysite.com/products/\${serverState.product.slug}\`,
117
+ }`, 'js'))}
118
+ ${callout('note', '<strong>Streaming caveat:</strong> when <code>stream: true</code> (the default), the <code>&lt;head&gt;</code> is written before server fetchers resolve, so <code>serverState</code> will be <code>null</code> for streaming responses. If your canonical depends on server data, set <code>stream: false</code> on that spec, or derive it from <code>ctx.params</code> instead.')}
119
+
99
120
  ${section('styles', 'Stylesheets')}
100
121
  <p>The <code>styles</code> array accepts any number of stylesheet URLs. They are emitted as <code>&lt;link rel="stylesheet"&gt;</code> tags in the <code>&lt;head&gt;</code> in the order declared:</p>
101
122
  ${codeBlock(highlight(`meta: {
@@ -27,7 +27,18 @@ export default {
27
27
  state: {},
28
28
  view: () => \`<h1>About</h1>\`,
29
29
  }`, 'js'))}
30
- <p>Pulse matches the exact path. Trailing slashes are normalised — <code>/about</code> and <code>/about/</code> are treated the same.</p>
30
+ <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>
31
+ ${table(
32
+ ['Value', 'Behaviour'],
33
+ [
34
+ ['<code>"remove"</code> (default)', 'Redirects <code>/about/</code> → <code>/about</code> (301)'],
35
+ ['<code>"add"</code>', 'Redirects <code>/about</code> → <code>/about/</code> (301)'],
36
+ ['<code>"allow"</code>', 'Serves both — no redirect'],
37
+ ]
38
+ )}
39
+ ${codeBlock(highlight(`createServer(specs, {
40
+ trailingSlash: 'add', // enforce trailing slashes
41
+ })`, 'js'))}
31
42
 
32
43
  ${section('dynamic', 'Dynamic segments')}
33
44
  <p>Use a colon prefix for dynamic path segments. Named segments are captured and available in <code>ctx.params</code> in <a href="/server-data">server data</a>:</p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@invisibleloop/pulse",
3
- "version": "0.1.28",
3
+ "version": "0.1.29",
4
4
  "type": "module",
5
5
  "description": "AI-first frontend framework. The spec is the source of truth.",
6
6
  "license": "MIT",
@@ -34,3 +34,23 @@ Convention: name dynamic-route files [param].js inside a subfolder:
34
34
  src/pages/blog/[slug].js with route: '/blog/:slug'
35
35
 
36
36
  This is purely a human readability convention. Pulse does not process [ ] in filenames.
37
+
38
+ ## Canonical URLs
39
+
40
+ Pulse auto-derives a canonical URL from every request and emits `<link rel="canonical">` in the `<head>`. No config needed in most cases.
41
+
42
+ Override with `meta.canonical` when content is accessible at multiple URLs, or for paginated series:
43
+
44
+ ```js
45
+ // Static override
46
+ meta: { canonical: 'https://mysite.com/blog' }
47
+
48
+ // From URL params (works in both streaming and string mode)
49
+ meta: { canonical: (ctx) => `https://mysite.com/products/${ctx.params.slug}` }
50
+
51
+ // From server data — e.g. canonical slug from a DB lookup
52
+ // Only works when stream: false. In streaming mode serverState is null at head-write time.
53
+ meta: { canonical: (ctx, serverState) => `https://mysite.com/products/${serverState.product.slug}` }
54
+ ```
55
+
56
+ The function signature is `(ctx, serverState) => string`. `serverState` is only populated on the string (non-streaming) path — if your canonical depends on server fetcher results, add `stream: false` to that spec.
@@ -4,7 +4,15 @@ Pulse is a spec-first frontend framework. Pages are JS files that export a defau
4
4
 
5
5
  ```js
6
6
  export default {
7
- meta: { title, description, styles: ['/app.css'] },
7
+ meta: {
8
+ title: 'Page Title', // string or (ctx) => string
9
+ description: 'Meta description', // string or (ctx) => string
10
+ styles: ['/app.css'], // string[] or (ctx) => string[]
11
+ ogTitle: 'Social title', // optional — falls back to title
12
+ ogImage: 'https://…/og.jpg', // optional — 1200×630 recommended
13
+ canonical: 'https://mysite.com/…', // optional — string or (ctx, serverState) => string
14
+ schema: { '@context': 'https://schema.org', '@type': 'WebPage', … }, // optional JSON-LD
15
+ },
8
16
  state: { /* initial state */ },
9
17
  mutations: {
10
18
  // Synchronous. Return partial state. First arg is state, second is the DOM event.
@@ -53,8 +53,9 @@ export async function scaffold(targetDir, options = {}) {
53
53
  ${port !== 3000 ? ` port: ${port},\n` : ''}}
54
54
  `)
55
55
 
56
- // Home page — working counter proves the app runs
57
- write(targetDir, 'src/pages/home.js', homePage(name))
56
+ // Home page + tests — working counter proves the app runs
57
+ write(targetDir, 'src/pages/home.js', homePage(name))
58
+ write(targetDir, 'src/pages/home.test.js', homePageTest(name))
58
59
 
59
60
  // Minimal stylesheet
60
61
  write(targetDir, 'public/app.css', baseCSS())
@@ -184,6 +185,15 @@ ${port !== 3000 ? ` port: ${port},\n` : ''}}
184
185
  } catch {
185
186
  execSync('npm install', { cwd: targetDir, stdio: 'inherit' })
186
187
  }
188
+
189
+ // Initialise git so the Stop hook can use `git status` to track only changed files.
190
+ // Without this the hook falls back to listing all src/pages/*.js and flags home.js.
191
+ try {
192
+ execSync('git init && git add -A && git commit -m "init"', { cwd: targetDir, stdio: 'pipe' })
193
+ console.log(' ✓ Git repository initialised')
194
+ } catch {
195
+ // Non-fatal — project still works without git, stop hook falls back gracefully.
196
+ }
187
197
  }
188
198
 
189
199
  // ---------------------------------------------------------------------------
@@ -244,6 +254,51 @@ export default {
244
254
  `
245
255
  }
246
256
 
257
+ function homePageTest(_appName) {
258
+ return `\
259
+ import assert from 'node:assert/strict'
260
+ import { test } from 'node:test'
261
+ import { renderSync } from '@invisibleloop/pulse/testing'
262
+ import spec from './home.js'
263
+
264
+ test('home page renders app name', () => {
265
+ const r = renderSync(spec)
266
+ assert(r.has('main#main-content'))
267
+ assert(r.has('h1'))
268
+ })
269
+
270
+ test('home page renders counter at 0', () => {
271
+ const r = renderSync(spec)
272
+ assert(r.has('span[aria-live]'))
273
+ assert.equal(r.get('span[aria-live]').text, '0')
274
+ })
275
+
276
+ test('home page decrement disabled at min', () => {
277
+ const r = renderSync(spec, { state: { count: 0 } })
278
+ const buttons = r.findAll('button')
279
+ const dec = buttons.find(b => b.attr('aria-label') === 'Decrease count')
280
+ assert(dec, 'decrement button not found')
281
+ assert(dec.attrs.disabled !== undefined, 'decrement should be disabled at 0')
282
+ })
283
+
284
+ test('home page increment disabled at max', () => {
285
+ const r = renderSync(spec, { state: { count: 10 } })
286
+ const buttons = r.findAll('button')
287
+ const inc = buttons.find(b => b.attr('aria-label') === 'Increase count')
288
+ assert(inc, 'increment button not found')
289
+ assert(inc.attrs.disabled !== undefined, 'increment should be disabled at 10')
290
+ })
291
+
292
+ test('increment mutation adds 1', () => {
293
+ assert.deepEqual(spec.mutations.increment({ count: 4 }), { count: 5 })
294
+ })
295
+
296
+ test('decrement mutation subtracts 1', () => {
297
+ assert.deepEqual(spec.mutations.decrement({ count: 4 }), { count: 3 })
298
+ })
299
+ `
300
+ }
301
+
247
302
  function baseCSS() {
248
303
  return `\
249
304
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
@@ -280,6 +335,12 @@ public/app.css ← global stylesheet
280
335
 
281
336
  The MCP guide is the single source of truth. Follow it for all technical decisions, component usage, and the mandatory verification workflow.
282
337
 
338
+ ## Before writing any code
339
+
340
+ **Always present a plan and wait for the user to confirm before writing a single line of code.**
341
+
342
+ The plan must include: route, page sections, components used, state shape, and whether hydration is needed. End the plan with an explicit question — "Shall I go ahead?" — and stop. Do not proceed until the user says yes (or equivalent). This applies to every new page or significant change, no matter how clear the task seems.
343
+
283
344
  ## After completing any feature
284
345
 
285
346
  Run these steps in order — do not declare work done without them: