@invisibleloop/pulse 0.20.11 → 0.20.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (266) hide show
  1. package/package.json +6 -5
  2. package/public/.pulse-ui-version +1 -1
  3. package/scripts/build.js +26 -0
  4. package/scripts/release-version.js +17 -8
  5. package/scripts/strip-server.test.js +60 -0
  6. package/src/agent/checklist.md +8 -12
  7. package/src/agent/commands/pulse-start.md +2 -0
  8. package/src/agent/commands/verify.md +30 -8
  9. package/src/agent/guide-components.md +7 -25
  10. package/src/agent/guide-routing.md +0 -12
  11. package/src/agent/guide-server.md +102 -1
  12. package/src/agent/guide-spec.md +8 -1
  13. package/src/agent/guide-styles.md +23 -0
  14. package/src/agent/identity.md +9 -0
  15. package/src/agent/workflow.md +4 -23
  16. package/src/cli/check-bundles.js +113 -0
  17. package/src/cli/check-bundles.test.js +137 -0
  18. package/src/cli/cli.test.js +105 -0
  19. package/src/cli/dev.js +48 -3
  20. package/src/cli/diagnose.js +105 -0
  21. package/src/cli/diagnose.test.js +124 -0
  22. package/src/cli/discover.js +9 -2
  23. package/src/cli/index.js +171 -9
  24. package/src/cli/process-utils.js +57 -0
  25. package/src/cli/process-utils.test.js +114 -0
  26. package/src/cli/review.js +391 -0
  27. package/src/cli/review.test.js +143 -0
  28. package/src/cli/scaffold.js +9 -4
  29. package/src/cli/stamp.js +102 -0
  30. package/src/cli/stamp.test.js +149 -0
  31. package/src/cli/start.js +15 -2
  32. package/src/cli/validate.js +184 -0
  33. package/src/cli/validate.test.js +153 -0
  34. package/src/mcp/mcp.test.js +261 -30
  35. package/src/mcp/server.js +207 -515
  36. package/src/mcp/validate-worker.js +18 -1
  37. package/src/runtime/index.js +144 -11
  38. package/src/runtime/runtime.test.js +199 -0
  39. package/src/runtime/ssr.js +13 -6
  40. package/src/runtime/ssr.test.js +31 -0
  41. package/src/server/index.js +162 -9
  42. package/src/server/server.test.js +225 -2
  43. package/src/spec/schema.js +1 -1
  44. package/src/ui/index.js +0 -5
  45. package/src/ui/ui.test.js +0 -75
  46. package/types/ui.d.ts +0 -21
  47. package/.claude/commands/build-page.md +0 -63
  48. package/.claude/commands/new-doc-page.md +0 -45
  49. package/.claude/commands/pulse-start.md +0 -11
  50. package/.claude/commands/pulse-stop.md +0 -5
  51. package/.claude/commands/verify.md +0 -118
  52. package/.claude/pulse-checklist.md +0 -189
  53. package/.github/workflows/ci.yml +0 -25
  54. package/.github/workflows/publish.yml +0 -45
  55. package/.pulse/load-reports/home/1773432711417.json +0 -22
  56. package/.pulse-verified +0 -1
  57. package/AGENTS.md +0 -697
  58. package/CLAUDE.md +0 -697
  59. package/README.md +0 -95
  60. package/benchmark/public/dist/counter.boot-PO6SNSJZ.js +0 -8
  61. package/benchmark/public/dist/home.boot-YK6VAEQI.js +0 -6
  62. package/benchmark/public/dist/manifest.json +0 -5
  63. package/benchmark/public/dist/runtime-5Y7ESAIB.js +0 -2
  64. package/benchmark/public/dist/runtime-KO4BHUQ3.js +0 -49
  65. package/benchmark/results/2026-03-27-09-20.json +0 -188
  66. package/benchmark/results/2026-03-27-09-21.json +0 -188
  67. package/benchmark/results/2026-03-27-09-29.json +0 -188
  68. package/benchmark/results/2026-03-27-10-14.json +0 -188
  69. package/benchmark/results/2026-03-27-10-18.json +0 -232
  70. package/benchmark/src/pages/counter.js +0 -37
  71. package/benchmark/src/pages/home.js +0 -21
  72. package/docs/.claude/commands/pulse-build.md +0 -7
  73. package/docs/.claude/commands/pulse-dev.md +0 -7
  74. package/docs/.claude/commands/pulse-start.md +0 -9
  75. package/docs/.claude/commands/pulse-stop.md +0 -5
  76. package/docs/.claude/commands/verify.md +0 -84
  77. package/docs/.claude/coverage-check.js +0 -196
  78. package/docs/.claude/pulse-checklist.md +0 -189
  79. package/docs/public/.pulse-ui-version +0 -1
  80. package/docs/public/andypixel.css +0 -237
  81. package/docs/public/bookworms.css +0 -422
  82. package/docs/public/coutumes.css +0 -561
  83. package/docs/public/docs.css +0 -2388
  84. package/docs/public/favicon.svg +0 -4
  85. package/docs/public/home-brut.css +0 -982
  86. package/docs/public/images/bookworms/hero-books.jpg +0 -0
  87. package/docs/public/images/bookworms/how-we-read-now.jpg +0 -0
  88. package/docs/public/images/bookworms/lantern-keepers.jpg +0 -0
  89. package/docs/public/images/bookworms/provincial-bookseller.jpg +0 -0
  90. package/docs/public/images/journey/178.jpg +0 -0
  91. package/docs/public/images/journey/29.jpg +0 -0
  92. package/docs/public/images/retrowheels/hero.jpg +0 -0
  93. package/docs/public/images/shop/devbook-pro.jpg +0 -0
  94. package/docs/public/images/suzukibento/1015.jpg +0 -0
  95. package/docs/public/images/suzukibento/1025.jpg +0 -0
  96. package/docs/public/images/suzukibento/1043.jpg +0 -0
  97. package/docs/public/images/suzukibento/1059.jpg +0 -0
  98. package/docs/public/images/suzukibento/1062.jpg +0 -0
  99. package/docs/public/images/suzukibento/1084.jpg +0 -0
  100. package/docs/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
  101. package/docs/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
  102. package/docs/public/images/swish/1485518882345-15568b007407.jpg +0 -0
  103. package/docs/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
  104. package/docs/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
  105. package/docs/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
  106. package/docs/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
  107. package/docs/public/images/swish/1551803091-e20673f15770.jpg +0 -0
  108. package/docs/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
  109. package/docs/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
  110. package/docs/public/img/heic1501a.jpg +0 -0
  111. package/docs/public/img/placeholder-landscape.svg +0 -18
  112. package/docs/public/img/placeholder-square.svg +0 -14
  113. package/docs/public/img/placeholder-tall.svg +0 -22
  114. package/docs/public/journey.css +0 -528
  115. package/docs/public/menu.js +0 -327
  116. package/docs/public/pulse-ui.css +0 -3028
  117. package/docs/public/pulse-ui.js +0 -257
  118. package/docs/public/retrowheels.css +0 -425
  119. package/docs/public/shop.css +0 -269
  120. package/docs/public/sparkys.css +0 -382
  121. package/docs/public/suzukibento.css +0 -517
  122. package/docs/public/swish.css +0 -394
  123. package/docs/public/theme.css +0 -52
  124. package/docs/public/themes/lumio.css +0 -50
  125. package/docs/public/themes/observatory.css +0 -104
  126. package/docs/server.js +0 -161
  127. package/docs/src/lib/component-page.js +0 -49
  128. package/docs/src/lib/highlight.js +0 -2
  129. package/docs/src/lib/layout.js +0 -190
  130. package/docs/src/lib/metrics-store.js +0 -6
  131. package/docs/src/lib/nav.js +0 -171
  132. package/docs/src/lib/stats.js +0 -81
  133. package/docs/src/pages/accessibility.js +0 -157
  134. package/docs/src/pages/actions.js +0 -194
  135. package/docs/src/pages/auth.js +0 -176
  136. package/docs/src/pages/caching.js +0 -95
  137. package/docs/src/pages/components/accordion.js +0 -48
  138. package/docs/src/pages/components/alert.js +0 -35
  139. package/docs/src/pages/components/app-badge.js +0 -41
  140. package/docs/src/pages/components/avatar.js +0 -35
  141. package/docs/src/pages/components/badge.js +0 -36
  142. package/docs/src/pages/components/breadcrumbs.js +0 -94
  143. package/docs/src/pages/components/button.js +0 -84
  144. package/docs/src/pages/components/card.js +0 -225
  145. package/docs/src/pages/components/charts.js +0 -283
  146. package/docs/src/pages/components/checkbox.js +0 -129
  147. package/docs/src/pages/components/cluster.js +0 -47
  148. package/docs/src/pages/components/container.js +0 -40
  149. package/docs/src/pages/components/cta.js +0 -54
  150. package/docs/src/pages/components/divider.js +0 -37
  151. package/docs/src/pages/components/empty.js +0 -36
  152. package/docs/src/pages/components/feature.js +0 -60
  153. package/docs/src/pages/components/fieldset.js +0 -65
  154. package/docs/src/pages/components/fileupload.js +0 -128
  155. package/docs/src/pages/components/footer.js +0 -58
  156. package/docs/src/pages/components/grid.js +0 -169
  157. package/docs/src/pages/components/heading.js +0 -107
  158. package/docs/src/pages/components/hero.js +0 -123
  159. package/docs/src/pages/components/icons.js +0 -410
  160. package/docs/src/pages/components/image.js +0 -71
  161. package/docs/src/pages/components/input.js +0 -51
  162. package/docs/src/pages/components/media.js +0 -51
  163. package/docs/src/pages/components/modal.js +0 -111
  164. package/docs/src/pages/components/nav.js +0 -136
  165. package/docs/src/pages/components/phone-frame.js +0 -74
  166. package/docs/src/pages/components/progress.js +0 -102
  167. package/docs/src/pages/components/prose.js +0 -111
  168. package/docs/src/pages/components/pullquote.js +0 -71
  169. package/docs/src/pages/components/radio.js +0 -194
  170. package/docs/src/pages/components/rating.js +0 -106
  171. package/docs/src/pages/components/search.js +0 -61
  172. package/docs/src/pages/components/section.js +0 -98
  173. package/docs/src/pages/components/segmented.js +0 -121
  174. package/docs/src/pages/components/select.js +0 -45
  175. package/docs/src/pages/components/slider.js +0 -115
  176. package/docs/src/pages/components/spinner.js +0 -73
  177. package/docs/src/pages/components/stack.js +0 -48
  178. package/docs/src/pages/components/stat.js +0 -55
  179. package/docs/src/pages/components/stepper.js +0 -66
  180. package/docs/src/pages/components/table.js +0 -45
  181. package/docs/src/pages/components/textarea.js +0 -31
  182. package/docs/src/pages/components/toggle.js +0 -84
  183. package/docs/src/pages/components/tooltip.js +0 -48
  184. package/docs/src/pages/components.js +0 -200
  185. package/docs/src/pages/config.js +0 -193
  186. package/docs/src/pages/constraints.js +0 -100
  187. package/docs/src/pages/deploy.js +0 -233
  188. package/docs/src/pages/error-pages.js +0 -99
  189. package/docs/src/pages/extending.js +0 -198
  190. package/docs/src/pages/faq.js +0 -99
  191. package/docs/src/pages/forms.js +0 -129
  192. package/docs/src/pages/getting-started.js +0 -147
  193. package/docs/src/pages/guard.js +0 -120
  194. package/docs/src/pages/home.js +0 -350
  195. package/docs/src/pages/how-it-works.js +0 -244
  196. package/docs/src/pages/hydration.js +0 -108
  197. package/docs/src/pages/images.js +0 -120
  198. package/docs/src/pages/markdown.js +0 -156
  199. package/docs/src/pages/meta.js +0 -193
  200. package/docs/src/pages/mutations.js +0 -107
  201. package/docs/src/pages/navigation.js +0 -85
  202. package/docs/src/pages/performance.js +0 -157
  203. package/docs/src/pages/persist.js +0 -89
  204. package/docs/src/pages/project-structure.js +0 -91
  205. package/docs/src/pages/prompt-examples.js +0 -186
  206. package/docs/src/pages/raw-responses.js +0 -122
  207. package/docs/src/pages/routing.js +0 -120
  208. package/docs/src/pages/server-api.js +0 -291
  209. package/docs/src/pages/server-data.js +0 -203
  210. package/docs/src/pages/slash-commands.js +0 -55
  211. package/docs/src/pages/spec.js +0 -207
  212. package/docs/src/pages/state.js +0 -102
  213. package/docs/src/pages/store.js +0 -203
  214. package/docs/src/pages/streaming.js +0 -107
  215. package/docs/src/pages/stripe.js +0 -194
  216. package/docs/src/pages/supabase.js +0 -324
  217. package/docs/src/pages/templates/andypixel.js +0 -260
  218. package/docs/src/pages/templates/blog-post.js +0 -226
  219. package/docs/src/pages/templates/bookworms.js +0 -227
  220. package/docs/src/pages/templates/coutumes.js +0 -237
  221. package/docs/src/pages/templates/journey.js +0 -186
  222. package/docs/src/pages/templates/mobile-app.js +0 -371
  223. package/docs/src/pages/templates/retrowheels.js +0 -355
  224. package/docs/src/pages/templates/shop.js +0 -268
  225. package/docs/src/pages/templates/sparkys.js +0 -264
  226. package/docs/src/pages/templates/suzukibento.js +0 -200
  227. package/docs/src/pages/templates/swish.js +0 -313
  228. package/docs/src/pages/templates.js +0 -534
  229. package/docs/src/pages/testing.js +0 -199
  230. package/docs/src/pages/validation.js +0 -138
  231. package/examples/.claude/commands/pulse-build.md +0 -7
  232. package/examples/.claude/commands/pulse-dev.md +0 -7
  233. package/examples/.claude/commands/pulse-start.md +0 -9
  234. package/examples/.claude/commands/pulse-stop.md +0 -5
  235. package/examples/.claude/commands/verify.md +0 -84
  236. package/examples/.claude/coverage-check.js +0 -196
  237. package/examples/.claude/pulse-checklist.md +0 -189
  238. package/examples/andypixel.css +0 -237
  239. package/examples/andypixel.js +0 -258
  240. package/examples/bookworms.css +0 -422
  241. package/examples/bookworms.js +0 -225
  242. package/examples/contact.js +0 -166
  243. package/examples/counter.js +0 -94
  244. package/examples/dev.server.js +0 -110
  245. package/examples/examples.test.js +0 -394
  246. package/examples/journey.css +0 -528
  247. package/examples/journey.js +0 -186
  248. package/examples/pricing.js +0 -262
  249. package/examples/products.js +0 -190
  250. package/examples/public/.pulse-ui-version +0 -1
  251. package/examples/public/pulse-ui.css +0 -3028
  252. package/examples/public/pulse-ui.js +0 -257
  253. package/examples/quiz.js +0 -208
  254. package/examples/retrowheels.css +0 -425
  255. package/examples/retrowheels.js +0 -353
  256. package/examples/shared.js +0 -84
  257. package/examples/suzukibento.css +0 -513
  258. package/examples/suzukibento.js +0 -200
  259. package/examples/swish.css +0 -394
  260. package/examples/swish.js +0 -311
  261. package/examples/todos.js +0 -162
  262. package/src/ui/fieldset.js +0 -37
  263. package/src/ui/phone-frame.js +0 -40
  264. package/src/ui/search.js +0 -78
  265. package/src/ui/segmented.js +0 -50
  266. 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, '&amp;').replace(/</g, '&lt;')
20
- .replace(/>/g, '&gt;').replace(/"/g, '&quot;')
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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }