@invisibleloop/pulse 0.20.10 → 0.20.19

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 (266) hide show
  1. package/package.json +6 -5
  2. package/public/.pulse-ui-version +1 -1
  3. package/scripts/build.js +26 -0
  4. package/scripts/release-version.js +17 -8
  5. package/scripts/strip-server.test.js +60 -0
  6. package/src/agent/checklist.md +8 -12
  7. package/src/agent/commands/pulse-start.md +2 -0
  8. package/src/agent/commands/verify.md +30 -8
  9. package/src/agent/guide-components.md +7 -25
  10. package/src/agent/guide-routing.md +0 -12
  11. package/src/agent/guide-server.md +102 -1
  12. package/src/agent/guide-spec.md +8 -1
  13. package/src/agent/guide-styles.md +23 -0
  14. package/src/agent/identity.md +9 -0
  15. package/src/agent/workflow.md +4 -23
  16. package/src/cli/check-bundles.js +113 -0
  17. package/src/cli/check-bundles.test.js +137 -0
  18. package/src/cli/cli.test.js +105 -0
  19. package/src/cli/dev.js +48 -3
  20. package/src/cli/diagnose.js +105 -0
  21. package/src/cli/diagnose.test.js +124 -0
  22. package/src/cli/discover.js +9 -2
  23. package/src/cli/index.js +171 -9
  24. package/src/cli/process-utils.js +57 -0
  25. package/src/cli/process-utils.test.js +114 -0
  26. package/src/cli/review.js +391 -0
  27. package/src/cli/review.test.js +143 -0
  28. package/src/cli/scaffold.js +9 -4
  29. package/src/cli/stamp.js +102 -0
  30. package/src/cli/stamp.test.js +149 -0
  31. package/src/cli/start.js +15 -2
  32. package/src/cli/validate.js +184 -0
  33. package/src/cli/validate.test.js +153 -0
  34. package/src/mcp/mcp.test.js +261 -30
  35. package/src/mcp/server.js +207 -515
  36. package/src/mcp/validate-worker.js +18 -1
  37. package/src/runtime/index.js +144 -11
  38. package/src/runtime/runtime.test.js +199 -0
  39. package/src/runtime/ssr.js +14 -7
  40. package/src/runtime/ssr.test.js +31 -0
  41. package/src/server/index.js +163 -10
  42. package/src/server/server.test.js +225 -2
  43. package/src/spec/schema.js +1 -1
  44. package/src/ui/index.js +0 -5
  45. package/src/ui/ui.test.js +0 -75
  46. package/types/ui.d.ts +0 -21
  47. package/.claude/commands/build-page.md +0 -63
  48. package/.claude/commands/new-doc-page.md +0 -45
  49. package/.claude/commands/pulse-start.md +0 -11
  50. package/.claude/commands/pulse-stop.md +0 -5
  51. package/.claude/commands/verify.md +0 -118
  52. package/.claude/pulse-checklist.md +0 -189
  53. package/.github/workflows/ci.yml +0 -25
  54. package/.github/workflows/publish.yml +0 -45
  55. package/.pulse/load-reports/home/1773432711417.json +0 -22
  56. package/.pulse-verified +0 -1
  57. package/AGENTS.md +0 -697
  58. package/CLAUDE.md +0 -697
  59. package/README.md +0 -95
  60. package/benchmark/public/dist/counter.boot-PO6SNSJZ.js +0 -8
  61. package/benchmark/public/dist/home.boot-YK6VAEQI.js +0 -6
  62. package/benchmark/public/dist/manifest.json +0 -5
  63. package/benchmark/public/dist/runtime-5Y7ESAIB.js +0 -2
  64. package/benchmark/public/dist/runtime-KO4BHUQ3.js +0 -49
  65. package/benchmark/results/2026-03-27-09-20.json +0 -188
  66. package/benchmark/results/2026-03-27-09-21.json +0 -188
  67. package/benchmark/results/2026-03-27-09-29.json +0 -188
  68. package/benchmark/results/2026-03-27-10-14.json +0 -188
  69. package/benchmark/results/2026-03-27-10-18.json +0 -232
  70. package/benchmark/src/pages/counter.js +0 -37
  71. package/benchmark/src/pages/home.js +0 -21
  72. package/docs/.claude/commands/pulse-build.md +0 -7
  73. package/docs/.claude/commands/pulse-dev.md +0 -7
  74. package/docs/.claude/commands/pulse-start.md +0 -9
  75. package/docs/.claude/commands/pulse-stop.md +0 -5
  76. package/docs/.claude/commands/verify.md +0 -84
  77. package/docs/.claude/coverage-check.js +0 -196
  78. package/docs/.claude/pulse-checklist.md +0 -189
  79. package/docs/public/.pulse-ui-version +0 -1
  80. package/docs/public/andypixel.css +0 -237
  81. package/docs/public/bookworms.css +0 -422
  82. package/docs/public/coutumes.css +0 -561
  83. package/docs/public/docs.css +0 -2388
  84. package/docs/public/favicon.svg +0 -4
  85. package/docs/public/home-brut.css +0 -982
  86. package/docs/public/images/bookworms/hero-books.jpg +0 -0
  87. package/docs/public/images/bookworms/how-we-read-now.jpg +0 -0
  88. package/docs/public/images/bookworms/lantern-keepers.jpg +0 -0
  89. package/docs/public/images/bookworms/provincial-bookseller.jpg +0 -0
  90. package/docs/public/images/journey/178.jpg +0 -0
  91. package/docs/public/images/journey/29.jpg +0 -0
  92. package/docs/public/images/retrowheels/hero.jpg +0 -0
  93. package/docs/public/images/shop/devbook-pro.jpg +0 -0
  94. package/docs/public/images/suzukibento/1015.jpg +0 -0
  95. package/docs/public/images/suzukibento/1025.jpg +0 -0
  96. package/docs/public/images/suzukibento/1043.jpg +0 -0
  97. package/docs/public/images/suzukibento/1059.jpg +0 -0
  98. package/docs/public/images/suzukibento/1062.jpg +0 -0
  99. package/docs/public/images/suzukibento/1084.jpg +0 -0
  100. package/docs/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
  101. package/docs/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
  102. package/docs/public/images/swish/1485518882345-15568b007407.jpg +0 -0
  103. package/docs/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
  104. package/docs/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
  105. package/docs/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
  106. package/docs/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
  107. package/docs/public/images/swish/1551803091-e20673f15770.jpg +0 -0
  108. package/docs/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
  109. package/docs/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
  110. package/docs/public/img/heic1501a.jpg +0 -0
  111. package/docs/public/img/placeholder-landscape.svg +0 -18
  112. package/docs/public/img/placeholder-square.svg +0 -14
  113. package/docs/public/img/placeholder-tall.svg +0 -22
  114. package/docs/public/journey.css +0 -528
  115. package/docs/public/menu.js +0 -327
  116. package/docs/public/pulse-ui.css +0 -3028
  117. package/docs/public/pulse-ui.js +0 -257
  118. package/docs/public/retrowheels.css +0 -425
  119. package/docs/public/shop.css +0 -269
  120. package/docs/public/sparkys.css +0 -382
  121. package/docs/public/suzukibento.css +0 -517
  122. package/docs/public/swish.css +0 -394
  123. package/docs/public/theme.css +0 -52
  124. package/docs/public/themes/lumio.css +0 -50
  125. package/docs/public/themes/observatory.css +0 -104
  126. package/docs/server.js +0 -161
  127. package/docs/src/lib/component-page.js +0 -49
  128. package/docs/src/lib/highlight.js +0 -2
  129. package/docs/src/lib/layout.js +0 -190
  130. package/docs/src/lib/metrics-store.js +0 -6
  131. package/docs/src/lib/nav.js +0 -171
  132. package/docs/src/lib/stats.js +0 -81
  133. package/docs/src/pages/accessibility.js +0 -157
  134. package/docs/src/pages/actions.js +0 -194
  135. package/docs/src/pages/auth.js +0 -176
  136. package/docs/src/pages/caching.js +0 -95
  137. package/docs/src/pages/components/accordion.js +0 -48
  138. package/docs/src/pages/components/alert.js +0 -35
  139. package/docs/src/pages/components/app-badge.js +0 -41
  140. package/docs/src/pages/components/avatar.js +0 -35
  141. package/docs/src/pages/components/badge.js +0 -36
  142. package/docs/src/pages/components/breadcrumbs.js +0 -94
  143. package/docs/src/pages/components/button.js +0 -84
  144. package/docs/src/pages/components/card.js +0 -225
  145. package/docs/src/pages/components/charts.js +0 -283
  146. package/docs/src/pages/components/checkbox.js +0 -129
  147. package/docs/src/pages/components/cluster.js +0 -47
  148. package/docs/src/pages/components/container.js +0 -40
  149. package/docs/src/pages/components/cta.js +0 -54
  150. package/docs/src/pages/components/divider.js +0 -37
  151. package/docs/src/pages/components/empty.js +0 -36
  152. package/docs/src/pages/components/feature.js +0 -60
  153. package/docs/src/pages/components/fieldset.js +0 -65
  154. package/docs/src/pages/components/fileupload.js +0 -128
  155. package/docs/src/pages/components/footer.js +0 -58
  156. package/docs/src/pages/components/grid.js +0 -169
  157. package/docs/src/pages/components/heading.js +0 -107
  158. package/docs/src/pages/components/hero.js +0 -123
  159. package/docs/src/pages/components/icons.js +0 -410
  160. package/docs/src/pages/components/image.js +0 -71
  161. package/docs/src/pages/components/input.js +0 -51
  162. package/docs/src/pages/components/media.js +0 -51
  163. package/docs/src/pages/components/modal.js +0 -111
  164. package/docs/src/pages/components/nav.js +0 -136
  165. package/docs/src/pages/components/phone-frame.js +0 -74
  166. package/docs/src/pages/components/progress.js +0 -102
  167. package/docs/src/pages/components/prose.js +0 -111
  168. package/docs/src/pages/components/pullquote.js +0 -71
  169. package/docs/src/pages/components/radio.js +0 -194
  170. package/docs/src/pages/components/rating.js +0 -106
  171. package/docs/src/pages/components/search.js +0 -61
  172. package/docs/src/pages/components/section.js +0 -98
  173. package/docs/src/pages/components/segmented.js +0 -121
  174. package/docs/src/pages/components/select.js +0 -45
  175. package/docs/src/pages/components/slider.js +0 -115
  176. package/docs/src/pages/components/spinner.js +0 -73
  177. package/docs/src/pages/components/stack.js +0 -48
  178. package/docs/src/pages/components/stat.js +0 -55
  179. package/docs/src/pages/components/stepper.js +0 -66
  180. package/docs/src/pages/components/table.js +0 -45
  181. package/docs/src/pages/components/textarea.js +0 -31
  182. package/docs/src/pages/components/toggle.js +0 -84
  183. package/docs/src/pages/components/tooltip.js +0 -48
  184. package/docs/src/pages/components.js +0 -200
  185. package/docs/src/pages/config.js +0 -193
  186. package/docs/src/pages/constraints.js +0 -100
  187. package/docs/src/pages/deploy.js +0 -233
  188. package/docs/src/pages/error-pages.js +0 -99
  189. package/docs/src/pages/extending.js +0 -198
  190. package/docs/src/pages/faq.js +0 -99
  191. package/docs/src/pages/forms.js +0 -129
  192. package/docs/src/pages/getting-started.js +0 -147
  193. package/docs/src/pages/guard.js +0 -120
  194. package/docs/src/pages/home.js +0 -345
  195. package/docs/src/pages/how-it-works.js +0 -244
  196. package/docs/src/pages/hydration.js +0 -108
  197. package/docs/src/pages/images.js +0 -120
  198. package/docs/src/pages/markdown.js +0 -156
  199. package/docs/src/pages/meta.js +0 -193
  200. package/docs/src/pages/mutations.js +0 -107
  201. package/docs/src/pages/navigation.js +0 -85
  202. package/docs/src/pages/performance.js +0 -157
  203. package/docs/src/pages/persist.js +0 -89
  204. package/docs/src/pages/project-structure.js +0 -91
  205. package/docs/src/pages/prompt-examples.js +0 -186
  206. package/docs/src/pages/raw-responses.js +0 -122
  207. package/docs/src/pages/routing.js +0 -120
  208. package/docs/src/pages/server-api.js +0 -291
  209. package/docs/src/pages/server-data.js +0 -203
  210. package/docs/src/pages/slash-commands.js +0 -55
  211. package/docs/src/pages/spec.js +0 -207
  212. package/docs/src/pages/state.js +0 -102
  213. package/docs/src/pages/store.js +0 -203
  214. package/docs/src/pages/streaming.js +0 -107
  215. package/docs/src/pages/stripe.js +0 -194
  216. package/docs/src/pages/supabase.js +0 -324
  217. package/docs/src/pages/templates/andypixel.js +0 -260
  218. package/docs/src/pages/templates/blog-post.js +0 -226
  219. package/docs/src/pages/templates/bookworms.js +0 -227
  220. package/docs/src/pages/templates/coutumes.js +0 -237
  221. package/docs/src/pages/templates/journey.js +0 -186
  222. package/docs/src/pages/templates/mobile-app.js +0 -371
  223. package/docs/src/pages/templates/retrowheels.js +0 -355
  224. package/docs/src/pages/templates/shop.js +0 -268
  225. package/docs/src/pages/templates/sparkys.js +0 -264
  226. package/docs/src/pages/templates/suzukibento.js +0 -200
  227. package/docs/src/pages/templates/swish.js +0 -313
  228. package/docs/src/pages/templates.js +0 -534
  229. package/docs/src/pages/testing.js +0 -199
  230. package/docs/src/pages/validation.js +0 -138
  231. package/examples/.claude/commands/pulse-build.md +0 -7
  232. package/examples/.claude/commands/pulse-dev.md +0 -7
  233. package/examples/.claude/commands/pulse-start.md +0 -9
  234. package/examples/.claude/commands/pulse-stop.md +0 -5
  235. package/examples/.claude/commands/verify.md +0 -84
  236. package/examples/.claude/coverage-check.js +0 -196
  237. package/examples/.claude/pulse-checklist.md +0 -189
  238. package/examples/andypixel.css +0 -237
  239. package/examples/andypixel.js +0 -258
  240. package/examples/bookworms.css +0 -422
  241. package/examples/bookworms.js +0 -225
  242. package/examples/contact.js +0 -166
  243. package/examples/counter.js +0 -94
  244. package/examples/dev.server.js +0 -110
  245. package/examples/examples.test.js +0 -394
  246. package/examples/journey.css +0 -528
  247. package/examples/journey.js +0 -186
  248. package/examples/pricing.js +0 -262
  249. package/examples/products.js +0 -190
  250. package/examples/public/.pulse-ui-version +0 -1
  251. package/examples/public/pulse-ui.css +0 -3028
  252. package/examples/public/pulse-ui.js +0 -257
  253. package/examples/quiz.js +0 -208
  254. package/examples/retrowheels.css +0 -425
  255. package/examples/retrowheels.js +0 -353
  256. package/examples/shared.js +0 -84
  257. package/examples/suzukibento.css +0 -513
  258. package/examples/suzukibento.js +0 -200
  259. package/examples/swish.css +0 -394
  260. package/examples/swish.js +0 -311
  261. package/examples/todos.js +0 -162
  262. package/src/ui/fieldset.js +0 -37
  263. package/src/ui/phone-frame.js +0 -40
  264. package/src/ui/search.js +0 -78
  265. package/src/ui/segmented.js +0 -50
  266. package/src/ui/tooltip.js +0 -28
