@invisibleloop/pulse 0.1.21

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 (344) hide show
  1. package/.claude/commands/build-page.md +59 -0
  2. package/.claude/commands/new-doc-page.md +45 -0
  3. package/.claude/commands/verify.md +52 -0
  4. package/.claude/pulse-checklist.md +111 -0
  5. package/.claude/settings.local.json +102 -0
  6. package/.github/workflows/ci.yml +22 -0
  7. package/.github/workflows/publish.yml +41 -0
  8. package/.pulse/load-reports/home/1773432711417.json +22 -0
  9. package/CLAUDE.md +383 -0
  10. package/README.md +95 -0
  11. package/docs/.claude/pulse-checklist.md +111 -0
  12. package/docs/public/.pulse-ui-version +1 -0
  13. package/docs/public/dist/accessibility.boot-5DVTARJU.js +115 -0
  14. package/docs/public/dist/actions.boot-P66HKQEM.js +164 -0
  15. package/docs/public/dist/auth.boot-IMAJAUPH.js +140 -0
  16. package/docs/public/dist/caching.boot-DVR6KDE7.js +53 -0
  17. package/docs/public/dist/components--accordion.boot-3HVKMNWC.js +11 -0
  18. package/docs/public/dist/components--alert.boot-GCEXOZAC.js +6 -0
  19. package/docs/public/dist/components--app-badge.boot-DVT3GCHJ.js +6 -0
  20. package/docs/public/dist/components--avatar.boot-PSW24EVA.js +5 -0
  21. package/docs/public/dist/components--badge.boot-TYDY2RMK.js +7 -0
  22. package/docs/public/dist/components--banner.boot-EI5PZSZK.js +7 -0
  23. package/docs/public/dist/components--breadcrumbs.boot-SMA2E2GO.js +34 -0
  24. package/docs/public/dist/components--button.boot-J54BQM2E.js +23 -0
  25. package/docs/public/dist/components--card.boot-PZGNDIB6.js +138 -0
  26. package/docs/public/dist/components--carousel.boot-TP6LPFZZ.js +12 -0
  27. package/docs/public/dist/components--charts.boot-2EOYQWKL.js +108 -0
  28. package/docs/public/dist/components--checkbox.boot-DS5BSL6T.js +54 -0
  29. package/docs/public/dist/components--cluster.boot-HHVIBBJG.js +9 -0
  30. package/docs/public/dist/components--code-window.boot-2GR2DV33.js +20 -0
  31. package/docs/public/dist/components--container.boot-7LOOGK2K.js +5 -0
  32. package/docs/public/dist/components--cta.boot-FSNZ5YRT.js +11 -0
  33. package/docs/public/dist/components--divider.boot-3NI2C3QG.js +6 -0
  34. package/docs/public/dist/components--empty.boot-YX2UR3PV.js +7 -0
  35. package/docs/public/dist/components--feature.boot-MUD7NSUO.js +13 -0
  36. package/docs/public/dist/components--fieldset.boot-J7BYHMKF.js +19 -0
  37. package/docs/public/dist/components--fileupload.boot-NIKVTTPD.js +52 -0
  38. package/docs/public/dist/components--footer.boot-EYUK5FRG.js +14 -0
  39. package/docs/public/dist/components--grid.boot-URDQVDDR.js +59 -0
  40. package/docs/public/dist/components--heading.boot-BPQKU43E.js +44 -0
  41. package/docs/public/dist/components--hero.boot-4RAPRGAB.js +17 -0
  42. package/docs/public/dist/components--icons.boot-ZITNU5JP.js +68 -0
  43. package/docs/public/dist/components--image.boot-XEEGHQZF.js +19 -0
  44. package/docs/public/dist/components--input.boot-SGASZG5K.js +7 -0
  45. package/docs/public/dist/components--list.boot-W3XC5MHD.js +55 -0
  46. package/docs/public/dist/components--media.boot-5VFIETZO.js +13 -0
  47. package/docs/public/dist/components--modal.boot-RZUYXBN2.js +47 -0
  48. package/docs/public/dist/components--nav.boot-ODBOHU7O.js +33 -0
  49. package/docs/public/dist/components--pricing.boot-4AQ4ZVBY.js +21 -0
  50. package/docs/public/dist/components--progress.boot-GHAGYZOK.js +30 -0
  51. package/docs/public/dist/components--prose.boot-QANJL6JI.js +67 -0
  52. package/docs/public/dist/components--pullquote.boot-Q2WMNAZU.js +22 -0
  53. package/docs/public/dist/components--radio.boot-TJRDQ2OL.js +75 -0
  54. package/docs/public/dist/components--rating.boot-QBAN6DEL.js +38 -0
  55. package/docs/public/dist/components--search.boot-PXH5O5AG.js +17 -0
  56. package/docs/public/dist/components--section.boot-AQGIYHWW.js +12 -0
  57. package/docs/public/dist/components--segmented.boot-BEVTKEJO.js +33 -0
  58. package/docs/public/dist/components--select.boot-47X5RHOC.js +10 -0
  59. package/docs/public/dist/components--slider.boot-PSRRX7XL.js +47 -0
  60. package/docs/public/dist/components--spinner.boot-MZ5MO2OH.js +22 -0
  61. package/docs/public/dist/components--stack.boot-DI4NJXBF.js +9 -0
  62. package/docs/public/dist/components--stat.boot-QMFUWBQT.js +9 -0
  63. package/docs/public/dist/components--stepper.boot-34PP2NEV.js +22 -0
  64. package/docs/public/dist/components--table.boot-FCQGSFIQ.js +11 -0
  65. package/docs/public/dist/components--testimonial.boot-DWQPDKYG.js +11 -0
  66. package/docs/public/dist/components--textarea.boot-QVXLBOJ5.js +4 -0
  67. package/docs/public/dist/components--timeline.boot-26LN52P2.js +95 -0
  68. package/docs/public/dist/components--toggle.boot-IQQEI76S.js +29 -0
  69. package/docs/public/dist/components--tooltip.boot-LGHCO6NN.js +9 -0
  70. package/docs/public/dist/components.boot-SE6PQ4P7.js +103 -0
  71. package/docs/public/dist/config.boot-DTRRWUE6.js +126 -0
  72. package/docs/public/dist/constraints.boot-DUHDZBMC.js +71 -0
  73. package/docs/public/dist/deploy.boot-SLAD3NI2.js +163 -0
  74. package/docs/public/dist/docs-8e3d4b5c.css +1 -0
  75. package/docs/public/dist/extending.boot-UA3CN243.js +159 -0
  76. package/docs/public/dist/faq.boot-6EQAWLQR.js +43 -0
  77. package/docs/public/dist/getting-started.boot-TDKIFL5U.js +86 -0
  78. package/docs/public/dist/guard.boot-AUHAWTG4.js +80 -0
  79. package/docs/public/dist/home.boot-BVQXRH32.js +383 -0
  80. package/docs/public/dist/how-it-works.boot-LTWAKWKW.js +104 -0
  81. package/docs/public/dist/hydration.boot-JRM6IPJL.js +78 -0
  82. package/docs/public/dist/images.boot-M6ZVKTZS.js +80 -0
  83. package/docs/public/dist/manifest.json +94 -0
  84. package/docs/public/dist/meta.boot-7NXGPHR4.js +79 -0
  85. package/docs/public/dist/mutations.boot-F6F43UDX.js +79 -0
  86. package/docs/public/dist/navigation.boot-AOXWS3ZF.js +57 -0
  87. package/docs/public/dist/performance.boot-C3UPCOBK.js +98 -0
  88. package/docs/public/dist/persist.boot-WT32PQOQ.js +61 -0
  89. package/docs/public/dist/project-structure.boot-FB3LRVJ4.js +63 -0
  90. package/docs/public/dist/prompt-examples.boot-YKR4VDK4.js +31 -0
  91. package/docs/public/dist/pulse-ui-81a85c03.css +1 -0
  92. package/docs/public/dist/raw-responses.boot-M4KA5YXL.js +104 -0
  93. package/docs/public/dist/routing.boot-FNX5FDGH.js +70 -0
  94. package/docs/public/dist/runtime-B73WLANC.js +1 -0
  95. package/docs/public/dist/runtime-KO4BHUQ3.js +49 -0
  96. package/docs/public/dist/runtime-L2HNXIHW.js +59 -0
  97. package/docs/public/dist/runtime-QFURDKA2.js +5 -0
  98. package/docs/public/dist/runtime-UVPXO4IR.js +375 -0
  99. package/docs/public/dist/runtime-VMJA3Z4N.js +10 -0
  100. package/docs/public/dist/runtime-ZJ4FXT5O.js +11 -0
  101. package/docs/public/dist/server-api.boot-K7X3LCFB.js +219 -0
  102. package/docs/public/dist/server-data.boot-Y7HQYC4R.js +157 -0
  103. package/docs/public/dist/slash-commands.boot-V2UV7OW2.js +26 -0
  104. package/docs/public/dist/spec.boot-2WU7ZHCV.js +159 -0
  105. package/docs/public/dist/state.boot-B24GUE3R.js +73 -0
  106. package/docs/public/dist/store.boot-TLIB4XHH.js +150 -0
  107. package/docs/public/dist/streaming.boot-W2DZSMW4.js +80 -0
  108. package/docs/public/dist/stripe.boot-QN3C2GEL.js +164 -0
  109. package/docs/public/dist/supabase.boot-BG4XXLZE.js +303 -0
  110. package/docs/public/dist/testing.boot-6U4WKMTE.js +130 -0
  111. package/docs/public/dist/validation.boot-PQHYGW5B.js +100 -0
  112. package/docs/public/docs.css +2020 -0
  113. package/docs/public/menu.js +83 -0
  114. package/docs/public/pulse-ui.css +2739 -0
  115. package/docs/public/pulse-ui.js +236 -0
  116. package/docs/server.js +192 -0
  117. package/docs/src/lib/component-page.js +47 -0
  118. package/docs/src/lib/highlight.js +255 -0
  119. package/docs/src/lib/layout.js +131 -0
  120. package/docs/src/lib/metrics-store.js +6 -0
  121. package/docs/src/lib/nav.js +159 -0
  122. package/docs/src/lib/stats.js +81 -0
  123. package/docs/src/pages/accessibility.js +157 -0
  124. package/docs/src/pages/actions.js +191 -0
  125. package/docs/src/pages/auth.js +177 -0
  126. package/docs/src/pages/caching.js +95 -0
  127. package/docs/src/pages/components/accordion.js +48 -0
  128. package/docs/src/pages/components/alert.js +35 -0
  129. package/docs/src/pages/components/app-badge.js +41 -0
  130. package/docs/src/pages/components/avatar.js +35 -0
  131. package/docs/src/pages/components/badge.js +36 -0
  132. package/docs/src/pages/components/banner.js +45 -0
  133. package/docs/src/pages/components/breadcrumbs.js +94 -0
  134. package/docs/src/pages/components/button.js +84 -0
  135. package/docs/src/pages/components/card.js +225 -0
  136. package/docs/src/pages/components/carousel.js +72 -0
  137. package/docs/src/pages/components/charts.js +278 -0
  138. package/docs/src/pages/components/checkbox.js +129 -0
  139. package/docs/src/pages/components/cluster.js +47 -0
  140. package/docs/src/pages/components/code-window.js +57 -0
  141. package/docs/src/pages/components/container.js +40 -0
  142. package/docs/src/pages/components/cta.js +53 -0
  143. package/docs/src/pages/components/divider.js +37 -0
  144. package/docs/src/pages/components/empty.js +36 -0
  145. package/docs/src/pages/components/feature.js +60 -0
  146. package/docs/src/pages/components/fieldset.js +65 -0
  147. package/docs/src/pages/components/fileupload.js +127 -0
  148. package/docs/src/pages/components/footer.js +58 -0
  149. package/docs/src/pages/components/grid.js +165 -0
  150. package/docs/src/pages/components/heading.js +107 -0
  151. package/docs/src/pages/components/hero.js +65 -0
  152. package/docs/src/pages/components/icons.js +285 -0
  153. package/docs/src/pages/components/image.js +71 -0
  154. package/docs/src/pages/components/input.js +51 -0
  155. package/docs/src/pages/components/list.js +112 -0
  156. package/docs/src/pages/components/media.js +51 -0
  157. package/docs/src/pages/components/modal.js +111 -0
  158. package/docs/src/pages/components/nav.js +86 -0
  159. package/docs/src/pages/components/pricing.js +68 -0
  160. package/docs/src/pages/components/progress.js +102 -0
  161. package/docs/src/pages/components/prose.js +111 -0
  162. package/docs/src/pages/components/pullquote.js +71 -0
  163. package/docs/src/pages/components/radio.js +194 -0
  164. package/docs/src/pages/components/rating.js +106 -0
  165. package/docs/src/pages/components/search.js +61 -0
  166. package/docs/src/pages/components/section.js +59 -0
  167. package/docs/src/pages/components/segmented.js +121 -0
  168. package/docs/src/pages/components/select.js +45 -0
  169. package/docs/src/pages/components/slider.js +114 -0
  170. package/docs/src/pages/components/spinner.js +73 -0
  171. package/docs/src/pages/components/stack.js +48 -0
  172. package/docs/src/pages/components/stat.js +55 -0
  173. package/docs/src/pages/components/stepper.js +66 -0
  174. package/docs/src/pages/components/table.js +45 -0
  175. package/docs/src/pages/components/testimonial.js +49 -0
  176. package/docs/src/pages/components/textarea.js +31 -0
  177. package/docs/src/pages/components/timeline.js +227 -0
  178. package/docs/src/pages/components/toggle.js +84 -0
  179. package/docs/src/pages/components/tooltip.js +48 -0
  180. package/docs/src/pages/components.js +204 -0
  181. package/docs/src/pages/config.js +193 -0
  182. package/docs/src/pages/constraints.js +99 -0
  183. package/docs/src/pages/deploy.js +233 -0
  184. package/docs/src/pages/extending.js +198 -0
  185. package/docs/src/pages/faq.js +96 -0
  186. package/docs/src/pages/getting-started.js +106 -0
  187. package/docs/src/pages/guard.js +121 -0
  188. package/docs/src/pages/home.js +401 -0
  189. package/docs/src/pages/how-it-works.js +183 -0
  190. package/docs/src/pages/hydration.js +98 -0
  191. package/docs/src/pages/images.js +121 -0
  192. package/docs/src/pages/meta.js +120 -0
  193. package/docs/src/pages/mutations.js +106 -0
  194. package/docs/src/pages/navigation.js +85 -0
  195. package/docs/src/pages/performance.js +157 -0
  196. package/docs/src/pages/persist.js +88 -0
  197. package/docs/src/pages/project-structure.js +90 -0
  198. package/docs/src/pages/prompt-examples.js +186 -0
  199. package/docs/src/pages/raw-responses.js +124 -0
  200. package/docs/src/pages/routing.js +99 -0
  201. package/docs/src/pages/server-api.js +281 -0
  202. package/docs/src/pages/server-data.js +185 -0
  203. package/docs/src/pages/slash-commands.js +55 -0
  204. package/docs/src/pages/spec.js +207 -0
  205. package/docs/src/pages/state.js +101 -0
  206. package/docs/src/pages/store.js +181 -0
  207. package/docs/src/pages/streaming.js +108 -0
  208. package/docs/src/pages/stripe.js +193 -0
  209. package/docs/src/pages/supabase.js +323 -0
  210. package/docs/src/pages/testing.js +198 -0
  211. package/docs/src/pages/validation.js +138 -0
  212. package/examples/contact.js +166 -0
  213. package/examples/counter.js +94 -0
  214. package/examples/dev.server.js +91 -0
  215. package/examples/examples.test.js +394 -0
  216. package/examples/pricing.js +244 -0
  217. package/examples/products.js +191 -0
  218. package/examples/quiz.js +208 -0
  219. package/examples/shared.js +78 -0
  220. package/examples/todos.js +162 -0
  221. package/package.json +75 -0
  222. package/public/.pulse-ui-version +1 -0
  223. package/public/chippy-bird.css +246 -0
  224. package/public/examples/contact.css +119 -0
  225. package/public/examples/counter.css +79 -0
  226. package/public/examples/pricing.css +132 -0
  227. package/public/examples/products.css +100 -0
  228. package/public/examples/quiz.css +200 -0
  229. package/public/examples/todos.css +137 -0
  230. package/public/favicon.ico +0 -0
  231. package/public/log-dashboard.css +383 -0
  232. package/public/pulse-ui.css +2740 -0
  233. package/public/pulse-ui.js +236 -0
  234. package/public/pulse.css +149 -0
  235. package/scripts/build.js +411 -0
  236. package/src/agent/checklist.md +111 -0
  237. package/src/agent/coverage-check.js +66 -0
  238. package/src/agent/guide-components.md +274 -0
  239. package/src/agent/guide-examples.md +54 -0
  240. package/src/agent/guide-routing.md +36 -0
  241. package/src/agent/guide-server.md +258 -0
  242. package/src/agent/guide-spec.md +103 -0
  243. package/src/agent/guide-styles.md +191 -0
  244. package/src/agent/guide.md +979 -0
  245. package/src/agent/identity.md +106 -0
  246. package/src/agent/workflow.md +108 -0
  247. package/src/cli/cli.test.js +82 -0
  248. package/src/cli/dev.js +195 -0
  249. package/src/cli/discover.js +113 -0
  250. package/src/cli/index.js +361 -0
  251. package/src/cli/load-report.js +91 -0
  252. package/src/cli/load-runner.js +121 -0
  253. package/src/cli/report-server.js +723 -0
  254. package/src/cli/report.js +116 -0
  255. package/src/cli/scaffold.archive.js +1371 -0
  256. package/src/cli/scaffold.js +349 -0
  257. package/src/cli/start.js +74 -0
  258. package/src/html.js +19 -0
  259. package/src/mcp/server.js +884 -0
  260. package/src/mcp/validate-worker.js +110 -0
  261. package/src/runtime/image.js +74 -0
  262. package/src/runtime/image.test.js +111 -0
  263. package/src/runtime/index.js +621 -0
  264. package/src/runtime/navigate.js +146 -0
  265. package/src/runtime/runtime.test.js +773 -0
  266. package/src/runtime/ssr.js +464 -0
  267. package/src/runtime/ssr.test.js +421 -0
  268. package/src/runtime/store.js +92 -0
  269. package/src/runtime/toast.js +163 -0
  270. package/src/server/index.js +1386 -0
  271. package/src/server/server.test.js +1248 -0
  272. package/src/spec/schema.js +428 -0
  273. package/src/spec/schema.test.js +291 -0
  274. package/src/store/index.js +102 -0
  275. package/src/store/store.test.js +210 -0
  276. package/src/testing/html.js +283 -0
  277. package/src/testing/index.js +249 -0
  278. package/src/testing/testing.test.js +450 -0
  279. package/src/ui/accordion.js +28 -0
  280. package/src/ui/alert.js +43 -0
  281. package/src/ui/app-badge.js +48 -0
  282. package/src/ui/avatar.js +47 -0
  283. package/src/ui/badge.js +24 -0
  284. package/src/ui/banner.js +26 -0
  285. package/src/ui/breadcrumbs.js +38 -0
  286. package/src/ui/button.js +66 -0
  287. package/src/ui/card.js +34 -0
  288. package/src/ui/carousel.js +59 -0
  289. package/src/ui/charts.js +321 -0
  290. package/src/ui/checkbox.js +65 -0
  291. package/src/ui/cluster.js +44 -0
  292. package/src/ui/code-window.js +39 -0
  293. package/src/ui/container.js +24 -0
  294. package/src/ui/cta.js +37 -0
  295. package/src/ui/divider.js +29 -0
  296. package/src/ui/empty.js +33 -0
  297. package/src/ui/feature.js +33 -0
  298. package/src/ui/fieldset.js +37 -0
  299. package/src/ui/fileupload.js +89 -0
  300. package/src/ui/footer.js +38 -0
  301. package/src/ui/grid.js +36 -0
  302. package/src/ui/heading.js +45 -0
  303. package/src/ui/hero.js +37 -0
  304. package/src/ui/icons.js +161 -0
  305. package/src/ui/index.js +89 -0
  306. package/src/ui/input.js +74 -0
  307. package/src/ui/list.js +36 -0
  308. package/src/ui/media.js +44 -0
  309. package/src/ui/modal.js +80 -0
  310. package/src/ui/nav.js +61 -0
  311. package/src/ui/pricing.js +56 -0
  312. package/src/ui/progress.js +62 -0
  313. package/src/ui/prose.js +29 -0
  314. package/src/ui/pullquote.js +34 -0
  315. package/src/ui/radio.js +102 -0
  316. package/src/ui/rating.js +93 -0
  317. package/src/ui/search.js +77 -0
  318. package/src/ui/section.js +69 -0
  319. package/src/ui/segmented.js +50 -0
  320. package/src/ui/select.js +77 -0
  321. package/src/ui/slider.js +84 -0
  322. package/src/ui/spinner.js +34 -0
  323. package/src/ui/stack.js +36 -0
  324. package/src/ui/stat.js +52 -0
  325. package/src/ui/stepper.js +46 -0
  326. package/src/ui/switch.js +57 -0
  327. package/src/ui/table.js +45 -0
  328. package/src/ui/testimonial.js +48 -0
  329. package/src/ui/textarea.js +72 -0
  330. package/src/ui/timeline.js +72 -0
  331. package/src/ui/tooltip.js +28 -0
  332. package/src/ui/ui.test.js +1241 -0
  333. package/src/ui/uiimage.js +65 -0
  334. package/tsconfig.json +13 -0
  335. package/types/html.d.ts +17 -0
  336. package/types/image.d.ts +70 -0
  337. package/types/index.d.ts +7 -0
  338. package/types/navigate.d.ts +38 -0
  339. package/types/runtime.d.ts +63 -0
  340. package/types/schema.d.ts +243 -0
  341. package/types/server.d.ts +145 -0
  342. package/types/ssr.d.ts +110 -0
  343. package/types/testing.d.ts +154 -0
  344. package/types/ui.d.ts +704 -0
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * validate-worker.js
4
+ *
5
+ * Runs spec validation in an isolated child process so a hanging import
6
+ * cannot block the MCP server. Called by validateContent() in server.js.
7
+ *
8
+ * Usage: node validate-worker.js <tmpFilePath>
9
+ * Exits with stdout containing the validation result text.
10
+ */
11
+
12
+ import { validateSpec } from '../spec/schema.js'
13
+
14
+ const tmpFile = process.argv[2]
15
+ if (!tmpFile) { process.stdout.write('Invalid: no file path provided'); process.exit(0) }
16
+
17
+ let mod
18
+ try {
19
+ mod = await import(tmpFile)
20
+ } catch (err) {
21
+ process.stdout.write(`Invalid: could not parse — ${err.message}`)
22
+ process.exit(0)
23
+ }
24
+
25
+ const spec = mod.default
26
+ if (!spec || typeof spec !== 'object') {
27
+ process.stdout.write('Invalid: file must export a default spec object')
28
+ process.exit(0)
29
+ }
30
+
31
+ const { valid, errors, warnings: schemaWarnings } = validateSpec(spec)
32
+ if (!valid) {
33
+ process.stdout.write('Invalid:\n' + errors.map(e => ` — ${e}`).join('\n'))
34
+ process.exit(0)
35
+ }
36
+
37
+ // Render the view and run additional HTML checks
38
+ const warnings = [...schemaWarnings]
39
+ try {
40
+ // Pass null for every declared server key so views that check `=== null`
41
+ // degrade gracefully (showing error branches) rather than throwing on
42
+ // `undefined.someProperty` when the real fetchers haven't run.
43
+ const serverMock = spec.server && typeof spec.server === 'object'
44
+ ? Object.fromEntries(Object.keys(spec.server).map(k => [k, null]))
45
+ : {}
46
+ const html = typeof spec.view === 'function'
47
+ ? spec.view(spec.state || {}, serverMock)
48
+ : ''
49
+
50
+ // Heading hierarchy — headings must not skip levels
51
+ const levels = [...html.matchAll(/<h([1-6])[\s>]/gi)].map(m => parseInt(m[1], 10))
52
+ for (let i = 1; i < levels.length; i++) {
53
+ if (levels[i] > levels[i - 1] + 1) {
54
+ warnings.push(`Heading order: h${levels[i - 1]} → h${levels[i]} skips a level (Lighthouse will flag this)`)
55
+ }
56
+ }
57
+ if (levels.length > 0 && levels[0] !== 1) {
58
+ warnings.push(`Heading order: page starts with h${levels[0]}, expected h1`)
59
+ }
60
+
61
+ // Missing main landmark
62
+ if (!/id=["']main-content["']/.test(html)) {
63
+ warnings.push('Missing <main id="main-content"> — required for accessibility and Lighthouse landmark audit')
64
+ }
65
+
66
+ // aria-live + aria-label on the same element — aria-label suppresses live region announcements
67
+ const tagPattern = /<[a-z][^>]*>/gi
68
+ let tagMatch
69
+ while ((tagMatch = tagPattern.exec(html)) !== null) {
70
+ const tag = tagMatch[0]
71
+ if (/aria-live/i.test(tag) && /aria-label/i.test(tag)) {
72
+ warnings.push('aria-live and aria-label on the same element — aria-label suppresses live region announcements in some screen readers. Remove aria-label and let the content speak for itself')
73
+ break
74
+ }
75
+ }
76
+
77
+ // data-event on text inputs — causes focus loss on every keystroke
78
+ const inputTags = [...html.matchAll(/<input[^>]+>/gi)].map(m => m[0])
79
+ for (const tag of inputTags) {
80
+ const isText = !/type=["'](checkbox|radio|range|color|submit|button|reset|file|hidden)/i.test(tag)
81
+ if (isText && /data-event/i.test(tag)) {
82
+ warnings.push('data-event on a text input causes focus loss on every keystroke — use data-action on the wrapping <form> and read values from FormData in action.onStart instead')
83
+ break
84
+ }
85
+ }
86
+
87
+ // Unknown u-* utility classes — catch invented classes that have no CSS definition
88
+ const KNOWN_UTILS = new Set(['u-absolute','u-bg-accent','u-bg-surface','u-bg-surface2','u-block','u-border','u-border-b','u-border-t','u-flex','u-flex-1','u-flex-col','u-flex-wrap','u-font-bold','u-font-medium','u-font-normal','u-font-semibold','u-gap-1','u-gap-2','u-gap-3','u-gap-4','u-gap-5','u-gap-6','u-gap-8','u-hidden','u-inline','u-inline-block','u-items-center','u-items-end','u-items-start','u-items-stretch','u-justify-between','u-justify-center','u-justify-end','u-justify-start','u-leading-loose','u-leading-normal','u-leading-relaxed','u-leading-snug','u-leading-tight','u-max-w-lg','u-max-w-md','u-max-w-prose','u-max-w-sm','u-max-w-xl','u-max-w-xs','u-mb-0','u-mb-1','u-mb-10','u-mb-12','u-mb-16','u-mb-2','u-mb-3','u-mb-4','u-mb-5','u-mb-6','u-mb-8','u-ml-auto','u-mr-auto','u-mt-0','u-mt-1','u-mt-10','u-mt-12','u-mt-16','u-mt-2','u-mt-3','u-mt-4','u-mt-5','u-mt-6','u-mt-8','u-mx-auto','u-opacity-50','u-opacity-75','u-overflow-auto','u-overflow-hidden','u-p-0','u-p-1','u-p-2','u-p-3','u-p-4','u-p-5','u-p-6','u-p-8','u-px-0','u-px-2','u-px-3','u-px-4','u-px-5','u-px-6','u-px-8','u-py-0','u-py-2','u-py-3','u-py-4','u-py-5','u-py-6','u-py-8','u-relative','u-rounded','u-rounded-full','u-rounded-lg','u-rounded-md','u-rounded-xl','u-shrink-0','u-text-2xl','u-text-3xl','u-text-4xl','u-text-accent','u-text-balance','u-text-base','u-text-blue','u-text-center','u-text-default','u-text-green','u-text-left','u-text-lg','u-text-muted','u-text-red','u-text-right','u-text-sm','u-text-xl','u-text-xs','u-text-yellow','u-w-auto','u-w-full'])
89
+ const usedUtils = new Set([...html.matchAll(/\bu-([\w-]+)/g)].map(m => 'u-' + m[1]))
90
+ const unknownUtils = [...usedUtils].filter(c => !KNOWN_UTILS.has(c))
91
+ if (unknownUtils.length) {
92
+ warnings.push(`Unknown utility class${unknownUtils.length > 1 ? 'es' : ''}: ${unknownUtils.join(', ')} — these have no CSS definition and will have no effect. Check the guide for the available u-* classes, or use layout components (container, section, grid, stack, cluster) instead.`)
93
+ }
94
+
95
+ // input[type="file"] used directly — fileUpload() component should be used instead
96
+ if (inputTags.some(tag => /type=["']file["']/i.test(tag))) {
97
+ warnings.push('input[type="file"] detected — use the fileUpload() component instead. It provides the drag-and-drop zone, correct styling, and pulse-ui.js integration. import { fileUpload } from \'@invisibleloop/pulse/ui\'')
98
+ }
99
+
100
+ // Unescaped apostrophes in attribute values
101
+ if (/=\s*'[^']*'[^']*'/.test(html)) {
102
+ warnings.push('Possible unescaped apostrophe in an HTML attribute — use &apos; or &#39; or switch to double quotes')
103
+ }
104
+ } catch { /* view may depend on server data — skip HTML checks */ }
105
+
106
+ if (warnings.length > 0) {
107
+ process.stdout.write('Valid ✓ — but fix these issues:\n' + warnings.map(w => ` ⚠ ${w}`).join('\n'))
108
+ } else {
109
+ process.stdout.write('Valid ✓')
110
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Pulse — Image helpers
3
+ *
4
+ * Generates optimised image markup with correct attributes for CLS and LCP.
5
+ * Import in page specs or components:
6
+ * import { img, picture } from '/@pulse/runtime/image.js' (dev)
7
+ * import { img, picture } from '@invisibleloop/pulse/image' (production)
8
+ */
9
+
10
+ function esc(str) {
11
+ return String(str)
12
+ .replace(/&/g, '&amp;')
13
+ .replace(/"/g, '&quot;')
14
+ .replace(/</g, '&lt;')
15
+ .replace(/>/g, '&gt;')
16
+ }
17
+
18
+ /**
19
+ * Generate an optimised <img> tag.
20
+ *
21
+ * Always include width + height to prevent CLS.
22
+ * Use priority: true for the LCP image (above the fold hero).
23
+ *
24
+ * @param {Object} options
25
+ * @param {string} options.src - Image URL
26
+ * @param {string} options.alt - Alt text (required for accessibility)
27
+ * @param {number} [options.width] - Intrinsic width in px — prevents CLS
28
+ * @param {number} [options.height] - Intrinsic height in px — prevents CLS
29
+ * @param {boolean} [options.priority] - true for LCP image: eager + high fetchpriority
30
+ * @param {string} [options.class] - CSS class
31
+ * @returns {string}
32
+ */
33
+ export function img({ src, alt, width, height, priority = false, class: cls }) {
34
+ const attrs = [
35
+ `src="${esc(src)}"`,
36
+ `alt="${esc(alt)}"`,
37
+ width != null ? `width="${width}"` : '',
38
+ height != null ? `height="${height}"` : '',
39
+ `loading="${priority ? 'eager' : 'lazy'}"`,
40
+ `decoding="async"`,
41
+ priority ? 'fetchpriority="high"' : '',
42
+ cls ? `class="${esc(cls)}"` : '',
43
+ ].filter(Boolean).join(' ')
44
+
45
+ return `<img ${attrs}>`
46
+ }
47
+
48
+ /**
49
+ * Generate a <picture> element with modern format sources + fallback <img>.
50
+ *
51
+ * Provide sources in preference order (AVIF first, then WebP, then fallback src).
52
+ * The fallback src on the <img> is the universal baseline (JPEG/PNG).
53
+ *
54
+ * @param {Object} options
55
+ * @param {string} options.src - Fallback image URL (JPEG/PNG)
56
+ * @param {string} options.alt - Alt text
57
+ * @param {number} [options.width] - Intrinsic width — prevents CLS
58
+ * @param {number} [options.height] - Intrinsic height — prevents CLS
59
+ * @param {boolean} [options.priority] - true for LCP image
60
+ * @param {string} [options.class] - CSS class applied to <img>
61
+ * @param {Array<{src: string, type: string}>} [options.sources]
62
+ * Modern format sources, e.g.:
63
+ * [{ src: '/hero.avif', type: 'image/avif' }, { src: '/hero.webp', type: 'image/webp' }]
64
+ * @returns {string}
65
+ */
66
+ export function picture({ src, alt, width, height, priority = false, class: cls, sources = [] }) {
67
+ const sourceEls = sources
68
+ .map(s => `<source srcset="${esc(s.src)}" type="${esc(s.type)}">`)
69
+ .join('\n ')
70
+
71
+ const imgEl = img({ src, alt, width, height, priority, class: cls })
72
+
73
+ return `<picture>\n ${sourceEls}\n ${imgEl}\n</picture>`
74
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Pulse — Image helper tests
3
+ * run: node src/runtime/image.test.js
4
+ */
5
+
6
+ import { img, picture } from './image.js'
7
+
8
+ let passed = 0
9
+ let failed = 0
10
+
11
+ function test(label, fn) {
12
+ try {
13
+ fn()
14
+ console.log(` ✓ ${label}`)
15
+ passed++
16
+ } catch (e) {
17
+ console.log(` ✗ ${label}`)
18
+ console.log(` ${e.message}`)
19
+ failed++
20
+ }
21
+ }
22
+
23
+ function assert(condition, msg) {
24
+ if (!condition) throw new Error(msg || 'Assertion failed')
25
+ }
26
+
27
+ // ---------------------------------------------------------------------------
28
+
29
+ console.log('\nimg()\n')
30
+
31
+ test('renders src and alt', () => {
32
+ const out = img({ src: '/photo.jpg', alt: 'A photo' })
33
+ assert(out.includes('src="/photo.jpg"'), `Missing src: ${out}`)
34
+ assert(out.includes('alt="A photo"'), `Missing alt: ${out}`)
35
+ })
36
+
37
+ test('renders width and height', () => {
38
+ const out = img({ src: '/x.jpg', alt: '', width: 800, height: 600 })
39
+ assert(out.includes('width="800"'), `Missing width: ${out}`)
40
+ assert(out.includes('height="600"'), `Missing height: ${out}`)
41
+ })
42
+
43
+ test('defaults to lazy loading', () => {
44
+ const out = img({ src: '/x.jpg', alt: '' })
45
+ assert(out.includes('loading="lazy"'), `Expected lazy: ${out}`)
46
+ assert(!out.includes('fetchpriority="high"'), `Should not have high priority: ${out}`)
47
+ })
48
+
49
+ test('priority sets eager loading and high fetchpriority', () => {
50
+ const out = img({ src: '/hero.jpg', alt: 'Hero', priority: true })
51
+ assert(out.includes('loading="eager"'), `Expected eager: ${out}`)
52
+ assert(out.includes('fetchpriority="high"'), `Expected high fetchpriority: ${out}`)
53
+ })
54
+
55
+ test('always includes decoding="async"', () => {
56
+ const out = img({ src: '/x.jpg', alt: '' })
57
+ assert(out.includes('decoding="async"'), `Missing decoding: ${out}`)
58
+ })
59
+
60
+ test('applies class attribute', () => {
61
+ const out = img({ src: '/x.jpg', alt: '', class: 'card-img' })
62
+ assert(out.includes('class="card-img"'), `Missing class: ${out}`)
63
+ })
64
+
65
+ test('escapes alt text', () => {
66
+ const out = img({ src: '/x.jpg', alt: 'A "quoted" & <tagged> image' })
67
+ assert(!out.includes('"quoted"'), `Should escape quotes in alt: ${out}`)
68
+ assert(out.includes('&amp;'), `Should escape & in alt: ${out}`)
69
+ })
70
+
71
+ // ---------------------------------------------------------------------------
72
+
73
+ console.log('\npicture()\n')
74
+
75
+ test('wraps img in picture element', () => {
76
+ const out = picture({ src: '/x.jpg', alt: 'test', width: 100, height: 100 })
77
+ assert(out.includes('<picture>'), `Missing <picture>: ${out}`)
78
+ assert(out.includes('</picture>'), `Missing </picture>: ${out}`)
79
+ assert(out.includes('<img '), `Missing <img>: ${out}`)
80
+ })
81
+
82
+ test('includes source elements for each format', () => {
83
+ const out = picture({
84
+ src: '/x.jpg', alt: '',
85
+ sources: [
86
+ { src: '/x.avif', type: 'image/avif' },
87
+ { src: '/x.webp', type: 'image/webp' },
88
+ ]
89
+ })
90
+ assert(out.includes('image/avif'), `Missing avif source: ${out}`)
91
+ assert(out.includes('image/webp'), `Missing webp source: ${out}`)
92
+ assert(out.includes('srcset="/x.avif"'), `Missing avif srcset: ${out}`)
93
+ })
94
+
95
+ test('sources appear before fallback img', () => {
96
+ const out = picture({
97
+ src: '/x.jpg', alt: '',
98
+ sources: [{ src: '/x.webp', type: 'image/webp' }]
99
+ })
100
+ assert(out.indexOf('<source') < out.indexOf('<img'), `source must precede img: ${out}`)
101
+ })
102
+
103
+ test('priority propagates to inner img', () => {
104
+ const out = picture({ src: '/x.jpg', alt: 'Hero', priority: true, sources: [] })
105
+ assert(out.includes('fetchpriority="high"'), `Expected high priority in picture: ${out}`)
106
+ })
107
+
108
+ // ---------------------------------------------------------------------------
109
+
110
+ console.log(`\n${passed + failed} tests: ${passed} passed, ${failed} failed\n`)
111
+ if (failed > 0) process.exit(1)