@invisibleloop/pulse 0.12.3 → 0.14.2

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 (181) hide show
  1. package/.claude/commands/build-page.md +5 -1
  2. package/.claude/commands/verify.md +31 -17
  3. package/.claude/pulse-checklist.md +21 -1
  4. package/.github/workflows/publish.yml +1 -0
  5. package/.pulse-verified +1 -0
  6. package/CLAUDE.md +56 -15
  7. package/docs/.claude/commands/verify.md +4 -0
  8. package/docs/.claude/pulse-checklist.md +20 -2
  9. package/docs/public/.pulse-ui-version +1 -1
  10. package/docs/public/andypixel.css +237 -0
  11. package/docs/public/bookworms.css +422 -0
  12. package/docs/public/coutumes.css +584 -0
  13. package/docs/public/images/bookworms/hero-books.jpg +0 -0
  14. package/docs/public/images/bookworms/how-we-read-now.jpg +0 -0
  15. package/docs/public/images/bookworms/lantern-keepers.jpg +0 -0
  16. package/docs/public/images/bookworms/provincial-bookseller.jpg +0 -0
  17. package/docs/public/images/journey/178.jpg +0 -0
  18. package/docs/public/images/journey/29.jpg +0 -0
  19. package/docs/public/images/retrowheels/hero.jpg +0 -0
  20. package/docs/public/images/shop/devbook-pro.jpg +0 -0
  21. package/docs/public/images/suzukibento/1015.jpg +0 -0
  22. package/docs/public/images/suzukibento/1025.jpg +0 -0
  23. package/docs/public/images/suzukibento/1043.jpg +0 -0
  24. package/docs/public/images/suzukibento/1059.jpg +0 -0
  25. package/docs/public/images/suzukibento/1062.jpg +0 -0
  26. package/docs/public/images/suzukibento/1084.jpg +0 -0
  27. package/docs/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
  28. package/docs/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
  29. package/docs/public/images/swish/1485518882345-15568b007407.jpg +0 -0
  30. package/docs/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
  31. package/docs/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
  32. package/docs/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
  33. package/docs/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
  34. package/docs/public/images/swish/1551803091-e20673f15770.jpg +0 -0
  35. package/docs/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
  36. package/docs/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
  37. package/docs/public/journey.css +528 -0
  38. package/docs/public/pulse-ui.css +1036 -22
  39. package/docs/public/retrowheels.css +402 -0
  40. package/docs/public/shop.css +235 -0
  41. package/docs/public/sparkys.css +349 -0
  42. package/docs/public/suzukibento.css +517 -0
  43. package/docs/public/swish.css +394 -0
  44. package/docs/server.js +19 -4
  45. package/docs/src/lib/nav.js +14 -3
  46. package/docs/src/pages/components/decorate.js +74 -0
  47. package/docs/src/pages/components/gallery.js +102 -0
  48. package/docs/src/pages/components/hero.js +1 -0
  49. package/docs/src/pages/components/marquee.js +76 -0
  50. package/docs/src/pages/components/photo-card.js +74 -0
  51. package/docs/src/pages/components/section.js +40 -1
  52. package/docs/src/pages/meta.js +26 -0
  53. package/docs/src/pages/templates/andypixel.js +260 -0
  54. package/docs/src/pages/templates/bookworms.js +227 -0
  55. package/docs/src/pages/templates/coutumes.js +226 -0
  56. package/docs/src/pages/templates/journey.js +186 -0
  57. package/docs/src/pages/templates/retrowheels.js +342 -0
  58. package/docs/src/pages/templates/shop.js +257 -0
  59. package/docs/src/pages/templates/sparkys.js +249 -0
  60. package/docs/src/pages/templates/suzukibento.js +200 -0
  61. package/docs/src/pages/templates/swish.js +313 -0
  62. package/docs/src/pages/templates.js +313 -2
  63. package/docs/src/pages/testing.js +2 -1
  64. package/examples/.claude/commands/pulse-build.md +7 -0
  65. package/examples/.claude/commands/pulse-dev.md +7 -0
  66. package/examples/.claude/commands/pulse-start.md +9 -0
  67. package/examples/.claude/commands/pulse-stop.md +5 -0
  68. package/examples/.claude/commands/verify.md +84 -0
  69. package/examples/.claude/coverage-check.js +127 -0
  70. package/examples/.claude/pulse-checklist.md +177 -0
  71. package/examples/andypixel.css +237 -0
  72. package/examples/andypixel.js +258 -0
  73. package/examples/bookworms.css +422 -0
  74. package/examples/bookworms.js +225 -0
  75. package/examples/contact.js +1 -0
  76. package/examples/counter.js +1 -0
  77. package/examples/dev.server.js +17 -0
  78. package/examples/journey.css +528 -0
  79. package/examples/journey.js +186 -0
  80. package/examples/pricing.js +1 -0
  81. package/examples/public/.pulse-ui-version +1 -0
  82. package/examples/public/pulse-ui.css +4031 -0
  83. package/examples/public/pulse-ui.js +257 -0
  84. package/examples/quiz.js +1 -0
  85. package/examples/retrowheels.css +402 -0
  86. package/examples/retrowheels.js +340 -0
  87. package/examples/shared.js +6 -0
  88. package/examples/suzukibento.css +513 -0
  89. package/examples/suzukibento.js +200 -0
  90. package/examples/swish.css +394 -0
  91. package/examples/swish.js +311 -0
  92. package/examples/todos.js +1 -0
  93. package/package.json +2 -3
  94. package/public/.pulse-ui-version +1 -1
  95. package/public/images/bookworms/hero-books.jpg +0 -0
  96. package/public/images/bookworms/how-we-read-now.jpg +0 -0
  97. package/public/images/bookworms/lantern-keepers.jpg +0 -0
  98. package/public/images/bookworms/provincial-bookseller.jpg +0 -0
  99. package/public/images/journey/178.jpg +0 -0
  100. package/public/images/journey/29.jpg +0 -0
  101. package/public/images/retrowheels/hero.jpg +0 -0
  102. package/public/images/shop/devbook-pro.jpg +0 -0
  103. package/public/images/suzukibento/1015.jpg +0 -0
  104. package/public/images/suzukibento/1025.jpg +0 -0
  105. package/public/images/suzukibento/1043.jpg +0 -0
  106. package/public/images/suzukibento/1059.jpg +0 -0
  107. package/public/images/suzukibento/1062.jpg +0 -0
  108. package/public/images/suzukibento/1084.jpg +0 -0
  109. package/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
  110. package/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
  111. package/public/images/swish/1485518882345-15568b007407.jpg +0 -0
  112. package/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
  113. package/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
  114. package/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
  115. package/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
  116. package/public/images/swish/1551803091-e20673f15770.jpg +0 -0
  117. package/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
  118. package/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
  119. package/public/pulse-ui.css +1080 -22
  120. package/scripts/build.js +72 -18
  121. package/src/agent/checklist.md +22 -2
  122. package/src/agent/commands/verify.md +4 -0
  123. package/src/agent/guide-components.md +146 -8
  124. package/src/agent/guide-design-gallery.md +288 -0
  125. package/src/agent/guide-design-references.md +172 -0
  126. package/src/agent/guide-explore.md +313 -0
  127. package/src/agent/guide-styles.md +212 -11
  128. package/src/agent/guide-templates.md +223 -10
  129. package/src/agent/guide.md +9 -4
  130. package/src/agent/identity.md +2 -1
  131. package/src/agent/skills/verify/SKILL.md +64 -28
  132. package/src/agent/workflow.md +247 -61
  133. package/src/cli/dev.js +39 -14
  134. package/src/cli/fmt.js +113 -0
  135. package/src/cli/index.js +60 -33
  136. package/src/cli/logger.js +145 -0
  137. package/src/cli/scaffold.js +57 -3
  138. package/src/html.js +6 -1
  139. package/src/mcp/server.js +2561 -99
  140. package/src/mcp/validate-worker.js +49 -5
  141. package/src/runtime/ssr.js +4 -1
  142. package/src/server/index.js +29 -5
  143. package/src/spec/schema.js +21 -0
  144. package/src/spec/schema.test.js +28 -0
  145. package/src/testing/html.js +130 -4
  146. package/src/testing/testing.test.js +58 -0
  147. package/src/ui/banner.js +24 -9
  148. package/src/ui/brand-statement.js +58 -0
  149. package/src/ui/button.js +13 -2
  150. package/src/ui/card.js +20 -1
  151. package/src/ui/colophon.js +29 -0
  152. package/src/ui/contact-row.js +41 -0
  153. package/src/ui/cta.js +18 -4
  154. package/src/ui/decorate.js +55 -0
  155. package/src/ui/display-heading.js +44 -0
  156. package/src/ui/drop-cap.js +29 -0
  157. package/src/ui/editorial-hero.js +63 -0
  158. package/src/ui/entry-list.js +36 -0
  159. package/src/ui/feature.js +20 -3
  160. package/src/ui/footer.js +78 -9
  161. package/src/ui/gallery.js +56 -0
  162. package/src/ui/hero.js +68 -18
  163. package/src/ui/image-pair.js +72 -0
  164. package/src/ui/index.js +20 -0
  165. package/src/ui/input.js +1 -0
  166. package/src/ui/marquee.js +48 -0
  167. package/src/ui/nav.js +19 -9
  168. package/src/ui/photo-card.js +44 -0
  169. package/src/ui/product-card.js +65 -0
  170. package/src/ui/pullquote.js +14 -3
  171. package/src/ui/search.js +1 -0
  172. package/src/ui/section-label.js +38 -0
  173. package/src/ui/section.js +1 -1
  174. package/src/ui/service-card.js +47 -0
  175. package/src/ui/stat-grid.js +40 -0
  176. package/src/ui/stat.js +16 -2
  177. package/src/ui/testimonial.js +17 -6
  178. package/src/ui/textarea.js +1 -0
  179. package/src/ui/top-strip.js +47 -0
  180. package/src/ui/trust-list.js +46 -0
  181. package/src/ui/ui.test.js +35 -0