@@ -1,257 +0,0 @@
1
- /**
2
- * pulse-ui.js — Vanilla JS for interactive Pulse UI components
3
- *
4
- * Handles:
5
- * - Slider (.ui-slider — live fill + value output)
6
- * - File Upload (.ui-upload — drag-and-drop, click-to-open)
7
- * - Modal (data-modal-open, <dialog>, backdrop close)
8
- * - Nav (.ui-nav — mobile burger menu)
9
- * - Carousel (.ui-carousel — prev/next/dots)
10
- *
11
- * No dependencies. No build step required.
12
- * CSP-safe: no inline handlers — all behaviour via event delegation.
13
- * Include once per page: <script src="/pulse-ui.js"></script>
14
- */
15
-
16
- // ─── Slider ──────────────────────────────────────────────────────────────────
17
- // Update --slider-fill on .ui-field wrapper + optional live value output.
18
-
19
- document.addEventListener('input', (e) => {
20
- const el = e.target
21
- if (!el.classList.contains('ui-slider')) return
22
- const field = el.closest('.ui-field')
23
- if (!field) return
24
- const pct = ((el.value - el.min) / (el.max - el.min) * 100).toFixed(2) + '%'
25
- field.style.setProperty('--slider-fill', pct)
26
- const out = field.querySelector('.ui-slider-output')
27
- if (out) out.textContent = el.value
28
- })
29
-
30
- // ─── File Upload ─────────────────────────────────────────────────────────────
31
- // Prevent browser navigating to dropped file anywhere on the page.
32
- // Handle drag visual feedback + file assignment on drop.
33
- // Handle click-to-open and keyboard activation.
34
-
35
- document.addEventListener('dragover', (e) => {
36
- e.preventDefault()
37
- if (!(e.target instanceof Element)) return
38
- const zone = e.target.closest('.ui-upload:not(.ui-upload--disabled)')
39
- if (zone) zone.classList.add('ui-upload--active')
40
- })
41
-
42
- document.addEventListener('dragleave', (e) => {
43
- if (!(e.target instanceof Element)) return
44
- const zone = e.target.closest('.ui-upload')
45
- if (zone && !zone.contains(e.relatedTarget)) {
46
- zone.classList.remove('ui-upload--active')
47
- }
48
- })
49
-
50
- document.addEventListener('drop', (e) => {
51
- e.preventDefault()
52
- if (!(e.target instanceof Element)) return
53
- const zone = e.target.closest('.ui-upload:not(.ui-upload--disabled)')
54
- if (!zone) return
55
- zone.classList.remove('ui-upload--active')
56
- const input = zone.querySelector('.ui-upload-input')
57
- if (!input || !e.dataTransfer.files.length) return
58
- const dt = new DataTransfer()
59
- Array.from(e.dataTransfer.files).forEach(f => dt.items.add(f))
60
- input.files = dt.files
61
- input.dispatchEvent(new Event('change', { bubbles: true }))
62
- })
63
-
64
- // File Upload: show selected filename in zone after file chosen
65
- document.addEventListener('change', (e) => {
66
- const input = e.target
67
- if (!input.classList.contains('ui-upload-input')) return
68
- const zone = input.closest('.ui-upload')
69
- if (!zone) return
70
- const textEl = zone.querySelector('.ui-upload-text')
71
- if (!textEl) return
72
- if (input.files && input.files.length > 0) {
73
- const names = Array.from(input.files).map(f => f.name).join(', ')
74
- textEl.textContent = names
75
- zone.classList.add('ui-upload--selected')
76
- } else {
77
- textEl.innerHTML = 'Drag &amp; drop or <span class="ui-upload-browse">browse</span>'
78
- zone.classList.remove('ui-upload--selected')
79
- }
80
- })
81
-
82
- // ─── Modal ──────────────────────────────────────────────────────────────────
83
-
84
- document.addEventListener('click', (e) => {
85
- // File upload: click zone → open file picker
86
- // Skip synthetic clicks emitted by input.click() itself to avoid loops
87
- if (!e.target.classList.contains('ui-upload-input')) {
88
- const zone = e.target.closest('.ui-upload')
89
- if (zone) {
90
- const input = zone.querySelector('.ui-upload-input')
91
- if (input && !input.disabled) { input.click(); return }
92
- }
93
- }
94
-
95
- // Dialog open: data-dialog-open="dialogId" (first-class) or data-modal-open="dialogId" (compat)
96
- const trigger = e.target.closest('[data-dialog-open],[data-modal-open]')
97
- if (trigger) {
98
- const id = trigger.dataset.dialogOpen ?? trigger.dataset.modalOpen
99
- const dialog = document.getElementById(id)
100
- if (dialog && typeof dialog.showModal === 'function') {
101
- dialog.showModal()
102
- }
103
- return
104
- }
105
-
106
- // Dialog close: data-dialog-close (closes nearest ancestor <dialog>)
107
- const closeTarget = e.target.closest('[data-dialog-close]')
108
- if (closeTarget) {
109
- const dialog = closeTarget.closest('dialog')
110
- if (dialog) dialog.close()
111
- return
112
- }
113
-
114
- // Backdrop close — click lands on <dialog> element itself
115
- if (e.target.tagName === 'DIALOG') {
116
- e.target.close()
117
- }
118
- })
119
-
120
- // File upload: Enter / Space on focused zone → open picker
121
- document.addEventListener('keydown', (e) => {
122
- if (e.key !== 'Enter' && e.key !== ' ') return
123
- const zone = e.target.closest('.ui-upload')
124
- if (!zone) return
125
- const input = zone.querySelector('.ui-upload-input')
126
- if (input && !input.disabled) { e.preventDefault(); input.click() }
127
- })
128
-
129
- // ─── Nav (mobile burger) ────────────────────────────────────────────────────
130
-
131
- function initNav(el) {
132
- if (el.dataset.pulseNavInit) return
133
- el.dataset.pulseNavInit = '1'
134
-
135
- const burger = el.querySelector('.ui-nav-burger')
136
- const mobile = el.querySelector('.ui-nav-mobile')
137
-
138
- // Mobile burger
139
- if (burger && mobile) {
140
- const open = () => { el.classList.add('ui-nav--open'); burger.setAttribute('aria-expanded', 'true') }
141
- const close = () => { el.classList.remove('ui-nav--open'); burger.setAttribute('aria-expanded', 'false') }
142
- const toggle = () => el.classList.contains('ui-nav--open') ? close() : open()
143
-
144
- burger.addEventListener('click', toggle)
145
- document.addEventListener('keydown', (e) => { if (e.key === 'Escape') close() })
146
- document.addEventListener('click', (e) => { if (!el.contains(e.target)) close() })
147
- mobile.querySelectorAll('.ui-nav-link').forEach(a => a.addEventListener('click', close))
148
- }
149
-
150
- // Mega nav dropdowns
151
- el.querySelectorAll('.ui-nav-mega-wrap').forEach(wrap => {
152
- const trigger = wrap.querySelector('.ui-nav-mega-trigger')
153
- const panel = wrap.querySelector('.ui-nav-mega-panel')
154
- if (!trigger || !panel) return
155
-
156
- const openMega = () => { panel.hidden = false; wrap.classList.add('ui-nav-mega-wrap--open'); trigger.setAttribute('aria-expanded', 'true') }
157
- const closeMega = () => { panel.hidden = true; wrap.classList.remove('ui-nav-mega-wrap--open'); trigger.setAttribute('aria-expanded', 'false') }
158
- const toggleMega = () => panel.hidden ? openMega() : closeMega()
159
-
160
- trigger.addEventListener('click', (e) => { e.stopPropagation(); toggleMega() })
161
- document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeMega() })
162
- document.addEventListener('click', (e) => { if (!wrap.contains(e.target)) closeMega() })
163
- panel.querySelectorAll('.ui-nav-mega-item').forEach(a => a.addEventListener('click', closeMega))
164
- })
165
- }
166
-
167
- function runNavs() { document.querySelectorAll('.ui-nav').forEach(initNav) }
168
- document.readyState === 'loading'
169
- ? document.addEventListener('DOMContentLoaded', runNavs)
170
- : runNavs()
171
- document.addEventListener('pulse:navigate', runNavs)
172
-
173
- // ─── Carousel ───────────────────────────────────────────────────────────────
174
-
175
- function initCarousel(el) {
176
- if (el.dataset.pulseCarouselInit) return
177
- el.dataset.pulseCarouselInit = '1'
178
-
179
- const track = el.querySelector('.ui-carousel-track')
180
- const prev = el.querySelector('.ui-carousel-prev')
181
- const next = el.querySelector('.ui-carousel-next')
182
- const dots = Array.from(el.querySelectorAll('.ui-carousel-dot'))
183
- const slides = Array.from(el.querySelectorAll('.ui-carousel-slide'))
184
-
185
- if (!track || slides.length === 0) return
186
-
187
- let current = 0
188
-
189
- const updateArrows = () => {
190
- if (prev) prev.hidden = current === 0
191
- if (next) next.hidden = current === slides.length - 1
192
- }
193
-
194
- const goTo = (i) => {
195
- current = Math.max(0, Math.min(i, slides.length - 1))
196
- track.scrollTo({ left: slides[current].offsetLeft, behavior: 'smooth' })
197
- dots.forEach((d, j) => {
198
- const active = j === current
199
- d.classList.toggle('active', active)
200
- d.setAttribute('aria-selected', String(active))
201
- d.setAttribute('tabindex', active ? '0' : '-1')
202
- })
203
- updateArrows()
204
- }
205
-
206
- prev?.addEventListener('click', () => goTo(current - 1))
207
- next?.addEventListener('click', () => goTo(current + 1))
208
- dots.forEach((d, i) => d.addEventListener('click', () => goTo(i)))
209
-
210
- // Keyboard navigation — roving tabindex for tablist
211
- el.addEventListener('keydown', (e) => {
212
- if (!e.target.classList.contains('ui-carousel-dot')) return
213
- let next = null
214
- if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
215
- e.preventDefault()
216
- next = current > 0 ? current - 1 : slides.length - 1
217
- } else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
218
- e.preventDefault()
219
- next = current < slides.length - 1 ? current + 1 : 0
220
- } else if (e.key === 'Home') {
221
- e.preventDefault()
222
- next = 0
223
- } else if (e.key === 'End') {
224
- e.preventDefault()
225
- next = slides.length - 1
226
- }
227
- if (next !== null) {
228
- goTo(next)
229
- dots[next]?.focus()
230
- }
231
- })
232
-
233
- // Sync dot state and arrows when user swipes / scrolls
234
- track.addEventListener('scroll', () => {
235
- const trackLeft = track.getBoundingClientRect().left
236
- const idx = slides.findIndex((s) => Math.abs(s.getBoundingClientRect().left - trackLeft) < 10)
237
- if (idx !== -1 && idx !== current) {
238
- current = idx
239
- dots.forEach((d, j) => {
240
- const active = j === current
241
- d.classList.toggle('active', active)
242
- d.setAttribute('aria-selected', String(active))
243
- d.setAttribute('tabindex', active ? '0' : '-1')
244
- })
245
- updateArrows()
246
- }
247
- }, { passive: true })
248
- }
249
-
250
- // Initialise all carousels on the page
251
- const run = () => document.querySelectorAll('.ui-carousel').forEach(initCarousel)
252
- document.readyState === 'loading'
253
- ? document.addEventListener('DOMContentLoaded', run)
254
- : run()
255
-
256
- // Re-initialise after Pulse client-side navigation swaps the DOM
257
- document.addEventListener('pulse:navigate', run)
package/examples/quiz.js DELETED
@@ -1,208 +0,0 @@
1
- /**
2
- * Pulse — Quiz example
3
- *
4
- * Demonstrates:
5
- * - Multi-step state machine (intro → question → results)
6
- * - Pure mutation logic with no server data
7
- * - Computed view output from state
8
- * - Restart flow
9
- *
10
- * Run: node examples/dev.server.js → http://localhost:3001/quiz
11
- */
12
-
13
- import { button, badge, card, container, section, stack, cluster, progress, iconCheck, iconX } from '../src/ui/index.js'
14
- import { examplesNav } from './shared.js'
15
-
16
- export const QUESTIONS = [
17
- {
18
- id: 1,
19
- text: 'What does SSR stand for in web development?',
20
- options: ['Static Site Rendering', 'Server-Side Rendering', 'Stylesheet Rendering', 'Synchronous Script Runtime'],
21
- answer: 1,
22
- },
23
- {
24
- id: 2,
25
- text: 'Which HTTP method is typically used to submit a form?',
26
- options: ['GET', 'PUT', 'POST', 'PATCH'],
27
- answer: 2,
28
- },
29
- {
30
- id: 3,
31
- text: 'What does CLS measure in Core Web Vitals?',
32
- options: ['Cumulative Layout Shift', 'Content Loading Speed', 'CSS Load Score', 'Cached Layout State'],
33
- answer: 0,
34
- },
35
- {
36
- id: 4,
37
- text: 'Which of these is NOT a valid HTTP status code?',
38
- options: ['200 OK', '304 Not Modified', '418 I\'m a teapot', '512 Server Timeout'],
39
- answer: 3,
40
- },
41
- {
42
- id: 5,
43
- text: 'What does the "hydrate" property in a Pulse spec enable?',
44
- options: [
45
- 'Server-side data fetching',
46
- 'Client-side interactivity by importing the spec in the browser',
47
- 'Automatic CSS injection',
48
- 'Database connection pooling',
49
- ],
50
- answer: 1,
51
- },
52
- ]
53
-
54
- export function scoreLabel(score, total) {
55
- const pct = score / total
56
- if (pct === 1) return { label: 'Perfect score!', variant: 'success' }
57
- if (pct >= 0.8) return { label: 'Excellent!', variant: 'success' }
58
- if (pct >= 0.6) return { label: 'Good effort!', variant: 'info' }
59
- if (pct >= 0.4) return { label: 'Keep practising!', variant: 'warning' }
60
- return { label: 'Better luck next time.', variant: 'error' }
61
- }
62
-
63
- export default {
64
- route: '/quiz',
65
-
66
- meta: {
67
- title: 'Quiz — Pulse',
68
- description: 'A multiple-choice quiz built with Pulse. Demonstrates multi-step state machine and computed views.',
69
- styles: ['/pulse-ui.css'],
70
- theme: 'dark',
71
- },
72
-
73
- state: {
74
- phase: 'intro', // intro | question | results
75
- current: 0, // index into QUESTIONS
76
- answers: [], // [number] — chosen option index per question
77
- score: 0,
78
- },
79
-
80
- mutations: {
81
- start: () => ({
82
- phase: 'question',
83
- current: 0,
84
- answers: [],
85
- score: 0,
86
- }),
87
-
88
- answer: (state, e) => {
89
- const chosen = parseInt(e.target.closest('[data-option]')?.dataset.option, 10)
90
- if (isNaN(chosen)) return state
91
-
92
- const q = QUESTIONS[state.current]
93
- const correct = chosen === q.answer
94
- const answers = [...state.answers, chosen]
95
- const score = state.score + (correct ? 1 : 0)
96
- const last = state.current === QUESTIONS.length - 1
97
-
98
- return {
99
- answers,
100
- score,
101
- current: last ? state.current : state.current + 1,
102
- phase: last ? 'results' : 'question',
103
- }
104
- },
105
-
106
- restart: () => ({
107
- phase: 'intro',
108
- current: 0,
109
- answers: [],
110
- score: 0,
111
- }),
112
- },
113
-
114
- view: (state) => {
115
- const nav = examplesNav('<span>🧠 Quiz</span>', '/quiz')
116
-
117
- if (state.phase === 'intro') {
118
- return `
119
- ${nav}
120
- <main id="main-content">
121
- ${section({ eyebrow: `${QUESTIONS.length} questions · multiple choice`, title: 'Web Dev Quiz', level: 1, subtitle: 'Test your knowledge of web development fundamentals — HTTP, performance, and Pulse.', align: 'center', content:
122
- container({ size: 'sm', content:
123
- cluster({ justify: 'center', content:
124
- button({ label: 'Start quiz', size: 'lg', attrs: { 'data-event': 'start' } })
125
- })
126
- })
127
- })}
128
- </main>`
129
- }
130
-
131
- if (state.phase === 'question') {
132
- const q = QUESTIONS[state.current]
133
- const num = state.current + 1
134
- const pct = Math.round((num / QUESTIONS.length) * 100)
135
-
136
- return `
137
- ${nav}
138
- <main id="main-content">
139
- ${section({ content:
140
- container({ size: 'sm', content:
141
- card({ content: stack({ gap: 'lg', content: `
142
-
143
- ${cluster({ justify: 'between', content:
144
- `<span class="u-text-sm u-text-muted">Question ${num} of ${QUESTIONS.length}</span>` +
145
- badge({ label: `${state.score} correct`, variant: 'success' })
146
- })}
147
-
148
- ${progress({ value: pct, max: 100, label: `Question ${num} of ${QUESTIONS.length}` })}
149
-
150
- ${stack({ gap: 'md', content: `
151
- <p class="u-text-lg u-font-semibold" id="question-text">${q.text}</p>
152
- <ul role="list" aria-labelledby="question-text">
153
- ${q.options.map((opt, i) => `<li>${
154
- button({
155
- label: `${String.fromCharCode(65 + i)}. ${opt}`,
156
- variant: 'secondary',
157
- fullWidth: true,
158
- attrs: { 'data-event': 'answer', 'data-option': String(i) },
159
- })
160
- }</li>`).join('')}
161
- </ul>
162
- ` })}
163
-
164
- ` }) })
165
- })
166
- })}
167
- </main>`
168
- }
169
-
170
- // Results
171
- const { label, variant } = scoreLabel(state.score, QUESTIONS.length)
172
-
173
- const resultRows = QUESTIONS.map((q, i) => {
174
- const chosen = state.answers[i]
175
- const correct = chosen === q.answer
176
- return `<div class="u-flex u-items-start u-gap-3 u-p-3 u-border-b">
177
- <span class="${correct ? 'u-text-green' : 'u-text-red'}">${correct ? iconCheck({ size: 16 }) : iconX({ size: 16 })}</span>
178
- <div>
179
- <p class="u-text-sm">${q.text}</p>
180
- ${!correct ? `<p class="u-text-sm u-text-green u-mt-1">Correct: <strong>${q.options[q.answer]}</strong></p>` : ''}
181
- </div>
182
- </div>`
183
- }).join('')
184
-
185
- return `
186
- ${nav}
187
- <main id="main-content">
188
- ${section({ content:
189
- container({ size: 'sm', content:
190
- card({ content: stack({ gap: 'lg', align: 'center', content: `
191
-
192
- <div class="u-flex u-items-end u-justify-center u-gap-1" aria-label="Your score">
193
- <span class="u-text-4xl u-font-bold u-text-accent">${state.score}</span>
194
- <span class="u-text-xl u-text-muted">/ ${QUESTIONS.length}</span>
195
- </div>
196
-
197
- ${badge({ label, variant })}
198
-
199
- <div class="u-w-full u-border u-rounded-md u-overflow-hidden">${resultRows}</div>
200
-
201
- ${button({ label: 'Try again', variant: 'secondary', attrs: { 'data-event': 'restart' } })}
202
-
203
- ` }) })
204
- })
205
- })}
206
- </main>`
207
- },
208
- }