@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.
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
@@ -1,258 +0,0 @@
1
- /**
2
- * Pulse — Andy Pixel example
3
- *
4
- * Demonstrates:
5
- * - Blog article layout — editorial light-theme, prose-width reading column
6
- * - Composing nav, badge, avatar, prose, input, button, footer
7
- * - Custom code block component (dark pre with labelled header)
8
- * - Blockquote / pullquote styling
9
- * - Async newsletter subscribe action with success/error state
10
- * - Author bio strip below article content
11
- * - iconCode, iconCheck from icons.js
12
- * - Orange accent palette with WCAG contrast fixes for badge + inline code
13
- *
14
- * Run: node examples/dev.server.js → http://localhost:3001/andypixel
15
- */
16
-
17
- import {
18
- nav,
19
- footer,
20
- badge,
21
- button,
22
- avatar,
23
- prose,
24
- input,
25
- } from '../src/ui/index.js'
26
-
27
- import { iconCode, iconCheck } from '../src/ui/icons.js'
28
-
29
- // ---------------------------------------------------------------------------
30
- // Code block helper
31
- // ---------------------------------------------------------------------------
32
-
33
- function codeBlock({ filename, code }) {
34
- return `
35
- <div class="ap-code-block" role="figure" aria-label="Code example: ${filename}">
36
- <div class="ap-code-block__header">
37
- ${iconCode({ size: 14 })}
38
- <span>${filename}</span>
39
- </div>
40
- <pre class="ap-code-block__pre"><code class="ap-code-block__code">${code}</code></pre>
41
- </div>
42
- `
43
- }
44
-
45
- // ---------------------------------------------------------------------------
46
- // Spec
47
- // ---------------------------------------------------------------------------
48
-
49
- export default {
50
- route: '/andypixel',
51
-
52
- meta: {
53
- title: 'JavaScript Proxies: The Hidden Superpower \u2014 Andy Pixel',
54
- description: 'Discover how JavaScript Proxies let you intercept and redefine fundamental operations on objects.',
55
- styles: ['/pulse-ui.css', '/examples/andypixel.css'],
56
- theme: 'light',
57
- },
58
-
59
- state: {
60
- status: 'idle',
61
- },
62
-
63
- view: (state) => `
64
- <main id="main-content">
65
-
66
- ${nav({
67
- logo: 'Andy Pixel',
68
- links: [
69
- { label: 'Blog', href: '#blog' },
70
- { label: 'Projects', href: '#projects' },
71
- { label: 'About', href: '#about' },
72
- ],
73
- })}
74
-
75
- <article>
76
-
77
- <header class="ap-article-header">
78
- <div class="ap-reading-col">
79
-
80
- <div class="ap-article-meta">
81
- ${badge({ label: 'JavaScript' })}
82
- ${badge({ label: 'Advanced' })}
83
- <span class="u-text-muted u-text-sm">\u00b7 May 28, 2026 \u00b7 8 min read</span>
84
- </div>
85
-
86
- <h1 class="ap-article-title">
87
- JavaScript Proxies: The Hidden Superpower You\u2019re Not Using
88
- </h1>
89
-
90
- <p class="ap-article-subtitle">
91
- Intercept, transform, and observe objects at a fundamental level \u2014
92
- without touching the source.
93
- </p>
94
-
95
- <div class="ap-article-author-strip">
96
- ${avatar({ name: 'Andy Pixel', size: 'sm' })}
97
- <div>
98
- <p class="u-text-sm u-font-bold">Andy Pixel</p>
99
- <p class="u-text-sm u-text-muted">Frontend Engineer</p>
100
- </div>
101
- </div>
102
-
103
- </div>
104
- </header>
105
-
106
- <div class="ap-article-body ap-reading-col">
107
-
108
- ${prose({ content: `
109
- <p class="ap-article-lead">If you\u2019ve ever wanted to validate object properties automatically,
110
- track changes reactively, or build a mock API client that feels real \u2014 the
111
- <code>Proxy</code> object is your tool. Yet somehow it flies under the radar
112
- for most developers.</p>
113
-
114
- <p>Introduced in ES2015, <code>Proxy</code> wraps an object and lets you intercept
115
- fundamental operations \u2014 property reads, writes, deletions, and more \u2014 via
116
- \u201ctraps\u201d defined on a handler object.</p>
117
-
118
- <h2>The anatomy of a Proxy</h2>
119
-
120
- <p>A Proxy takes two arguments: the <strong>target</strong> (the object being wrapped)
121
- and the <strong>handler</strong> (an object defining which operations to intercept).
122
- Every interceptable operation has a corresponding trap method on the handler.</p>
123
- ` })}
124
-
125
- ${codeBlock({
126
- filename: 'proxy-basics.js',
127
- code: `const handler = {
128
- get(target, key) {
129
- return key in target
130
- ? target[key]
131
- : \`Property "\${key}" doesn\u2019t exist\`
132
- },
133
- set(target, key, value) {
134
- if (typeof value !== 'number') {
135
- throw new TypeError('Only numbers allowed')
136
- }
137
- target[key] = value
138
- return true
139
- }
140
- }
141
-
142
- const proxy = new Proxy({}, handler)
143
- proxy.score = 42 // \u2713 works
144
- proxy.score = 'oops' // \u2717 TypeError`,
145
- })}
146
-
147
- ${prose({ content: `
148
- <p>The <code>get</code> trap runs on every property access. The <code>set</code> trap
149
- intercepts every assignment. Return <code>true</code> from <code>set</code> to signal
150
- success \u2014 otherwise you\u2019ll get a <code>TypeError</code> in strict mode.</p>
151
-
152
- <h2>Real-world use cases</h2>
153
-
154
- <p>The real power isn\u2019t abstract. Proxy unlocks patterns that are otherwise
155
- painful or impossible.</p>
156
- ` })}
157
-
158
- <blockquote class="ap-pullquote">
159
- <p>\u201cA Proxy doesn\u2019t change what your object <em>is</em> \u2014 it changes what your
160
- object <em>does</em> when the runtime touches it.\u201d</p>
161
- </blockquote>
162
-
163
- ${prose({ content: `
164
- <h3>Reactive state</h3>
165
- <p>Vue 3\u2019s reactivity system is built entirely on Proxy. When you access a reactive
166
- property, Vue\u2019s <code>get</code> trap tracks the dependency. When you mutate it,
167
- the <code>set</code> trap schedules re-renders. No syntax overhead. Just assignment.</p>
168
-
169
- <h3>Input validation</h3>
170
- <p>Instead of scattering guard clauses through your codebase, centralise validation
171
- in a Proxy handler. The contract lives in one place. Every consumer benefits
172
- automatically.</p>
173
-
174
- <h3>API mocking</h3>
175
- <p>Wrap a plain object in a Proxy that intercepts property access and returns async
176
- functions. You get a fully-typed mock client that mirrors your real API surface \u2014
177
- without any code generation.</p>
178
-
179
- <h2>Performance considerations</h2>
180
-
181
- <p>Proxies are not free. Every trap fires a function call. In tight loops over large
182
- collections, this adds up. Benchmark before you commit. For most use cases \u2014 form
183
- state, reactive stores, config validation \u2014 the overhead is completely negligible.</p>
184
-
185
- <p>The V8 team has steadily optimised Proxy performance since ES2015. Don\u2019t avoid
186
- Proxy out of dated fear \u2014 measure it first.</p>
187
- ` })}
188
-
189
- </div>
190
-
191
- </article>
192
-
193
- <div class="ap-author-bio-wrap">
194
- <div class="ap-reading-col">
195
- <div class="ap-author-bio">
196
- ${avatar({ name: 'Andy Pixel', size: 'lg' })}
197
- <div class="ap-author-bio__text">
198
- <p class="ap-author-bio__name">Andy Pixel</p>
199
- <p class="u-text-muted u-text-sm">
200
- Frontend engineer obsessed with the web platform. Writing about JavaScript,
201
- CSS, and creative code. Building things at the edge of what browsers can do.
202
- </p>
203
- </div>
204
- </div>
205
- </div>
206
- </div>
207
-
208
- ${ state.status === 'success'
209
- ? `<div class="ap-newsletter-success">
210
- <p class="u-font-bold u-text-lg">${iconCheck({ size: 20, class: 'u-inline' })} You\u2019re in!</p>
211
- <p class="u-text-muted u-mt-2">Next article lands in your inbox.</p>
212
- </div>`
213
- : `<div class="ap-newsletter-section">
214
- <p class="ap-newsletter-eyebrow">Stay in the loop</p>
215
- <h2 class="ap-newsletter-title">More like this, in your inbox.</h2>
216
- <p class="u-text-muted">
217
- Deep-dives on JavaScript, the web platform, and frontend engineering \u2014
218
- whenever I publish. No noise.
219
- </p>
220
- ${ state.status === 'error'
221
- ? `<p class="ap-newsletter-error u-mt-3">Something went wrong. Please try again.</p>`
222
- : '' }
223
- <form class="ap-newsletter-form" data-action="subscribe">
224
- ${input({ type: 'email', name: 'email', placeholder: 'you@example.com', label: 'Email address', attrs: { autocomplete: 'email' } })}
225
- ${button({ label: 'Subscribe', variant: 'primary', attrs: { type: 'submit' } })}
226
- </form>
227
- </div>`
228
- }
229
-
230
- ${footer({
231
- logo: 'Andy Pixel',
232
- links: [
233
- { label: 'Blog', href: '#blog' },
234
- { label: 'Projects', href: '#projects' },
235
- { label: 'About', href: '#about' },
236
- { label: 'RSS', href: '#feed' },
237
- ],
238
- })}
239
-
240
- </main>
241
- `,
242
-
243
- actions: {
244
- subscribe: {
245
- onStart: (state, formData) => ({
246
- status: 'loading',
247
- }),
248
- run: async (state, serverState, formData) => {
249
- // Newsletter subscription would call an API here
250
- },
251
- onSuccess: (state) => ({ status: 'success' }),
252
- onError: (state, err) => ({
253
- status: 'error',
254
- errors: err?.validation ?? [{ message: err.message }],
255
- }),
256
- },
257
- },
258
- }
@@ -1,422 +0,0 @@
1
- /*
2
- * bookworms.css — BookWorms independent bookshop example
3
- *
4
- * Palette: warm off-white, deep ink, oxblood accent.
5
- * Font: Fraunces (display headings, loaded via Google Fonts in meta.styles)
6
- *
7
- * Split into two logical sections:
8
- * 1. Token overrides — hex values, palette definitions ([data-theme="light"])
9
- * 2. Component styles — layout, typography, custom elements (var() only)
10
- */
11
-
12
- /* ==========================================================================
13
- 1. Token overrides (hex values live here)
14
- ========================================================================== */
15
-
16
- /*
17
- * Override --font and --font-display so --ui-font / --ui-font-display
18
- * cascade to Fraunces throughout components and custom elements.
19
- */
20
- :root {
21
- --font: 'Fraunces', Georgia, 'Times New Roman', serif;
22
- --font-display: 'Fraunces', Georgia, 'Times New Roman', serif;
23
- }
24
-
25
- [data-theme="light"] {
26
- --ui-bg: #f6f2ec;
27
- --ui-surface: #ffffff;
28
- --ui-surface-2: #efe9df;
29
- --ui-border: #d8cfc1;
30
- --ui-text: #1a1a1a;
31
- --ui-muted: #5a5043;
32
- --ui-accent: #6b2a2a;
33
- --ui-accent-hover: #4f1d1d;
34
- --ui-accent-dim: #f0e3e3;
35
- --ui-accent-text: #ffffff;
36
-
37
- /* BookWorms-specific token */
38
- --bw-bg-alt: #efe9df;
39
- }
40
-
41
- /* ==========================================================================
42
- 2. Component styles (var() only — no hex values below this line)
43
- ========================================================================== */
44
-
45
- body { line-height: 1.6; }
46
-
47
- /* --------------------------------------------------------------------------
48
- Shell (centred max-width wrapper)
49
- -------------------------------------------------------------------------- */
50
- .bw-shell {
51
- max-width: 1180px;
52
- margin: 0 auto;
53
- padding: 0 var(--ui-space-6);
54
- }
55
-
56
- /* --------------------------------------------------------------------------
57
- Hero
58
- -------------------------------------------------------------------------- */
59
- .bw-hero {
60
- padding: var(--ui-space-20) 0 var(--ui-space-16);
61
- border-bottom: 1px solid var(--ui-border);
62
- overflow: hidden;
63
- }
64
-
65
- .bw-hero__inner {
66
- display: grid;
67
- grid-template-columns: 1fr 1fr;
68
- gap: var(--ui-space-12);
69
- align-items: center;
70
- }
71
-
72
- .bw-hero__content {
73
- min-width: 0;
74
- }
75
-
76
- .bw-hero__image {
77
- position: relative;
78
- border-radius: var(--ui-radius-lg);
79
- overflow: hidden;
80
- aspect-ratio: 3 / 4;
81
- }
82
-
83
- .bw-hero__image img {
84
- width: 100%;
85
- height: 100%;
86
- object-fit: cover;
87
- display: block;
88
- }
89
-
90
- @media (max-width: 700px) {
91
- .bw-hero__inner {
92
- grid-template-columns: 1fr;
93
- }
94
- .bw-hero__image {
95
- aspect-ratio: 4 / 3;
96
- order: -1;
97
- }
98
- }
99
-
100
- .bw-eyebrow {
101
- font-size: var(--ui-text-xs);
102
- letter-spacing: 0.18em;
103
- text-transform: uppercase;
104
- color: var(--ui-muted);
105
- margin-bottom: var(--ui-space-6);
106
- }
107
-
108
- .bw-hero-title {
109
- font-family: var(--ui-font-display);
110
- font-size: clamp(2.5rem, 7vw, 5.5rem);
111
- font-weight: 500;
112
- line-height: 1.02;
113
- letter-spacing: -0.025em;
114
- color: var(--ui-text);
115
- margin-bottom: var(--ui-space-8);
116
- max-width: 18ch;
117
- }
118
-
119
- .bw-hero-lede {
120
- font-size: var(--ui-text-xl);
121
- line-height: 1.55;
122
- color: var(--ui-muted);
123
- max-width: 60ch;
124
- margin-bottom: var(--ui-space-8);
125
- }
126
-
127
- /* --------------------------------------------------------------------------
128
- Section template
129
- -------------------------------------------------------------------------- */
130
- .bw-section {
131
- padding: var(--ui-space-16) 0;
132
- border-bottom: 1px solid var(--ui-border);
133
- }
134
-
135
- .bw-section-label {
136
- font-size: var(--ui-text-xs);
137
- letter-spacing: 0.18em;
138
- text-transform: uppercase;
139
- color: var(--ui-muted);
140
- margin-bottom: var(--ui-space-8);
141
- }
142
-
143
- .bw-section-title {
144
- font-family: var(--ui-font-display);
145
- font-size: clamp(1.75rem, 3.2vw, 2.5rem);
146
- font-weight: 500;
147
- line-height: 1.1;
148
- letter-spacing: -0.02em;
149
- color: var(--ui-text);
150
- margin-bottom: var(--ui-space-10);
151
- max-width: 24ch;
152
- }
153
-
154
- /* --------------------------------------------------------------------------
155
- Staff picks grid
156
- -------------------------------------------------------------------------- */
157
- .bw-picks {
158
- display: grid;
159
- grid-template-columns: repeat(3, 1fr);
160
- gap: var(--ui-space-10);
161
- }
162
-
163
- @media (max-width: 800px) {
164
- .bw-picks {
165
- grid-template-columns: 1fr;
166
- gap: var(--ui-space-8);
167
- }
168
- }
169
-
170
- .bw-pick-cover-wrap {
171
- position: relative;
172
- overflow: hidden;
173
- margin-bottom: var(--ui-space-6);
174
- box-shadow: 0 2px 12px rgb(0 0 0 / 0.08);
175
- }
176
-
177
- .bw-pick-cover {
178
- display: block;
179
- width: 100%;
180
- height: auto;
181
- aspect-ratio: 2 / 3;
182
- object-fit: cover;
183
- background: var(--bw-bg-alt);
184
- }
185
-
186
- .bw-pick-overlay {
187
- position: absolute;
188
- inset: 0;
189
- display: flex;
190
- flex-direction: column;
191
- justify-content: flex-end;
192
- padding: var(--ui-space-6);
193
- background: linear-gradient(to top, rgba(0,0,0,0.72) 0%, rgba(0,0,0,0.2) 50%, transparent 100%);
194
- }
195
-
196
- /* First book only — text at top */
197
- .bw-picks article:first-child .bw-pick-overlay {
198
- justify-content: flex-start;
199
- background: linear-gradient(to bottom, rgba(0,0,0,0.72) 0%, rgba(0,0,0,0.2) 50%, transparent 100%);
200
- }
201
-
202
- .bw-picks article:first-child .bw-pick-overlay-title {
203
- font-family: Arial, sans-serif;
204
- font-weight: 700;
205
- text-transform: uppercase;
206
- }
207
-
208
- /* Second book — solid semi-transparent block behind text */
209
- .bw-picks article:nth-child(2) .bw-pick-overlay {
210
- inset: auto 0 0 0;
211
- background: rgba(0, 0, 0, 0.55);
212
- padding: var(--ui-space-5) var(--ui-space-6);
213
- }
214
-
215
- /* Third book — Arial, no italic on author */
216
- .bw-picks article:nth-child(3) .bw-pick-overlay-title {
217
- font-family: Arial, sans-serif;
218
- font-size: 3.75rem;
219
- }
220
-
221
- .bw-picks article:nth-child(3) .bw-pick-overlay-author {
222
- font-family: Arial, sans-serif;
223
- font-style: normal;
224
- }
225
-
226
- .bw-pick-overlay-tag {
227
- font-size: var(--ui-text-xs);
228
- letter-spacing: 0.14em;
229
- text-transform: uppercase;
230
- color: rgba(255,255,255,0.7);
231
- margin-bottom: var(--ui-space-2);
232
- }
233
-
234
- .bw-pick-overlay-title {
235
- font-family: var(--ui-font);
236
- font-size: var(--ui-text-2xl);
237
- font-weight: 600;
238
- line-height: 1.15;
239
- color: #fff;
240
- margin-bottom: var(--ui-space-1);
241
- }
242
-
243
- .bw-pick-overlay-author {
244
- font-style: italic;
245
- font-family: var(--ui-font);
246
- font-size: var(--ui-text-sm);
247
- color: rgba(255,255,255,0.7);
248
- }
249
-
250
- .bw-pick-meta {
251
- font-size: var(--ui-text-xs);
252
- letter-spacing: 0.14em;
253
- text-transform: uppercase;
254
- color: var(--ui-accent);
255
- margin-bottom: var(--ui-space-3);
256
- }
257
-
258
- .bw-pick-title {
259
- font-family: var(--ui-font-display);
260
- font-size: var(--ui-text-2xl);
261
- font-weight: 500;
262
- line-height: 1.2;
263
- margin-bottom: var(--ui-space-2);
264
- color: var(--ui-text);
265
- }
266
-
267
- .bw-pick-author {
268
- font-style: italic;
269
- color: var(--ui-muted);
270
- margin-bottom: var(--ui-space-4);
271
- }
272
-
273
- .bw-pick-note {
274
- font-size: var(--ui-text-base);
275
- line-height: 1.6;
276
- color: var(--ui-text);
277
- }
278
-
279
- /* --------------------------------------------------------------------------
280
- Pull-quote
281
- -------------------------------------------------------------------------- */
282
- .bw-pull {
283
- padding: var(--ui-space-20) 0;
284
- text-align: center;
285
- border-bottom: 1px solid var(--ui-border);
286
- }
287
-
288
- .bw-pull-quote {
289
- font-family: var(--ui-font-display);
290
- font-size: clamp(1.75rem, 4vw, 2.75rem);
291
- font-weight: 400;
292
- font-style: italic;
293
- line-height: 1.25;
294
- letter-spacing: -0.015em;
295
- color: var(--ui-text);
296
- max-width: 32ch;
297
- margin: 0 auto var(--ui-space-6);
298
- }
299
-
300
- .bw-pull-cite {
301
- font-size: var(--ui-text-sm);
302
- letter-spacing: 0.14em;
303
- text-transform: uppercase;
304
- color: var(--ui-muted);
305
- }
306
-
307
- /* --------------------------------------------------------------------------
308
- Feature grid (what we do)
309
- -------------------------------------------------------------------------- */
310
- .bw-features {
311
- display: grid;
312
- grid-template-columns: 1fr 1fr;
313
- gap: var(--ui-space-12) var(--ui-space-16);
314
- }
315
-
316
- @media (max-width: 800px) {
317
- .bw-features {
318
- grid-template-columns: 1fr;
319
- gap: var(--ui-space-10);
320
- }
321
- }
322
-
323
- .bw-feature-title {
324
- font-family: var(--ui-font-display);
325
- font-size: var(--ui-text-xl);
326
- font-weight: 500;
327
- margin-bottom: var(--ui-space-2);
328
- color: var(--ui-text);
329
- }
330
-
331
- .bw-feature-body {
332
- color: var(--ui-muted);
333
- line-height: 1.6;
334
- }
335
-
336
- /* --------------------------------------------------------------------------
337
- Events list
338
- -------------------------------------------------------------------------- */
339
- .bw-events {
340
- display: flex;
341
- flex-direction: column;
342
- }
343
-
344
- .bw-event {
345
- display: grid;
346
- grid-template-columns: 140px 1fr auto;
347
- align-items: baseline;
348
- gap: var(--ui-space-8);
349
- padding: var(--ui-space-6) 0;
350
- border-top: 1px solid var(--ui-border);
351
- }
352
-
353
- .bw-event:last-child {
354
- border-bottom: 1px solid var(--ui-border);
355
- }
356
-
357
- @media (max-width: 700px) {
358
- .bw-event {
359
- grid-template-columns: 1fr;
360
- gap: var(--ui-space-2);
361
- }
362
- }
363
-
364
- .bw-event-date {
365
- font-family: var(--ui-font-display);
366
- font-size: var(--ui-text-lg);
367
- color: var(--ui-accent);
368
- letter-spacing: 0.02em;
369
- }
370
-
371
- .bw-event-title {
372
- font-family: var(--ui-font-display);
373
- font-size: var(--ui-text-xl);
374
- font-weight: 500;
375
- color: var(--ui-text);
376
- }
377
-
378
- .bw-event-host {
379
- color: var(--ui-muted);
380
- font-style: italic;
381
- }
382
-
383
- /* --------------------------------------------------------------------------
384
- CTA section
385
- -------------------------------------------------------------------------- */
386
- .bw-cta {
387
- padding: var(--ui-space-24) 0;
388
- text-align: center;
389
- }
390
-
391
- .bw-cta-title {
392
- font-family: var(--ui-font-display);
393
- font-size: clamp(2rem, 5vw, 3.5rem);
394
- font-weight: 500;
395
- line-height: 1.05;
396
- letter-spacing: -0.02em;
397
- margin-bottom: var(--ui-space-6);
398
- color: var(--ui-text);
399
- max-width: 20ch;
400
- margin-left: auto;
401
- margin-right: auto;
402
- }
403
-
404
- .bw-cta-sub {
405
- color: var(--ui-muted);
406
- font-size: var(--ui-text-lg);
407
- margin-bottom: var(--ui-space-8);
408
- max-width: 50ch;
409
- margin-left: auto;
410
- margin-right: auto;
411
- }
412
-
413
- .bw-cta-actions {
414
- display: flex;
415
- gap: var(--ui-space-4);
416
- justify-content: center;
417
- flex-wrap: wrap;
418
- }
419
-
420
- .bw-cta-actions--start {
421
- justify-content: flex-start;
422
- }