@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/examples/todos.js
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse — Todo list example
|
|
3
|
-
*
|
|
4
|
-
* Demonstrates:
|
|
5
|
-
* - Client-side CRUD mutations
|
|
6
|
-
* - persist (survives page reload)
|
|
7
|
-
* - Constraints (max 20 items)
|
|
8
|
-
* - Filter state (all / active / done)
|
|
9
|
-
* - Empty state handling
|
|
10
|
-
*
|
|
11
|
-
* Run: node examples/dev.server.js → http://localhost:3001/todos
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { button, card, checkbox, empty, input, section, container, stack, cluster } from '../src/ui/index.js'
|
|
15
|
-
import { examplesNav } from './shared.js'
|
|
16
|
-
|
|
17
|
-
function esc(s) {
|
|
18
|
-
return String(s)
|
|
19
|
-
.replace(/&/g, '&').replace(/</g, '<')
|
|
20
|
-
.replace(/>/g, '>').replace(/"/g, '"')
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function filterTodos(todos, filter) {
|
|
24
|
-
if (filter === 'active') return todos.filter(t => !t.done)
|
|
25
|
-
if (filter === 'done') return todos.filter(t => t.done)
|
|
26
|
-
return todos
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function countByStatus(todos) {
|
|
30
|
-
const active = todos.filter(t => !t.done).length
|
|
31
|
-
return { active, done: todos.length - active, total: todos.length }
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export default {
|
|
35
|
-
route: '/todos',
|
|
36
|
-
|
|
37
|
-
meta: {
|
|
38
|
-
title: 'Todos — Pulse',
|
|
39
|
-
description: 'A todo list built with Pulse. Demonstrates client mutations, persist, constraints, and filter state.',
|
|
40
|
-
styles: ['/pulse-ui.css'],
|
|
41
|
-
theme: 'dark',
|
|
42
|
-
},
|
|
43
|
-
|
|
44
|
-
state: {
|
|
45
|
-
todos: [], // [{ id, text, done }]
|
|
46
|
-
filter: 'all', // all | active | done
|
|
47
|
-
nextId: 1,
|
|
48
|
-
},
|
|
49
|
-
|
|
50
|
-
persist: ['todos', 'nextId'],
|
|
51
|
-
|
|
52
|
-
constraints: {
|
|
53
|
-
nextId: { min: 1 },
|
|
54
|
-
},
|
|
55
|
-
|
|
56
|
-
mutations: {
|
|
57
|
-
add: (state, formData) => {
|
|
58
|
-
const text = formData.get('text')?.trim()
|
|
59
|
-
if (!text || state.todos.length >= 20) return state
|
|
60
|
-
return {
|
|
61
|
-
todos: [...state.todos, { id: state.nextId, text, done: false }],
|
|
62
|
-
nextId: state.nextId + 1,
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
|
|
66
|
-
toggle: (state, e) => {
|
|
67
|
-
const id = parseInt(e.target.closest('[data-id]')?.dataset.id, 10)
|
|
68
|
-
return {
|
|
69
|
-
todos: state.todos.map(t => t.id === id ? { ...t, done: !t.done } : t),
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
|
|
73
|
-
remove: (state, e) => {
|
|
74
|
-
const id = parseInt(e.target.closest('[data-id]')?.dataset.id, 10)
|
|
75
|
-
return { todos: state.todos.filter(t => t.id !== id) }
|
|
76
|
-
},
|
|
77
|
-
|
|
78
|
-
clearDone: (state) => ({
|
|
79
|
-
todos: state.todos.filter(t => !t.done),
|
|
80
|
-
}),
|
|
81
|
-
|
|
82
|
-
setFilter: (_state, e) => ({
|
|
83
|
-
filter: e.target.closest('[data-filter]')?.dataset.filter || 'all',
|
|
84
|
-
}),
|
|
85
|
-
},
|
|
86
|
-
|
|
87
|
-
view: (state) => {
|
|
88
|
-
const counts = countByStatus(state.todos)
|
|
89
|
-
const visible = filterTodos(state.todos, state.filter)
|
|
90
|
-
const atLimit = state.todos.length >= 20
|
|
91
|
-
|
|
92
|
-
const filterBtn = (value, label, count) => button({
|
|
93
|
-
label: `${label} (${count})`,
|
|
94
|
-
variant: state.filter === value ? 'primary' : 'ghost',
|
|
95
|
-
size: 'sm',
|
|
96
|
-
attrs: { 'data-event': 'setFilter', 'data-filter': value, 'aria-pressed': String(state.filter === value) },
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
const todoItem = (todo, last = false) => `
|
|
100
|
-
<li class="u-flex u-items-center u-gap-3 u-p-3${last ? '' : ' u-border-b'}" data-id="${todo.id}">
|
|
101
|
-
${checkbox({
|
|
102
|
-
id: `todo-${todo.id}`,
|
|
103
|
-
event: 'change:toggle',
|
|
104
|
-
checked: todo.done,
|
|
105
|
-
class: 'u-flex-1',
|
|
106
|
-
labelHtml: `<span class="${todo.done ? 'u-text-muted' : ''}">${esc(todo.text)}</span>`,
|
|
107
|
-
})}
|
|
108
|
-
${button({
|
|
109
|
-
label: '',
|
|
110
|
-
variant: 'ghost',
|
|
111
|
-
size: 'sm',
|
|
112
|
-
attrs: { 'data-event': 'remove', 'aria-label': `Remove "${esc(todo.text)}"` },
|
|
113
|
-
icon: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`,
|
|
114
|
-
})}
|
|
115
|
-
</li>`
|
|
116
|
-
|
|
117
|
-
const listContent = visible.length === 0
|
|
118
|
-
? `<div class="u-p-6">${empty({
|
|
119
|
-
title: state.filter === 'done' ? 'No completed todos yet'
|
|
120
|
-
: state.filter === 'active' ? 'Nothing left to do!'
|
|
121
|
-
: 'No todos yet',
|
|
122
|
-
description: state.filter === 'all' ? 'Add something above to get started.' : '',
|
|
123
|
-
})}</div>`
|
|
124
|
-
: `<ul aria-label="Todo items">${visible.map((t, i) => todoItem(t, i === visible.length - 1)).join('')}</ul>`
|
|
125
|
-
|
|
126
|
-
return `
|
|
127
|
-
${examplesNav('<span>✓ Todos</span>', '/todos')}
|
|
128
|
-
|
|
129
|
-
<main id="main-content">
|
|
130
|
-
${section({ eyebrow: 'Client State & Persistence', title: 'My Todos', level: 1, align: 'center',
|
|
131
|
-
subtitle: 'Todos persist across reloads. Mutations are pure — add, toggle, remove, and filter without a server round-trip.',
|
|
132
|
-
content:
|
|
133
|
-
container({ size: 'md', content: stack({ gap: 'md', content: `
|
|
134
|
-
|
|
135
|
-
<form data-action="add" data-reset aria-label="Add a todo">
|
|
136
|
-
${stack({ gap: 'xs', content: `
|
|
137
|
-
<div class="u-flex u-gap-2 u-items-end">
|
|
138
|
-
${input({ name: 'text', placeholder: 'What needs doing?', disabled: atLimit, class: 'u-flex-1', attrs: { maxlength: '120', 'aria-label': 'New todo text' } })}
|
|
139
|
-
${button({ label: 'Add', type: 'submit', disabled: atLimit })}
|
|
140
|
-
</div>
|
|
141
|
-
${atLimit ? `<p class="u-text-sm u-text-yellow" role="alert">Maximum of 20 todos reached.</p>` : ''}
|
|
142
|
-
` })}
|
|
143
|
-
</form>
|
|
144
|
-
|
|
145
|
-
${cluster({ justify: 'between', content: `
|
|
146
|
-
<div role="group" aria-label="Filter todos" class="u-flex u-gap-1">
|
|
147
|
-
${filterBtn('all', 'All', counts.total)}
|
|
148
|
-
${filterBtn('active', 'Active', counts.active)}
|
|
149
|
-
${filterBtn('done', 'Done', counts.done)}
|
|
150
|
-
</div>
|
|
151
|
-
${counts.done > 0 && state.filter !== 'active'
|
|
152
|
-
? button({ label: 'Clear done', variant: 'ghost', size: 'sm', attrs: { 'data-event': 'clearDone' } })
|
|
153
|
-
: ''}
|
|
154
|
-
` })}
|
|
155
|
-
|
|
156
|
-
${card({ flush: true, content: listContent })}
|
|
157
|
-
|
|
158
|
-
` }) })
|
|
159
|
-
})}
|
|
160
|
-
</main>`
|
|
161
|
-
},
|
|
162
|
-
}
|
package/src/ui/fieldset.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse UI — Fieldset
|
|
3
|
-
*
|
|
4
|
-
* Semantic grouping of related form fields. Renders a <fieldset> with an
|
|
5
|
-
* accessible <legend>, styled to match the design system. Use inside a
|
|
6
|
-
* card() or directly inside a <form>.
|
|
7
|
-
*
|
|
8
|
-
* @param {object} opts
|
|
9
|
-
* @param {string} opts.legend - Group label (renders as <legend>)
|
|
10
|
-
* @param {string} opts.content - Raw HTML slot — input(), select(), grid(), etc.
|
|
11
|
-
* @param {'xs'|'sm'|'md'|'lg'} opts.gap - Gap between fields (default: 'md')
|
|
12
|
-
* @param {string} opts.class
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { escHtml as e } from '../html.js'
|
|
16
|
-
|
|
17
|
-
const GAPS = new Set(['xs', 'sm', 'md', 'lg'])
|
|
18
|
-
|
|
19
|
-
export function fieldset({
|
|
20
|
-
legend = '',
|
|
21
|
-
content = '',
|
|
22
|
-
gap = 'md',
|
|
23
|
-
class: cls = '',
|
|
24
|
-
} = {}) {
|
|
25
|
-
if (!GAPS.has(gap)) gap = 'md'
|
|
26
|
-
|
|
27
|
-
const classes = [
|
|
28
|
-
'ui-fieldset',
|
|
29
|
-
gap !== 'md' && `ui-fieldset--gap-${gap}`,
|
|
30
|
-
cls,
|
|
31
|
-
].filter(Boolean).join(' ')
|
|
32
|
-
|
|
33
|
-
return `<fieldset class="${e(classes)}">
|
|
34
|
-
${legend ? `<legend class="ui-fieldset-legend">${e(legend)}</legend>` : ''}
|
|
35
|
-
<div class="ui-fieldset-body">${content}</div>
|
|
36
|
-
</fieldset>`
|
|
37
|
-
}
|
package/src/ui/phone-frame.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse UI — Phone Frame
|
|
3
|
-
*
|
|
4
|
-
* A realistic CSS mobile phone frame. Pass screen content in the `content` slot.
|
|
5
|
-
* Renders the phone body, dynamic island, side buttons, and home indicator.
|
|
6
|
-
* No images — pure CSS/HTML.
|
|
7
|
-
*
|
|
8
|
-
* @param {object} opts
|
|
9
|
-
* @param {string} opts.content - HTML to render inside the screen area
|
|
10
|
-
* @param {boolean} opts.animate - enable gentle 3-D tilt on mouse hover (default: false)
|
|
11
|
-
* @param {string} opts.class
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
export function phoneFrame({ content = '', animate = false, class: cls = '' } = {}) {
|
|
15
|
-
const classes = ['ui-phone', animate && 'ui-phone--tilt', cls].filter(Boolean).join(' ')
|
|
16
|
-
|
|
17
|
-
return `<div class="${classes}" role="img" aria-label="Phone screen preview">
|
|
18
|
-
<!-- Left buttons: volume up, volume down, mute -->
|
|
19
|
-
<div class="ui-phone-btn ui-phone-btn--vol-up" aria-hidden="true"></div>
|
|
20
|
-
<div class="ui-phone-btn ui-phone-btn--vol-down" aria-hidden="true"></div>
|
|
21
|
-
<div class="ui-phone-btn ui-phone-btn--mute" aria-hidden="true"></div>
|
|
22
|
-
<!-- Right button: power -->
|
|
23
|
-
<div class="ui-phone-btn ui-phone-btn--power" aria-hidden="true"></div>
|
|
24
|
-
|
|
25
|
-
<div class="ui-phone-inner">
|
|
26
|
-
<!-- Dynamic island -->
|
|
27
|
-
<div class="ui-phone-island" aria-hidden="true">
|
|
28
|
-
<span class="ui-phone-camera"></span>
|
|
29
|
-
</div>
|
|
30
|
-
|
|
31
|
-
<!-- Screen -->
|
|
32
|
-
<div class="ui-phone-screen">
|
|
33
|
-
${content}
|
|
34
|
-
</div>
|
|
35
|
-
|
|
36
|
-
<!-- Home indicator -->
|
|
37
|
-
<div class="ui-phone-home" aria-hidden="true"></div>
|
|
38
|
-
</div>
|
|
39
|
-
</div>`
|
|
40
|
-
}
|
package/src/ui/search.js
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse UI — Search
|
|
3
|
-
*
|
|
4
|
-
* Search input with icon, optional clear button, and label.
|
|
5
|
-
* Handles the native browser cancel button, debounce binding,
|
|
6
|
-
* and the clear event all in one component.
|
|
7
|
-
*
|
|
8
|
-
* @param {object} opts
|
|
9
|
-
* @param {string} opts.name - Field name (also used as id base)
|
|
10
|
-
* @param {string} opts.label - Label text (visually shown or screen-reader only)
|
|
11
|
-
* @param {boolean} opts.labelHidden - Hide label visually but keep for screen readers
|
|
12
|
-
* @param {string} opts.placeholder
|
|
13
|
-
* @param {string} opts.value - Current search value (for re-renders)
|
|
14
|
-
* @param {string} opts.event - data-event binding, e.g. 'input:setSearch'
|
|
15
|
-
* @param {number} opts.debounce - Debounce delay in ms (default: 200)
|
|
16
|
-
* @param {string} opts.clearEvent - Click event fired by the clear button (shown when value is non-empty)
|
|
17
|
-
* @param {boolean} opts.disabled
|
|
18
|
-
* @param {string} opts.id - Override generated id
|
|
19
|
-
* @param {string} opts.class
|
|
20
|
-
* @param {object} opts.attrs - Extra HTML attributes for the <input>
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import { escHtml as e } from '../html.js'
|
|
24
|
-
import { iconSearch, iconX } from './icons.js'
|
|
25
|
-
|
|
26
|
-
export function search({
|
|
27
|
-
name = '',
|
|
28
|
-
label = '',
|
|
29
|
-
labelHidden = false,
|
|
30
|
-
placeholder = '',
|
|
31
|
-
value = '',
|
|
32
|
-
event = '',
|
|
33
|
-
debounce = 200,
|
|
34
|
-
clearEvent = '',
|
|
35
|
-
disabled = false,
|
|
36
|
-
id = '',
|
|
37
|
-
class: cls = '',
|
|
38
|
-
attrs = {},
|
|
39
|
-
} = {}) {
|
|
40
|
-
const fieldId = e(id || `field-${name}`)
|
|
41
|
-
|
|
42
|
-
const wrapClasses = ['ui-field', 'ui-search', cls].filter(Boolean).join(' ')
|
|
43
|
-
|
|
44
|
-
const labelClasses = ['ui-label', labelHidden ? 'ui-sr-only' : ''].filter(Boolean).join(' ')
|
|
45
|
-
|
|
46
|
-
const labelHtml = label
|
|
47
|
-
? `<label for="${fieldId}" class="${labelClasses}">${e(label)}</label>`
|
|
48
|
-
: ''
|
|
49
|
-
|
|
50
|
-
const attrsStr = Object.entries(attrs)
|
|
51
|
-
.filter(([, v]) => v !== undefined)
|
|
52
|
-
.map(([k, v]) => ` ${e(k)}="${e(String(v))}"`)
|
|
53
|
-
.join('')
|
|
54
|
-
|
|
55
|
-
const clearBtn = clearEvent && value
|
|
56
|
-
? `<button class="ui-search-clear" data-event="${e(clearEvent)}" type="button" aria-label="Clear search">${iconX({ size: 14 })}</button>`
|
|
57
|
-
: ''
|
|
58
|
-
|
|
59
|
-
return `<div class="${e(wrapClasses)}">
|
|
60
|
-
${labelHtml}
|
|
61
|
-
<div class="ui-search-wrap">
|
|
62
|
-
<span class="ui-search-icon" aria-hidden="true">${iconSearch({ size: 16 })}</span>
|
|
63
|
-
<input
|
|
64
|
-
id="${fieldId}"
|
|
65
|
-
name="${e(name)}"
|
|
66
|
-
type="search"
|
|
67
|
-
class="ui-search-input"
|
|
68
|
-
${placeholder ? `placeholder="${e(placeholder)}"` : ''}
|
|
69
|
-
${value ? `value="${e(value)}"` : ''}
|
|
70
|
-
${disabled ? 'disabled' : ''}
|
|
71
|
-
${event ? `data-event="${e(event)}"` : ''}
|
|
72
|
-
${event && debounce > 0 ? `data-debounce="${debounce}"` : ''}
|
|
73
|
-
${attrsStr}
|
|
74
|
-
>
|
|
75
|
-
${clearBtn}
|
|
76
|
-
</div>
|
|
77
|
-
</div>`
|
|
78
|
-
}
|
package/src/ui/segmented.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse UI — Segmented Control
|
|
3
|
-
*
|
|
4
|
-
* iOS-style segmented control implemented with hidden radio inputs.
|
|
5
|
-
* The selected option's label is highlighted via input:checked + label CSS.
|
|
6
|
-
*
|
|
7
|
-
* @param {object} opts
|
|
8
|
-
* @param {string} opts.name - Field name (submitted in FormData)
|
|
9
|
-
* @param {Array} opts.options - Array of { value, label }
|
|
10
|
-
* @param {string} opts.value - Currently selected value
|
|
11
|
-
* @param {boolean} opts.disabled
|
|
12
|
-
* @param {'sm'|'md'|'lg'} opts.size - Size variant (default: 'md')
|
|
13
|
-
* @param {string} opts.event - data-event binding, e.g. 'change:setTab'
|
|
14
|
-
* @param {string} opts.class
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { escHtml as e } from '../html.js'
|
|
18
|
-
|
|
19
|
-
export function segmented({
|
|
20
|
-
name = '',
|
|
21
|
-
options = [],
|
|
22
|
-
value = '',
|
|
23
|
-
disabled = false,
|
|
24
|
-
size = 'md',
|
|
25
|
-
event = '',
|
|
26
|
-
class: cls = '',
|
|
27
|
-
} = {}) {
|
|
28
|
-
const sizeClass = size === 'sm' ? 'ui-segmented--sm'
|
|
29
|
-
: size === 'lg' ? 'ui-segmented--lg'
|
|
30
|
-
: ''
|
|
31
|
-
|
|
32
|
-
const wrapClasses = ['ui-segmented', sizeClass, cls].filter(Boolean).join(' ')
|
|
33
|
-
|
|
34
|
-
const items = options.map((opt, i) => {
|
|
35
|
-
const optId = e(`seg-${name}-${i}`)
|
|
36
|
-
const checked = String(opt.value) === String(value)
|
|
37
|
-
return `<input
|
|
38
|
-
type="radio"
|
|
39
|
-
class="ui-segmented-input"
|
|
40
|
-
id="${optId}"
|
|
41
|
-
name="${e(name)}"
|
|
42
|
-
value="${e(String(opt.value))}"
|
|
43
|
-
${checked ? 'checked' : ''}
|
|
44
|
-
${disabled ? 'disabled' : ''}
|
|
45
|
-
${event ? `data-event="${e(event)}"` : ''}
|
|
46
|
-
><label class="ui-segmented-label" for="${optId}">${e(String(opt.label))}</label>`
|
|
47
|
-
}).join('')
|
|
48
|
-
|
|
49
|
-
return `<div class="${e(wrapClasses)}" role="group">${items}</div>`
|
|
50
|
-
}
|
package/src/ui/tooltip.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse UI — Tooltip
|
|
3
|
-
*
|
|
4
|
-
* CSS-powered tooltip. Wraps any trigger element — no JavaScript required.
|
|
5
|
-
*
|
|
6
|
-
* @param {object} opts
|
|
7
|
-
* @param {string} opts.content - Tooltip text (plain text; HTML not rendered)
|
|
8
|
-
* @param {string} opts.trigger - Raw HTML slot — the element the tooltip appears on
|
|
9
|
-
* @param {'top'|'bottom'|'left'|'right'} opts.position - Placement (default: 'top')
|
|
10
|
-
* @param {string} opts.class
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { escHtml as e } from '../html.js'
|
|
14
|
-
|
|
15
|
-
const POSITIONS = new Set(['top', 'bottom', 'left', 'right'])
|
|
16
|
-
|
|
17
|
-
export function tooltip({
|
|
18
|
-
content = '',
|
|
19
|
-
trigger = '',
|
|
20
|
-
position = 'top',
|
|
21
|
-
class: cls = '',
|
|
22
|
-
} = {}) {
|
|
23
|
-
if (!POSITIONS.has(position)) position = 'top'
|
|
24
|
-
|
|
25
|
-
const classes = ['ui-tooltip', `ui-tooltip--${position}`, cls].filter(Boolean).join(' ')
|
|
26
|
-
|
|
27
|
-
return `<span class="${e(classes)}" data-tip="${e(content)}" aria-label="${e(content)}">${trigger}</span>`
|
|
28
|
-
}
|