@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.
- package/.claude/commands/build-page.md +59 -0
- package/.claude/commands/new-doc-page.md +45 -0
- package/.claude/commands/verify.md +52 -0
- package/.claude/pulse-checklist.md +111 -0
- package/.claude/settings.local.json +102 -0
- package/.github/workflows/ci.yml +22 -0
- package/.github/workflows/publish.yml +41 -0
- package/.pulse/load-reports/home/1773432711417.json +22 -0
- package/CLAUDE.md +383 -0
- package/README.md +95 -0
- package/docs/.claude/pulse-checklist.md +111 -0
- package/docs/public/.pulse-ui-version +1 -0
- package/docs/public/dist/accessibility.boot-5DVTARJU.js +115 -0
- package/docs/public/dist/actions.boot-P66HKQEM.js +164 -0
- package/docs/public/dist/auth.boot-IMAJAUPH.js +140 -0
- package/docs/public/dist/caching.boot-DVR6KDE7.js +53 -0
- package/docs/public/dist/components--accordion.boot-3HVKMNWC.js +11 -0
- package/docs/public/dist/components--alert.boot-GCEXOZAC.js +6 -0
- package/docs/public/dist/components--app-badge.boot-DVT3GCHJ.js +6 -0
- package/docs/public/dist/components--avatar.boot-PSW24EVA.js +5 -0
- package/docs/public/dist/components--badge.boot-TYDY2RMK.js +7 -0
- package/docs/public/dist/components--banner.boot-EI5PZSZK.js +7 -0
- package/docs/public/dist/components--breadcrumbs.boot-SMA2E2GO.js +34 -0
- package/docs/public/dist/components--button.boot-J54BQM2E.js +23 -0
- package/docs/public/dist/components--card.boot-PZGNDIB6.js +138 -0
- package/docs/public/dist/components--carousel.boot-TP6LPFZZ.js +12 -0
- package/docs/public/dist/components--charts.boot-2EOYQWKL.js +108 -0
- package/docs/public/dist/components--checkbox.boot-DS5BSL6T.js +54 -0
- package/docs/public/dist/components--cluster.boot-HHVIBBJG.js +9 -0
- package/docs/public/dist/components--code-window.boot-2GR2DV33.js +20 -0
- package/docs/public/dist/components--container.boot-7LOOGK2K.js +5 -0
- package/docs/public/dist/components--cta.boot-FSNZ5YRT.js +11 -0
- package/docs/public/dist/components--divider.boot-3NI2C3QG.js +6 -0
- package/docs/public/dist/components--empty.boot-YX2UR3PV.js +7 -0
- package/docs/public/dist/components--feature.boot-MUD7NSUO.js +13 -0
- package/docs/public/dist/components--fieldset.boot-J7BYHMKF.js +19 -0
- package/docs/public/dist/components--fileupload.boot-NIKVTTPD.js +52 -0
- package/docs/public/dist/components--footer.boot-EYUK5FRG.js +14 -0
- package/docs/public/dist/components--grid.boot-URDQVDDR.js +59 -0
- package/docs/public/dist/components--heading.boot-BPQKU43E.js +44 -0
- package/docs/public/dist/components--hero.boot-4RAPRGAB.js +17 -0
- package/docs/public/dist/components--icons.boot-ZITNU5JP.js +68 -0
- package/docs/public/dist/components--image.boot-XEEGHQZF.js +19 -0
- package/docs/public/dist/components--input.boot-SGASZG5K.js +7 -0
- package/docs/public/dist/components--list.boot-W3XC5MHD.js +55 -0
- package/docs/public/dist/components--media.boot-5VFIETZO.js +13 -0
- package/docs/public/dist/components--modal.boot-RZUYXBN2.js +47 -0
- package/docs/public/dist/components--nav.boot-ODBOHU7O.js +33 -0
- package/docs/public/dist/components--pricing.boot-4AQ4ZVBY.js +21 -0
- package/docs/public/dist/components--progress.boot-GHAGYZOK.js +30 -0
- package/docs/public/dist/components--prose.boot-QANJL6JI.js +67 -0
- package/docs/public/dist/components--pullquote.boot-Q2WMNAZU.js +22 -0
- package/docs/public/dist/components--radio.boot-TJRDQ2OL.js +75 -0
- package/docs/public/dist/components--rating.boot-QBAN6DEL.js +38 -0
- package/docs/public/dist/components--search.boot-PXH5O5AG.js +17 -0
- package/docs/public/dist/components--section.boot-AQGIYHWW.js +12 -0
- package/docs/public/dist/components--segmented.boot-BEVTKEJO.js +33 -0
- package/docs/public/dist/components--select.boot-47X5RHOC.js +10 -0
- package/docs/public/dist/components--slider.boot-PSRRX7XL.js +47 -0
- package/docs/public/dist/components--spinner.boot-MZ5MO2OH.js +22 -0
- package/docs/public/dist/components--stack.boot-DI4NJXBF.js +9 -0
- package/docs/public/dist/components--stat.boot-QMFUWBQT.js +9 -0
- package/docs/public/dist/components--stepper.boot-34PP2NEV.js +22 -0
- package/docs/public/dist/components--table.boot-FCQGSFIQ.js +11 -0
- package/docs/public/dist/components--testimonial.boot-DWQPDKYG.js +11 -0
- package/docs/public/dist/components--textarea.boot-QVXLBOJ5.js +4 -0
- package/docs/public/dist/components--timeline.boot-26LN52P2.js +95 -0
- package/docs/public/dist/components--toggle.boot-IQQEI76S.js +29 -0
- package/docs/public/dist/components--tooltip.boot-LGHCO6NN.js +9 -0
- package/docs/public/dist/components.boot-SE6PQ4P7.js +103 -0
- package/docs/public/dist/config.boot-DTRRWUE6.js +126 -0
- package/docs/public/dist/constraints.boot-DUHDZBMC.js +71 -0
- package/docs/public/dist/deploy.boot-SLAD3NI2.js +163 -0
- package/docs/public/dist/docs-8e3d4b5c.css +1 -0
- package/docs/public/dist/extending.boot-UA3CN243.js +159 -0
- package/docs/public/dist/faq.boot-6EQAWLQR.js +43 -0
- package/docs/public/dist/getting-started.boot-TDKIFL5U.js +86 -0
- package/docs/public/dist/guard.boot-AUHAWTG4.js +80 -0
- package/docs/public/dist/home.boot-BVQXRH32.js +383 -0
- package/docs/public/dist/how-it-works.boot-LTWAKWKW.js +104 -0
- package/docs/public/dist/hydration.boot-JRM6IPJL.js +78 -0
- package/docs/public/dist/images.boot-M6ZVKTZS.js +80 -0
- package/docs/public/dist/manifest.json +94 -0
- package/docs/public/dist/meta.boot-7NXGPHR4.js +79 -0
- package/docs/public/dist/mutations.boot-F6F43UDX.js +79 -0
- package/docs/public/dist/navigation.boot-AOXWS3ZF.js +57 -0
- package/docs/public/dist/performance.boot-C3UPCOBK.js +98 -0
- package/docs/public/dist/persist.boot-WT32PQOQ.js +61 -0
- package/docs/public/dist/project-structure.boot-FB3LRVJ4.js +63 -0
- package/docs/public/dist/prompt-examples.boot-YKR4VDK4.js +31 -0
- package/docs/public/dist/pulse-ui-81a85c03.css +1 -0
- package/docs/public/dist/raw-responses.boot-M4KA5YXL.js +104 -0
- package/docs/public/dist/routing.boot-FNX5FDGH.js +70 -0
- package/docs/public/dist/runtime-B73WLANC.js +1 -0
- package/docs/public/dist/runtime-KO4BHUQ3.js +49 -0
- package/docs/public/dist/runtime-L2HNXIHW.js +59 -0
- package/docs/public/dist/runtime-QFURDKA2.js +5 -0
- package/docs/public/dist/runtime-UVPXO4IR.js +375 -0
- package/docs/public/dist/runtime-VMJA3Z4N.js +10 -0
- package/docs/public/dist/runtime-ZJ4FXT5O.js +11 -0
- package/docs/public/dist/server-api.boot-K7X3LCFB.js +219 -0
- package/docs/public/dist/server-data.boot-Y7HQYC4R.js +157 -0
- package/docs/public/dist/slash-commands.boot-V2UV7OW2.js +26 -0
- package/docs/public/dist/spec.boot-2WU7ZHCV.js +159 -0
- package/docs/public/dist/state.boot-B24GUE3R.js +73 -0
- package/docs/public/dist/store.boot-TLIB4XHH.js +150 -0
- package/docs/public/dist/streaming.boot-W2DZSMW4.js +80 -0
- package/docs/public/dist/stripe.boot-QN3C2GEL.js +164 -0
- package/docs/public/dist/supabase.boot-BG4XXLZE.js +303 -0
- package/docs/public/dist/testing.boot-6U4WKMTE.js +130 -0
- package/docs/public/dist/validation.boot-PQHYGW5B.js +100 -0
- package/docs/public/docs.css +2020 -0
- package/docs/public/menu.js +83 -0
- package/docs/public/pulse-ui.css +2739 -0
- package/docs/public/pulse-ui.js +236 -0
- package/docs/server.js +192 -0
- package/docs/src/lib/component-page.js +47 -0
- package/docs/src/lib/highlight.js +255 -0
- package/docs/src/lib/layout.js +131 -0
- package/docs/src/lib/metrics-store.js +6 -0
- package/docs/src/lib/nav.js +159 -0
- package/docs/src/lib/stats.js +81 -0
- package/docs/src/pages/accessibility.js +157 -0
- package/docs/src/pages/actions.js +191 -0
- package/docs/src/pages/auth.js +177 -0
- package/docs/src/pages/caching.js +95 -0
- package/docs/src/pages/components/accordion.js +48 -0
- package/docs/src/pages/components/alert.js +35 -0
- package/docs/src/pages/components/app-badge.js +41 -0
- package/docs/src/pages/components/avatar.js +35 -0
- package/docs/src/pages/components/badge.js +36 -0
- package/docs/src/pages/components/banner.js +45 -0
- package/docs/src/pages/components/breadcrumbs.js +94 -0
- package/docs/src/pages/components/button.js +84 -0
- package/docs/src/pages/components/card.js +225 -0
- package/docs/src/pages/components/carousel.js +72 -0
- package/docs/src/pages/components/charts.js +278 -0
- package/docs/src/pages/components/checkbox.js +129 -0
- package/docs/src/pages/components/cluster.js +47 -0
- package/docs/src/pages/components/code-window.js +57 -0
- package/docs/src/pages/components/container.js +40 -0
- package/docs/src/pages/components/cta.js +53 -0
- package/docs/src/pages/components/divider.js +37 -0
- package/docs/src/pages/components/empty.js +36 -0
- package/docs/src/pages/components/feature.js +60 -0
- package/docs/src/pages/components/fieldset.js +65 -0
- package/docs/src/pages/components/fileupload.js +127 -0
- package/docs/src/pages/components/footer.js +58 -0
- package/docs/src/pages/components/grid.js +165 -0
- package/docs/src/pages/components/heading.js +107 -0
- package/docs/src/pages/components/hero.js +65 -0
- package/docs/src/pages/components/icons.js +285 -0
- package/docs/src/pages/components/image.js +71 -0
- package/docs/src/pages/components/input.js +51 -0
- package/docs/src/pages/components/list.js +112 -0
- package/docs/src/pages/components/media.js +51 -0
- package/docs/src/pages/components/modal.js +111 -0
- package/docs/src/pages/components/nav.js +86 -0
- package/docs/src/pages/components/pricing.js +68 -0
- package/docs/src/pages/components/progress.js +102 -0
- package/docs/src/pages/components/prose.js +111 -0
- package/docs/src/pages/components/pullquote.js +71 -0
- package/docs/src/pages/components/radio.js +194 -0
- package/docs/src/pages/components/rating.js +106 -0
- package/docs/src/pages/components/search.js +61 -0
- package/docs/src/pages/components/section.js +59 -0
- package/docs/src/pages/components/segmented.js +121 -0
- package/docs/src/pages/components/select.js +45 -0
- package/docs/src/pages/components/slider.js +114 -0
- package/docs/src/pages/components/spinner.js +73 -0
- package/docs/src/pages/components/stack.js +48 -0
- package/docs/src/pages/components/stat.js +55 -0
- package/docs/src/pages/components/stepper.js +66 -0
- package/docs/src/pages/components/table.js +45 -0
- package/docs/src/pages/components/testimonial.js +49 -0
- package/docs/src/pages/components/textarea.js +31 -0
- package/docs/src/pages/components/timeline.js +227 -0
- package/docs/src/pages/components/toggle.js +84 -0
- package/docs/src/pages/components/tooltip.js +48 -0
- package/docs/src/pages/components.js +204 -0
- package/docs/src/pages/config.js +193 -0
- package/docs/src/pages/constraints.js +99 -0
- package/docs/src/pages/deploy.js +233 -0
- package/docs/src/pages/extending.js +198 -0
- package/docs/src/pages/faq.js +96 -0
- package/docs/src/pages/getting-started.js +106 -0
- package/docs/src/pages/guard.js +121 -0
- package/docs/src/pages/home.js +401 -0
- package/docs/src/pages/how-it-works.js +183 -0
- package/docs/src/pages/hydration.js +98 -0
- package/docs/src/pages/images.js +121 -0
- package/docs/src/pages/meta.js +120 -0
- package/docs/src/pages/mutations.js +106 -0
- package/docs/src/pages/navigation.js +85 -0
- package/docs/src/pages/performance.js +157 -0
- package/docs/src/pages/persist.js +88 -0
- package/docs/src/pages/project-structure.js +90 -0
- package/docs/src/pages/prompt-examples.js +186 -0
- package/docs/src/pages/raw-responses.js +124 -0
- package/docs/src/pages/routing.js +99 -0
- package/docs/src/pages/server-api.js +281 -0
- package/docs/src/pages/server-data.js +185 -0
- package/docs/src/pages/slash-commands.js +55 -0
- package/docs/src/pages/spec.js +207 -0
- package/docs/src/pages/state.js +101 -0
- package/docs/src/pages/store.js +181 -0
- package/docs/src/pages/streaming.js +108 -0
- package/docs/src/pages/stripe.js +193 -0
- package/docs/src/pages/supabase.js +323 -0
- package/docs/src/pages/testing.js +198 -0
- package/docs/src/pages/validation.js +138 -0
- package/examples/contact.js +166 -0
- package/examples/counter.js +94 -0
- package/examples/dev.server.js +91 -0
- package/examples/examples.test.js +394 -0
- package/examples/pricing.js +244 -0
- package/examples/products.js +191 -0
- package/examples/quiz.js +208 -0
- package/examples/shared.js +78 -0
- package/examples/todos.js +162 -0
- package/package.json +75 -0
- package/public/.pulse-ui-version +1 -0
- package/public/chippy-bird.css +246 -0
- package/public/examples/contact.css +119 -0
- package/public/examples/counter.css +79 -0
- package/public/examples/pricing.css +132 -0
- package/public/examples/products.css +100 -0
- package/public/examples/quiz.css +200 -0
- package/public/examples/todos.css +137 -0
- package/public/favicon.ico +0 -0
- package/public/log-dashboard.css +383 -0
- package/public/pulse-ui.css +2740 -0
- package/public/pulse-ui.js +236 -0
- package/public/pulse.css +149 -0
- package/scripts/build.js +411 -0
- package/src/agent/checklist.md +111 -0
- package/src/agent/coverage-check.js +66 -0
- package/src/agent/guide-components.md +274 -0
- package/src/agent/guide-examples.md +54 -0
- package/src/agent/guide-routing.md +36 -0
- package/src/agent/guide-server.md +258 -0
- package/src/agent/guide-spec.md +103 -0
- package/src/agent/guide-styles.md +191 -0
- package/src/agent/guide.md +979 -0
- package/src/agent/identity.md +106 -0
- package/src/agent/workflow.md +108 -0
- package/src/cli/cli.test.js +82 -0
- package/src/cli/dev.js +195 -0
- package/src/cli/discover.js +113 -0
- package/src/cli/index.js +361 -0
- package/src/cli/load-report.js +91 -0
- package/src/cli/load-runner.js +121 -0
- package/src/cli/report-server.js +723 -0
- package/src/cli/report.js +116 -0
- package/src/cli/scaffold.archive.js +1371 -0
- package/src/cli/scaffold.js +349 -0
- package/src/cli/start.js +74 -0
- package/src/html.js +19 -0
- package/src/mcp/server.js +884 -0
- package/src/mcp/validate-worker.js +110 -0
- package/src/runtime/image.js +74 -0
- package/src/runtime/image.test.js +111 -0
- package/src/runtime/index.js +621 -0
- package/src/runtime/navigate.js +146 -0
- package/src/runtime/runtime.test.js +773 -0
- package/src/runtime/ssr.js +464 -0
- package/src/runtime/ssr.test.js +421 -0
- package/src/runtime/store.js +92 -0
- package/src/runtime/toast.js +163 -0
- package/src/server/index.js +1386 -0
- package/src/server/server.test.js +1248 -0
- package/src/spec/schema.js +428 -0
- package/src/spec/schema.test.js +291 -0
- package/src/store/index.js +102 -0
- package/src/store/store.test.js +210 -0
- package/src/testing/html.js +283 -0
- package/src/testing/index.js +249 -0
- package/src/testing/testing.test.js +450 -0
- package/src/ui/accordion.js +28 -0
- package/src/ui/alert.js +43 -0
- package/src/ui/app-badge.js +48 -0
- package/src/ui/avatar.js +47 -0
- package/src/ui/badge.js +24 -0
- package/src/ui/banner.js +26 -0
- package/src/ui/breadcrumbs.js +38 -0
- package/src/ui/button.js +66 -0
- package/src/ui/card.js +34 -0
- package/src/ui/carousel.js +59 -0
- package/src/ui/charts.js +321 -0
- package/src/ui/checkbox.js +65 -0
- package/src/ui/cluster.js +44 -0
- package/src/ui/code-window.js +39 -0
- package/src/ui/container.js +24 -0
- package/src/ui/cta.js +37 -0
- package/src/ui/divider.js +29 -0
- package/src/ui/empty.js +33 -0
- package/src/ui/feature.js +33 -0
- package/src/ui/fieldset.js +37 -0
- package/src/ui/fileupload.js +89 -0
- package/src/ui/footer.js +38 -0
- package/src/ui/grid.js +36 -0
- package/src/ui/heading.js +45 -0
- package/src/ui/hero.js +37 -0
- package/src/ui/icons.js +161 -0
- package/src/ui/index.js +89 -0
- package/src/ui/input.js +74 -0
- package/src/ui/list.js +36 -0
- package/src/ui/media.js +44 -0
- package/src/ui/modal.js +80 -0
- package/src/ui/nav.js +61 -0
- package/src/ui/pricing.js +56 -0
- package/src/ui/progress.js +62 -0
- package/src/ui/prose.js +29 -0
- package/src/ui/pullquote.js +34 -0
- package/src/ui/radio.js +102 -0
- package/src/ui/rating.js +93 -0
- package/src/ui/search.js +77 -0
- package/src/ui/section.js +69 -0
- package/src/ui/segmented.js +50 -0
- package/src/ui/select.js +77 -0
- package/src/ui/slider.js +84 -0
- package/src/ui/spinner.js +34 -0
- package/src/ui/stack.js +36 -0
- package/src/ui/stat.js +52 -0
- package/src/ui/stepper.js +46 -0
- package/src/ui/switch.js +57 -0
- package/src/ui/table.js +45 -0
- package/src/ui/testimonial.js +48 -0
- package/src/ui/textarea.js +72 -0
- package/src/ui/timeline.js +72 -0
- package/src/ui/tooltip.js +28 -0
- package/src/ui/ui.test.js +1241 -0
- package/src/ui/uiimage.js +65 -0
- package/tsconfig.json +13 -0
- package/types/html.d.ts +17 -0
- package/types/image.d.ts +70 -0
- package/types/index.d.ts +7 -0
- package/types/navigate.d.ts +38 -0
- package/types/runtime.d.ts +63 -0
- package/types/schema.d.ts +243 -0
- package/types/server.d.ts +145 -0
- package/types/ssr.d.ts +110 -0
- package/types/testing.d.ts +154 -0
- 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 ' or ' 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, '&')
|
|
13
|
+
.replace(/"/g, '"')
|
|
14
|
+
.replace(/</g, '<')
|
|
15
|
+
.replace(/>/g, '>')
|
|
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('&'), `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)
|