@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.
- package/package.json +6 -5
- package/public/.pulse-ui-version +1 -1
- package/scripts/build.js +26 -0
- package/scripts/release-version.js +17 -8
- package/scripts/strip-server.test.js +60 -0
- package/src/agent/checklist.md +8 -12
- package/src/agent/commands/pulse-start.md +2 -0
- package/src/agent/commands/verify.md +30 -8
- package/src/agent/guide-components.md +7 -25
- package/src/agent/guide-routing.md +0 -12
- package/src/agent/guide-server.md +102 -1
- package/src/agent/guide-spec.md +8 -1
- package/src/agent/guide-styles.md +23 -0
- package/src/agent/identity.md +9 -0
- package/src/agent/workflow.md +4 -23
- package/src/cli/check-bundles.js +113 -0
- package/src/cli/check-bundles.test.js +137 -0
- package/src/cli/cli.test.js +105 -0
- package/src/cli/dev.js +48 -3
- package/src/cli/diagnose.js +105 -0
- package/src/cli/diagnose.test.js +124 -0
- package/src/cli/discover.js +9 -2
- package/src/cli/index.js +171 -9
- package/src/cli/process-utils.js +57 -0
- package/src/cli/process-utils.test.js +114 -0
- package/src/cli/review.js +391 -0
- package/src/cli/review.test.js +143 -0
- package/src/cli/scaffold.js +9 -4
- package/src/cli/stamp.js +102 -0
- package/src/cli/stamp.test.js +149 -0
- package/src/cli/start.js +15 -2
- package/src/cli/validate.js +184 -0
- package/src/cli/validate.test.js +153 -0
- package/src/mcp/mcp.test.js +261 -30
- package/src/mcp/server.js +207 -515
- package/src/mcp/validate-worker.js +18 -1
- package/src/runtime/index.js +144 -11
- package/src/runtime/runtime.test.js +199 -0
- package/src/runtime/ssr.js +14 -7
- package/src/runtime/ssr.test.js +31 -0
- package/src/server/index.js +163 -10
- package/src/server/server.test.js +225 -2
- package/src/spec/schema.js +1 -1
- package/src/ui/index.js +0 -5
- package/src/ui/ui.test.js +0 -75
- package/types/ui.d.ts +0 -21
- package/.claude/commands/build-page.md +0 -63
- package/.claude/commands/new-doc-page.md +0 -45
- package/.claude/commands/pulse-start.md +0 -11
- package/.claude/commands/pulse-stop.md +0 -5
- package/.claude/commands/verify.md +0 -118
- package/.claude/pulse-checklist.md +0 -189
- package/.github/workflows/ci.yml +0 -25
- package/.github/workflows/publish.yml +0 -45
- package/.pulse/load-reports/home/1773432711417.json +0 -22
- package/.pulse-verified +0 -1
- package/AGENTS.md +0 -697
- package/CLAUDE.md +0 -697
- package/README.md +0 -95
- package/benchmark/public/dist/counter.boot-PO6SNSJZ.js +0 -8
- package/benchmark/public/dist/home.boot-YK6VAEQI.js +0 -6
- package/benchmark/public/dist/manifest.json +0 -5
- package/benchmark/public/dist/runtime-5Y7ESAIB.js +0 -2
- package/benchmark/public/dist/runtime-KO4BHUQ3.js +0 -49
- package/benchmark/results/2026-03-27-09-20.json +0 -188
- package/benchmark/results/2026-03-27-09-21.json +0 -188
- package/benchmark/results/2026-03-27-09-29.json +0 -188
- package/benchmark/results/2026-03-27-10-14.json +0 -188
- package/benchmark/results/2026-03-27-10-18.json +0 -232
- package/benchmark/src/pages/counter.js +0 -37
- package/benchmark/src/pages/home.js +0 -21
- package/docs/.claude/commands/pulse-build.md +0 -7
- package/docs/.claude/commands/pulse-dev.md +0 -7
- package/docs/.claude/commands/pulse-start.md +0 -9
- package/docs/.claude/commands/pulse-stop.md +0 -5
- package/docs/.claude/commands/verify.md +0 -84
- package/docs/.claude/coverage-check.js +0 -196
- package/docs/.claude/pulse-checklist.md +0 -189
- package/docs/public/.pulse-ui-version +0 -1
- package/docs/public/andypixel.css +0 -237
- package/docs/public/bookworms.css +0 -422
- package/docs/public/coutumes.css +0 -561
- package/docs/public/docs.css +0 -2388
- package/docs/public/favicon.svg +0 -4
- package/docs/public/home-brut.css +0 -982
- package/docs/public/images/bookworms/hero-books.jpg +0 -0
- package/docs/public/images/bookworms/how-we-read-now.jpg +0 -0
- package/docs/public/images/bookworms/lantern-keepers.jpg +0 -0
- package/docs/public/images/bookworms/provincial-bookseller.jpg +0 -0
- package/docs/public/images/journey/178.jpg +0 -0
- package/docs/public/images/journey/29.jpg +0 -0
- package/docs/public/images/retrowheels/hero.jpg +0 -0
- package/docs/public/images/shop/devbook-pro.jpg +0 -0
- package/docs/public/images/suzukibento/1015.jpg +0 -0
- package/docs/public/images/suzukibento/1025.jpg +0 -0
- package/docs/public/images/suzukibento/1043.jpg +0 -0
- package/docs/public/images/suzukibento/1059.jpg +0 -0
- package/docs/public/images/suzukibento/1062.jpg +0 -0
- package/docs/public/images/suzukibento/1084.jpg +0 -0
- package/docs/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
- package/docs/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
- package/docs/public/images/swish/1485518882345-15568b007407.jpg +0 -0
- package/docs/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
- package/docs/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
- package/docs/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
- package/docs/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
- package/docs/public/images/swish/1551803091-e20673f15770.jpg +0 -0
- package/docs/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
- package/docs/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
- package/docs/public/img/heic1501a.jpg +0 -0
- package/docs/public/img/placeholder-landscape.svg +0 -18
- package/docs/public/img/placeholder-square.svg +0 -14
- package/docs/public/img/placeholder-tall.svg +0 -22
- package/docs/public/journey.css +0 -528
- package/docs/public/menu.js +0 -327
- package/docs/public/pulse-ui.css +0 -3028
- package/docs/public/pulse-ui.js +0 -257
- package/docs/public/retrowheels.css +0 -425
- package/docs/public/shop.css +0 -269
- package/docs/public/sparkys.css +0 -382
- package/docs/public/suzukibento.css +0 -517
- package/docs/public/swish.css +0 -394
- package/docs/public/theme.css +0 -52
- package/docs/public/themes/lumio.css +0 -50
- package/docs/public/themes/observatory.css +0 -104
- package/docs/server.js +0 -161
- package/docs/src/lib/component-page.js +0 -49
- package/docs/src/lib/highlight.js +0 -2
- package/docs/src/lib/layout.js +0 -190
- package/docs/src/lib/metrics-store.js +0 -6
- package/docs/src/lib/nav.js +0 -171
- package/docs/src/lib/stats.js +0 -81
- package/docs/src/pages/accessibility.js +0 -157
- package/docs/src/pages/actions.js +0 -194
- package/docs/src/pages/auth.js +0 -176
- package/docs/src/pages/caching.js +0 -95
- package/docs/src/pages/components/accordion.js +0 -48
- package/docs/src/pages/components/alert.js +0 -35
- package/docs/src/pages/components/app-badge.js +0 -41
- package/docs/src/pages/components/avatar.js +0 -35
- package/docs/src/pages/components/badge.js +0 -36
- package/docs/src/pages/components/breadcrumbs.js +0 -94
- package/docs/src/pages/components/button.js +0 -84
- package/docs/src/pages/components/card.js +0 -225
- package/docs/src/pages/components/charts.js +0 -283
- package/docs/src/pages/components/checkbox.js +0 -129
- package/docs/src/pages/components/cluster.js +0 -47
- package/docs/src/pages/components/container.js +0 -40
- package/docs/src/pages/components/cta.js +0 -54
- package/docs/src/pages/components/divider.js +0 -37
- package/docs/src/pages/components/empty.js +0 -36
- package/docs/src/pages/components/feature.js +0 -60
- package/docs/src/pages/components/fieldset.js +0 -65
- package/docs/src/pages/components/fileupload.js +0 -128
- package/docs/src/pages/components/footer.js +0 -58
- package/docs/src/pages/components/grid.js +0 -169
- package/docs/src/pages/components/heading.js +0 -107
- package/docs/src/pages/components/hero.js +0 -123
- package/docs/src/pages/components/icons.js +0 -410
- package/docs/src/pages/components/image.js +0 -71
- package/docs/src/pages/components/input.js +0 -51
- package/docs/src/pages/components/media.js +0 -51
- package/docs/src/pages/components/modal.js +0 -111
- package/docs/src/pages/components/nav.js +0 -136
- package/docs/src/pages/components/phone-frame.js +0 -74
- package/docs/src/pages/components/progress.js +0 -102
- package/docs/src/pages/components/prose.js +0 -111
- package/docs/src/pages/components/pullquote.js +0 -71
- package/docs/src/pages/components/radio.js +0 -194
- package/docs/src/pages/components/rating.js +0 -106
- package/docs/src/pages/components/search.js +0 -61
- package/docs/src/pages/components/section.js +0 -98
- package/docs/src/pages/components/segmented.js +0 -121
- package/docs/src/pages/components/select.js +0 -45
- package/docs/src/pages/components/slider.js +0 -115
- package/docs/src/pages/components/spinner.js +0 -73
- package/docs/src/pages/components/stack.js +0 -48
- package/docs/src/pages/components/stat.js +0 -55
- package/docs/src/pages/components/stepper.js +0 -66
- package/docs/src/pages/components/table.js +0 -45
- package/docs/src/pages/components/textarea.js +0 -31
- package/docs/src/pages/components/toggle.js +0 -84
- package/docs/src/pages/components/tooltip.js +0 -48
- package/docs/src/pages/components.js +0 -200
- package/docs/src/pages/config.js +0 -193
- package/docs/src/pages/constraints.js +0 -100
- package/docs/src/pages/deploy.js +0 -233
- package/docs/src/pages/error-pages.js +0 -99
- package/docs/src/pages/extending.js +0 -198
- package/docs/src/pages/faq.js +0 -99
- package/docs/src/pages/forms.js +0 -129
- package/docs/src/pages/getting-started.js +0 -147
- package/docs/src/pages/guard.js +0 -120
- package/docs/src/pages/home.js +0 -345
- package/docs/src/pages/how-it-works.js +0 -244
- package/docs/src/pages/hydration.js +0 -108
- package/docs/src/pages/images.js +0 -120
- package/docs/src/pages/markdown.js +0 -156
- package/docs/src/pages/meta.js +0 -193
- package/docs/src/pages/mutations.js +0 -107
- package/docs/src/pages/navigation.js +0 -85
- package/docs/src/pages/performance.js +0 -157
- package/docs/src/pages/persist.js +0 -89
- package/docs/src/pages/project-structure.js +0 -91
- package/docs/src/pages/prompt-examples.js +0 -186
- package/docs/src/pages/raw-responses.js +0 -122
- package/docs/src/pages/routing.js +0 -120
- package/docs/src/pages/server-api.js +0 -291
- package/docs/src/pages/server-data.js +0 -203
- package/docs/src/pages/slash-commands.js +0 -55
- package/docs/src/pages/spec.js +0 -207
- package/docs/src/pages/state.js +0 -102
- package/docs/src/pages/store.js +0 -203
- package/docs/src/pages/streaming.js +0 -107
- package/docs/src/pages/stripe.js +0 -194
- package/docs/src/pages/supabase.js +0 -324
- package/docs/src/pages/templates/andypixel.js +0 -260
- package/docs/src/pages/templates/blog-post.js +0 -226
- package/docs/src/pages/templates/bookworms.js +0 -227
- package/docs/src/pages/templates/coutumes.js +0 -237
- package/docs/src/pages/templates/journey.js +0 -186
- package/docs/src/pages/templates/mobile-app.js +0 -371
- package/docs/src/pages/templates/retrowheels.js +0 -355
- package/docs/src/pages/templates/shop.js +0 -268
- package/docs/src/pages/templates/sparkys.js +0 -264
- package/docs/src/pages/templates/suzukibento.js +0 -200
- package/docs/src/pages/templates/swish.js +0 -313
- package/docs/src/pages/templates.js +0 -534
- package/docs/src/pages/testing.js +0 -199
- package/docs/src/pages/validation.js +0 -138
- package/examples/.claude/commands/pulse-build.md +0 -7
- package/examples/.claude/commands/pulse-dev.md +0 -7
- package/examples/.claude/commands/pulse-start.md +0 -9
- package/examples/.claude/commands/pulse-stop.md +0 -5
- package/examples/.claude/commands/verify.md +0 -84
- package/examples/.claude/coverage-check.js +0 -196
- package/examples/.claude/pulse-checklist.md +0 -189
- package/examples/andypixel.css +0 -237
- package/examples/andypixel.js +0 -258
- package/examples/bookworms.css +0 -422
- package/examples/bookworms.js +0 -225
- package/examples/contact.js +0 -166
- package/examples/counter.js +0 -94
- package/examples/dev.server.js +0 -110
- package/examples/examples.test.js +0 -394
- package/examples/journey.css +0 -528
- package/examples/journey.js +0 -186
- package/examples/pricing.js +0 -262
- package/examples/products.js +0 -190
- package/examples/public/.pulse-ui-version +0 -1
- package/examples/public/pulse-ui.css +0 -3028
- package/examples/public/pulse-ui.js +0 -257
- package/examples/quiz.js +0 -208
- package/examples/retrowheels.css +0 -425
- package/examples/retrowheels.js +0 -353
- package/examples/shared.js +0 -84
- package/examples/suzukibento.css +0 -513
- package/examples/suzukibento.js +0 -200
- package/examples/swish.css +0 -394
- package/examples/swish.js +0 -311
- package/examples/todos.js +0 -162
- package/src/ui/fieldset.js +0 -37
- package/src/ui/phone-frame.js +0 -40
- package/src/ui/search.js +0 -78
- package/src/ui/segmented.js +0 -50
- package/src/ui/tooltip.js +0 -28
package/src/ui/ui.test.js
CHANGED
|
@@ -31,11 +31,9 @@ import { stack } from './stack.js'
|
|
|
31
31
|
import { cluster } from './cluster.js'
|
|
32
32
|
import { divider } from './divider.js'
|
|
33
33
|
import { media } from './media.js'
|
|
34
|
-
import { fieldset } from './fieldset.js'
|
|
35
34
|
import { slider } from './slider.js'
|
|
36
35
|
import { toggle } from './switch.js'
|
|
37
36
|
import { radio, radioGroup } from './radio.js'
|
|
38
|
-
import { segmented } from './segmented.js'
|
|
39
37
|
import { fileUpload } from './fileupload.js'
|
|
40
38
|
import { modal, modalTrigger } from './modal.js'
|
|
41
39
|
import { uiImage } from './uiimage.js'
|
|
@@ -806,40 +804,6 @@ test('media: gap lg adds modifier class', () => {
|
|
|
806
804
|
assert.match(media({ image: '', content: '', gap: 'lg' }), /ui-media--gap-lg/)
|
|
807
805
|
})
|
|
808
806
|
|
|
809
|
-
// ---------------------------------------------------------------------------
|
|
810
|
-
// fieldset
|
|
811
|
-
// ---------------------------------------------------------------------------
|
|
812
|
-
|
|
813
|
-
test('fieldset: renders fieldset element', () => {
|
|
814
|
-
assert.match(fieldset({ legend: 'Address', content: '<p>fields</p>' }), /<fieldset/)
|
|
815
|
-
})
|
|
816
|
-
|
|
817
|
-
test('fieldset: renders legend', () => {
|
|
818
|
-
assert.match(fieldset({ legend: 'Billing', content: '' }), /ui-fieldset-legend/)
|
|
819
|
-
assert.match(fieldset({ legend: 'Billing', content: '' }), /Billing/)
|
|
820
|
-
})
|
|
821
|
-
|
|
822
|
-
test('fieldset: no legend element when omitted', () => {
|
|
823
|
-
assert.doesNotMatch(fieldset({ content: 'fields' }), /ui-fieldset-legend/)
|
|
824
|
-
})
|
|
825
|
-
|
|
826
|
-
test('fieldset: renders content in body', () => {
|
|
827
|
-
assert.match(fieldset({ content: '<input>' }), /ui-fieldset-body/)
|
|
828
|
-
assert.match(fieldset({ content: '<input>' }), /<input>/)
|
|
829
|
-
})
|
|
830
|
-
|
|
831
|
-
test('fieldset: gap lg adds modifier class', () => {
|
|
832
|
-
assert.match(fieldset({ gap: 'lg', content: '' }), /ui-fieldset--gap-lg/)
|
|
833
|
-
})
|
|
834
|
-
|
|
835
|
-
test('fieldset: no gap modifier for default md', () => {
|
|
836
|
-
assert.doesNotMatch(fieldset({ content: '' }), /ui-fieldset--gap/)
|
|
837
|
-
})
|
|
838
|
-
|
|
839
|
-
test('fieldset: escapes legend to prevent XSS', () => {
|
|
840
|
-
assert.match(fieldset({ legend: '<script>bad</script>', content: '' }), /<script>/)
|
|
841
|
-
})
|
|
842
|
-
|
|
843
807
|
// ─── slider ──────────────────────────────────────────────────────────────────
|
|
844
808
|
|
|
845
809
|
test('slider: renders range input with label', () => {
|
|
@@ -989,45 +953,6 @@ test('radioGroup: error renders role=alert', () => {
|
|
|
989
953
|
assert.match(html, /Pick one/)
|
|
990
954
|
})
|
|
991
955
|
|
|
992
|
-
// ─── segmented ───────────────────────────────────────────────────────────────
|
|
993
|
-
|
|
994
|
-
test('segmented: renders radio inputs for each option', () => {
|
|
995
|
-
const html = segmented({ name: 'tab', options: [{ value: 'a', label: 'A' }, { value: 'b', label: 'B' }] })
|
|
996
|
-
const inputs = [...html.matchAll(/type="radio"/g)]
|
|
997
|
-
assert.equal(inputs.length, 2)
|
|
998
|
-
assert.match(html, /name="tab"/)
|
|
999
|
-
})
|
|
1000
|
-
|
|
1001
|
-
test('segmented: marks selected value as checked', () => {
|
|
1002
|
-
const html = segmented({ name: 'x', options: [{ value: 'a', label: 'A' }, { value: 'b', label: 'B' }], value: 'b' })
|
|
1003
|
-
assert.match(html, /value="b"[\s\S]*?checked/)
|
|
1004
|
-
})
|
|
1005
|
-
|
|
1006
|
-
test('segmented: event prop renders data-event on all inputs', () => {
|
|
1007
|
-
const html = segmented({ name: 'x', options: [{ value: 'a', label: 'A' }, { value: 'b', label: 'B' }], event: 'change:setTab' })
|
|
1008
|
-
const matches = [...html.matchAll(/data-event="change:setTab"/g)]
|
|
1009
|
-
assert.equal(matches.length, 2)
|
|
1010
|
-
})
|
|
1011
|
-
|
|
1012
|
-
test('segmented: no data-event when event not provided', () => {
|
|
1013
|
-
const html = segmented({ name: 'x', options: [{ value: 'a', label: 'A' }] })
|
|
1014
|
-
assert.doesNotMatch(html, /data-event/)
|
|
1015
|
-
})
|
|
1016
|
-
|
|
1017
|
-
test('segmented: size sm adds modifier class', () => {
|
|
1018
|
-
assert.match(segmented({ name: 'x', options: [], size: 'sm' }), /ui-segmented--sm/)
|
|
1019
|
-
})
|
|
1020
|
-
|
|
1021
|
-
test('segmented: size lg adds modifier class', () => {
|
|
1022
|
-
assert.match(segmented({ name: 'x', options: [], size: 'lg' }), /ui-segmented--lg/)
|
|
1023
|
-
})
|
|
1024
|
-
|
|
1025
|
-
test('segmented: disabled applies to all inputs', () => {
|
|
1026
|
-
const html = segmented({ name: 'x', options: [{ value: 'a', label: 'A' }, { value: 'b', label: 'B' }], disabled: true })
|
|
1027
|
-
const matches = [...html.matchAll(/disabled/g)]
|
|
1028
|
-
assert.ok(matches.length >= 2)
|
|
1029
|
-
})
|
|
1030
|
-
|
|
1031
956
|
// ─── fileUpload ───────────────────────────────────────────────────────────────
|
|
1032
957
|
|
|
1033
958
|
test('fileUpload: renders file input with correct name', () => {
|
package/types/ui.d.ts
CHANGED
|
@@ -277,14 +277,6 @@ export interface ModalTriggerProps {
|
|
|
277
277
|
}
|
|
278
278
|
export function modalTrigger(props?: ModalTriggerProps): string
|
|
279
279
|
|
|
280
|
-
export interface TooltipProps {
|
|
281
|
-
content?: string
|
|
282
|
-
trigger?: string
|
|
283
|
-
position?: 'top' | 'bottom' | 'left' | 'right'
|
|
284
|
-
class?: string
|
|
285
|
-
}
|
|
286
|
-
export function tooltip(props?: TooltipProps): string
|
|
287
|
-
|
|
288
280
|
// ---------------------------------------------------------------------------
|
|
289
281
|
// Data display
|
|
290
282
|
// ---------------------------------------------------------------------------
|
|
@@ -407,19 +399,6 @@ export interface SliderProps {
|
|
|
407
399
|
}
|
|
408
400
|
export function slider(props?: SliderProps): string
|
|
409
401
|
|
|
410
|
-
export interface SegmentedOption {
|
|
411
|
-
label: string
|
|
412
|
-
value: string
|
|
413
|
-
}
|
|
414
|
-
export interface SegmentedProps {
|
|
415
|
-
name?: string
|
|
416
|
-
options?: Array<string | SegmentedOption>
|
|
417
|
-
value?: string
|
|
418
|
-
event?: string
|
|
419
|
-
class?: string
|
|
420
|
-
}
|
|
421
|
-
export function segmented(props?: SegmentedProps): string
|
|
422
|
-
|
|
423
402
|
export interface RadioOption {
|
|
424
403
|
label: string
|
|
425
404
|
value: string
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
# Build Page
|
|
2
|
-
|
|
3
|
-
Scaffold a new Pulse page spec, validate it, and visually confirm it renders correctly.
|
|
4
|
-
|
|
5
|
-
## What to build
|
|
6
|
-
|
|
7
|
-
$ARGUMENTS
|
|
8
|
-
|
|
9
|
-
## Steps — follow in order
|
|
10
|
-
|
|
11
|
-
### 1. Check existing components
|
|
12
|
-
|
|
13
|
-
Read `src/ui/index.js` to see what UI components are available before writing any HTML. Note which components apply to this page.
|
|
14
|
-
|
|
15
|
-
### 2. Plan
|
|
16
|
-
|
|
17
|
-
State before writing any files:
|
|
18
|
-
- The route (e.g. `/dashboard`)
|
|
19
|
-
- The file path (e.g. `src/pages/dashboard.js`)
|
|
20
|
-
- State shape
|
|
21
|
-
- Mutations and/or actions needed
|
|
22
|
-
- Which UI components will be used
|
|
23
|
-
- Whether `server` data fetchers are needed
|
|
24
|
-
- Whether `hydrate` is required (yes, if any mutations/actions)
|
|
25
|
-
|
|
26
|
-
### 3. Build the spec
|
|
27
|
-
|
|
28
|
-
Write the spec file. Follow these rules:
|
|
29
|
-
- Use `class` not `className`, template literals not JSX
|
|
30
|
-
- No React imports, hooks, or patterns — see CLAUDE.md "Pulse vs React" table
|
|
31
|
-
- Use components from `src/ui/` — do not hand-write HTML that a component already covers
|
|
32
|
-
- Include `hydrate` if the spec has mutations or actions
|
|
33
|
-
- Use optional chaining (`?.`) on any server data that could be null
|
|
34
|
-
- Escape any user-supplied values interpolated into HTML
|
|
35
|
-
|
|
36
|
-
### 4. Validate
|
|
37
|
-
|
|
38
|
-
Use `pulse_validate` on the spec. Fix any errors before continuing.
|
|
39
|
-
|
|
40
|
-
### 5. Register with the server
|
|
41
|
-
|
|
42
|
-
Check the project's server entry file (e.g. `src/server.js` or `pulse.config.js`) and add the new spec if needed.
|
|
43
|
-
|
|
44
|
-
### 6. Screenshot and design approval
|
|
45
|
-
|
|
46
|
-
Use `mcp__chrome-devtools__navigate_page` to load the page, then `mcp__chrome-devtools__take_screenshot`.
|
|
47
|
-
|
|
48
|
-
Check for:
|
|
49
|
-
- Page renders without a blank screen or error
|
|
50
|
-
- Layout looks correct
|
|
51
|
-
- No console errors via `mcp__chrome-devtools__list_console_messages`
|
|
52
|
-
|
|
53
|
-
Then **call `pulse_await_approval`** and ask the user: "Happy with the design and layout, or would you like any changes before I run tests and Lighthouse?" — if your host provides a question tool (e.g. AskUserQuestion), offer the choices `["Yes, looks good — run tests and Lighthouse", "I'd like some changes first"]`; otherwise ask in plain prose. Always call the tool before asking, whichever way you ask: in some hosts (including Claude Code) even a question tool ends the turn and fires the Stop hooks; the marker is harmless if the turn doesn't end. If a `VERIFY REQUIRED` stop-hook block fires while your question is unanswered, that is not permission to continue — call `pulse_await_approval`, re-ask, and end your turn.
|
|
54
|
-
|
|
55
|
-
**Do not proceed to step 7 until the user explicitly confirms.** If they want changes, make them and return to step 6.
|
|
56
|
-
|
|
57
|
-
### 7. Run the checklist
|
|
58
|
-
|
|
59
|
-
Go through `src/agent/checklist.md` and confirm every applicable point passes. Fix anything that fails.
|
|
60
|
-
|
|
61
|
-
### 8. Report
|
|
62
|
-
|
|
63
|
-
Summarise what was built, what components were used, and confirm validation + visual check passed.
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# New Docs Page
|
|
2
|
-
|
|
3
|
-
Create a new documentation page for the Pulse docs site.
|
|
4
|
-
|
|
5
|
-
## What to build
|
|
6
|
-
|
|
7
|
-
$ARGUMENTS
|
|
8
|
-
|
|
9
|
-
## Steps — follow in order
|
|
10
|
-
|
|
11
|
-
### 1. Research
|
|
12
|
-
- Read `docs/src/lib/nav.js` to understand the full nav structure and find where the new page fits
|
|
13
|
-
- Read 2–3 adjacent pages (the pages that will be `prev`/`next` neighbours) to match tone, section depth, and helper usage (`h1`, `lead`, `section`, `sub`, `codeBlock`, `callout`, `table`)
|
|
14
|
-
- Read `docs/src/lib/layout.js` exported functions to confirm available helpers
|
|
15
|
-
- If the feature being documented has a source file (e.g. in `src/`), read it to ensure accuracy
|
|
16
|
-
|
|
17
|
-
### 2. Plan
|
|
18
|
-
Before writing any files, state:
|
|
19
|
-
- The route (e.g. `/request-bodies`)
|
|
20
|
-
- Where it sits in the nav (section, between which two pages)
|
|
21
|
-
- Sections the page will cover (IDs + titles)
|
|
22
|
-
- Any doc pages that need updating to link to or mention this page
|
|
23
|
-
|
|
24
|
-
Wait for approval if anything is uncertain.
|
|
25
|
-
|
|
26
|
-
### 3. Implement
|
|
27
|
-
|
|
28
|
-
**a) Write the page file** at `docs/src/pages/<name>.js`:
|
|
29
|
-
- Import from `../lib/layout.js`, `../lib/nav.js`, `../lib/highlight.js`
|
|
30
|
-
- Use `prevNext('/your-route')` for prev/next links
|
|
31
|
-
- Export a valid Pulse spec as `export default { route, meta, state, view }`
|
|
32
|
-
- `meta.styles` must be `['/docs.css']`
|
|
33
|
-
- Use `renderLayout({ currentHref, prev, next, content })` for the page body
|
|
34
|
-
- All code examples must use `${codeBlock(highlight(\`...\`, 'js'))}` — never raw `<pre>`
|
|
35
|
-
- Tables use `${table(['Col1', 'Col2'], [['val', 'val']])}`
|
|
36
|
-
- Callouts use `${callout('note|tip|warning|info', 'text')}`
|
|
37
|
-
- Section anchors use `${section('anchor-id', 'Section Title')}`
|
|
38
|
-
|
|
39
|
-
**b) Register in nav** — add the entry to `docs/src/lib/nav.js` in the correct position
|
|
40
|
-
|
|
41
|
-
**c) Verify syntax** — run `node --check docs/src/pages/<name>.js`
|
|
42
|
-
|
|
43
|
-
**d) Check neighbours** — read the prev and next pages. If they reference topics this new page covers, add a cross-link.
|
|
44
|
-
|
|
45
|
-
**e) Final check** — confirm the new route appears in `prevNext` output for its neighbours (the nav order drives prev/next automatically, so adding to NAV is sufficient).
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
Start the Pulse production server for this project — **locally, on the production port (dev port + 1, normally 3001)** so it can run alongside the dev server on 3000.
|
|
2
|
-
|
|
3
|
-
First ensure a build exists (`public/dist/` — run `pulse build` if not). Then:
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
pulse start --port 3001
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
If `pulse.config.js` sets a custom `port`, use that value + 1 instead.
|
|
10
|
-
|
|
11
|
-
> **Why the flag:** plain `pulse start` binds the configured port (3000) because in real deployment it is the only server and platforms inject `PORT`. Locally that collides with the dev server and breaks the convention used by `pulse_build`, Lighthouse, and the performance trace — production is always dev port + 1. Never use `--port` in actual deployment.
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
# Verify
|
|
2
|
-
|
|
3
|
-
Run the verification loop on the current page or the page specified in $ARGUMENTS.
|
|
4
|
-
|
|
5
|
-
**Modes:**
|
|
6
|
-
- `/verify` — full loop including Lighthouse (desktop + mobile), perf trace, and mobile layout check. Use when the user approves the design or signals they're ready to ship.
|
|
7
|
-
- `/verify --quick` — skips Lighthouse and perf trace; just validate → screenshot → console → review → stamp. Use during iterative building: design rounds, debugging visual issues, mid-build checkpoints. Fast enough to run between every change.
|
|
8
|
-
|
|
9
|
-
If `--quick` appears anywhere in $ARGUMENTS, run in quick mode (steps 6, 7, 8, and 9 are skipped). The route/file path is whatever remains in $ARGUMENTS after removing `--quick`.
|
|
10
|
-
|
|
11
|
-
## Steps
|
|
12
|
-
|
|
13
|
-
### 1. Identify the target
|
|
14
|
-
|
|
15
|
-
If $ARGUMENTS is provided, use it (minus `--quick` if present) as the file path or route. Otherwise, identify the most recently edited spec file from context.
|
|
16
|
-
|
|
17
|
-
### 2. Design approval (new builds only)
|
|
18
|
-
|
|
19
|
-
**Skip this step for edits, bug fixes, or "add X to existing Y" tasks.**
|
|
20
|
-
|
|
21
|
-
If this is a new page build (came through `pulse_intake` or `build-page`):
|
|
22
|
-
|
|
23
|
-
1. Take a screenshot and describe what you see to the user.
|
|
24
|
-
2. **Call `pulse_await_approval` first**, then ask the user: "Happy with the design and layout, or would you like any changes before I run Lighthouse?" — if your host provides a question tool (e.g. AskUserQuestion), offer the choices `["Yes, looks good — proceed", "I'd like some changes first"]`; otherwise ask in plain prose. Always call the tool before asking, whichever way you ask: in some hosts (including Claude Code) even a question tool ends the turn and fires the Stop hooks. The marker is harmless if the turn doesn't end — it is consumed when the user replies.
|
|
25
|
-
3. **Do not proceed to step 3 until the user explicitly confirms.** If they want changes, stop here, make the changes, then restart from step 2. Lighthouse takes ~90 seconds — do not waste it on a design the user hasn't approved. If a `VERIFY REQUIRED` stop-hook block fires while your question is unanswered, that is not permission to continue — call `pulse_await_approval`, re-ask, and end your turn.
|
|
26
|
-
|
|
27
|
-
### 3. Validate the spec
|
|
28
|
-
|
|
29
|
-
Use `pulse_validate` with the spec file content. If validation fails, report every error clearly and stop — do not proceed to screenshot until the spec is valid.
|
|
30
|
-
|
|
31
|
-
### 4. Check the dev server
|
|
32
|
-
|
|
33
|
-
Use `pulse_fetch_page` to confirm the server is responding for the route. If it errors, use `pulse_restart_server` and retry once.
|
|
34
|
-
|
|
35
|
-
### 5. Screenshot
|
|
36
|
-
|
|
37
|
-
Use `mcp__chrome-devtools__navigate_page` to load the page route, then `mcp__chrome-devtools__take_screenshot` to capture the result. Describe what you see — layout, content, any obvious rendering issues.
|
|
38
|
-
|
|
39
|
-
### 6. Lighthouse — desktop *(skip in quick mode)*
|
|
40
|
-
|
|
41
|
-
**Pre-flight (required before every Lighthouse run):**
|
|
42
|
-
1. Call `pulse_build` to produce a production build and start the production server on port 3001.
|
|
43
|
-
2. Call `navigate_page` with `url: "http://localhost:3001/"` so the browser is on the production server.
|
|
44
|
-
|
|
45
|
-
Then run `mcp__chrome-devtools__lighthouse_audit` with `{ "device": "desktop" }`.
|
|
46
|
-
|
|
47
|
-
**Pass bar: Accessibility, Best Practices, and SEO must all be 100.** Performance is measured and reported but is not a hard requirement (it varies with machine load). Report the actual scores. If Accessibility, Best Practices, or SEO is below 100, identify the failing audit(s), fix the issue, and restart from step 3.
|
|
48
|
-
|
|
49
|
-
### 7. Lighthouse — mobile *(skip in quick mode)*
|
|
50
|
-
|
|
51
|
-
The browser should still be on `http://localhost:3001/` from step 6. Run `mcp__chrome-devtools__lighthouse_audit` with `{ "device": "mobile" }`.
|
|
52
|
-
|
|
53
|
-
**Same pass bar: Accessibility, Best Practices, and SEO must all be 100.** Fix any failures and restart from step 3.
|
|
54
|
-
|
|
55
|
-
**Stay on the production server (port 3001)** — steps 8 and 9 use it too. Do not call `pulse_restart_server` yet.
|
|
56
|
-
|
|
57
|
-
### 8. Mobile layout check *(skip in quick mode)*
|
|
58
|
-
|
|
59
|
-
Emulate a mobile viewport and take a screenshot to catch wrapping, overflow, and layout issues before they become Lighthouse failures.
|
|
60
|
-
|
|
61
|
-
```
|
|
62
|
-
mcp__chrome-devtools__emulate viewport: "390x844x2,mobile,touch"
|
|
63
|
-
mcp__chrome-devtools__navigate_page url: "http://localhost:3001/"
|
|
64
|
-
mcp__chrome-devtools__take_screenshot
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
Describe what you see. Look specifically for:
|
|
68
|
-
- Text or buttons overflowing the viewport
|
|
69
|
-
- Content too small to read or tap
|
|
70
|
-
- Navigation that overlaps content
|
|
71
|
-
- Images that break the layout
|
|
72
|
-
|
|
73
|
-
Fix any issues, then reset to desktop before continuing:
|
|
74
|
-
|
|
75
|
-
```
|
|
76
|
-
mcp__chrome-devtools__emulate viewport: "1440x900x1"
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### 9. Performance *(skip in quick mode)*
|
|
80
|
-
|
|
81
|
-
**Run the trace against the production server, not dev.** Navigate to `http://localhost:3001/<route>` first — if the production server is no longer running, run `pulse_build` again. Tracing the dev server (port 3000) reports misleading LCP/CLS. Then run `mcp__chrome-devtools__performance_start_trace` with `reload: true` and `autoStop: true`. Report LCP and CLS. Flag any LCP insight that suggests a fixable problem (render-blocking resources, large image load delay, etc.).
|
|
82
|
-
|
|
83
|
-
After the trace, call `pulse_restart_server` to return to the dev server.
|
|
84
|
-
|
|
85
|
-
### 10. Console errors
|
|
86
|
-
|
|
87
|
-
Use `mcp__chrome-devtools__list_console_messages` — report any errors or unexpected warnings.
|
|
88
|
-
|
|
89
|
-
### 11. Code review
|
|
90
|
-
|
|
91
|
-
Call `pulse_review` with the spec file path. This runs a full code review AND atomically writes the `.pulse-verified` stamp server-side, eliminating any race condition with the stop hook.
|
|
92
|
-
|
|
93
|
-
Work through every item the review returns. Fix anything that fails before proceeding.
|
|
94
|
-
|
|
95
|
-
### 12. Close extra browser tabs
|
|
96
|
-
|
|
97
|
-
Use `mcp__chrome-devtools__list_pages` to get all open pages. Close every page **except the last one** — `close_page` refuses to close the final tab. `pageId` must be a number, not a string.
|
|
98
|
-
|
|
99
|
-
If there is only one page open, skip this step — there is nothing to close.
|
|
100
|
-
|
|
101
|
-
### 13. Write verification stamp
|
|
102
|
-
|
|
103
|
-
Call `pulse_stamp`. This writes `.pulse-verified` via the MCP server, which is more reliable than the `date +%s` shell command — the MCP write always lands after all spec edits, avoiding the mtime race condition that can cause the stop hook to block immediately after verification.
|
|
104
|
-
|
|
105
|
-
**This must be the last operation before stopping.** The stop hook compares each edited spec's mtime against this stamp — if any spec is newer than the stamp, the hook blocks. Do not edit any spec file after calling `pulse_stamp`.
|
|
106
|
-
|
|
107
|
-
### 14. Report
|
|
108
|
-
|
|
109
|
-
Summarise:
|
|
110
|
-
- Validation: pass or fail (with errors if any)
|
|
111
|
-
- Visual: desktop screenshot — any issues found and fixed
|
|
112
|
-
- Console: any errors
|
|
113
|
-
- Review: pass or issues found and fixed
|
|
114
|
-
- **Full mode only:** Lighthouse desktop/mobile scores, mobile layout check, LCP and CLS values
|
|
115
|
-
|
|
116
|
-
In quick mode, end with: "Quick check passed — run `/verify` when you're ready for the full Lighthouse audit."
|
|
117
|
-
|
|
118
|
-
In full mode, only confirm the page is good when validation passes, both Lighthouse runs are 100/100/100, CLS is 0.00, and there are no console errors. Otherwise, fix and run `/verify` again.
|
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
## Spec review checklist
|
|
2
|
-
|
|
3
|
-
Before finishing any spec, verify every point below. Fix anything that fails.
|
|
4
|
-
|
|
5
|
-
### Critical
|
|
6
|
-
|
|
7
|
-
- **`meta` must be a plain object — never a function.** Individual fields (`title`, `description`, `styles`) can be `async (ctx) => value` functions, but `meta` itself is always `{}`.
|
|
8
|
-
|
|
9
|
-
```js
|
|
10
|
-
// WRONG — meta is not a function factory
|
|
11
|
-
meta: async (ctx) => ({ title: '...', styles: [...] })
|
|
12
|
-
|
|
13
|
-
// CORRECT — meta is a plain object; individual fields are functions
|
|
14
|
-
meta: {
|
|
15
|
-
title: async (ctx) => (await post(ctx)).frontmatter.title,
|
|
16
|
-
description: async (ctx) => (await post(ctx)).frontmatter.description,
|
|
17
|
-
styles: ['/pulse-ui.css', '/app.css'],
|
|
18
|
-
}
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
- **Do not set `hydrate` in specs.** It is auto-derived by the framework from the URL entry passed to `createServer`. Specs with `mutations`, `actions`, or `persist` are hydrated automatically. Purely server-rendered specs get zero JavaScript — no configuration needed.
|
|
22
|
-
|
|
23
|
-
- **The default theme is DARK.** When `meta.theme` is unset, the page renders with the dark palette. If the design calls for a light page, set `meta.theme: 'light'` when you **first write the spec** — do not assume light and discover dark at the screenshot, which costs an edit → restart → re-approval cycle. Declare the theme in your plan/build brief, and ask the user if the brief doesn't say.
|
|
24
|
-
|
|
25
|
-
- **Always restart the server and reload the browser after every file edit — before checking the result.** The dev server restarts on file changes but the browser tab stays stale. The required sequence is: `pulse_restart_server` → `navigate_page`. Never attempt to debug a visual problem without doing both steps first.
|
|
26
|
-
|
|
27
|
-
### Components first — or creative override
|
|
28
|
-
|
|
29
|
-
- **Before writing any HTML by hand, check `src/ui/index.js`.** There are 50+ components. Use `button`, `card`, `alert`, `input`, `spinner`, `badge`, `modal`, `nav`, `pagination`, `table`, etc. before writing equivalent HTML from scratch.
|
|
30
|
-
- **Do not reinvent component patterns.** These patterns have dedicated components — never write custom HTML for them unless a creative override applies (see below):
|
|
31
|
-
- Hero sections → `hero({ title, subtitle, actions, image, ... })`
|
|
32
|
-
- Product/service cards → `card({ title, body, image, footer, ... })`
|
|
33
|
-
- Image + text two-column layouts → `media({ image, content, reverse })`
|
|
34
|
-
- Feature tiles → `feature({ icon, title, description })`
|
|
35
|
-
- Testimonials → `card({ content, footer })` with quote + avatar/name in the content slot
|
|
36
|
-
- CTAs → `cta({ title, subtitle, actions })`
|
|
37
|
-
- Navigation → `nav({ logo, links, actions })`
|
|
38
|
-
- Footers → `footer({ logo, links, columns, legal, ... })`
|
|
39
|
-
|
|
40
|
-
- **Creative override — raw HTML throughout is permitted** when the design intent genuinely calls for it: a highly custom visual identity, an asymmetric or typographically-driven layout, a brutalist/editorial/retro/neon/paper vibe where components would constrain the expression, or a layout that requires features a component simply doesn't support (e.g. full-viewport height, custom gradient glows, clamp-scaled display type). This is a deliberate design decision, not a shortcut. When taking this path:
|
|
41
|
-
- **Declare it in a comment at the top of the spec** so the reviewer recognises it: `// component-free — creative override: <reason>`. This is the signal the review tool reads.
|
|
42
|
-
- Functional atoms (`button`, `input`, `badge`, `modal`) should still come from components unless there is a specific design reason not to.
|
|
43
|
-
- `/pulse-ui.css` is always required — it provides the token system your custom CSS will use.
|
|
44
|
-
- The quality gate replaces the component checklist: **Lighthouse 100 on Accessibility, Best Practices, and SEO — desktop and mobile — plus CLS 0.00 is the pass bar.** (Performance is reported but not gated; it varies with machine load.) A component-free page that passes every audit is correct. A component-heavy page that fails accessibility is not.
|
|
45
|
-
|
|
46
|
-
- **When reviewing**: before flagging a class name like `.hero` as a violation, first ask whether the `hero()` component could actually reproduce the layout. If the answer is no, it is a creative override — check Lighthouse instead of refactoring. Do not flatten a good custom layout into a generic component just to clear a pattern-match.
|
|
47
|
-
|
|
48
|
-
- **The review tool auto-detects creative override** from the spec comment. When detected, component pattern checks are shown as advisory (`⚡`) not as failures, and the Lighthouse gate is the stated pass bar.
|
|
49
|
-
|
|
50
|
-
- **Never write App Store or Google Play download buttons by hand.** Always use `appBadge({ store: 'apple', href })` and `appBadge({ store: 'google', href })`. Raw `<a>` tags with badge images are incorrect — `appBadge` renders the correct accessible, styled badge for each store.
|
|
51
|
-
|
|
52
|
-
```js
|
|
53
|
-
import { appBadge } from '@invisibleloop/pulse/ui'
|
|
54
|
-
|
|
55
|
-
// In a hero() actions slot:
|
|
56
|
-
${hero({
|
|
57
|
-
actions: `
|
|
58
|
-
${appBadge({ store: 'apple', href: 'https://apps.apple.com/...' })}
|
|
59
|
-
${appBadge({ store: 'google', href: 'https://play.google.com/...' })}
|
|
60
|
-
`,
|
|
61
|
-
})}
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
### Reuse (DRY)
|
|
65
|
-
|
|
66
|
-
- **Extract a view helper when the same HTML pattern appears 3 or more times in a single spec.** A plain JS function returning an HTML string is sufficient — no framework needed:
|
|
67
|
-
```js
|
|
68
|
-
const card = ({ title, body }) => `<div class="card"><h3>${title}</h3><p>${body}</p></div>`
|
|
69
|
-
```
|
|
70
|
-
- **Create a shared component in `src/components/` when the same pattern is needed across 2 or more different specs.** A named export that returns an HTML string — same shape as the `layout()` pattern in the routing guide. Never paste a section's HTML into a second spec: extract it at the second use, then import it from both. (`src/ui/` is the framework's own library inside the package — project-level shared code always goes in `src/components/`.)
|
|
71
|
-
- **Do not abstract a pattern that appears only once.** Duplication is cheaper than the wrong abstraction. Within a single spec, wait until the third use before extracting a helper; across specs, extract at the second use.
|
|
72
|
-
|
|
73
|
-
### Correctness
|
|
74
|
-
|
|
75
|
-
- Mutations return plain partial-state objects and have no side effects (no fetch, no DOM access).
|
|
76
|
-
- `persist` contains only serialisable state that should survive a page reload — not ephemeral UI state like a loading flag or temporary selection.
|
|
77
|
-
- `e.target` assumptions in mutations are safe if the element has child nodes — use `e.target.closest('[data-index]')` rather than assuming `e.target` is the element with the attribute.
|
|
78
|
-
- State shape is consistent — avoid a single field that is sometimes `null`, sometimes a string, sometimes a boolean. Use a dedicated `status` field instead.
|
|
79
|
-
- **Never use `state.modalOpen` or conditional modal rendering.** This destroys the `<dialog>` on every render, breaking focus, animation, and native ESC handling. Instead, always render the `<dialog>` in the DOM and use `data-dialog-open="id"` to open it — the runtime handles this without any spec state:
|
|
80
|
-
```html
|
|
81
|
-
<!-- always in the view, never conditional -->
|
|
82
|
-
${modal({ id: 'confirm', title: 'Confirm', content: '...' })}
|
|
83
|
-
<!-- anywhere on the page — opens the dialog, no mutation needed -->
|
|
84
|
-
${modalTrigger({ target: 'confirm', label: 'Open' })}
|
|
85
|
-
<!-- or inline: -->
|
|
86
|
-
<button data-dialog-open="confirm">Open</button>
|
|
87
|
-
```
|
|
88
|
-
Close is handled natively by `<form method="dialog">` (inside the modal), ESC key, backdrop click, or `data-dialog-close` on any element.
|
|
89
|
-
|
|
90
|
-
### Defensive data handling
|
|
91
|
-
|
|
92
|
-
- **Always check `res.ok` before parsing fetch responses.** Never call `res.json()` on a response that may have failed. Never use `await res.text()` as an error message — on a 404 or 500, this returns raw HTML which surfaces directly in toasts and error alerts. The correct pattern:
|
|
93
|
-
```js
|
|
94
|
-
const res = await fetch('/api/...')
|
|
95
|
-
if (!res.ok) {
|
|
96
|
-
let message = `Request failed: ${res.status}`
|
|
97
|
-
try { const j = await res.json(); message = j.message || j.error || message } catch {}
|
|
98
|
-
throw new Error(message)
|
|
99
|
-
}
|
|
100
|
-
return await res.json()
|
|
101
|
-
```
|
|
102
|
-
- **Never assume the shape of data from external APIs or server fetchers.** Use optional chaining (`?.`) and nullish coalescing (`??`) at every access point. If a server fetcher can return `null`, the view must handle it — `server.user?.name ?? 'Guest'` not `server.user.name`.
|
|
103
|
-
- **Validate FormData fields before use.** `formData.get('email')` returns `null` if the field is missing. Check for null/empty before passing to an API or database.
|
|
104
|
-
- **Do not trust URL params.** `ctx.params.id` is a raw string from the URL. Validate it before use — check it exists, is the right type, and refers to a real resource. Return a 404 or redirect if it doesn't.
|
|
105
|
-
|
|
106
|
-
### CSS
|
|
107
|
-
|
|
108
|
-
- **`app.css` must contain no hex values or raw colour values.** A lint hook enforces this and will block the build. Hex values belong in `public/theme.css` as token definitions; `app.css` references them via `var()` only.
|
|
109
|
-
- **Load order in `meta.styles`:** `pulse-ui.css` → `theme.css` → `app.css`. Theme tokens must be defined before `app.css` references them.
|
|
110
|
-
- **CSS for a shared section lives in the shared `app.css`, once.** Every spec that renders the section lists the same file in `meta.styles`. Never copy a CSS block into a second per-page stylesheet — two copies drift apart. Per-page stylesheets are for styles only one page uses.
|
|
111
|
-
|
|
112
|
-
### Security
|
|
113
|
-
|
|
114
|
-
- Any value from user input (URL params, form fields, external APIs) interpolated into view HTML must be escaped.
|
|
115
|
-
- **Every `<form method="POST">` on a `submit` page includes `${server.csrf}`.** CSRF is enforced — a form without the hidden token field gets 403 on submit. Never set `csrf: false` to "fix" a 403; that disables the protection. `csrf: false` is only for endpoints with their own authentication (e.g. signed webhooks).
|
|
116
|
-
- **`submit` returns `{ redirect }` after a successful mutation** (POST-redirect-GET). Rendering success directly from the POST means browser refresh resubmits the form.
|
|
117
|
-
|
|
118
|
-
### Server-side forms & error pages
|
|
119
|
-
|
|
120
|
-
- **Forms that must work without JavaScript use `spec.submit`** + `<form method="POST">` with `${server.csrf}` inside. Hydrated `data-action` on the same form is the enhancement, not the requirement.
|
|
121
|
-
- On a validation re-render, show `server.form?.errors` and echo `server.form?.values` back into the inputs via `value=` — failed validation must not wipe what the user typed.
|
|
122
|
-
- **Every site has a custom 404 page** — a spec with `route: '*'`, using the shared `layout()` so it carries the site's nav and footer. Without it, bad URLs show the framework's unbranded default.
|
|
123
|
-
|
|
124
|
-
### Tests
|
|
125
|
-
|
|
126
|
-
- Pure logic functions extracted from a spec (e.g. `checkResult`, `validate`, `formatPrice`) must have unit tests in a corresponding `.test.js` file.
|
|
127
|
-
- **When fixing a bug, write a failing test first.** The test must reproduce the bug before the fix is applied, then pass after. This pins the behaviour so the bug cannot silently return. A fix without a regression test is incomplete.
|
|
128
|
-
- **Use `renderSync` / `render` from `@invisibleloop/pulse/testing` to test view HTML output.** Do not test views with raw `html.includes()` — use the query helpers instead:
|
|
129
|
-
```js
|
|
130
|
-
import { renderSync, render } from '@invisibleloop/pulse/testing'
|
|
131
|
-
|
|
132
|
-
// Sync — call view directly with mock state/server
|
|
133
|
-
const result = renderSync(mySpec, { state: { count: 5 }, server: { items: [] } })
|
|
134
|
-
assert(result.has('button'))
|
|
135
|
-
assert.equal(result.get('#count').text, '5')
|
|
136
|
-
assert.equal(result.count('li'), 0)
|
|
137
|
-
|
|
138
|
-
// Async — run real server fetchers (integration), or pass server to skip them
|
|
139
|
-
const result = await render(mySpec, { server: { product: mockProduct } })
|
|
140
|
-
assert.equal(result.get('h1').text, mockProduct.name)
|
|
141
|
-
assert.equal(result.attr('img', 'src'), mockProduct.image)
|
|
142
|
-
```
|
|
143
|
-
Supported selectors: `tag`, `.class`, `#id`, `[attr]`, `[attr="value"]`, combinations (`button.primary[disabled]`), and **descendant combinators** (`result.count('.parent li')`, `result.find('tbody tr')`). Descendant selectors match the rightmost token within the ancestor context — they do not return 0 if the ancestor doesn't exist, they simply find no matching descendants.
|
|
144
|
-
|
|
145
|
-
### Markdown
|
|
146
|
-
|
|
147
|
-
- **Use `md()` from `@invisibleloop/pulse/md` for `.md` file content.** Never read and parse markdown manually. Pass the result's `html` to `prose()`.
|
|
148
|
-
- **Call the same `md()` fetcher in both `meta` and `server`** — it caches on `ctx._mdCache` so the file is only read once per request. Do not create two separate fetchers for the same file.
|
|
149
|
-
- **Always add `onViewError` on dynamic markdown routes** (routes with `:param` segments loading `.md` files). A missing file throws `{ status: 404 }` which must be caught gracefully.
|
|
150
|
-
|
|
151
|
-
### View error handling
|
|
152
|
-
|
|
153
|
-
- **Define `onViewError` on any page where the view could throw due to bad or missing data.** Without it, a runtime view error returns a 500 on the server and shows a generic inline message on the client. With it, the server returns 200 with your fallback HTML instead of a 500, and the client renders your fallback:
|
|
154
|
-
```js
|
|
155
|
-
onViewError: (err, state, serverState) => `
|
|
156
|
-
<div class="u-p-4 u-text-center">
|
|
157
|
-
<p>Something went wrong. <a href="">Reload</a></p>
|
|
158
|
-
</div>
|
|
159
|
-
`
|
|
160
|
-
```
|
|
161
|
-
Use this on pages that render data from external APIs or user-supplied content — any path where the view can encounter `null`, `undefined`, or unexpected shapes that would cause a crash. It is not required on simple pages with predictable data.
|
|
162
|
-
|
|
163
|
-
### Store updates from actions
|
|
164
|
-
|
|
165
|
-
- **Use `_storeUpdate` to push changes to the global store from an action.** Return it from `onSuccess` alongside the local state update — it is stripped from page state and forwarded to the store. All mounted pages that subscribe to the affected keys re-render immediately:
|
|
166
|
-
```js
|
|
167
|
-
onSuccess: (state, theme) => ({
|
|
168
|
-
saved: true,
|
|
169
|
-
_storeUpdate: { settings: { theme } }, // ← merged into store state
|
|
170
|
-
}),
|
|
171
|
-
```
|
|
172
|
-
`_storeUpdate` only merges into the store — it does not appear in the page's own state. The rest of the return is merged into local state as normal. Use this instead of a full-page reload when a user action changes shared data (theme, cart count, user profile, etc.).
|
|
173
|
-
|
|
174
|
-
### Accessibility
|
|
175
|
-
|
|
176
|
-
- Interactive elements without visible text have an `aria-label`.
|
|
177
|
-
- Disabled state is reflected with the `disabled` attribute, not just CSS.
|
|
178
|
-
- The page has a `<main id="main-content">` landmark.
|
|
179
|
-
- **Every element with `data-event`, `data-store-event`, `data-dialog-open`, or `data-dialog-close` is natively interactive (`button`, `a`, `input`, `select`, `textarea`, `summary`) or has `tabindex="0"`.** Non-interactive tags (`div`, `span`, `li`, `td`, etc.) with these attributes are not keyboard reachable. Prefer `<button>` — it is focusable, fires on Enter/Space, and carries the correct ARIA role for free. Only use `tabindex="0"` on a non-interactive element when a `<button>` genuinely cannot be used.
|
|
180
|
-
- **Every interactive element communicates its purpose and state:**
|
|
181
|
-
- Buttons have visible text or `aria-label`. Links use descriptive text — "click here", "here", "read more" are failures.
|
|
182
|
-
- Form inputs have an associated `<label for="id">` or `aria-label`. `placeholder` alone is not a label.
|
|
183
|
-
- Toggle controls (open/close, show/hide, expand/collapse) carry `aria-expanded="true|false"` or `aria-pressed="true|false"`.
|
|
184
|
-
- While an action is in flight, the trigger button has `aria-busy="true"` or its label changes (e.g. "Saving…"). A spinner alone is not enough.
|
|
185
|
-
- Active nav items have `aria-current="page"`. Selected items in a list, tab set, or option group have `aria-selected="true"`.
|
|
186
|
-
- **The page has a logical tab order:**
|
|
187
|
-
- No `tabindex` value greater than 0. Positive tabindex values override the natural DOM order and create a broken tab sequence. Remove them and reorder the DOM instead.
|
|
188
|
-
- Visually hidden interactive content (off-canvas menus, collapsed panels, `opacity:0` elements) is removed from the tab order with `tabindex="-1"` or the `inert` attribute — CSS alone does not remove an element from tab order.
|
|
189
|
-
- DOM order matches the visual reading order. When flexbox `order` or CSS grid placement visually reposition elements, the tab sequence still follows the DOM — author the DOM in the order a user would read and interact with the page.
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [main]
|
|
8
|
-
|
|
9
|
-
permissions:
|
|
10
|
-
contents: read
|
|
11
|
-
|
|
12
|
-
jobs:
|
|
13
|
-
test:
|
|
14
|
-
runs-on: ubuntu-latest
|
|
15
|
-
steps:
|
|
16
|
-
- uses: actions/checkout@v4
|
|
17
|
-
|
|
18
|
-
- uses: actions/setup-node@v4
|
|
19
|
-
with:
|
|
20
|
-
node-version: 22
|
|
21
|
-
cache: npm
|
|
22
|
-
|
|
23
|
-
- run: npm ci
|
|
24
|
-
|
|
25
|
-
- run: npm test
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
name: Release
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main]
|
|
6
|
-
|
|
7
|
-
jobs:
|
|
8
|
-
release:
|
|
9
|
-
runs-on: ubuntu-latest
|
|
10
|
-
permissions:
|
|
11
|
-
contents: write
|
|
12
|
-
id-token: write
|
|
13
|
-
steps:
|
|
14
|
-
- uses: actions/checkout@v4
|
|
15
|
-
with:
|
|
16
|
-
token: ${{ secrets.RELEASE_PAT }}
|
|
17
|
-
|
|
18
|
-
- uses: actions/setup-node@v4
|
|
19
|
-
with:
|
|
20
|
-
node-version: 22
|
|
21
|
-
cache: npm
|
|
22
|
-
registry-url: https://registry.npmjs.org
|
|
23
|
-
|
|
24
|
-
- run: npm ci
|
|
25
|
-
|
|
26
|
-
- run: npm test
|
|
27
|
-
|
|
28
|
-
- name: Bump version from conventional commits, then publish
|
|
29
|
-
env:
|
|
30
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
31
|
-
run: |
|
|
32
|
-
VERSION=$(node scripts/release-version.js)
|
|
33
|
-
# Fallback: if this exact version is somehow already on npm, bump patch
|
|
34
|
-
while npm view @invisibleloop/pulse@$VERSION version 2>/dev/null | grep -q .; do
|
|
35
|
-
VERSION=$(node -e "const [a,b,c]='$VERSION'.split('.').map(Number);console.log(a+'.'+b+'.'+(c+1))")
|
|
36
|
-
done
|
|
37
|
-
npm version $VERSION --no-git-tag-version --allow-same-version
|
|
38
|
-
echo "$VERSION" > public/.pulse-ui-version
|
|
39
|
-
git config user.name "github-actions[bot]"
|
|
40
|
-
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
41
|
-
git add package.json package-lock.json public/.pulse-ui-version
|
|
42
|
-
git commit -m "chore: release $VERSION [skip ci]" || true
|
|
43
|
-
git pull --rebase
|
|
44
|
-
git push
|
|
45
|
-
npm publish --provenance --access public
|