@@ -0,0 +1,394 @@
1
+ /*
2
+ * swish.css — Swish editorial fashion example
3
+ *
4
+ * Palette: warm off-white paper, deep ink, oxblood accent.
5
+ * Font: Playfair Display (display headings), system-ui (body)
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
+ /* Override --font-display so --ui-font-display cascades to Playfair Display */
17
+ :root {
18
+ --font-display: 'Playfair Display', Georgia, 'Times New Roman', serif;
19
+ }
20
+
21
+ [data-theme="light"] {
22
+ --ui-bg: #f5f1ea;
23
+ --ui-surface: #faf7f1;
24
+ --ui-surface-2: #ede7dc;
25
+ --ui-border: #d8cfc0;
26
+ --ui-text: #14110d;
27
+ --ui-muted: #6b6357;
28
+ --ui-accent: #6b1d1d;
29
+ --ui-accent-hover: #4a1212;
30
+ --ui-accent-dim: rgba(107, 29, 29, 0.10);
31
+ --ui-accent-text: #faf7f1;
32
+
33
+ /* Swish-specific tokens */
34
+ --swish-paper: #faf7f1;
35
+ --swish-ink: #14110d;
36
+ --swish-hero-veil: rgba(20, 17, 13, 0.55);
37
+ --swish-hero-veil-top: rgba(20, 17, 13, 0.05);
38
+ --swish-label-bg: rgba(20, 17, 13, 0.55);
39
+ }
40
+
41
+ /* Primary button on light theme — needs paper text, not default */
42
+ [data-theme="light"] .ui-btn--primary {
43
+ color: var(--swish-paper);
44
+ }
45
+
46
+ /* Frosted nav over editorial imagery */
47
+ [data-theme="light"] .ui-nav {
48
+ backdrop-filter: blur(14px);
49
+ -webkit-backdrop-filter: blur(14px);
50
+ background: rgba(245, 241, 234, 0.78);
51
+ }
52
+
53
+ [data-theme="light"] .ui-nav-mobile .ui-nav-link,
54
+ [data-theme="light"] .ui-nav-mobile .ui-nav-mega-item-label,
55
+ [data-theme="light"] .ui-nav-mobile-section {
56
+ color: var(--ui-text);
57
+ }
58
+
59
+ /* ==========================================================================
60
+ 2. Component styles (var() only — no hex values below this line)
61
+ ========================================================================== */
62
+
63
+ /* --------------------------------------------------------------------------
64
+ Skip link
65
+ -------------------------------------------------------------------------- */
66
+ .skip-link {
67
+ position: absolute;
68
+ left: -9999px;
69
+ top: 0;
70
+ background: var(--ui-text);
71
+ color: var(--ui-bg);
72
+ padding: var(--ui-space-2) var(--ui-space-4);
73
+ z-index: 100;
74
+ }
75
+
76
+ .skip-link:focus {
77
+ left: var(--ui-space-4);
78
+ top: var(--ui-space-4);
79
+ }
80
+
81
+ /* --------------------------------------------------------------------------
82
+ Brand wordmark
83
+ -------------------------------------------------------------------------- */
84
+ .swish-mark {
85
+ font-family: var(--ui-font-display);
86
+ font-weight: 400;
87
+ font-size: var(--ui-text-2xl);
88
+ letter-spacing: 0.32em;
89
+ text-transform: uppercase;
90
+ }
91
+
92
+ /* --------------------------------------------------------------------------
93
+ Full-bleed hero
94
+ -------------------------------------------------------------------------- */
95
+ .swish-hero {
96
+ position: relative;
97
+ min-height: 88vh;
98
+ display: flex;
99
+ align-items: center;
100
+ background-image:
101
+ linear-gradient(180deg, var(--swish-hero-veil-top) 0%, var(--swish-hero-veil) 100%),
102
+ url('/images/swish/1490481651871-ab68de25d43d.jpg');
103
+ background-size: cover;
104
+ background-position: center;
105
+ color: var(--swish-paper);
106
+ padding: var(--ui-space-12) var(--ui-space-6) var(--ui-space-16);
107
+ }
108
+
109
+ .swish-hero__inner {
110
+ max-width: 880px;
111
+ margin: 0 auto;
112
+ width: 100%;
113
+ }
114
+
115
+ .swish-hero__eyebrow {
116
+ display: inline-block;
117
+ font-size: var(--ui-text-xs);
118
+ letter-spacing: 0.24em;
119
+ text-transform: uppercase;
120
+ color: var(--swish-ink);
121
+ background: var(--swish-paper);
122
+ padding: var(--ui-space-1) var(--ui-space-3);
123
+ margin-bottom: var(--ui-space-5);
124
+ }
125
+
126
+ .swish-hero__title {
127
+ font-family: var(--ui-font-display);
128
+ font-weight: 400;
129
+ font-size: clamp(3rem, 9vw, 7rem);
130
+ line-height: 0.95;
131
+ letter-spacing: -0.02em;
132
+ margin-bottom: var(--ui-space-6);
133
+ }
134
+
135
+ .swish-hero__deck {
136
+ font-size: var(--ui-text-lg);
137
+ line-height: 1.6;
138
+ max-width: 36rem;
139
+ margin-bottom: var(--ui-space-8);
140
+ opacity: 0.92;
141
+ }
142
+
143
+ /* Ghost button inside the hero — force white so it's visible over the dark veil */
144
+ .swish-hero .ui-btn--ghost {
145
+ color: #fff;
146
+ border-color: rgba(255,255,255,0.6);
147
+ }
148
+ .swish-hero .ui-btn--ghost:hover {
149
+ background: rgba(255,255,255,0.12);
150
+ border-color: #fff;
151
+ }
152
+
153
+ /* Quote section — equal top and bottom padding */
154
+ .swish-quote-section { padding-bottom: var(--ui-space-16); }
155
+
156
+ /* --------------------------------------------------------------------------
157
+ Section eyebrow / issue marker
158
+ -------------------------------------------------------------------------- */
159
+ .swish-issue {
160
+ display: flex;
161
+ align-items: center;
162
+ gap: var(--ui-space-4);
163
+ font-size: var(--ui-text-xs);
164
+ letter-spacing: 0.24em;
165
+ text-transform: uppercase;
166
+ color: var(--ui-muted);
167
+ margin-bottom: var(--ui-space-6);
168
+ }
169
+
170
+ .swish-issue::before,
171
+ .swish-issue::after {
172
+ content: '';
173
+ flex: 1;
174
+ height: 1px;
175
+ background: var(--ui-border);
176
+ }
177
+
178
+ .swish-issue--left::after {
179
+ display: none;
180
+ }
181
+
182
+ /* --------------------------------------------------------------------------
183
+ Featured looks (product cards)
184
+ -------------------------------------------------------------------------- */
185
+ .swish-look {
186
+ display: block;
187
+ text-decoration: none;
188
+ color: var(--ui-text);
189
+ }
190
+
191
+ .swish-look__frame {
192
+ aspect-ratio: 3 / 4;
193
+ overflow: hidden;
194
+ background: var(--ui-surface-2);
195
+ margin-bottom: var(--ui-space-4);
196
+ }
197
+
198
+ .swish-look__frame img {
199
+ width: 100%;
200
+ height: 100%;
201
+ object-fit: cover;
202
+ display: block;
203
+ transition: transform 600ms ease;
204
+ }
205
+
206
+ .swish-look:hover .swish-look__frame img {
207
+ transform: scale(1.04);
208
+ }
209
+
210
+ .swish-look__name {
211
+ font-family: var(--ui-font-display);
212
+ font-size: var(--ui-text-lg);
213
+ font-weight: 400;
214
+ margin-bottom: var(--ui-space-1);
215
+ }
216
+
217
+ .swish-look__meta {
218
+ font-size: var(--ui-text-sm);
219
+ color: var(--ui-muted);
220
+ letter-spacing: 0.04em;
221
+ }
222
+
223
+ /* --------------------------------------------------------------------------
224
+ Atelier split layout
225
+ -------------------------------------------------------------------------- */
226
+ .swish-split {
227
+ display: grid;
228
+ grid-template-columns: 1fr 1fr;
229
+ gap: var(--ui-space-12);
230
+ align-items: center;
231
+ }
232
+
233
+ .swish-split__media {
234
+ aspect-ratio: 4 / 5;
235
+ overflow: hidden;
236
+ background: var(--ui-surface-2);
237
+ }
238
+
239
+ .swish-split__media img {
240
+ width: 100%;
241
+ height: 100%;
242
+ object-fit: cover;
243
+ display: block;
244
+ }
245
+
246
+ .swish-split__title {
247
+ font-family: var(--ui-font-display);
248
+ font-size: clamp(2rem, 4vw, 3rem);
249
+ font-weight: 400;
250
+ line-height: 1.05;
251
+ letter-spacing: -0.015em;
252
+ margin-bottom: var(--ui-space-5);
253
+ }
254
+
255
+ .swish-split__body p {
256
+ font-size: var(--ui-text-lg);
257
+ line-height: 1.75;
258
+ color: var(--ui-text);
259
+ margin-bottom: var(--ui-space-4);
260
+ }
261
+
262
+ @media (max-width: 720px) {
263
+ .swish-split {
264
+ grid-template-columns: 1fr;
265
+ gap: var(--ui-space-8);
266
+ }
267
+ }
268
+
269
+ /* --------------------------------------------------------------------------
270
+ Lookbook strip
271
+ -------------------------------------------------------------------------- */
272
+ .swish-strip {
273
+ display: grid;
274
+ grid-template-columns: repeat(3, 1fr);
275
+ gap: var(--ui-space-4);
276
+ }
277
+
278
+ .swish-strip__cell {
279
+ position: relative;
280
+ aspect-ratio: 4 / 5;
281
+ overflow: hidden;
282
+ background: var(--ui-surface-2);
283
+ }
284
+
285
+ .swish-strip__cell img {
286
+ width: 100%;
287
+ height: 100%;
288
+ object-fit: cover;
289
+ display: block;
290
+ }
291
+
292
+ .swish-strip__label {
293
+ position: absolute;
294
+ left: var(--ui-space-4);
295
+ bottom: var(--ui-space-4);
296
+ font-size: var(--ui-text-xs);
297
+ letter-spacing: 0.24em;
298
+ text-transform: uppercase;
299
+ color: var(--swish-paper);
300
+ background: var(--swish-label-bg);
301
+ padding: var(--ui-space-2) var(--ui-space-3);
302
+ }
303
+
304
+ @media (max-width: 720px) {
305
+ .swish-strip {
306
+ grid-template-columns: 1fr;
307
+ }
308
+ }
309
+
310
+ /* --------------------------------------------------------------------------
311
+ Journal cards
312
+ -------------------------------------------------------------------------- */
313
+ .swish-journal-card {
314
+ display: flex;
315
+ flex-direction: column;
316
+ gap: var(--ui-space-3);
317
+ }
318
+
319
+ .swish-journal-card__frame {
320
+ aspect-ratio: 16 / 10;
321
+ overflow: hidden;
322
+ background: var(--ui-surface-2);
323
+ }
324
+
325
+ .swish-journal-card__frame img {
326
+ width: 100%;
327
+ height: 100%;
328
+ object-fit: cover;
329
+ display: block;
330
+ }
331
+
332
+ .swish-journal-card__category {
333
+ font-size: var(--ui-text-xs);
334
+ letter-spacing: 0.24em;
335
+ text-transform: uppercase;
336
+ color: var(--ui-accent);
337
+ }
338
+
339
+ .swish-journal-card__title {
340
+ font-family: var(--ui-font-display);
341
+ font-size: var(--ui-text-xl);
342
+ font-weight: 400;
343
+ line-height: 1.25;
344
+ }
345
+
346
+ .swish-journal-card__deck {
347
+ font-size: var(--ui-text-base);
348
+ color: var(--ui-muted);
349
+ line-height: 1.6;
350
+ }
351
+
352
+ .swish-journal-card__byline {
353
+ font-size: var(--ui-text-xs);
354
+ letter-spacing: 0.08em;
355
+ text-transform: uppercase;
356
+ color: var(--ui-muted);
357
+ }
358
+
359
+ /* --------------------------------------------------------------------------
360
+ Newsletter section
361
+ -------------------------------------------------------------------------- */
362
+ .swish-newsletter {
363
+ text-align: center;
364
+ }
365
+
366
+ .swish-newsletter__title {
367
+ font-family: var(--ui-font-display);
368
+ font-size: clamp(2rem, 4vw, 2.75rem);
369
+ font-weight: 400;
370
+ letter-spacing: -0.01em;
371
+ margin-bottom: var(--ui-space-4);
372
+ }
373
+
374
+ .swish-newsletter__deck {
375
+ max-width: 32rem;
376
+ margin: 0 auto var(--ui-space-8);
377
+ color: var(--ui-muted);
378
+ line-height: 1.6;
379
+ }
380
+
381
+ .swish-newsletter__form {
382
+ max-width: 32rem;
383
+ margin: 0 auto;
384
+ display: flex;
385
+ gap: var(--ui-space-3);
386
+ align-items: flex-end;
387
+ flex-wrap: wrap;
388
+ justify-content: center;
389
+ }
390
+
391
+ /* Safe area padding for mobile devices (home indicator bar) */
392
+ @supports (padding-bottom: env(safe-area-inset-bottom)) {
393
+ footer { padding-bottom: max(var(--ui-space-6), env(safe-area-inset-bottom)); }
394
+ }
package/docs/server.js CHANGED
@@ -73,9 +73,12 @@ await createServer(
73
73
  new URL('./src/pages/components/heading.js', import.meta.url),
74
74
  new URL('./src/pages/components/icons.js', import.meta.url),
75
75
  new URL('./src/pages/components/image.js', import.meta.url),
76
+ new URL('./src/pages/components/gallery.js', import.meta.url),
76
77
  new URL('./src/pages/components/input.js', import.meta.url),
77
78
  new URL('./src/pages/components/list.js', import.meta.url),
79
+ new URL('./src/pages/components/marquee.js', import.meta.url),
78
80
  new URL('./src/pages/components/modal.js', import.meta.url),
81
+ new URL('./src/pages/components/photo-card.js', import.meta.url),
79
82
  new URL('./src/pages/components/progress.js', import.meta.url),
80
83
  new URL('./src/pages/components/prose.js', import.meta.url),
81
84
  new URL('./src/pages/components/pullquote.js', import.meta.url),
@@ -104,14 +107,24 @@ await createServer(
104
107
  new URL('./src/pages/components/pricing.js', import.meta.url),
105
108
  new URL('./src/pages/components/testimonial.js', import.meta.url),
106
109
  // Templates
107
- new URL('./src/pages/templates.js', import.meta.url),
108
- new URL('./src/pages/templates/mobile-app.js', import.meta.url),
109
- new URL('./src/pages/templates/blog-post.js', import.meta.url),
110
+ new URL('./src/pages/templates.js', import.meta.url),
111
+ new URL('./src/pages/templates/mobile-app.js', import.meta.url),
112
+ new URL('./src/pages/templates/blog-post.js', import.meta.url),
113
+ new URL('./src/pages/templates/coutumes.js', import.meta.url),
114
+ new URL('./src/pages/templates/sparkys.js', import.meta.url),
115
+ new URL('./src/pages/templates/swish.js', import.meta.url),
116
+ new URL('./src/pages/templates/bookworms.js', import.meta.url),
117
+ new URL('./src/pages/templates/retrowheels.js', import.meta.url),
118
+ new URL('./src/pages/templates/suzukibento.js', import.meta.url),
119
+ new URL('./src/pages/templates/andypixel.js', import.meta.url),
120
+ new URL('./src/pages/templates/journey.js', import.meta.url),
121
+ new URL('./src/pages/templates/shop.js', import.meta.url),
110
122
  // Layout Components
111
123
  new URL('./src/pages/components/banner.js', import.meta.url),
112
124
  new URL('./src/pages/components/cluster.js', import.meta.url),
113
125
  new URL('./src/pages/components/code-window.js', import.meta.url),
114
126
  new URL('./src/pages/components/container.js', import.meta.url),
127
+ new URL('./src/pages/components/decorate.js', import.meta.url),
115
128
  new URL('./src/pages/components/divider.js', import.meta.url),
116
129
  new URL('./src/pages/components/footer.js', import.meta.url),
117
130
  new URL('./src/pages/components/grid.js', import.meta.url),
@@ -125,7 +138,9 @@ await createServer(
125
138
  root: new URL('.', import.meta.url),
126
139
  defaultCache: true,
127
140
  csp: {
128
- 'img-src': ['https://picsum.photos', 'https://images.unsplash.com'],
141
+ 'img-src': ['https://picsum.photos', 'https://fastly.picsum.photos', 'https://images.unsplash.com'],
142
+ 'style-src': ['https://fonts.googleapis.com'],
143
+ 'font-src': ['https://fonts.gstatic.com'],
129
144
  },
130
145
  }
131
146
  )
@@ -90,9 +90,12 @@ export const NAV = [
90
90
  { label: 'Heading', href: '/components/heading' },
91
91
  { label: 'Icons', href: '/components/icons' },
92
92
  { label: 'Image', href: '/components/image' },
93
+ { label: 'Gallery', href: '/components/gallery' },
93
94
  { label: 'Input', href: '/components/input' },
94
95
  { label: 'List', href: '/components/list' },
96
+ { label: 'Marquee', href: '/components/marquee' },
95
97
  { label: 'Modal', href: '/components/modal' },
98
+ { label: 'Photo Card', href: '/components/photo-card' },
96
99
  { label: 'Progress', href: '/components/progress' },
97
100
  { label: 'Prose', href: '/components/prose' },
98
101
  { label: 'Pullquote', href: '/components/pullquote' },
@@ -133,6 +136,7 @@ export const NAV = [
133
136
  { label: 'Cluster', href: '/components/cluster' },
134
137
  { label: 'Code Window', href: '/components/code-window' },
135
138
  { label: 'Container', href: '/components/container' },
139
+ { label: 'Decorate', href: '/components/decorate' },
136
140
  { label: 'Divider', href: '/components/divider' },
137
141
  { label: 'Footer', href: '/components/footer' },
138
142
  { label: 'Grid', href: '/components/grid' },
@@ -152,9 +156,16 @@ export const NAV = [
152
156
  {
153
157
  section: 'Templates',
154
158
  items: [
155
- { label: 'All Templates', href: '/templates' },
156
- { label: 'Mobile App Landing', href: '/templates/mobile-app' },
157
- { label: 'Blog Post', href: '/templates/blog-post' },
159
+ { label: 'All Templates', href: '/templates' },
160
+ { label: 'Mobile App Landing', href: '/templates/mobile-app' },
161
+ { label: 'Blog Post', href: '/templates/blog-post' },
162
+ { label: 'Swish — Fashion', href: '/templates/swish' },
163
+ { label: 'BookWorms', href: '/templates/bookworms' },
164
+ { label: 'Retro Wheels Co.', href: '/templates/retrowheels' },
165
+ { label: 'Susuki Bento', href: '/templates/suzukibento' },
166
+ { label: 'Andy Pixel — Blog', href: '/templates/andypixel' },
167
+ { label: 'Journey — App', href: '/templates/journey' },
168
+ { label: 'Shop — Product Page', href: '/templates/shop' },
158
169
  ],
159
170
  },
160
171
  ]
@@ -0,0 +1,74 @@
1
+ import { renderComponentPage, demo } from '../../lib/component-page.js'
2
+ import { prevNext } from '../../lib/nav.js'
3
+ import { table, callout } from '../../lib/layout.js'
4
+ import { decorate } from '../../../../src/ui/index.js'
5
+
6
+ const { prev, next } = prevNext('/components/decorate')
7
+
8
+ const swatch = (pattern, label) => `
9
+ <div style="position:relative;width:200px;height:120px;border-radius:var(--ui-radius);border:1px solid var(--ui-border);overflow:hidden;display:flex;align-items:center;justify-content:center">
10
+ ${decorate({ pattern })}
11
+ <span style="position:relative;z-index:1;font-weight:600;font-size:.875rem">${label}</span>
12
+ </div>`
13
+
14
+ export default {
15
+ route: '/components/decorate',
16
+ meta: {
17
+ title: 'Decorate — Pulse Docs',
18
+ description: 'Inline SVG background pattern overlay for adding subtle texture to sections in Pulse UI.',
19
+ styles: ['/pulse-ui.css', '/docs.css'],
20
+ },
21
+ view: () => renderComponentPage({
22
+ currentHref: '/components/decorate',
23
+ prev,
24
+ next,
25
+ name: 'decorate',
26
+ description: 'Inline SVG background pattern. Drop it inside any container with <code>position: relative</code> to add subtle texture without external assets. Five patterns: dots, grid, lines, zigzag, cross. Zero JavaScript — pure inline SVG.',
27
+ content: `
28
+ <h2 class="doc-h2" id="patterns">Patterns</h2>
29
+ <p>Five built-in patterns. The SVG fills the parent completely with <code>position: absolute; inset: 0</code>.</p>
30
+ ${demo(
31
+ `<div style="display:flex;flex-wrap:wrap;gap:1rem">` +
32
+ swatch('dots', 'dots') +
33
+ swatch('grid', 'grid') +
34
+ swatch('lines', 'lines') +
35
+ swatch('zigzag', 'zigzag') +
36
+ swatch('cross', 'cross') +
37
+ `</div>`,
38
+ `// Parent must have position: relative
39
+ <div style="position:relative">
40
+ ${`${decorate({ pattern: 'dots' })}`}
41
+ <p>Your content sits on top</p>
42
+ </div>`
43
+ )}
44
+
45
+ <h2 class="doc-h2" id="color">Color and opacity</h2>
46
+ <p>The default color is <code>var(--ui-border)</code> — it fits both light and dark themes. Override with any CSS value. Control density with <code>opacity</code>.</p>
47
+ ${demo(
48
+ `<div style="display:flex;flex-wrap:wrap;gap:1rem">` +
49
+ `<div style="position:relative;width:200px;height:120px;border-radius:var(--ui-radius);overflow:hidden;background:var(--ui-accent);display:flex;align-items:center;justify-content:center">` +
50
+ decorate({ pattern: 'dots', color: '#fff', opacity: 0.3 }) +
51
+ `<span style="position:relative;z-index:1;color:#fff;font-weight:600">opacity: 0.3</span>` +
52
+ `</div>` +
53
+ `<div style="position:relative;width:200px;height:120px;border-radius:var(--ui-radius);overflow:hidden;background:var(--ui-surface-raised);display:flex;align-items:center;justify-content:center">` +
54
+ decorate({ pattern: 'grid', opacity: 0.6 }) +
55
+ `<span style="position:relative;z-index:1;font-weight:600">opacity: 0.6</span>` +
56
+ `</div>` +
57
+ `</div>`,
58
+ `decorate({ pattern: 'dots', color: '#fff', opacity: 0.3 })`
59
+ )}
60
+
61
+ ${callout('note', 'The <code>decorate</code> element is <code>aria-hidden="true"</code> — it is purely decorative. Make sure surrounding content has sufficient contrast without relying on the pattern.')}
62
+
63
+ ${table(
64
+ ['Prop', 'Type', 'Default', 'Description'],
65
+ [
66
+ ['<code>pattern</code>', 'string', "'dots'", "'dots' · 'grid' · 'lines' · 'zigzag' · 'cross'"],
67
+ ['<code>color</code>', 'string', "'var(--ui-border)'", 'Any CSS color value — <code>var()</code> tokens and hex are both accepted'],
68
+ ['<code>opacity</code>', 'number', '0.4', 'SVG opacity — 0 (invisible) to 1 (solid)'],
69
+ ['<code>size</code>', 'number', '20', 'Pattern tile size in pixels — smaller values = denser pattern'],
70
+ ]
71
+ )}
72
+ `,
73
+ }),
74
+ }
@@ -0,0 +1,102 @@
1
+ import { renderComponentPage, demo } from '../../lib/component-page.js'
2
+ import { prevNext } from '../../lib/nav.js'
3
+ import { table } from '../../lib/layout.js'
4
+ import { gallery } from '../../../../src/ui/index.js'
5
+
6
+ const { prev, next } = prevNext('/components/gallery')
7
+
8
+ const PHOTOS = [
9
+ { src: '/img/placeholder-landscape.svg', alt: 'Mountain landscape' },
10
+ { src: '/img/placeholder-square.svg', alt: 'Portrait photo' },
11
+ { src: '/img/placeholder-tall.svg', alt: 'Urban street' },
12
+ { src: '/img/placeholder-landscape.svg', alt: 'Coastal path' },
13
+ { src: '/img/placeholder-square.svg', alt: 'Dog in the park' },
14
+ { src: '/img/placeholder-tall.svg', alt: 'City skyline' },
15
+ ]
16
+
17
+ const CAPTIONED = [
18
+ { src: '/img/placeholder-landscape.svg', alt: 'Mountain landscape', caption: 'Glen Coe, Scotland' },
19
+ { src: '/img/placeholder-square.svg', alt: 'Portrait', caption: 'Lisbon, Portugal' },
20
+ { src: '/img/placeholder-tall.svg', alt: 'Urban', caption: 'Tokyo, Japan' },
21
+ ]
22
+
23
+ export default {
24
+ route: '/components/gallery',
25
+ meta: {
26
+ title: 'Gallery — Pulse Docs',
27
+ description: 'Responsive image gallery with grid, strip, and masonry layouts for Pulse UI.',
28
+ styles: ['/pulse-ui.css', '/docs.css'],
29
+ },
30
+ view: () => renderComponentPage({
31
+ currentHref: '/components/gallery',
32
+ prev,
33
+ next,
34
+ name: 'gallery',
35
+ description: 'Responsive image gallery. Three layout modes: <code>grid</code> (equal aspect-ratio cells), <code>strip</code> (single horizontally scrollable row), and <code>masonry</code> (Pinterest-style varying heights). All images are lazy-loaded.',
36
+ content: `
37
+ <h2 class="doc-h2" id="grid">Grid layout</h2>
38
+ <p>Equal-sized cells with a consistent aspect ratio. Good for portfolios and photo libraries.</p>
39
+ ${demo(
40
+ gallery({ images: PHOTOS, layout: 'grid', cols: 3, ratio: '4/3' }),
41
+ `gallery({
42
+ images: photos.map(p => ({ src: p.url, alt: p.alt })),
43
+ layout: 'grid',
44
+ cols: 3,
45
+ ratio: '4/3',
46
+ })`
47
+ )}
48
+
49
+ <h2 class="doc-h2" id="strip">Strip layout</h2>
50
+ <p>A single horizontally scrollable row — ideal for a quick photo preview or a scrolling carousel of product shots.</p>
51
+ ${demo(
52
+ gallery({ images: PHOTOS, layout: 'strip', ratio: '4/3' }),
53
+ `gallery({
54
+ images: photos.map(p => ({ src: p.url, alt: p.alt })),
55
+ layout: 'strip',
56
+ ratio: '4/3',
57
+ })`
58
+ )}
59
+
60
+ <h2 class="doc-h2" id="masonry">Masonry layout</h2>
61
+ <p>Varying heights — images render at their natural ratio. Best for editorial photography where portrait and landscape images are mixed.</p>
62
+ ${demo(
63
+ gallery({ images: PHOTOS, layout: 'masonry', cols: 3 }),
64
+ `gallery({
65
+ images: photos.map(p => ({ src: p.url, alt: p.alt })),
66
+ layout: 'masonry',
67
+ cols: 3,
68
+ })`
69
+ )}
70
+
71
+ <h2 class="doc-h2" id="captions">Captions</h2>
72
+ <p>Add a <code>caption</code> to any image in the array — rendered as a <code>&lt;figcaption&gt;</code>.</p>
73
+ ${demo(
74
+ gallery({ images: CAPTIONED, layout: 'grid', cols: 3, ratio: '4/3' }),
75
+ `gallery({
76
+ images: [
77
+ { src: '/img/landscape.jpg', alt: 'Mountain', caption: 'Glen Coe, Scotland' },
78
+ { src: '/img/portrait.jpg', alt: 'Street', caption: 'Lisbon, Portugal' },
79
+ ],
80
+ layout: 'grid',
81
+ cols: 3,
82
+ ratio: '4/3',
83
+ })`
84
+ )}
85
+
86
+ <h2 class="doc-h2" id="linked">Linked images</h2>
87
+ <p>Add an <code>href</code> to any image to wrap it in a link.</p>
88
+
89
+ ${table(
90
+ ['Prop', 'Type', 'Default', 'Description'],
91
+ [
92
+ ['<code>images</code>', 'object[]', '[]', 'Array of image objects: <code>{ src, alt, caption?, href? }</code>'],
93
+ ['<code>layout</code>', 'string', "'grid'", "'grid' · 'strip' · 'masonry'"],
94
+ ['<code>cols</code>', 'number', '3', 'Columns in grid/masonry (1–4)'],
95
+ ['<code>ratio</code>', 'string', "'4/3'", "CSS aspect-ratio for grid/strip cells — e.g. <code>'16/9'</code>, <code>'1/1'</code>. Ignored in masonry."],
96
+ ['<code>gap</code>', 'string', "'md'", "'sm' · 'md' · 'lg'"],
97
+ ['<code>rounded</code>', 'boolean', 'false', 'Adds border-radius to each image'],
98
+ ]
99
+ )}
100
+ `,
101
+ }),
102
+ }
@@ -137,6 +137,7 @@ hero({ title: 'Headline', background: 'linear-gradient(135deg, #1e1b4b, #312e81)
137
137
  ['<code>subtitle</code>', 'string', '—', ''],
138
138
  ['<code>actions</code>', 'string (HTML)', '—', 'Raw HTML slot — buttons, badges, etc.'],
139
139
  ['<code>image</code>', 'string (HTML)', '—', 'Raw HTML slot — activates split layout when set'],
140
+ ['<code>layout</code>', 'string', "'split'", "When <code>image</code> is set: <code>'split'</code> (even columns) · <code>'asymmetric'</code> (wider text, narrower image) · <code>'overlap'</code> (image overlaps below text)"],
140
141
  ['<code>imageAlign</code>', 'string', "'right'", "'right' (text left) or 'left' (text right) — only applies when image is set"],
141
142
  ['<code>align</code>', 'string', "'center'", "'center' or 'left' — text alignment when no image"],
142
143
  ['<code>size</code>', 'string', "'md'", "'md' (5rem padding) or 'sm' (2.5rem top, no bottom) — use sm for inner-page headers"],