@invisibleloop/pulse 0.12.2 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/verify.md +9 -5
- package/.claude/pulse-checklist.md +21 -1
- package/.github/workflows/publish.yml +1 -0
- package/.pulse-verified +1 -0
- package/CLAUDE.md +56 -15
- package/docs/.claude/commands/verify.md +4 -0
- package/docs/.claude/pulse-checklist.md +36 -2
- package/docs/public/.pulse-ui-version +1 -1
- package/docs/public/andypixel.css +237 -0
- package/docs/public/bookworms.css +422 -0
- package/docs/public/coutumes.css +584 -0
- package/docs/public/images/bookworms/hero-books.jpg +0 -0
- package/docs/public/images/bookworms/how-we-read-now.jpg +0 -0
- package/docs/public/images/bookworms/lantern-keepers.jpg +0 -0
- package/docs/public/images/bookworms/provincial-bookseller.jpg +0 -0
- package/docs/public/images/journey/178.jpg +0 -0
- package/docs/public/images/journey/29.jpg +0 -0
- package/docs/public/images/retrowheels/hero.jpg +0 -0
- package/docs/public/images/shop/devbook-pro.jpg +0 -0
- package/docs/public/images/suzukibento/1015.jpg +0 -0
- package/docs/public/images/suzukibento/1025.jpg +0 -0
- package/docs/public/images/suzukibento/1043.jpg +0 -0
- package/docs/public/images/suzukibento/1059.jpg +0 -0
- package/docs/public/images/suzukibento/1062.jpg +0 -0
- package/docs/public/images/suzukibento/1084.jpg +0 -0
- package/docs/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
- package/docs/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
- package/docs/public/images/swish/1485518882345-15568b007407.jpg +0 -0
- package/docs/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
- package/docs/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
- package/docs/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
- package/docs/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
- package/docs/public/images/swish/1551803091-e20673f15770.jpg +0 -0
- package/docs/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
- package/docs/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
- package/docs/public/journey.css +528 -0
- package/docs/public/pulse-ui.css +1051 -25
- package/docs/public/pulse-ui.js +6 -0
- package/docs/public/retrowheels.css +402 -0
- package/docs/public/shop.css +235 -0
- package/docs/public/sparkys.css +349 -0
- package/docs/public/suzukibento.css +517 -0
- package/docs/public/swish.css +394 -0
- package/docs/server.js +19 -4
- package/docs/src/lib/nav.js +14 -3
- package/docs/src/pages/components/decorate.js +74 -0
- package/docs/src/pages/components/gallery.js +102 -0
- package/docs/src/pages/components/hero.js +1 -0
- package/docs/src/pages/components/marquee.js +76 -0
- package/docs/src/pages/components/phone-frame.js +26 -7
- package/docs/src/pages/components/photo-card.js +74 -0
- package/docs/src/pages/components/section.js +40 -1
- package/docs/src/pages/meta.js +26 -0
- package/docs/src/pages/templates/andypixel.js +260 -0
- package/docs/src/pages/templates/bookworms.js +227 -0
- package/docs/src/pages/templates/coutumes.js +226 -0
- package/docs/src/pages/templates/journey.js +186 -0
- package/docs/src/pages/templates/retrowheels.js +342 -0
- package/docs/src/pages/templates/shop.js +257 -0
- package/docs/src/pages/templates/sparkys.js +249 -0
- package/docs/src/pages/templates/suzukibento.js +200 -0
- package/docs/src/pages/templates/swish.js +313 -0
- package/docs/src/pages/templates.js +313 -2
- package/docs/src/pages/testing.js +2 -1
- package/examples/andypixel.css +237 -0
- package/examples/andypixel.js +258 -0
- package/examples/bookworms.css +422 -0
- package/examples/bookworms.js +225 -0
- package/examples/contact.js +1 -0
- package/examples/counter.js +1 -0
- package/examples/dev.server.js +17 -0
- package/examples/journey.css +528 -0
- package/examples/journey.js +186 -0
- package/examples/pricing.js +1 -0
- package/examples/quiz.js +1 -0
- package/examples/retrowheels.css +402 -0
- package/examples/retrowheels.js +340 -0
- package/examples/shared.js +6 -0
- package/examples/suzukibento.css +513 -0
- package/examples/suzukibento.js +200 -0
- package/examples/swish.css +394 -0
- package/examples/swish.js +311 -0
- package/examples/todos.js +1 -0
- package/package.json +1 -1
- package/public/.pulse-ui-version +1 -1
- package/public/images/bookworms/hero-books.jpg +0 -0
- package/public/images/bookworms/how-we-read-now.jpg +0 -0
- package/public/images/bookworms/lantern-keepers.jpg +0 -0
- package/public/images/bookworms/provincial-bookseller.jpg +0 -0
- package/public/images/journey/178.jpg +0 -0
- package/public/images/journey/29.jpg +0 -0
- package/public/images/retrowheels/hero.jpg +0 -0
- package/public/images/shop/devbook-pro.jpg +0 -0
- package/public/images/suzukibento/1015.jpg +0 -0
- package/public/images/suzukibento/1025.jpg +0 -0
- package/public/images/suzukibento/1043.jpg +0 -0
- package/public/images/suzukibento/1059.jpg +0 -0
- package/public/images/suzukibento/1062.jpg +0 -0
- package/public/images/suzukibento/1084.jpg +0 -0
- package/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
- package/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
- package/public/images/swish/1485518882345-15568b007407.jpg +0 -0
- package/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
- package/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
- package/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
- package/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
- package/public/images/swish/1551803091-e20673f15770.jpg +0 -0
- package/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
- package/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
- package/public/pulse-ui.css +1051 -22
- package/public/pulse-ui.js +6 -0
- package/scripts/build.js +72 -18
- package/src/agent/checklist.md +20 -2
- package/src/agent/commands/verify.md +4 -0
- package/src/agent/guide-components.md +146 -8
- package/src/agent/guide-design-gallery.md +288 -0
- package/src/agent/guide-design-references.md +172 -0
- package/src/agent/guide-explore.md +313 -0
- package/src/agent/guide-styles.md +197 -11
- package/src/agent/guide-templates.md +220 -10
- package/src/agent/guide.md +9 -4
- package/src/agent/workflow.md +208 -58
- package/src/cli/dev.js +39 -7
- package/src/cli/fmt.js +113 -0
- package/src/cli/index.js +63 -30
- package/src/cli/scaffold.js +2 -2
- package/src/html.js +6 -1
- package/src/mcp/server.js +2415 -101
- package/src/mcp/validate-worker.js +34 -5
- package/src/runtime/ssr.js +4 -1
- package/src/server/index.js +22 -2
- package/src/spec/schema.js +5 -0
- package/src/testing/html.js +130 -4
- package/src/testing/testing.test.js +58 -0
- package/src/ui/banner.js +24 -9
- package/src/ui/brand-statement.js +58 -0
- package/src/ui/button.js +12 -1
- package/src/ui/card.js +20 -1
- package/src/ui/colophon.js +29 -0
- package/src/ui/contact-row.js +41 -0
- package/src/ui/cta.js +18 -4
- package/src/ui/decorate.js +55 -0
- package/src/ui/display-heading.js +44 -0
- package/src/ui/drop-cap.js +29 -0
- package/src/ui/editorial-hero.js +63 -0
- package/src/ui/entry-list.js +36 -0
- package/src/ui/feature.js +20 -3
- package/src/ui/footer.js +78 -9
- package/src/ui/gallery.js +56 -0
- package/src/ui/hero.js +68 -18
- package/src/ui/image-pair.js +72 -0
- package/src/ui/index.js +20 -0
- package/src/ui/input.js +1 -0
- package/src/ui/marquee.js +48 -0
- package/src/ui/nav.js +19 -9
- package/src/ui/phone-frame.js +6 -5
- package/src/ui/photo-card.js +44 -0
- package/src/ui/product-card.js +65 -0
- package/src/ui/pullquote.js +14 -3
- package/src/ui/search.js +1 -0
- package/src/ui/section-label.js +38 -0
- package/src/ui/section.js +1 -1
- package/src/ui/service-card.js +47 -0
- package/src/ui/stat-grid.js +40 -0
- package/src/ui/stat.js +16 -2
- package/src/ui/testimonial.js +17 -6
- package/src/ui/textarea.js +1 -0
- package/src/ui/top-strip.js +47 -0
- package/src/ui/trust-list.js +46 -0
- package/src/ui/ui.test.js +35 -0
|
@@ -0,0 +1,76 @@
|
|
|
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 { marquee } from '../../../../src/ui/index.js'
|
|
5
|
+
|
|
6
|
+
const { prev, next } = prevNext('/components/marquee')
|
|
7
|
+
|
|
8
|
+
const LOGOS = [
|
|
9
|
+
`<span style="font-weight:600;font-size:.875rem;opacity:.6;white-space:nowrap;letter-spacing:.5px">ACME CORP</span>`,
|
|
10
|
+
`<span style="font-weight:600;font-size:.875rem;opacity:.6;white-space:nowrap;letter-spacing:.5px">NOVA INC</span>`,
|
|
11
|
+
`<span style="font-weight:600;font-size:.875rem;opacity:.6;white-space:nowrap;letter-spacing:.5px">STELLAR</span>`,
|
|
12
|
+
`<span style="font-weight:600;font-size:.875rem;opacity:.6;white-space:nowrap;letter-spacing:.5px">ORBIT CO</span>`,
|
|
13
|
+
`<span style="font-weight:600;font-size:.875rem;opacity:.6;white-space:nowrap;letter-spacing:.5px">HELIX</span>`,
|
|
14
|
+
`<span style="font-weight:600;font-size:.875rem;opacity:.6;white-space:nowrap;letter-spacing:.5px">PRISM AI</span>`,
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
export default {
|
|
18
|
+
route: '/components/marquee',
|
|
19
|
+
meta: {
|
|
20
|
+
title: 'Marquee — Pulse Docs',
|
|
21
|
+
description: 'CSS-only infinite scrolling strip for logo clouds, trust badges, and tag lists in Pulse UI.',
|
|
22
|
+
styles: ['/pulse-ui.css', '/docs.css'],
|
|
23
|
+
},
|
|
24
|
+
view: () => renderComponentPage({
|
|
25
|
+
currentHref: '/components/marquee',
|
|
26
|
+
prev,
|
|
27
|
+
next,
|
|
28
|
+
name: 'marquee',
|
|
29
|
+
description: 'CSS-only infinite scrolling strip. No JavaScript — pure CSS animation. Use for logo clouds, press mentions, trust badges, or feature tags. <strong>The strip is hidden from screen readers</strong> (<code>aria-hidden="true"</code>) — it is decorative, not content.',
|
|
30
|
+
content: `
|
|
31
|
+
<h2 class="doc-h2" id="logos">Logo cloud</h2>
|
|
32
|
+
<p>A common use-case: a strip of client or partner logos scrolling slowly from right to left.</p>
|
|
33
|
+
${demo(
|
|
34
|
+
marquee({ items: LOGOS }),
|
|
35
|
+
`marquee({
|
|
36
|
+
items: ['<img src="/logos/acme.svg" alt="">',
|
|
37
|
+
'<img src="/logos/nova.svg" alt="">',
|
|
38
|
+
'<img src="/logos/helix.svg" alt="">'],
|
|
39
|
+
})`
|
|
40
|
+
)}
|
|
41
|
+
|
|
42
|
+
<h2 class="doc-h2" id="slow">Custom speed</h2>
|
|
43
|
+
<p>The default <code>speed</code> is 30 seconds per loop. Slow it down for fewer items, or speed it up for a more energetic feel.</p>
|
|
44
|
+
${demo(
|
|
45
|
+
marquee({ items: LOGOS, speed: 60 }),
|
|
46
|
+
`marquee({ items, speed: 60 }) // slow`
|
|
47
|
+
)}
|
|
48
|
+
|
|
49
|
+
<h2 class="doc-h2" id="direction">Direction</h2>
|
|
50
|
+
<p>Set <code>direction: 'right'</code> to scroll right-to-left instead.</p>
|
|
51
|
+
${demo(
|
|
52
|
+
marquee({ items: LOGOS, direction: 'right' }),
|
|
53
|
+
`marquee({ items, direction: 'right' })`
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
<h2 class="doc-h2" id="no-fade">Without fade</h2>
|
|
57
|
+
<p>Edge fading is on by default. Disable it with <code>fade: false</code>.</p>
|
|
58
|
+
${demo(
|
|
59
|
+
marquee({ items: LOGOS, fade: false }),
|
|
60
|
+
`marquee({ items, fade: false })`
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
${table(
|
|
64
|
+
['Prop', 'Type', 'Default', 'Description'],
|
|
65
|
+
[
|
|
66
|
+
['<code>items</code>', 'string[]', '[]', 'Array of raw HTML strings — images, badges, or text'],
|
|
67
|
+
['<code>speed</code>', 'number', '30', 'Duration (seconds) for one full loop. Lower = faster.'],
|
|
68
|
+
['<code>gap</code>', 'string', "'lg'", "'sm' · 'md' · 'lg'"],
|
|
69
|
+
['<code>direction</code>', 'string', "'left'", "'left' (right-to-left scroll) or 'right'"],
|
|
70
|
+
['<code>pause</code>', 'boolean', 'true', 'Pause animation on hover'],
|
|
71
|
+
['<code>fade</code>', 'boolean', 'true', 'Apply gradient mask to fade the left and right edges'],
|
|
72
|
+
]
|
|
73
|
+
)}
|
|
74
|
+
`,
|
|
75
|
+
}),
|
|
76
|
+
}
|
|
@@ -6,15 +6,22 @@ import { phoneFrame } from '../../../../src/ui/index.js'
|
|
|
6
6
|
const { prev, next } = prevNext('/components/phone-frame')
|
|
7
7
|
|
|
8
8
|
const sampleScreen = `
|
|
9
|
-
<div style="background:#1a2c24;height:
|
|
9
|
+
<div style="background:#1a2c24;min-height:520px;padding:0.75rem;display:flex;flex-direction:column;gap:0.75rem;font-family:system-ui,sans-serif;">
|
|
10
10
|
<div style="display:flex;justify-content:space-between;align-items:center;padding:0 0.25rem;font-size:0.6rem;color:rgba(255,255,255,0.5)">
|
|
11
11
|
<span>9:41</span>
|
|
12
12
|
<svg width="20" height="8" viewBox="0 0 20 8" fill="none" aria-hidden="true"><rect x="0.5" y="0.5" width="16" height="7" rx="2" stroke="rgba(255,255,255,0.35)" stroke-width="1"/><rect x="1.5" y="1.5" width="12" height="5" rx="1" fill="rgba(255,255,255,0.7)"/><path d="M18 3v2c.8-.3.8-1.7 0-2z" fill="rgba(255,255,255,0.6)"/></svg>
|
|
13
13
|
</div>
|
|
14
14
|
<div style="font-size:0.85rem;font-weight:700;color:#fff">Good morning, Sarah</div>
|
|
15
|
-
<div style="background:rgba(255,255,255,0.06);border-radius:0.
|
|
16
|
-
<div style="font-size:0.6rem;color:rgba(255,255,255,0.5);
|
|
17
|
-
<div style="font-size:0.72rem;color:rgba(255,255,255,0.85);line-height:1.
|
|
15
|
+
<div style="background:rgba(255,255,255,0.06);border-radius:0.75rem;padding:0.75rem;flex:1;display:flex;flex-direction:column;gap:0.5rem">
|
|
16
|
+
<div style="font-size:0.6rem;color:rgba(255,255,255,0.5);letter-spacing:0.04em">TODAY'S ENTRY</div>
|
|
17
|
+
<div style="font-size:0.72rem;color:rgba(255,255,255,0.85);line-height:1.6">Feeling grateful today. The early morning light was something else — golden and soft through the curtains.</div>
|
|
18
|
+
</div>
|
|
19
|
+
<div style="background:rgba(255,255,255,0.04);border-radius:0.75rem;padding:0.75rem">
|
|
20
|
+
<div style="font-size:0.6rem;color:rgba(255,255,255,0.5);letter-spacing:0.04em;margin-bottom:0.4rem">STREAK</div>
|
|
21
|
+
<div style="display:flex;gap:0.3rem">
|
|
22
|
+
${[1,2,3,4,5,6,7].map(d => `<div style="flex:1;height:28px;background:${d < 6 ? 'rgba(74,222,128,0.7)' : 'rgba(255,255,255,0.08)'};border-radius:4px"></div>`).join('')}
|
|
23
|
+
</div>
|
|
24
|
+
<div style="font-size:0.6rem;color:rgba(255,255,255,0.4);margin-top:0.4rem;text-align:right">5 day streak 🔥</div>
|
|
18
25
|
</div>
|
|
19
26
|
</div>`
|
|
20
27
|
|
|
@@ -36,17 +43,29 @@ export default {
|
|
|
36
43
|
${demo(
|
|
37
44
|
phoneFrame({ content: sampleScreen }),
|
|
38
45
|
`phoneFrame({
|
|
39
|
-
content: \`<div style="background:#1a2c24;height:
|
|
46
|
+
content: \`<div style="background:#1a2c24;min-height:520px;padding:0.75rem;">
|
|
40
47
|
<!-- your app screen UI -->
|
|
41
48
|
</div>\`,
|
|
42
49
|
})`,
|
|
43
50
|
)}
|
|
44
51
|
|
|
52
|
+
<h2>Hover animation</h2>
|
|
53
|
+
<p>Pass <code>animate: true</code> to enable a gentle 3-D tilt when the user hovers over the frame. Ideal for hero sections — hover the phone below to see it.</p>
|
|
54
|
+
|
|
55
|
+
${demo(
|
|
56
|
+
phoneFrame({ content: sampleScreen, animate: true }),
|
|
57
|
+
`phoneFrame({
|
|
58
|
+
content: \`...\`,
|
|
59
|
+
animate: true,
|
|
60
|
+
})`,
|
|
61
|
+
)}
|
|
62
|
+
|
|
45
63
|
${table(
|
|
46
64
|
['Prop', 'Type', 'Default', 'Description'],
|
|
47
65
|
[
|
|
48
|
-
['<code>content</code>', 'string (HTML)', "''",
|
|
49
|
-
['<code>
|
|
66
|
+
['<code>content</code>', 'string (HTML)', "''", 'HTML rendered inside the screen area'],
|
|
67
|
+
['<code>animate</code>', 'boolean', 'false', 'Enables a gentle 3-D tilt on mouse hover — recommended for hero usage'],
|
|
68
|
+
['<code>class</code>', 'string', "''", 'Extra CSS classes on the root element'],
|
|
50
69
|
]
|
|
51
70
|
)}
|
|
52
71
|
`,
|
|
@@ -0,0 +1,74 @@
|
|
|
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 { photoCard } from '../../../../src/ui/index.js'
|
|
5
|
+
|
|
6
|
+
const { prev, next } = prevNext('/components/photo-card')
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
route: '/components/photo-card',
|
|
10
|
+
meta: {
|
|
11
|
+
title: 'Photo Card — Pulse Docs',
|
|
12
|
+
description: 'Polaroid-style photo card with optional tilt and caption for Pulse UI.',
|
|
13
|
+
styles: ['/pulse-ui.css', '/docs.css'],
|
|
14
|
+
},
|
|
15
|
+
view: () => renderComponentPage({
|
|
16
|
+
currentHref: '/components/photo-card',
|
|
17
|
+
prev,
|
|
18
|
+
next,
|
|
19
|
+
name: 'photoCard',
|
|
20
|
+
description: 'Polaroid-style photo card with a border, white padding, optional caption, and CSS tilt. Ideal for "meet the team" sections, testimonial portraits, and lifestyle photography.',
|
|
21
|
+
content: `
|
|
22
|
+
<h2 class="doc-h2" id="basic">Basic photo card</h2>
|
|
23
|
+
<p>A framed image with a white polaroid border.</p>
|
|
24
|
+
${demo(
|
|
25
|
+
`<div style="display:flex;gap:2rem;align-items:center;justify-content:center;flex-wrap:wrap;padding:2rem">` +
|
|
26
|
+
photoCard({ src: '/img/placeholder-square.svg', alt: 'Team member portrait', caption: 'Sarah, Designer' }) +
|
|
27
|
+
`</div>`,
|
|
28
|
+
`photoCard({
|
|
29
|
+
src: '/img/team/sarah.jpg',
|
|
30
|
+
alt: 'Sarah, Designer',
|
|
31
|
+
caption: 'Sarah, Designer',
|
|
32
|
+
})`
|
|
33
|
+
)}
|
|
34
|
+
|
|
35
|
+
<h2 class="doc-h2" id="tilt">With tilt</h2>
|
|
36
|
+
<p>Pass a <code>tilt</code> value (degrees) to rotate the card. Negative values lean left, positive values lean right. Useful for giving sections a hand-placed, organic feeling.</p>
|
|
37
|
+
${demo(
|
|
38
|
+
`<div style="display:flex;gap:2rem;align-items:center;justify-content:center;flex-wrap:wrap;padding:2rem">` +
|
|
39
|
+
photoCard({ src: '/img/placeholder-square.svg', alt: 'Team photo', caption: 'Andy, founder', tilt: -2 }) +
|
|
40
|
+
photoCard({ src: '/img/placeholder-tall.svg', alt: 'Office dog', caption: 'Biscuit, mascot', tilt: 1.5 }) +
|
|
41
|
+
photoCard({ src: '/img/placeholder-square.svg', alt: 'Team photo', caption: 'Jen, engineering', tilt: -1 }) +
|
|
42
|
+
`</div>`,
|
|
43
|
+
`photoCard({ src, alt: 'Andy, founder', caption: 'Andy, founder', tilt: -2 })
|
|
44
|
+
photoCard({ src, alt: 'Office dog', caption: 'Biscuit, mascot', tilt: 1.5 })
|
|
45
|
+
photoCard({ src, alt: 'Jen, engineering', caption: 'Jen, engineering', tilt: -1 })`
|
|
46
|
+
)}
|
|
47
|
+
|
|
48
|
+
<h2 class="doc-h2" id="ratios">Aspect ratios</h2>
|
|
49
|
+
<p>Set <code>ratio</code> to control the crop. <code>'1/1'</code> for square portraits, <code>'4/3'</code> for landscape shots.</p>
|
|
50
|
+
${demo(
|
|
51
|
+
`<div style="display:flex;gap:2rem;align-items:flex-start;justify-content:center;flex-wrap:wrap;padding:2rem">` +
|
|
52
|
+
photoCard({ src: '/img/placeholder-square.svg', alt: 'Square crop', caption: "ratio: '1/1'", ratio: '1/1', tilt: 1 }) +
|
|
53
|
+
photoCard({ src: '/img/placeholder-landscape.svg', alt: 'Landscape crop', caption: "ratio: '4/3'", ratio: '4/3', tilt: -1.5 }) +
|
|
54
|
+
photoCard({ src: '/img/placeholder-tall.svg', alt: 'Tall crop', caption: "ratio: '3/4'", ratio: '3/4', tilt: 2 }) +
|
|
55
|
+
`</div>`,
|
|
56
|
+
`photoCard({ src, alt: 'Square portrait', ratio: '1/1', tilt: 1 })
|
|
57
|
+
photoCard({ src, alt: 'Landscape photo', ratio: '4/3', tilt: -1.5 })
|
|
58
|
+
photoCard({ src, alt: 'Tall portrait', ratio: '3/4', tilt: 2 })`
|
|
59
|
+
)}
|
|
60
|
+
|
|
61
|
+
${table(
|
|
62
|
+
['Prop', 'Type', 'Default', 'Description'],
|
|
63
|
+
[
|
|
64
|
+
['<code>src</code>', 'string', '—', 'Image source URL'],
|
|
65
|
+
['<code>alt</code>', 'string', '—', 'Alt text — required for accessibility'],
|
|
66
|
+
['<code>caption</code>', 'string', '—', 'Caption text below the photo'],
|
|
67
|
+
['<code>tilt</code>', 'number', '0', 'CSS rotate degrees — negative leans left, positive leans right. 0 = no tilt'],
|
|
68
|
+
['<code>ratio</code>', 'string', "'4/3'", 'CSS aspect-ratio for the image crop'],
|
|
69
|
+
['<code>rounded</code>', 'boolean', 'false', 'Rounds the image corners inside the card frame'],
|
|
70
|
+
]
|
|
71
|
+
)}
|
|
72
|
+
`,
|
|
73
|
+
}),
|
|
74
|
+
}
|
|
@@ -39,11 +39,50 @@ export default {
|
|
|
39
39
|
})`
|
|
40
40
|
)}
|
|
41
41
|
|
|
42
|
+
<h2 class="doc-h2" id="diagonal">Diagonal variant</h2>
|
|
43
|
+
<p>The <code>diagonal</code> variant clips the bottom edge to a slanted angle, creating a layered stacking effect when multiple sections follow each other. The section after it should start with no top gap.</p>
|
|
44
|
+
${demo(
|
|
45
|
+
uiSection({
|
|
46
|
+
variant: 'diagonal',
|
|
47
|
+
eyebrow: 'Diagonal',
|
|
48
|
+
title: 'Slanted bottom edge.',
|
|
49
|
+
align: 'center',
|
|
50
|
+
content: container({ size: 'md', content: '<p style="text-align:center;color:var(--ui-muted)">Content here</p>' }),
|
|
51
|
+
}),
|
|
52
|
+
`section({ variant: 'diagonal', title: 'Slanted edge', align: 'center', content })`
|
|
53
|
+
)}
|
|
54
|
+
|
|
55
|
+
<h2 class="doc-h2" id="paper">Paper variant</h2>
|
|
56
|
+
<p>The <code>paper</code> variant applies a subtle off-white background and slight inset box shadow — useful for content sections that need to feel warmer and tactile.</p>
|
|
57
|
+
${demo(
|
|
58
|
+
uiSection({
|
|
59
|
+
variant: 'paper',
|
|
60
|
+
eyebrow: 'Paper',
|
|
61
|
+
title: 'Warm and tactile.',
|
|
62
|
+
align: 'center',
|
|
63
|
+
content: container({ size: 'md', content: '<p style="text-align:center;color:var(--ui-muted)">Content here</p>' }),
|
|
64
|
+
}),
|
|
65
|
+
`section({ variant: 'paper', title: 'Warm and tactile', align: 'center', content })`
|
|
66
|
+
)}
|
|
67
|
+
|
|
68
|
+
<h2 class="doc-h2" id="spotlight">Spotlight variant</h2>
|
|
69
|
+
<p>The <code>spotlight</code> variant adds a radial gradient light source at the top centre — gives the section a dramatic, focused feeling.</p>
|
|
70
|
+
${demo(
|
|
71
|
+
uiSection({
|
|
72
|
+
variant: 'spotlight',
|
|
73
|
+
eyebrow: 'Spotlight',
|
|
74
|
+
title: 'Dramatically focused.',
|
|
75
|
+
align: 'center',
|
|
76
|
+
content: container({ size: 'md', content: '<p style="text-align:center;color:var(--ui-muted)">Content here</p>' }),
|
|
77
|
+
}),
|
|
78
|
+
`section({ variant: 'spotlight', title: 'Dramatically focused', align: 'center', content })`
|
|
79
|
+
)}
|
|
80
|
+
|
|
42
81
|
${table(
|
|
43
82
|
['Prop', 'Type', 'Default', 'Description'],
|
|
44
83
|
[
|
|
45
84
|
['<code>content</code>', 'string (HTML)', '—', 'Raw HTML slot'],
|
|
46
|
-
['<code>variant</code>', 'string', "'default'", "'default' · 'alt' · 'dark'"],
|
|
85
|
+
['<code>variant</code>', 'string', "'default'", "'default' · 'alt' · 'dark' · 'diagonal' · 'paper' · 'spotlight'"],
|
|
47
86
|
['<code>padding</code>', 'string', "'md'", "'sm' · 'md' · 'lg'"],
|
|
48
87
|
['<code>id</code>', 'string', '—', 'Anchor id for nav links'],
|
|
49
88
|
['<code>eyebrow</code>', 'string', '—', 'Small label above the title'],
|
package/docs/src/pages/meta.js
CHANGED
|
@@ -126,6 +126,32 @@ meta: {
|
|
|
126
126
|
}`, 'js'))}
|
|
127
127
|
${callout('note', 'For maximum performance, host your own fonts rather than using Google Fonts. External stylesheet requests add render-blocking latency and a DNS lookup.')}
|
|
128
128
|
|
|
129
|
+
${section('vibe', 'Personality presets (meta.vibe)')}
|
|
130
|
+
<p>Set <code>meta.vibe</code> to apply a personality preset that adjusts typography, border radius, and letter-spacing across all components on the page. The preset sets a <code>data-vibe</code> attribute on <code><body></code> — pair it with a <code>theme.css</code> for complete brand expression.</p>
|
|
131
|
+
${codeBlock(highlight(`export default {
|
|
132
|
+
route: '/landing',
|
|
133
|
+
meta: {
|
|
134
|
+
vibe: 'playful',
|
|
135
|
+
styles: ['/pulse-ui.css', '/theme.css'],
|
|
136
|
+
},
|
|
137
|
+
}`, 'js'))}
|
|
138
|
+
${table(
|
|
139
|
+
['Vibe', 'Character', 'Best for'],
|
|
140
|
+
[
|
|
141
|
+
['<code>warm</code>', 'Friendly, rounded (14px), humanist feel', 'Local businesses, wellness, food, services'],
|
|
142
|
+
['<code>editorial</code>', 'Serif display, zero radius, tight tracking', 'Publications, blogs, journalism, literary'],
|
|
143
|
+
['<code>playful</code>', 'Extra-large radius (22px), loose spacing', 'Consumer apps, kids, lifestyle, creators'],
|
|
144
|
+
['<code>minimal</code>', 'Ultra-low radius (2px), refined whitespace', 'Portfolios, luxury, developer tools'],
|
|
145
|
+
['<code>bold</code>', 'Tight tracking, strong presence', 'Agencies, startups, sports'],
|
|
146
|
+
['<code>brutalist</code>', 'Zero radius, oversized Arial Black / Impact headings', 'Art, fashion, counter-culture, zines'],
|
|
147
|
+
['<code>retro</code>', 'Slab serif, medium radius (8px), nostalgic geometry', 'Craft, breweries, vintage, heritage brands'],
|
|
148
|
+
['<code>corporate</code>', 'Conservative radius (4px), tight tracking, professional', 'B2B, finance, legal, consultancies'],
|
|
149
|
+
['<code>neon</code>', 'Monospace headline, futuristic, sharp edges', 'Gaming, crypto, dark-tech, cyberpunk'],
|
|
150
|
+
['<code>paper</code>', 'Organic radius (10px), serif body, tactile feel', 'Journals, bookshops, newsletters, diaries'],
|
|
151
|
+
]
|
|
152
|
+
)}
|
|
153
|
+
${callout('tip', 'Vibe only adjusts shape and type tokens — it does not set colours. Add a <code>theme.css</code> that defines <code>--ui-accent</code>, <code>--color-bg</code>, and other colour tokens to complete the personality.')}
|
|
154
|
+
|
|
129
155
|
${section('seo-tips', 'SEO tips')}
|
|
130
156
|
<ul>
|
|
131
157
|
<li>Write a unique <code>title</code> and <code>description</code> for every page — duplicate metadata prevents pages from competing in search results.</li>
|
|
@@ -0,0 +1,260 @@
|
|
|
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
|
+
import { asset } from '../../lib/layout.js'
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Code block helper
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
function codeBlock({ filename, code }) {
|
|
35
|
+
return `
|
|
36
|
+
<div class="ap-code-block" role="figure" aria-label="Code example: ${filename}">
|
|
37
|
+
<div class="ap-code-block__header">
|
|
38
|
+
${iconCode({ size: 14 })}
|
|
39
|
+
<span>${filename}</span>
|
|
40
|
+
</div>
|
|
41
|
+
<pre class="ap-code-block__pre"><code class="ap-code-block__code">${code}</code></pre>
|
|
42
|
+
</div>
|
|
43
|
+
`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Spec
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
export default {
|
|
51
|
+
route: '/templates/andypixel',
|
|
52
|
+
|
|
53
|
+
meta: {
|
|
54
|
+
title: 'JavaScript Proxies: The Hidden Superpower \u2014 Andy Pixel',
|
|
55
|
+
description: 'Discover how JavaScript Proxies let you intercept and redefine fundamental operations on objects.',
|
|
56
|
+
styles: ['/pulse-ui.css', '/andypixel.css'],
|
|
57
|
+
theme: 'light',
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
state: {
|
|
61
|
+
status: 'idle',
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
view: (state) => `
|
|
65
|
+
<main id="main-content">
|
|
66
|
+
|
|
67
|
+
${nav({
|
|
68
|
+
logo: 'Andy Pixel',
|
|
69
|
+
links: [
|
|
70
|
+
{ label: 'Blog', href: '#blog' },
|
|
71
|
+
{ label: 'Projects', href: '#projects' },
|
|
72
|
+
{ label: 'About', href: '#about' },
|
|
73
|
+
],
|
|
74
|
+
})}
|
|
75
|
+
|
|
76
|
+
<article>
|
|
77
|
+
|
|
78
|
+
<header class="ap-article-header">
|
|
79
|
+
<div class="ap-reading-col">
|
|
80
|
+
|
|
81
|
+
<div class="ap-article-meta">
|
|
82
|
+
${badge({ label: 'JavaScript' })}
|
|
83
|
+
${badge({ label: 'Advanced' })}
|
|
84
|
+
<span class="u-text-muted u-text-sm">\u00b7 May 28, 2026 \u00b7 8 min read</span>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<h1 class="ap-article-title">
|
|
88
|
+
JavaScript Proxies: The Hidden Superpower You\u2019re Not Using
|
|
89
|
+
</h1>
|
|
90
|
+
|
|
91
|
+
<p class="ap-article-subtitle">
|
|
92
|
+
Intercept, transform, and observe objects at a fundamental level \u2014
|
|
93
|
+
without touching the source.
|
|
94
|
+
</p>
|
|
95
|
+
|
|
96
|
+
<div class="ap-article-author-strip">
|
|
97
|
+
${avatar({ name: 'Andy Pixel', size: 'sm' })}
|
|
98
|
+
<div>
|
|
99
|
+
<p class="u-text-sm u-font-bold">Andy Pixel</p>
|
|
100
|
+
<p class="u-text-sm u-text-muted">Frontend Engineer</p>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
</div>
|
|
105
|
+
</header>
|
|
106
|
+
|
|
107
|
+
<div class="ap-article-body ap-reading-col">
|
|
108
|
+
|
|
109
|
+
${prose({ content: `
|
|
110
|
+
<p class="ap-article-lead">If you\u2019ve ever wanted to validate object properties automatically,
|
|
111
|
+
track changes reactively, or build a mock API client that feels real \u2014 the
|
|
112
|
+
<code>Proxy</code> object is your tool. Yet somehow it flies under the radar
|
|
113
|
+
for most developers.</p>
|
|
114
|
+
|
|
115
|
+
<p>Introduced in ES2015, <code>Proxy</code> wraps an object and lets you intercept
|
|
116
|
+
fundamental operations \u2014 property reads, writes, deletions, and more \u2014 via
|
|
117
|
+
\u201ctraps\u201d defined on a handler object.</p>
|
|
118
|
+
|
|
119
|
+
<h2>The anatomy of a Proxy</h2>
|
|
120
|
+
|
|
121
|
+
<p>A Proxy takes two arguments: the <strong>target</strong> (the object being wrapped)
|
|
122
|
+
and the <strong>handler</strong> (an object defining which operations to intercept).
|
|
123
|
+
Every interceptable operation has a corresponding trap method on the handler.</p>
|
|
124
|
+
` })}
|
|
125
|
+
|
|
126
|
+
${codeBlock({
|
|
127
|
+
filename: 'proxy-basics.js',
|
|
128
|
+
code: `const handler = {
|
|
129
|
+
get(target, key) {
|
|
130
|
+
return key in target
|
|
131
|
+
? target[key]
|
|
132
|
+
: \`Property "\${key}" doesn\u2019t exist\`
|
|
133
|
+
},
|
|
134
|
+
set(target, key, value) {
|
|
135
|
+
if (typeof value !== 'number') {
|
|
136
|
+
throw new TypeError('Only numbers allowed')
|
|
137
|
+
}
|
|
138
|
+
target[key] = value
|
|
139
|
+
return true
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const proxy = new Proxy({}, handler)
|
|
144
|
+
proxy.score = 42 // \u2713 works
|
|
145
|
+
proxy.score = 'oops' // \u2717 TypeError`,
|
|
146
|
+
})}
|
|
147
|
+
|
|
148
|
+
${prose({ content: `
|
|
149
|
+
<p>The <code>get</code> trap runs on every property access. The <code>set</code> trap
|
|
150
|
+
intercepts every assignment. Return <code>true</code> from <code>set</code> to signal
|
|
151
|
+
success \u2014 otherwise you\u2019ll get a <code>TypeError</code> in strict mode.</p>
|
|
152
|
+
|
|
153
|
+
<h2>Real-world use cases</h2>
|
|
154
|
+
|
|
155
|
+
<p>The real power isn\u2019t abstract. Proxy unlocks patterns that are otherwise
|
|
156
|
+
painful or impossible.</p>
|
|
157
|
+
` })}
|
|
158
|
+
|
|
159
|
+
<blockquote class="ap-pullquote">
|
|
160
|
+
<p>\u201cA Proxy doesn\u2019t change what your object <em>is</em> \u2014 it changes what your
|
|
161
|
+
object <em>does</em> when the runtime touches it.\u201d</p>
|
|
162
|
+
</blockquote>
|
|
163
|
+
|
|
164
|
+
${prose({ content: `
|
|
165
|
+
<h3>Reactive state</h3>
|
|
166
|
+
<p>Vue 3\u2019s reactivity system is built entirely on Proxy. When you access a reactive
|
|
167
|
+
property, Vue\u2019s <code>get</code> trap tracks the dependency. When you mutate it,
|
|
168
|
+
the <code>set</code> trap schedules re-renders. No syntax overhead. Just assignment.</p>
|
|
169
|
+
|
|
170
|
+
<h3>Input validation</h3>
|
|
171
|
+
<p>Instead of scattering guard clauses through your codebase, centralise validation
|
|
172
|
+
in a Proxy handler. The contract lives in one place. Every consumer benefits
|
|
173
|
+
automatically.</p>
|
|
174
|
+
|
|
175
|
+
<h3>API mocking</h3>
|
|
176
|
+
<p>Wrap a plain object in a Proxy that intercepts property access and returns async
|
|
177
|
+
functions. You get a fully-typed mock client that mirrors your real API surface \u2014
|
|
178
|
+
without any code generation.</p>
|
|
179
|
+
|
|
180
|
+
<h2>Performance considerations</h2>
|
|
181
|
+
|
|
182
|
+
<p>Proxies are not free. Every trap fires a function call. In tight loops over large
|
|
183
|
+
collections, this adds up. Benchmark before you commit. For most use cases \u2014 form
|
|
184
|
+
state, reactive stores, config validation \u2014 the overhead is completely negligible.</p>
|
|
185
|
+
|
|
186
|
+
<p>The V8 team has steadily optimised Proxy performance since ES2015. Don\u2019t avoid
|
|
187
|
+
Proxy out of dated fear \u2014 measure it first.</p>
|
|
188
|
+
` })}
|
|
189
|
+
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
</article>
|
|
193
|
+
|
|
194
|
+
<div class="ap-author-bio-wrap">
|
|
195
|
+
<div class="ap-reading-col">
|
|
196
|
+
<div class="ap-author-bio">
|
|
197
|
+
${avatar({ name: 'Andy Pixel', size: 'lg' })}
|
|
198
|
+
<div class="ap-author-bio__text">
|
|
199
|
+
<p class="ap-author-bio__name">Andy Pixel</p>
|
|
200
|
+
<p class="u-text-muted u-text-sm">
|
|
201
|
+
Frontend engineer obsessed with the web platform. Writing about JavaScript,
|
|
202
|
+
CSS, and creative code. Building things at the edge of what browsers can do.
|
|
203
|
+
</p>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
${ state.status === 'success'
|
|
210
|
+
? `<div class="ap-newsletter-success">
|
|
211
|
+
<p class="u-font-bold u-text-lg">${iconCheck({ size: 20, class: 'u-inline' })} You\u2019re in!</p>
|
|
212
|
+
<p class="u-text-muted u-mt-2">Next article lands in your inbox.</p>
|
|
213
|
+
</div>`
|
|
214
|
+
: `<div class="ap-newsletter-section">
|
|
215
|
+
<p class="ap-newsletter-eyebrow">Stay in the loop</p>
|
|
216
|
+
<h2 class="ap-newsletter-title">More like this, in your inbox.</h2>
|
|
217
|
+
<p class="u-text-muted">
|
|
218
|
+
Deep-dives on JavaScript, the web platform, and frontend engineering \u2014
|
|
219
|
+
whenever I publish. No noise.
|
|
220
|
+
</p>
|
|
221
|
+
${ state.status === 'error'
|
|
222
|
+
? `<p class="ap-newsletter-error u-mt-3">Something went wrong. Please try again.</p>`
|
|
223
|
+
: '' }
|
|
224
|
+
<form class="ap-newsletter-form" data-action="subscribe">
|
|
225
|
+
${input({ type: 'email', name: 'email', placeholder: 'you@example.com', label: 'Email address', attrs: { autocomplete: 'email' } })}
|
|
226
|
+
${button({ label: 'Subscribe', variant: 'primary', attrs: { type: 'submit' } })}
|
|
227
|
+
</form>
|
|
228
|
+
</div>`
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
${footer({
|
|
232
|
+
logo: 'Andy Pixel',
|
|
233
|
+
links: [
|
|
234
|
+
{ label: 'Blog', href: '#blog' },
|
|
235
|
+
{ label: 'Projects', href: '#projects' },
|
|
236
|
+
{ label: 'About', href: '#about' },
|
|
237
|
+
{ label: 'RSS', href: '#feed' },
|
|
238
|
+
],
|
|
239
|
+
})}
|
|
240
|
+
|
|
241
|
+
</main>
|
|
242
|
+
<script src="${asset('/pulse-ui.js')}" defer></script>
|
|
243
|
+
`,
|
|
244
|
+
|
|
245
|
+
actions: {
|
|
246
|
+
subscribe: {
|
|
247
|
+
onStart: (state, formData) => ({
|
|
248
|
+
status: 'loading',
|
|
249
|
+
}),
|
|
250
|
+
run: async (state, serverState, formData) => {
|
|
251
|
+
// Newsletter subscription would call an API here
|
|
252
|
+
},
|
|
253
|
+
onSuccess: (state) => ({ status: 'success' }),
|
|
254
|
+
onError: (state, err) => ({
|
|
255
|
+
status: 'error',
|
|
256
|
+
errors: err?.validation ?? [{ message: err.message }],
|
|
257
|
+
}),
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
}
|