@invisibleloop/pulse 0.20.11 → 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 +13 -6
- package/src/runtime/ssr.test.js +31 -0
- package/src/server/index.js +162 -9
- 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 -350
- 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
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* pulse-ui.js — Vanilla JS for interactive Pulse UI components
|
|
3
|
-
*
|
|
4
|
-
* Handles:
|
|
5
|
-
* - Slider (.ui-slider — live fill + value output)
|
|
6
|
-
* - File Upload (.ui-upload — drag-and-drop, click-to-open)
|
|
7
|
-
* - Modal (data-modal-open, <dialog>, backdrop close)
|
|
8
|
-
* - Nav (.ui-nav — mobile burger menu)
|
|
9
|
-
* - Carousel (.ui-carousel — prev/next/dots)
|
|
10
|
-
*
|
|
11
|
-
* No dependencies. No build step required.
|
|
12
|
-
* CSP-safe: no inline handlers — all behaviour via event delegation.
|
|
13
|
-
* Include once per page: <script src="/pulse-ui.js"></script>
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
// ─── Slider ──────────────────────────────────────────────────────────────────
|
|
17
|
-
// Update --slider-fill on .ui-field wrapper + optional live value output.
|
|
18
|
-
|
|
19
|
-
document.addEventListener('input', (e) => {
|
|
20
|
-
const el = e.target
|
|
21
|
-
if (!el.classList.contains('ui-slider')) return
|
|
22
|
-
const field = el.closest('.ui-field')
|
|
23
|
-
if (!field) return
|
|
24
|
-
const pct = ((el.value - el.min) / (el.max - el.min) * 100).toFixed(2) + '%'
|
|
25
|
-
field.style.setProperty('--slider-fill', pct)
|
|
26
|
-
const out = field.querySelector('.ui-slider-output')
|
|
27
|
-
if (out) out.textContent = el.value
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
// ─── File Upload ─────────────────────────────────────────────────────────────
|
|
31
|
-
// Prevent browser navigating to dropped file anywhere on the page.
|
|
32
|
-
// Handle drag visual feedback + file assignment on drop.
|
|
33
|
-
// Handle click-to-open and keyboard activation.
|
|
34
|
-
|
|
35
|
-
document.addEventListener('dragover', (e) => {
|
|
36
|
-
e.preventDefault()
|
|
37
|
-
if (!(e.target instanceof Element)) return
|
|
38
|
-
const zone = e.target.closest('.ui-upload:not(.ui-upload--disabled)')
|
|
39
|
-
if (zone) zone.classList.add('ui-upload--active')
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
document.addEventListener('dragleave', (e) => {
|
|
43
|
-
if (!(e.target instanceof Element)) return
|
|
44
|
-
const zone = e.target.closest('.ui-upload')
|
|
45
|
-
if (zone && !zone.contains(e.relatedTarget)) {
|
|
46
|
-
zone.classList.remove('ui-upload--active')
|
|
47
|
-
}
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
document.addEventListener('drop', (e) => {
|
|
51
|
-
e.preventDefault()
|
|
52
|
-
if (!(e.target instanceof Element)) return
|
|
53
|
-
const zone = e.target.closest('.ui-upload:not(.ui-upload--disabled)')
|
|
54
|
-
if (!zone) return
|
|
55
|
-
zone.classList.remove('ui-upload--active')
|
|
56
|
-
const input = zone.querySelector('.ui-upload-input')
|
|
57
|
-
if (!input || !e.dataTransfer.files.length) return
|
|
58
|
-
const dt = new DataTransfer()
|
|
59
|
-
Array.from(e.dataTransfer.files).forEach(f => dt.items.add(f))
|
|
60
|
-
input.files = dt.files
|
|
61
|
-
input.dispatchEvent(new Event('change', { bubbles: true }))
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
// File Upload: show selected filename in zone after file chosen
|
|
65
|
-
document.addEventListener('change', (e) => {
|
|
66
|
-
const input = e.target
|
|
67
|
-
if (!input.classList.contains('ui-upload-input')) return
|
|
68
|
-
const zone = input.closest('.ui-upload')
|
|
69
|
-
if (!zone) return
|
|
70
|
-
const textEl = zone.querySelector('.ui-upload-text')
|
|
71
|
-
if (!textEl) return
|
|
72
|
-
if (input.files && input.files.length > 0) {
|
|
73
|
-
const names = Array.from(input.files).map(f => f.name).join(', ')
|
|
74
|
-
textEl.textContent = names
|
|
75
|
-
zone.classList.add('ui-upload--selected')
|
|
76
|
-
} else {
|
|
77
|
-
textEl.innerHTML = 'Drag & drop or <span class="ui-upload-browse">browse</span>'
|
|
78
|
-
zone.classList.remove('ui-upload--selected')
|
|
79
|
-
}
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
// ─── Modal ──────────────────────────────────────────────────────────────────
|
|
83
|
-
|
|
84
|
-
document.addEventListener('click', (e) => {
|
|
85
|
-
// File upload: click zone → open file picker
|
|
86
|
-
// Skip synthetic clicks emitted by input.click() itself to avoid loops
|
|
87
|
-
if (!e.target.classList.contains('ui-upload-input')) {
|
|
88
|
-
const zone = e.target.closest('.ui-upload')
|
|
89
|
-
if (zone) {
|
|
90
|
-
const input = zone.querySelector('.ui-upload-input')
|
|
91
|
-
if (input && !input.disabled) { input.click(); return }
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Dialog open: data-dialog-open="dialogId" (first-class) or data-modal-open="dialogId" (compat)
|
|
96
|
-
const trigger = e.target.closest('[data-dialog-open],[data-modal-open]')
|
|
97
|
-
if (trigger) {
|
|
98
|
-
const id = trigger.dataset.dialogOpen ?? trigger.dataset.modalOpen
|
|
99
|
-
const dialog = document.getElementById(id)
|
|
100
|
-
if (dialog && typeof dialog.showModal === 'function') {
|
|
101
|
-
dialog.showModal()
|
|
102
|
-
}
|
|
103
|
-
return
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Dialog close: data-dialog-close (closes nearest ancestor <dialog>)
|
|
107
|
-
const closeTarget = e.target.closest('[data-dialog-close]')
|
|
108
|
-
if (closeTarget) {
|
|
109
|
-
const dialog = closeTarget.closest('dialog')
|
|
110
|
-
if (dialog) dialog.close()
|
|
111
|
-
return
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Backdrop close — click lands on <dialog> element itself
|
|
115
|
-
if (e.target.tagName === 'DIALOG') {
|
|
116
|
-
e.target.close()
|
|
117
|
-
}
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
// File upload: Enter / Space on focused zone → open picker
|
|
121
|
-
document.addEventListener('keydown', (e) => {
|
|
122
|
-
if (e.key !== 'Enter' && e.key !== ' ') return
|
|
123
|
-
const zone = e.target.closest('.ui-upload')
|
|
124
|
-
if (!zone) return
|
|
125
|
-
const input = zone.querySelector('.ui-upload-input')
|
|
126
|
-
if (input && !input.disabled) { e.preventDefault(); input.click() }
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
// ─── Nav (mobile burger) ────────────────────────────────────────────────────
|
|
130
|
-
|
|
131
|
-
function initNav(el) {
|
|
132
|
-
if (el.dataset.pulseNavInit) return
|
|
133
|
-
el.dataset.pulseNavInit = '1'
|
|
134
|
-
|
|
135
|
-
const burger = el.querySelector('.ui-nav-burger')
|
|
136
|
-
const mobile = el.querySelector('.ui-nav-mobile')
|
|
137
|
-
|
|
138
|
-
// Mobile burger
|
|
139
|
-
if (burger && mobile) {
|
|
140
|
-
const open = () => { el.classList.add('ui-nav--open'); burger.setAttribute('aria-expanded', 'true') }
|
|
141
|
-
const close = () => { el.classList.remove('ui-nav--open'); burger.setAttribute('aria-expanded', 'false') }
|
|
142
|
-
const toggle = () => el.classList.contains('ui-nav--open') ? close() : open()
|
|
143
|
-
|
|
144
|
-
burger.addEventListener('click', toggle)
|
|
145
|
-
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') close() })
|
|
146
|
-
document.addEventListener('click', (e) => { if (!el.contains(e.target)) close() })
|
|
147
|
-
mobile.querySelectorAll('.ui-nav-link').forEach(a => a.addEventListener('click', close))
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Mega nav dropdowns
|
|
151
|
-
el.querySelectorAll('.ui-nav-mega-wrap').forEach(wrap => {
|
|
152
|
-
const trigger = wrap.querySelector('.ui-nav-mega-trigger')
|
|
153
|
-
const panel = wrap.querySelector('.ui-nav-mega-panel')
|
|
154
|
-
if (!trigger || !panel) return
|
|
155
|
-
|
|
156
|
-
const openMega = () => { panel.hidden = false; wrap.classList.add('ui-nav-mega-wrap--open'); trigger.setAttribute('aria-expanded', 'true') }
|
|
157
|
-
const closeMega = () => { panel.hidden = true; wrap.classList.remove('ui-nav-mega-wrap--open'); trigger.setAttribute('aria-expanded', 'false') }
|
|
158
|
-
const toggleMega = () => panel.hidden ? openMega() : closeMega()
|
|
159
|
-
|
|
160
|
-
trigger.addEventListener('click', (e) => { e.stopPropagation(); toggleMega() })
|
|
161
|
-
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeMega() })
|
|
162
|
-
document.addEventListener('click', (e) => { if (!wrap.contains(e.target)) closeMega() })
|
|
163
|
-
panel.querySelectorAll('.ui-nav-mega-item').forEach(a => a.addEventListener('click', closeMega))
|
|
164
|
-
})
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function runNavs() { document.querySelectorAll('.ui-nav').forEach(initNav) }
|
|
168
|
-
document.readyState === 'loading'
|
|
169
|
-
? document.addEventListener('DOMContentLoaded', runNavs)
|
|
170
|
-
: runNavs()
|
|
171
|
-
document.addEventListener('pulse:navigate', runNavs)
|
|
172
|
-
|
|
173
|
-
// ─── Carousel ───────────────────────────────────────────────────────────────
|
|
174
|
-
|
|
175
|
-
function initCarousel(el) {
|
|
176
|
-
if (el.dataset.pulseCarouselInit) return
|
|
177
|
-
el.dataset.pulseCarouselInit = '1'
|
|
178
|
-
|
|
179
|
-
const track = el.querySelector('.ui-carousel-track')
|
|
180
|
-
const prev = el.querySelector('.ui-carousel-prev')
|
|
181
|
-
const next = el.querySelector('.ui-carousel-next')
|
|
182
|
-
const dots = Array.from(el.querySelectorAll('.ui-carousel-dot'))
|
|
183
|
-
const slides = Array.from(el.querySelectorAll('.ui-carousel-slide'))
|
|
184
|
-
|
|
185
|
-
if (!track || slides.length === 0) return
|
|
186
|
-
|
|
187
|
-
let current = 0
|
|
188
|
-
|
|
189
|
-
const updateArrows = () => {
|
|
190
|
-
if (prev) prev.hidden = current === 0
|
|
191
|
-
if (next) next.hidden = current === slides.length - 1
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const goTo = (i) => {
|
|
195
|
-
current = Math.max(0, Math.min(i, slides.length - 1))
|
|
196
|
-
track.scrollTo({ left: slides[current].offsetLeft, behavior: 'smooth' })
|
|
197
|
-
dots.forEach((d, j) => {
|
|
198
|
-
const active = j === current
|
|
199
|
-
d.classList.toggle('active', active)
|
|
200
|
-
d.setAttribute('aria-selected', String(active))
|
|
201
|
-
d.setAttribute('tabindex', active ? '0' : '-1')
|
|
202
|
-
})
|
|
203
|
-
updateArrows()
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
prev?.addEventListener('click', () => goTo(current - 1))
|
|
207
|
-
next?.addEventListener('click', () => goTo(current + 1))
|
|
208
|
-
dots.forEach((d, i) => d.addEventListener('click', () => goTo(i)))
|
|
209
|
-
|
|
210
|
-
// Keyboard navigation — roving tabindex for tablist
|
|
211
|
-
el.addEventListener('keydown', (e) => {
|
|
212
|
-
if (!e.target.classList.contains('ui-carousel-dot')) return
|
|
213
|
-
let next = null
|
|
214
|
-
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
|
215
|
-
e.preventDefault()
|
|
216
|
-
next = current > 0 ? current - 1 : slides.length - 1
|
|
217
|
-
} else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
|
218
|
-
e.preventDefault()
|
|
219
|
-
next = current < slides.length - 1 ? current + 1 : 0
|
|
220
|
-
} else if (e.key === 'Home') {
|
|
221
|
-
e.preventDefault()
|
|
222
|
-
next = 0
|
|
223
|
-
} else if (e.key === 'End') {
|
|
224
|
-
e.preventDefault()
|
|
225
|
-
next = slides.length - 1
|
|
226
|
-
}
|
|
227
|
-
if (next !== null) {
|
|
228
|
-
goTo(next)
|
|
229
|
-
dots[next]?.focus()
|
|
230
|
-
}
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
// Sync dot state and arrows when user swipes / scrolls
|
|
234
|
-
track.addEventListener('scroll', () => {
|
|
235
|
-
const trackLeft = track.getBoundingClientRect().left
|
|
236
|
-
const idx = slides.findIndex((s) => Math.abs(s.getBoundingClientRect().left - trackLeft) < 10)
|
|
237
|
-
if (idx !== -1 && idx !== current) {
|
|
238
|
-
current = idx
|
|
239
|
-
dots.forEach((d, j) => {
|
|
240
|
-
const active = j === current
|
|
241
|
-
d.classList.toggle('active', active)
|
|
242
|
-
d.setAttribute('aria-selected', String(active))
|
|
243
|
-
d.setAttribute('tabindex', active ? '0' : '-1')
|
|
244
|
-
})
|
|
245
|
-
updateArrows()
|
|
246
|
-
}
|
|
247
|
-
}, { passive: true })
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Initialise all carousels on the page
|
|
251
|
-
const run = () => document.querySelectorAll('.ui-carousel').forEach(initCarousel)
|
|
252
|
-
document.readyState === 'loading'
|
|
253
|
-
? document.addEventListener('DOMContentLoaded', run)
|
|
254
|
-
: run()
|
|
255
|
-
|
|
256
|
-
// Re-initialise after Pulse client-side navigation swaps the DOM
|
|
257
|
-
document.addEventListener('pulse:navigate', run)
|
package/examples/quiz.js
DELETED
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse — Quiz example
|
|
3
|
-
*
|
|
4
|
-
* Demonstrates:
|
|
5
|
-
* - Multi-step state machine (intro → question → results)
|
|
6
|
-
* - Pure mutation logic with no server data
|
|
7
|
-
* - Computed view output from state
|
|
8
|
-
* - Restart flow
|
|
9
|
-
*
|
|
10
|
-
* Run: node examples/dev.server.js → http://localhost:3001/quiz
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { button, badge, card, container, section, stack, cluster, progress, iconCheck, iconX } from '../src/ui/index.js'
|
|
14
|
-
import { examplesNav } from './shared.js'
|
|
15
|
-
|
|
16
|
-
export const QUESTIONS = [
|
|
17
|
-
{
|
|
18
|
-
id: 1,
|
|
19
|
-
text: 'What does SSR stand for in web development?',
|
|
20
|
-
options: ['Static Site Rendering', 'Server-Side Rendering', 'Stylesheet Rendering', 'Synchronous Script Runtime'],
|
|
21
|
-
answer: 1,
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
id: 2,
|
|
25
|
-
text: 'Which HTTP method is typically used to submit a form?',
|
|
26
|
-
options: ['GET', 'PUT', 'POST', 'PATCH'],
|
|
27
|
-
answer: 2,
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
id: 3,
|
|
31
|
-
text: 'What does CLS measure in Core Web Vitals?',
|
|
32
|
-
options: ['Cumulative Layout Shift', 'Content Loading Speed', 'CSS Load Score', 'Cached Layout State'],
|
|
33
|
-
answer: 0,
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
id: 4,
|
|
37
|
-
text: 'Which of these is NOT a valid HTTP status code?',
|
|
38
|
-
options: ['200 OK', '304 Not Modified', '418 I\'m a teapot', '512 Server Timeout'],
|
|
39
|
-
answer: 3,
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
id: 5,
|
|
43
|
-
text: 'What does the "hydrate" property in a Pulse spec enable?',
|
|
44
|
-
options: [
|
|
45
|
-
'Server-side data fetching',
|
|
46
|
-
'Client-side interactivity by importing the spec in the browser',
|
|
47
|
-
'Automatic CSS injection',
|
|
48
|
-
'Database connection pooling',
|
|
49
|
-
],
|
|
50
|
-
answer: 1,
|
|
51
|
-
},
|
|
52
|
-
]
|
|
53
|
-
|
|
54
|
-
export function scoreLabel(score, total) {
|
|
55
|
-
const pct = score / total
|
|
56
|
-
if (pct === 1) return { label: 'Perfect score!', variant: 'success' }
|
|
57
|
-
if (pct >= 0.8) return { label: 'Excellent!', variant: 'success' }
|
|
58
|
-
if (pct >= 0.6) return { label: 'Good effort!', variant: 'info' }
|
|
59
|
-
if (pct >= 0.4) return { label: 'Keep practising!', variant: 'warning' }
|
|
60
|
-
return { label: 'Better luck next time.', variant: 'error' }
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export default {
|
|
64
|
-
route: '/quiz',
|
|
65
|
-
|
|
66
|
-
meta: {
|
|
67
|
-
title: 'Quiz — Pulse',
|
|
68
|
-
description: 'A multiple-choice quiz built with Pulse. Demonstrates multi-step state machine and computed views.',
|
|
69
|
-
styles: ['/pulse-ui.css'],
|
|
70
|
-
theme: 'dark',
|
|
71
|
-
},
|
|
72
|
-
|
|
73
|
-
state: {
|
|
74
|
-
phase: 'intro', // intro | question | results
|
|
75
|
-
current: 0, // index into QUESTIONS
|
|
76
|
-
answers: [], // [number] — chosen option index per question
|
|
77
|
-
score: 0,
|
|
78
|
-
},
|
|
79
|
-
|
|
80
|
-
mutations: {
|
|
81
|
-
start: () => ({
|
|
82
|
-
phase: 'question',
|
|
83
|
-
current: 0,
|
|
84
|
-
answers: [],
|
|
85
|
-
score: 0,
|
|
86
|
-
}),
|
|
87
|
-
|
|
88
|
-
answer: (state, e) => {
|
|
89
|
-
const chosen = parseInt(e.target.closest('[data-option]')?.dataset.option, 10)
|
|
90
|
-
if (isNaN(chosen)) return state
|
|
91
|
-
|
|
92
|
-
const q = QUESTIONS[state.current]
|
|
93
|
-
const correct = chosen === q.answer
|
|
94
|
-
const answers = [...state.answers, chosen]
|
|
95
|
-
const score = state.score + (correct ? 1 : 0)
|
|
96
|
-
const last = state.current === QUESTIONS.length - 1
|
|
97
|
-
|
|
98
|
-
return {
|
|
99
|
-
answers,
|
|
100
|
-
score,
|
|
101
|
-
current: last ? state.current : state.current + 1,
|
|
102
|
-
phase: last ? 'results' : 'question',
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
|
-
|
|
106
|
-
restart: () => ({
|
|
107
|
-
phase: 'intro',
|
|
108
|
-
current: 0,
|
|
109
|
-
answers: [],
|
|
110
|
-
score: 0,
|
|
111
|
-
}),
|
|
112
|
-
},
|
|
113
|
-
|
|
114
|
-
view: (state) => {
|
|
115
|
-
const nav = examplesNav('<span>🧠 Quiz</span>', '/quiz')
|
|
116
|
-
|
|
117
|
-
if (state.phase === 'intro') {
|
|
118
|
-
return `
|
|
119
|
-
${nav}
|
|
120
|
-
<main id="main-content">
|
|
121
|
-
${section({ eyebrow: `${QUESTIONS.length} questions · multiple choice`, title: 'Web Dev Quiz', level: 1, subtitle: 'Test your knowledge of web development fundamentals — HTTP, performance, and Pulse.', align: 'center', content:
|
|
122
|
-
container({ size: 'sm', content:
|
|
123
|
-
cluster({ justify: 'center', content:
|
|
124
|
-
button({ label: 'Start quiz', size: 'lg', attrs: { 'data-event': 'start' } })
|
|
125
|
-
})
|
|
126
|
-
})
|
|
127
|
-
})}
|
|
128
|
-
</main>`
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (state.phase === 'question') {
|
|
132
|
-
const q = QUESTIONS[state.current]
|
|
133
|
-
const num = state.current + 1
|
|
134
|
-
const pct = Math.round((num / QUESTIONS.length) * 100)
|
|
135
|
-
|
|
136
|
-
return `
|
|
137
|
-
${nav}
|
|
138
|
-
<main id="main-content">
|
|
139
|
-
${section({ content:
|
|
140
|
-
container({ size: 'sm', content:
|
|
141
|
-
card({ content: stack({ gap: 'lg', content: `
|
|
142
|
-
|
|
143
|
-
${cluster({ justify: 'between', content:
|
|
144
|
-
`<span class="u-text-sm u-text-muted">Question ${num} of ${QUESTIONS.length}</span>` +
|
|
145
|
-
badge({ label: `${state.score} correct`, variant: 'success' })
|
|
146
|
-
})}
|
|
147
|
-
|
|
148
|
-
${progress({ value: pct, max: 100, label: `Question ${num} of ${QUESTIONS.length}` })}
|
|
149
|
-
|
|
150
|
-
${stack({ gap: 'md', content: `
|
|
151
|
-
<p class="u-text-lg u-font-semibold" id="question-text">${q.text}</p>
|
|
152
|
-
<ul role="list" aria-labelledby="question-text">
|
|
153
|
-
${q.options.map((opt, i) => `<li>${
|
|
154
|
-
button({
|
|
155
|
-
label: `${String.fromCharCode(65 + i)}. ${opt}`,
|
|
156
|
-
variant: 'secondary',
|
|
157
|
-
fullWidth: true,
|
|
158
|
-
attrs: { 'data-event': 'answer', 'data-option': String(i) },
|
|
159
|
-
})
|
|
160
|
-
}</li>`).join('')}
|
|
161
|
-
</ul>
|
|
162
|
-
` })}
|
|
163
|
-
|
|
164
|
-
` }) })
|
|
165
|
-
})
|
|
166
|
-
})}
|
|
167
|
-
</main>`
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Results
|
|
171
|
-
const { label, variant } = scoreLabel(state.score, QUESTIONS.length)
|
|
172
|
-
|
|
173
|
-
const resultRows = QUESTIONS.map((q, i) => {
|
|
174
|
-
const chosen = state.answers[i]
|
|
175
|
-
const correct = chosen === q.answer
|
|
176
|
-
return `<div class="u-flex u-items-start u-gap-3 u-p-3 u-border-b">
|
|
177
|
-
<span class="${correct ? 'u-text-green' : 'u-text-red'}">${correct ? iconCheck({ size: 16 }) : iconX({ size: 16 })}</span>
|
|
178
|
-
<div>
|
|
179
|
-
<p class="u-text-sm">${q.text}</p>
|
|
180
|
-
${!correct ? `<p class="u-text-sm u-text-green u-mt-1">Correct: <strong>${q.options[q.answer]}</strong></p>` : ''}
|
|
181
|
-
</div>
|
|
182
|
-
</div>`
|
|
183
|
-
}).join('')
|
|
184
|
-
|
|
185
|
-
return `
|
|
186
|
-
${nav}
|
|
187
|
-
<main id="main-content">
|
|
188
|
-
${section({ content:
|
|
189
|
-
container({ size: 'sm', content:
|
|
190
|
-
card({ content: stack({ gap: 'lg', align: 'center', content: `
|
|
191
|
-
|
|
192
|
-
<div class="u-flex u-items-end u-justify-center u-gap-1" aria-label="Your score">
|
|
193
|
-
<span class="u-text-4xl u-font-bold u-text-accent">${state.score}</span>
|
|
194
|
-
<span class="u-text-xl u-text-muted">/ ${QUESTIONS.length}</span>
|
|
195
|
-
</div>
|
|
196
|
-
|
|
197
|
-
${badge({ label, variant })}
|
|
198
|
-
|
|
199
|
-
<div class="u-w-full u-border u-rounded-md u-overflow-hidden">${resultRows}</div>
|
|
200
|
-
|
|
201
|
-
${button({ label: 'Try again', variant: 'secondary', attrs: { 'data-event': 'restart' } })}
|
|
202
|
-
|
|
203
|
-
` }) })
|
|
204
|
-
})
|
|
205
|
-
})}
|
|
206
|
-
</main>`
|
|
207
|
-
},
|
|
208
|
-
}
|