@invisibleloop/pulse 0.20.4 → 0.20.6
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/pulse-checklist.md +1 -2
- package/.github/workflows/ci.yml +3 -0
- package/CLAUDE.md +1 -2
- package/docs/.claude/pulse-checklist.md +1 -2
- package/docs/public/coutumes.css +21 -44
- package/docs/public/docs.css +67 -0
- package/docs/public/pulse-ui.css +8 -1037
- package/docs/public/retrowheels.css +23 -0
- package/docs/public/shop.css +35 -1
- package/docs/public/sparkys.css +56 -23
- package/docs/server.js +0 -11
- package/docs/src/lib/nav.js +0 -11
- package/docs/src/pages/components/charts.js +2 -1
- package/docs/src/pages/components/grid.js +9 -5
- package/docs/src/pages/components.js +0 -4
- package/docs/src/pages/home.js +16 -1
- package/docs/src/pages/how-it-works.js +20 -14
- package/docs/src/pages/templates/coutumes.js +83 -72
- package/docs/src/pages/templates/mobile-app.js +36 -9
- package/docs/src/pages/templates/retrowheels.js +17 -4
- package/docs/src/pages/templates/shop.js +21 -10
- package/docs/src/pages/templates/sparkys.js +35 -20
- package/examples/.claude/pulse-checklist.md +1 -2
- package/examples/examples.test.js +4 -4
- package/examples/pricing.js +21 -3
- package/examples/public/pulse-ui.css +8 -1037
- package/examples/retrowheels.css +23 -0
- package/examples/retrowheels.js +17 -4
- package/package.json +5 -1
- package/public/.pulse-ui-version +1 -1
- package/public/pulse-ui.css +8 -1037
- package/src/agent/checklist.md +1 -2
- package/src/agent/guide-components.md +43 -67
- package/src/agent/guide-design-gallery.md +14 -14
- package/src/agent/guide-design-references.md +21 -21
- package/src/agent/guide-styles.md +2 -2
- package/src/agent/guide-templates.md +6 -6
- package/src/agent/identity.md +1 -1
- package/src/agent/workflow.md +1 -2
- package/src/mcp/server.js +31 -24
- package/src/ui/index.js +0 -27
- package/src/ui/ui.test.js +0 -137
- package/types/charts.d.ts +61 -0
- package/types/ui.d.ts +1 -129
- package/docs/src/pages/components/banner.js +0 -44
- package/docs/src/pages/components/carousel.js +0 -71
- package/docs/src/pages/components/code-window.js +0 -56
- package/docs/src/pages/components/decorate.js +0 -74
- package/docs/src/pages/components/gallery.js +0 -102
- package/docs/src/pages/components/list.js +0 -111
- package/docs/src/pages/components/marquee.js +0 -76
- package/docs/src/pages/components/photo-card.js +0 -74
- package/docs/src/pages/components/pricing.js +0 -67
- package/docs/src/pages/components/testimonial.js +0 -48
- package/docs/src/pages/components/timeline.js +0 -226
- package/src/ui/banner.js +0 -41
- package/src/ui/brand-statement.js +0 -58
- package/src/ui/carousel.js +0 -59
- package/src/ui/code-window.js +0 -39
- package/src/ui/colophon.js +0 -29
- package/src/ui/contact-row.js +0 -41
- package/src/ui/decorate.js +0 -55
- package/src/ui/display-heading.js +0 -44
- package/src/ui/drop-cap.js +0 -29
- package/src/ui/editorial-hero.js +0 -63
- package/src/ui/entry-list.js +0 -36
- package/src/ui/gallery.js +0 -60
- package/src/ui/image-pair.js +0 -72
- package/src/ui/list.js +0 -36
- package/src/ui/marquee.js +0 -48
- package/src/ui/photo-card.js +0 -44
- package/src/ui/pricing.js +0 -56
- package/src/ui/product-card.js +0 -65
- package/src/ui/section-label.js +0 -38
- package/src/ui/service-card.js +0 -47
- package/src/ui/stat-grid.js +0 -40
- package/src/ui/testimonial.js +0 -59
- package/src/ui/timeline.js +0 -72
- package/src/ui/top-strip.js +0 -47
- package/src/ui/trust-list.js +0 -46
|
@@ -1,48 +0,0 @@
|
|
|
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 { testimonial } from '../../../../src/ui/index.js'
|
|
5
|
-
|
|
6
|
-
const { prev, next } = prevNext('/components/testimonial')
|
|
7
|
-
|
|
8
|
-
export default {
|
|
9
|
-
route: '/components/testimonial',
|
|
10
|
-
meta: {
|
|
11
|
-
title: 'Testimonial — Pulse Docs',
|
|
12
|
-
description: 'Testimonial component for Pulse UI.',
|
|
13
|
-
styles: ['/pulse-ui.css', '/docs.css'],
|
|
14
|
-
},
|
|
15
|
-
view: () => renderComponentPage({
|
|
16
|
-
currentHref: '/components/testimonial',
|
|
17
|
-
prev,
|
|
18
|
-
next,
|
|
19
|
-
name: 'testimonial',
|
|
20
|
-
description: 'Customer quote card with star rating, avatar, and attribution. When <code>src</code> is omitted the avatar shows initials derived from <code>name</code>.',
|
|
21
|
-
content: `
|
|
22
|
-
${demo(
|
|
23
|
-
`<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.5rem">` +
|
|
24
|
-
testimonial({ quote: 'This is the fastest app I have ever used. It replaced three tools I was paying for.', name: 'Alice Marsh', role: 'Head of Product, Stride', rating: 5 }) +
|
|
25
|
-
testimonial({ quote: 'Switched from the competition six months ago and have not looked back once.', name: 'Ben Carter', role: 'Founder, Loopback', rating: 5 }) +
|
|
26
|
-
`</div>`,
|
|
27
|
-
`testimonial({
|
|
28
|
-
quote: 'This is the fastest app I have ever used.',
|
|
29
|
-
name: 'Alice Marsh',
|
|
30
|
-
role: 'Head of Product, Stride',
|
|
31
|
-
rating: 5,
|
|
32
|
-
src: user.avatarUrl, // optional — falls back to initials
|
|
33
|
-
})`
|
|
34
|
-
)}
|
|
35
|
-
|
|
36
|
-
${table(
|
|
37
|
-
['Prop', 'Type', 'Default', 'Description'],
|
|
38
|
-
[
|
|
39
|
-
['<code>quote</code>', 'string', '—', ''],
|
|
40
|
-
['<code>name</code>', 'string', '—', 'Author name — also used for avatar initials'],
|
|
41
|
-
['<code>role</code>', 'string', '—', ''],
|
|
42
|
-
['<code>src</code>', 'string', '—', 'Avatar image URL; omit to show initials'],
|
|
43
|
-
['<code>rating</code>', 'number', '0', '1–5 stars; omit or set 0 to hide'],
|
|
44
|
-
]
|
|
45
|
-
)}
|
|
46
|
-
`,
|
|
47
|
-
}),
|
|
48
|
-
}
|
|
@@ -1,226 +0,0 @@
|
|
|
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 { timeline, timelineItem, badge, card, stat, button } from '../../../../src/ui/index.js'
|
|
5
|
-
|
|
6
|
-
const { prev, next } = prevNext('/components/timeline')
|
|
7
|
-
|
|
8
|
-
// Shared check SVG for reuse
|
|
9
|
-
const checkIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`
|
|
10
|
-
const starIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>`
|
|
11
|
-
const alertIcon = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>`
|
|
12
|
-
|
|
13
|
-
export default {
|
|
14
|
-
route: '/components/timeline',
|
|
15
|
-
meta: {
|
|
16
|
-
title: 'Timeline — Pulse Docs',
|
|
17
|
-
description: 'Timeline component for Pulse UI.',
|
|
18
|
-
styles: ['/pulse-ui.css', '/docs.css'],
|
|
19
|
-
},
|
|
20
|
-
view: () => renderComponentPage({
|
|
21
|
-
currentHref: '/components/timeline',
|
|
22
|
-
prev,
|
|
23
|
-
next,
|
|
24
|
-
name: 'timeline',
|
|
25
|
-
description: 'Ordered sequence of events or steps connected by a line. Supports vertical (default) and horizontal orientations. Each item accepts a raw HTML content slot — pass any text, component, or markup.',
|
|
26
|
-
content: `
|
|
27
|
-
|
|
28
|
-
<h2 class="doc-h2" id="vertical">Vertical (default)</h2>
|
|
29
|
-
<p>Steps flow downward. The connector line links each dot to the next.</p>
|
|
30
|
-
${demo(
|
|
31
|
-
timeline({
|
|
32
|
-
items: [
|
|
33
|
-
{ label: 'Jan 2023', content: '<strong style="color:var(--ui-text)">Project kicked off</strong><p style="color:var(--ui-muted);margin:.25rem 0 0">Initial scope agreed with stakeholders. Repository created.</p>' },
|
|
34
|
-
{ label: 'Mar 2023', content: '<strong style="color:var(--ui-text)">Alpha release</strong><p style="color:var(--ui-muted);margin:.25rem 0 0">Internal testing with 12 pilot users. Core features stable.</p>' },
|
|
35
|
-
{ label: 'Jun 2023', content: '<strong style="color:var(--ui-text)">Public beta</strong><p style="color:var(--ui-muted);margin:.25rem 0 0">Open sign-up enabled. 400 users in first week.</p>' },
|
|
36
|
-
{ label: 'Sep 2023', content: '<strong style="color:var(--ui-text)">v1.0 launched</strong><p style="color:var(--ui-muted);margin:.25rem 0 0">Billing live, docs published, ProductHunt launch.</p>' },
|
|
37
|
-
],
|
|
38
|
-
}),
|
|
39
|
-
`timeline({
|
|
40
|
-
items: [
|
|
41
|
-
{ label: 'Jan 2023', content: '<strong>Project kicked off</strong><p>Initial scope agreed.</p>' },
|
|
42
|
-
{ label: 'Mar 2023', content: '<strong>Alpha release</strong><p>Internal testing with 12 pilot users.</p>' },
|
|
43
|
-
{ label: 'Jun 2023', content: '<strong>Public beta</strong><p>Open sign-up enabled. 400 users in first week.</p>' },
|
|
44
|
-
{ label: 'Sep 2023', content: '<strong>v1.0 launched</strong><p>Billing live, docs published.</p>' },
|
|
45
|
-
],
|
|
46
|
-
})`
|
|
47
|
-
)}
|
|
48
|
-
|
|
49
|
-
<h2 class="doc-h2" id="horizontal">Horizontal</h2>
|
|
50
|
-
<p>Steps flow left to right — good for process flows or numbered stages.</p>
|
|
51
|
-
${demo(
|
|
52
|
-
timeline({
|
|
53
|
-
direction: 'horizontal',
|
|
54
|
-
items: [
|
|
55
|
-
{ label: 'Step 1', content: '<p style="color:var(--ui-muted);margin:0;font-size:.85rem">Sign up</p>' },
|
|
56
|
-
{ label: 'Step 2', content: '<p style="color:var(--ui-muted);margin:0;font-size:.85rem">Connect data</p>' },
|
|
57
|
-
{ label: 'Step 3', content: '<p style="color:var(--ui-muted);margin:0;font-size:.85rem">Invite team</p>' },
|
|
58
|
-
{ label: 'Step 4', content: '<p style="color:var(--ui-muted);margin:0;font-size:.85rem">Go live</p>' },
|
|
59
|
-
],
|
|
60
|
-
}),
|
|
61
|
-
`timeline({
|
|
62
|
-
direction: 'horizontal',
|
|
63
|
-
items: [
|
|
64
|
-
{ label: 'Step 1', content: '<p>Sign up</p>' },
|
|
65
|
-
{ label: 'Step 2', content: '<p>Connect data</p>' },
|
|
66
|
-
{ label: 'Step 3', content: '<p>Invite team</p>' },
|
|
67
|
-
{ label: 'Step 4', content: '<p>Go live</p>' },
|
|
68
|
-
],
|
|
69
|
-
})`
|
|
70
|
-
)}
|
|
71
|
-
|
|
72
|
-
<h2 class="doc-h2" id="dot-colors">Dot colours</h2>
|
|
73
|
-
<p>Use <code>dotColor</code> to convey status: <code>'accent'</code> · <code>'success'</code> · <code>'warning'</code> · <code>'error'</code> · <code>'muted'</code>.</p>
|
|
74
|
-
${demo(
|
|
75
|
-
timeline({
|
|
76
|
-
items: [
|
|
77
|
-
{ dotColor: 'success', label: 'Deployed', content: '<p style="color:var(--ui-muted);margin:0">Production deploy completed successfully.</p>' },
|
|
78
|
-
{ dotColor: 'success', label: 'Tested', content: '<p style="color:var(--ui-muted);margin:0">All 92 tests passed. Coverage 98%.</p>' },
|
|
79
|
-
{ dotColor: 'warning', label: 'Review', content: '<p style="color:var(--ui-muted);margin:0">Awaiting sign-off from design lead.</p>' },
|
|
80
|
-
{ dotColor: 'error', label: 'Blocked', content: '<p style="color:var(--ui-muted);margin:0">Dependency on payment API not yet ready.</p>' },
|
|
81
|
-
{ dotColor: 'muted', label: 'Planned', content: '<p style="color:var(--ui-muted);margin:0">Mobile app release — Q1 2025.</p>' },
|
|
82
|
-
],
|
|
83
|
-
}),
|
|
84
|
-
`timeline({
|
|
85
|
-
items: [
|
|
86
|
-
{ dotColor: 'success', label: 'Deployed', content: '...' },
|
|
87
|
-
{ dotColor: 'success', label: 'Tested', content: '...' },
|
|
88
|
-
{ dotColor: 'warning', label: 'Review', content: '...' },
|
|
89
|
-
{ dotColor: 'error', label: 'Blocked', content: '...' },
|
|
90
|
-
{ dotColor: 'muted', label: 'Planned', content: '...' },
|
|
91
|
-
],
|
|
92
|
-
})`
|
|
93
|
-
)}
|
|
94
|
-
|
|
95
|
-
<h2 class="doc-h2" id="icon-dots">Icon dots</h2>
|
|
96
|
-
<p>Pass any SVG or emoji as <code>dot</code>. The dot grows to accommodate the content and uses a tinted background matching its colour variant.</p>
|
|
97
|
-
${demo(
|
|
98
|
-
timeline({
|
|
99
|
-
items: [
|
|
100
|
-
{ dot: checkIcon, dotColor: 'success', label: 'Completed', content: '<strong style="color:var(--ui-text)">Onboarding</strong><p style="color:var(--ui-muted);margin:.2rem 0 0">Profile set up, preferences saved.</p>' },
|
|
101
|
-
{ dot: starIcon, dotColor: 'accent', label: 'Milestone', content: '<strong style="color:var(--ui-text)">First 1,000 users</strong><p style="color:var(--ui-muted);margin:.2rem 0 0">Reached organically in 18 days.</p>' },
|
|
102
|
-
{ dot: alertIcon, dotColor: 'warning', label: 'Incident', content: '<strong style="color:var(--ui-text)">Partial outage</strong><p style="color:var(--ui-muted);margin:.2rem 0 0">CDN edge node failed — resolved in 4 minutes.</p>' },
|
|
103
|
-
],
|
|
104
|
-
}),
|
|
105
|
-
`timeline({
|
|
106
|
-
items: [
|
|
107
|
-
{
|
|
108
|
-
dot: checkSvg,
|
|
109
|
-
dotColor: 'success',
|
|
110
|
-
label: 'Completed',
|
|
111
|
-
content: '<strong>Onboarding</strong><p>Profile set up, preferences saved.</p>',
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
dot: starSvg,
|
|
115
|
-
dotColor: 'accent',
|
|
116
|
-
label: 'Milestone',
|
|
117
|
-
content: '<strong>First 1,000 users</strong>',
|
|
118
|
-
},
|
|
119
|
-
],
|
|
120
|
-
})`
|
|
121
|
-
)}
|
|
122
|
-
|
|
123
|
-
<h2 class="doc-h2" id="rich-content">Rich content slot</h2>
|
|
124
|
-
<p>The <code>content</code> slot accepts any HTML — including other Pulse components like <code>card()</code>, <code>badge()</code>, or <code>stat()</code>.</p>
|
|
125
|
-
${demo(
|
|
126
|
-
timeline({
|
|
127
|
-
items: [
|
|
128
|
-
{
|
|
129
|
-
dotColor: 'success',
|
|
130
|
-
label: 'Q1 2024',
|
|
131
|
-
content: card({
|
|
132
|
-
title: 'Series A closed',
|
|
133
|
-
content: `<div style="display:flex;gap:1.5rem;flex-wrap:wrap">` +
|
|
134
|
-
stat({ label: 'Raised', value: '$4.2M' }) +
|
|
135
|
-
stat({ label: 'Valuation', value: '$18M' }) +
|
|
136
|
-
stat({ label: 'Investors', value: '6' }) +
|
|
137
|
-
`</div>`,
|
|
138
|
-
}),
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
dotColor: 'accent',
|
|
142
|
-
label: 'Q3 2024',
|
|
143
|
-
content: card({
|
|
144
|
-
title: 'Product launch',
|
|
145
|
-
content: `<p style="color:var(--ui-muted);margin:0 0 .75rem">Shipped v1.0 to general availability. Three tiers, 14-day trial.</p>` +
|
|
146
|
-
`<div style="display:flex;gap:.5rem;flex-wrap:wrap">` +
|
|
147
|
-
badge({ label: 'Launch', variant: 'info' }) +
|
|
148
|
-
badge({ label: 'Billing live', variant: 'success' }) +
|
|
149
|
-
`</div>`,
|
|
150
|
-
}),
|
|
151
|
-
},
|
|
152
|
-
{
|
|
153
|
-
dotColor: 'muted',
|
|
154
|
-
label: 'Q1 2025 (planned)',
|
|
155
|
-
content: card({
|
|
156
|
-
title: 'Mobile apps',
|
|
157
|
-
content: `<p style="color:var(--ui-muted);margin:0 0 .75rem">iOS and Android apps in development. Public beta planned.</p>` +
|
|
158
|
-
button({ label: 'Join waitlist', size: 'sm', variant: 'secondary' }),
|
|
159
|
-
}),
|
|
160
|
-
},
|
|
161
|
-
],
|
|
162
|
-
}),
|
|
163
|
-
`timeline({
|
|
164
|
-
items: [
|
|
165
|
-
{
|
|
166
|
-
dotColor: 'success',
|
|
167
|
-
label: 'Q1 2024',
|
|
168
|
-
content: card({
|
|
169
|
-
title: 'Series A closed',
|
|
170
|
-
content: stat({ label: 'Raised', value: '$4.2M' }) + ...,
|
|
171
|
-
}),
|
|
172
|
-
},
|
|
173
|
-
{
|
|
174
|
-
dotColor: 'accent',
|
|
175
|
-
label: 'Q3 2024',
|
|
176
|
-
content: card({
|
|
177
|
-
title: 'Product launch',
|
|
178
|
-
content: '<p>Shipped v1.0 to general availability.</p>' +
|
|
179
|
-
badge({ label: 'Billing live', variant: 'success' }),
|
|
180
|
-
}),
|
|
181
|
-
},
|
|
182
|
-
],
|
|
183
|
-
})`
|
|
184
|
-
)}
|
|
185
|
-
|
|
186
|
-
<h2 class="doc-h2" id="item-fn">Using timelineItem()</h2>
|
|
187
|
-
<p>Build items individually with <code>timelineItem()</code> and pass the joined HTML as <code>content</code>. Useful for dynamic or conditional lists.</p>
|
|
188
|
-
${demo(
|
|
189
|
-
timeline({
|
|
190
|
-
content:
|
|
191
|
-
timelineItem({ dotColor: 'success', label: 'Done', content: '<p style="color:var(--ui-muted);margin:0">Design system tokens agreed</p>' }) +
|
|
192
|
-
timelineItem({ dotColor: 'success', label: 'Done', content: '<p style="color:var(--ui-muted);margin:0">Component library built</p>' }) +
|
|
193
|
-
timelineItem({ dotColor: 'accent', label: 'Current', content: '<p style="color:var(--ui-muted);margin:0">Documentation in progress</p>' }) +
|
|
194
|
-
timelineItem({ dotColor: 'muted', label: 'Next', content: '<p style="color:var(--ui-muted);margin:0">Public launch</p>' }),
|
|
195
|
-
}),
|
|
196
|
-
`timeline({
|
|
197
|
-
content:
|
|
198
|
-
timelineItem({ dotColor: 'success', label: 'Done', content: '...' }) +
|
|
199
|
-
timelineItem({ dotColor: 'success', label: 'Done', content: '...' }) +
|
|
200
|
-
timelineItem({ dotColor: 'accent', label: 'Current', content: '...' }) +
|
|
201
|
-
timelineItem({ dotColor: 'muted', label: 'Next', content: '...' }),
|
|
202
|
-
})`
|
|
203
|
-
)}
|
|
204
|
-
|
|
205
|
-
${table(
|
|
206
|
-
['Prop', 'Type', 'Default', 'Description'],
|
|
207
|
-
[
|
|
208
|
-
['<code>direction</code>', 'string', "'vertical'", "'vertical' · 'horizontal'"],
|
|
209
|
-
['<code>items</code>', 'array', '[]', 'Array of <code>timelineItem</code> option objects'],
|
|
210
|
-
['<code>content</code>', 'string (HTML)', '—', 'Raw HTML alternative to <code>items</code> — use with <code>timelineItem()</code>'],
|
|
211
|
-
]
|
|
212
|
-
)}
|
|
213
|
-
|
|
214
|
-
<h3 class="doc-h3" style="margin-top:2rem">timelineItem() props</h3>
|
|
215
|
-
${table(
|
|
216
|
-
['Prop', 'Type', 'Default', 'Description'],
|
|
217
|
-
[
|
|
218
|
-
['<code>content</code>', 'string (HTML)', '—', 'Raw HTML body — accepts any component output'],
|
|
219
|
-
['<code>label</code>', 'string', '—', 'Timestamp or step label (escaped)'],
|
|
220
|
-
['<code>dot</code>', 'string (HTML)', '—', 'Raw HTML inside the dot — SVG or emoji; grows the dot to 2rem'],
|
|
221
|
-
['<code>dotColor</code>', 'string', "'accent'", "'accent' · 'success' · 'warning' · 'error' · 'muted'"],
|
|
222
|
-
]
|
|
223
|
-
)}
|
|
224
|
-
`,
|
|
225
|
-
}),
|
|
226
|
-
}
|
package/src/ui/banner.js
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse UI — Banner
|
|
3
|
-
*
|
|
4
|
-
* Full-width announcement bar — sits above the nav for promotions,
|
|
5
|
-
* notices, or launch announcements.
|
|
6
|
-
*
|
|
7
|
-
* @param {object} opts
|
|
8
|
-
* @param {string} opts.content - Raw HTML slot
|
|
9
|
-
* @param {'info'|'promo'|'warning'} opts.variant - Visual style (default: 'info')
|
|
10
|
-
* @param {'top'|'bottom'} opts.position - 'bottom' fixes the bar to the bottom of the viewport
|
|
11
|
-
* @param {boolean} opts.dismissible - Show a close (×) button
|
|
12
|
-
* @param {string} opts.dismissEvent - Mutation name fired on close (default: 'dismissBanner')
|
|
13
|
-
* @param {string} opts.class
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { escHtml as e } from '../html.js'
|
|
17
|
-
|
|
18
|
-
const VARIANTS = new Set(['info', 'promo', 'warning'])
|
|
19
|
-
|
|
20
|
-
export function banner({
|
|
21
|
-
content = '',
|
|
22
|
-
variant = 'info',
|
|
23
|
-
position = 'top',
|
|
24
|
-
dismissible = false,
|
|
25
|
-
dismissEvent = 'dismissBanner',
|
|
26
|
-
class: cls = '',
|
|
27
|
-
} = {}) {
|
|
28
|
-
if (!VARIANTS.has(variant)) variant = 'info'
|
|
29
|
-
const classes = [
|
|
30
|
-
'ui-banner',
|
|
31
|
-
`ui-banner--${variant}`,
|
|
32
|
-
position === 'bottom' && 'ui-banner--bottom',
|
|
33
|
-
cls,
|
|
34
|
-
].filter(Boolean).join(' ')
|
|
35
|
-
|
|
36
|
-
const closeBtn = dismissible
|
|
37
|
-
? `<button class="ui-banner-close" data-event="${e(dismissEvent)}" aria-label="Close announcement">×</button>`
|
|
38
|
-
: ''
|
|
39
|
-
|
|
40
|
-
return `<div class="${e(classes)}" role="banner">${content}${closeBtn}</div>`
|
|
41
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse UI — Brand Statement
|
|
3
|
-
*
|
|
4
|
-
* Full-width centred editorial statement — large serif or display type used
|
|
5
|
-
* for brand voice paragraphs between visual sections. This is not a pull-quote
|
|
6
|
-
* (no attribution border) — it is a first-person brand declaration.
|
|
7
|
-
*
|
|
8
|
-
* Typical use: "pieces that are both eclectic and everyday. Subtly precious,
|
|
9
|
-
* expressive, and refined…"
|
|
10
|
-
*
|
|
11
|
-
* @param {object} opts
|
|
12
|
-
* @param {string} opts.text - Statement text (can include basic HTML like <em>, <br>)
|
|
13
|
-
* @param {string} opts.attribution - Optional small attribution or tagline below
|
|
14
|
-
* @param {'serif'|'display'|'sans'} opts.font - Font style (default: 'serif')
|
|
15
|
-
* @param {'sm'|'md'|'lg'} opts.size - Text size (default: 'md')
|
|
16
|
-
* @param {'left'|'center'|'right'} opts.align - Text alignment (default: 'center')
|
|
17
|
-
* @param {string} opts.maxWidth - CSS max-width value (default: '860px')
|
|
18
|
-
* @param {string} opts.class
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { escHtml as e } from '../html.js'
|
|
22
|
-
|
|
23
|
-
const FONTS = new Set(['serif', 'display', 'sans'])
|
|
24
|
-
const SIZES = new Set(['sm', 'md', 'lg'])
|
|
25
|
-
const ALIGNS = new Set(['left', 'center', 'right'])
|
|
26
|
-
|
|
27
|
-
export function brandStatement({
|
|
28
|
-
text = '',
|
|
29
|
-
attribution = '',
|
|
30
|
-
font = 'serif',
|
|
31
|
-
size = 'md',
|
|
32
|
-
align = 'center',
|
|
33
|
-
maxWidth = '',
|
|
34
|
-
class: cls = '',
|
|
35
|
-
} = {}) {
|
|
36
|
-
if (!FONTS.has(font)) font = 'serif'
|
|
37
|
-
if (!SIZES.has(size)) size = 'md'
|
|
38
|
-
if (!ALIGNS.has(align)) align = 'center'
|
|
39
|
-
|
|
40
|
-
const classes = [
|
|
41
|
-
'ui-brand-statement',
|
|
42
|
-
font !== 'serif' && `ui-brand-statement--${font}`,
|
|
43
|
-
size !== 'md' && `ui-brand-statement--${size}`,
|
|
44
|
-
align !== 'center' && `ui-brand-statement--${align}`,
|
|
45
|
-
cls,
|
|
46
|
-
].filter(Boolean).join(' ')
|
|
47
|
-
|
|
48
|
-
const innerStyle = maxWidth
|
|
49
|
-
? ` style="max-width:${e(typeof maxWidth === 'number' ? `${maxWidth}px` : maxWidth)}"`
|
|
50
|
-
: ''
|
|
51
|
-
|
|
52
|
-
return `<div class="${e(classes)}">
|
|
53
|
-
<div class="ui-brand-statement-inner"${innerStyle}>
|
|
54
|
-
<p class="ui-brand-statement-text">${text}</p>
|
|
55
|
-
${attribution ? `<p class="ui-brand-statement-attr">${e(attribution)}</p>` : ''}
|
|
56
|
-
</div>
|
|
57
|
-
</div>`
|
|
58
|
-
}
|
package/src/ui/carousel.js
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse UI — Carousel / Slider
|
|
3
|
-
*
|
|
4
|
-
* CSS scroll-snap carousel with optional prev/next arrows and dot navigation.
|
|
5
|
-
* Requires pulse-ui.js for button and dot interactivity.
|
|
6
|
-
*
|
|
7
|
-
* @param {object} opts
|
|
8
|
-
* @param {string[]} opts.slides - Array of raw HTML strings — one per slide
|
|
9
|
-
* @param {boolean} opts.arrows - Show prev/next arrow buttons (default: true)
|
|
10
|
-
* @param {boolean} opts.dots - Show dot navigation (default: true)
|
|
11
|
-
* @param {string} opts.class
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { escHtml as e } from '../html.js'
|
|
15
|
-
import { iconChevronLeft, iconChevronRight } from './icons.js'
|
|
16
|
-
|
|
17
|
-
let _carouselId = 0
|
|
18
|
-
|
|
19
|
-
export function carousel({
|
|
20
|
-
slides = [],
|
|
21
|
-
arrows = true,
|
|
22
|
-
dots = true,
|
|
23
|
-
class: cls = '',
|
|
24
|
-
} = {}) {
|
|
25
|
-
const id = `carousel-${++_carouselId}`
|
|
26
|
-
const classes = ['ui-carousel', cls].filter(Boolean).join(' ')
|
|
27
|
-
|
|
28
|
-
const slidesHtml = slides
|
|
29
|
-
.map((s, i) => {
|
|
30
|
-
const panelId = `${id}-panel-${i + 1}`
|
|
31
|
-
const tabId = `${id}-tab-${i + 1}`
|
|
32
|
-
return `<div class="ui-carousel-slide" id="${panelId}" role="tabpanel" aria-labelledby="${tabId}" tabindex="0">${s}</div>`
|
|
33
|
-
})
|
|
34
|
-
.join('\n ')
|
|
35
|
-
|
|
36
|
-
const arrowsHtml = arrows ? `
|
|
37
|
-
<button class="ui-carousel-btn ui-carousel-prev" type="button" aria-label="Previous slide" hidden>
|
|
38
|
-
${iconChevronLeft({ size: 16 })}
|
|
39
|
-
</button>
|
|
40
|
-
<button class="ui-carousel-btn ui-carousel-next" type="button" aria-label="Next slide"${slides.length <= 1 ? ' hidden' : ''}>
|
|
41
|
-
${iconChevronRight({ size: 16 })}
|
|
42
|
-
</button>` : ''
|
|
43
|
-
|
|
44
|
-
const dotsHtml = dots && slides.length > 1 ? `
|
|
45
|
-
<div class="ui-carousel-dots" role="tablist" aria-label="Slides">
|
|
46
|
-
${slides.map((_, i) => {
|
|
47
|
-
const tabId = `${id}-tab-${i + 1}`
|
|
48
|
-
const panelId = `${id}-panel-${i + 1}`
|
|
49
|
-
const active = i === 0
|
|
50
|
-
return `<button class="ui-carousel-dot${active ? ' active' : ''}" id="${tabId}" type="button" role="tab" aria-selected="${active}" aria-controls="${panelId}" tabindex="${active ? '0' : '-1'}" aria-label="Slide ${i + 1}"></button>`
|
|
51
|
-
}).join('\n ')}
|
|
52
|
-
</div>` : ''
|
|
53
|
-
|
|
54
|
-
return `<div class="${e(classes)}">
|
|
55
|
-
<div class="ui-carousel-track">
|
|
56
|
-
${slidesHtml}
|
|
57
|
-
</div>${arrowsHtml}${dotsHtml}
|
|
58
|
-
</div>`
|
|
59
|
-
}
|
package/src/ui/code-window.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse UI — Code Window
|
|
3
|
-
*
|
|
4
|
-
* A code block with macOS-style window chrome — three coloured dots and an
|
|
5
|
-
* optional filename label. The content slot accepts pre-highlighted HTML
|
|
6
|
-
* (spans with syntax-token classes) or plain text.
|
|
7
|
-
*
|
|
8
|
-
* Interactive syntax highlighting is out of scope — pass pre-rendered HTML
|
|
9
|
-
* or plain text. The component handles all the chrome, layout, scroll, and
|
|
10
|
-
* monospace typography.
|
|
11
|
-
*
|
|
12
|
-
* @param {object} opts
|
|
13
|
-
* @param {string} opts.content - Raw HTML slot — highlighted code HTML or plain text
|
|
14
|
-
* @param {string} opts.filename - Filename shown in the chrome bar (e.g. 'home.js')
|
|
15
|
-
* @param {string} opts.lang - Language label shown on the right of the chrome (e.g. 'JavaScript')
|
|
16
|
-
* @param {string} opts.class
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { escHtml as e } from '../html.js'
|
|
20
|
-
|
|
21
|
-
export function codeWindow({
|
|
22
|
-
content = '',
|
|
23
|
-
filename = '',
|
|
24
|
-
lang = '',
|
|
25
|
-
class: cls = '',
|
|
26
|
-
} = {}) {
|
|
27
|
-
const classes = ['ui-code-window', cls].filter(Boolean).join(' ')
|
|
28
|
-
|
|
29
|
-
return `<div class="${e(classes)}" role="region"${filename ? ` aria-label="${e(filename)}"` : ''}>
|
|
30
|
-
<div class="ui-code-window-chrome" aria-hidden="true">
|
|
31
|
-
<span class="ui-code-window-dot"></span>
|
|
32
|
-
<span class="ui-code-window-dot"></span>
|
|
33
|
-
<span class="ui-code-window-dot"></span>
|
|
34
|
-
${filename ? `<span class="ui-code-window-filename">${e(filename)}</span>` : ''}
|
|
35
|
-
${lang ? `<span class="ui-code-window-lang">${e(lang)}</span>` : ''}
|
|
36
|
-
</div>
|
|
37
|
-
<pre class="ui-code-window-pre"><code class="ui-code-window-code">${content}</code></pre>
|
|
38
|
-
</div>`
|
|
39
|
-
}
|
package/src/ui/colophon.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse UI — Colophon
|
|
3
|
-
*
|
|
4
|
-
* Paperback-style end-of-page metadata block. A minimal footer
|
|
5
|
-
* variant with tight type, no columns, centered layout.
|
|
6
|
-
*
|
|
7
|
-
* @param {object} opts
|
|
8
|
-
* @param {string} opts.content - Raw HTML content slot
|
|
9
|
-
* @param {'left'|'center'} opts.align - Text alignment (default 'center')
|
|
10
|
-
* @param {string} opts.class
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { escHtml as e } from '../html.js'
|
|
14
|
-
|
|
15
|
-
export function colophon({
|
|
16
|
-
content = '',
|
|
17
|
-
align = 'center',
|
|
18
|
-
class: cls = '',
|
|
19
|
-
} = {}) {
|
|
20
|
-
const classes = [
|
|
21
|
-
'ui-colophon',
|
|
22
|
-
align === 'left' && 'ui-colophon--left',
|
|
23
|
-
cls,
|
|
24
|
-
].filter(Boolean).join(' ')
|
|
25
|
-
|
|
26
|
-
return `<footer class="${e(classes)}">
|
|
27
|
-
<div class="ui-colophon-inner">${content}</div>
|
|
28
|
-
</footer>`
|
|
29
|
-
}
|
package/src/ui/contact-row.js
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse UI — ContactRow
|
|
3
|
-
*
|
|
4
|
-
* A horizontal strip of contact items — phone, email, address, hours.
|
|
5
|
-
* Universal on local-business and trades sites. Renders as a <ul> of
|
|
6
|
-
* icon + label pairs, each optionally linked.
|
|
7
|
-
*
|
|
8
|
-
* @param {object} opts
|
|
9
|
-
* @param {Array<{icon?: string, label: string, href?: string}>} opts.items
|
|
10
|
-
* @param {'sm'|'md'} opts.size - Text size (default: 'md')
|
|
11
|
-
* @param {string} opts.class
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { escHtml as e } from '../html.js'
|
|
15
|
-
|
|
16
|
-
const SIZES = new Set(['sm', 'md'])
|
|
17
|
-
|
|
18
|
-
export function contactRow({
|
|
19
|
-
items = [],
|
|
20
|
-
size = 'md',
|
|
21
|
-
class: cls = '',
|
|
22
|
-
} = {}) {
|
|
23
|
-
if (!SIZES.has(size)) size = 'md'
|
|
24
|
-
|
|
25
|
-
const classes = [
|
|
26
|
-
'ui-contact-row',
|
|
27
|
-
size !== 'md' && `ui-contact-row--${size}`,
|
|
28
|
-
cls,
|
|
29
|
-
].filter(Boolean).join(' ')
|
|
30
|
-
|
|
31
|
-
const itemsHtml = items.map(({ icon = '', label = '', href = '' }) => {
|
|
32
|
-
const iconHtml = icon ? `<span class="ui-contact-row-icon" aria-hidden="true">${icon}</span>` : ''
|
|
33
|
-
const inner = `${iconHtml}<span>${e(label)}</span>`
|
|
34
|
-
const content = href
|
|
35
|
-
? `<a href="${e(href)}">${inner}</a>`
|
|
36
|
-
: inner
|
|
37
|
-
return `<li class="ui-contact-row-item">${content}</li>`
|
|
38
|
-
}).join('')
|
|
39
|
-
|
|
40
|
-
return `<ul class="${e(classes)}">${itemsHtml}</ul>`
|
|
41
|
-
}
|
package/src/ui/decorate.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse UI — Decorate
|
|
3
|
-
*
|
|
4
|
-
* Inline SVG background pattern element. Drop it inside any container as an
|
|
5
|
-
* absolute overlay to add texture — dots, grid, diagonal lines, or zigzag.
|
|
6
|
-
* The SVG is positioned absolute so it doesn't affect layout; the parent must
|
|
7
|
-
* have position: relative (or use a `u-relative` class).
|
|
8
|
-
*
|
|
9
|
-
* @param {object} opts
|
|
10
|
-
* @param {'dots'|'grid'|'lines'|'zigzag'|'cross'} opts.pattern - Pattern type (default: 'dots')
|
|
11
|
-
* @param {string} opts.color - Stroke/fill color, any CSS value (default: var(--ui-border))
|
|
12
|
-
* @param {number} opts.opacity - Opacity 0–1 (default: 0.4)
|
|
13
|
-
* @param {number} opts.size - Pattern tile size in px (default: 20)
|
|
14
|
-
* @param {string} opts.class
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
const PATTERNS = new Set(['dots', 'grid', 'lines', 'zigzag', 'cross'])
|
|
18
|
-
|
|
19
|
-
let _uid = 0
|
|
20
|
-
|
|
21
|
-
export function decorate({
|
|
22
|
-
pattern = 'dots',
|
|
23
|
-
color = 'var(--ui-border)',
|
|
24
|
-
opacity = 0.4,
|
|
25
|
-
size = 20,
|
|
26
|
-
class: cls = '',
|
|
27
|
-
} = {}) {
|
|
28
|
-
if (!PATTERNS.has(pattern)) pattern = 'dots'
|
|
29
|
-
const s = Math.max(4, parseInt(size) || 20)
|
|
30
|
-
const safeOpac = Math.min(1, Math.max(0, parseFloat(opacity) || 0.4))
|
|
31
|
-
// Use a color we can reference in the SVG — replace var() with currentColor trick
|
|
32
|
-
const useColor = color.startsWith('var(') ? 'currentColor' : color
|
|
33
|
-
const style = color.startsWith('var(') ? ` style="color:${color}"` : ''
|
|
34
|
-
|
|
35
|
-
const classes = ['ui-decorate', cls].filter(Boolean).join(' ')
|
|
36
|
-
|
|
37
|
-
const pid = `dp${++_uid}`
|
|
38
|
-
let patternSvg
|
|
39
|
-
const half = s / 2
|
|
40
|
-
|
|
41
|
-
if (pattern === 'dots') {
|
|
42
|
-
patternSvg = `<pattern id="${pid}" patternUnits="userSpaceOnUse" width="${s}" height="${s}"><circle cx="${half}" cy="${half}" r="1" fill="${useColor}"/></pattern>`
|
|
43
|
-
} else if (pattern === 'grid') {
|
|
44
|
-
patternSvg = `<pattern id="${pid}" patternUnits="userSpaceOnUse" width="${s}" height="${s}"><path d="M ${s} 0 L 0 0 0 ${s}" fill="none" stroke="${useColor}" stroke-width="0.5"/></pattern>`
|
|
45
|
-
} else if (pattern === 'lines') {
|
|
46
|
-
patternSvg = `<pattern id="${pid}" patternUnits="userSpaceOnUse" width="${s}" height="${s}" patternTransform="rotate(45)"><line x1="0" y1="0" x2="0" y2="${s}" stroke="${useColor}" stroke-width="0.75"/></pattern>`
|
|
47
|
-
} else if (pattern === 'zigzag') {
|
|
48
|
-
const q = s / 4
|
|
49
|
-
patternSvg = `<pattern id="${pid}" patternUnits="userSpaceOnUse" width="${s}" height="${half}"><polyline points="0,${half} ${q},0 ${half},${half} ${s - q},0 ${s},${half}" fill="none" stroke="${useColor}" stroke-width="0.75"/></pattern>`
|
|
50
|
-
} else { // cross
|
|
51
|
-
patternSvg = `<pattern id="${pid}" patternUnits="userSpaceOnUse" width="${s}" height="${s}"><line x1="${half}" y1="0" x2="${half}" y2="${s}" stroke="${useColor}" stroke-width="0.5"/><line x1="0" y1="${half}" x2="${s}" y2="${half}" stroke="${useColor}" stroke-width="0.5"/><circle cx="${half}" cy="${half}" r="1.5" fill="${useColor}"/></pattern>`
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return `<svg class="${classes}"${style} xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" style="position:absolute;inset:0;pointer-events:none;opacity:${safeOpac}" aria-hidden="true"><defs>${patternSvg}</defs><rect width="100%" height="100%" fill="url(#${pid})"/></svg>`
|
|
55
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse UI — Display Heading
|
|
3
|
-
*
|
|
4
|
-
* Large typographic heading for editorial moments. Controls line-break,
|
|
5
|
-
* tracking, max-width in ch. Use for hero titles, chapter openings,
|
|
6
|
-
* section leads.
|
|
7
|
-
*
|
|
8
|
-
* @param {object} opts
|
|
9
|
-
* @param {string} opts.text - Heading text (HTML entities supported)
|
|
10
|
-
* @param {number} opts.level - Heading level 1–6 (default 1)
|
|
11
|
-
* @param {number} opts.maxWidth - Max width in ch units (default 20)
|
|
12
|
-
* @param {boolean} opts.balance - Use text-wrap: balance (default true)
|
|
13
|
-
* @param {string} opts.tracking - Letter-spacing: 'tight' | 'normal' | 'wide' (default 'tight')
|
|
14
|
-
* @param {string} opts.class
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { escHtml as e } from '../html.js'
|
|
18
|
-
|
|
19
|
-
const TRACKING = {
|
|
20
|
-
tight: '-0.02em',
|
|
21
|
-
normal: '0',
|
|
22
|
-
wide: '0.05em',
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function displayHeading({
|
|
26
|
-
text = '',
|
|
27
|
-
level = 1,
|
|
28
|
-
maxWidth = 20,
|
|
29
|
-
balance = true,
|
|
30
|
-
tracking = 'tight',
|
|
31
|
-
class: cls = '',
|
|
32
|
-
} = {}) {
|
|
33
|
-
const tag = `h${Math.min(Math.max(Math.floor(level), 1), 6)}`
|
|
34
|
-
const classes = ['ui-display-heading', cls].filter(Boolean).join(' ')
|
|
35
|
-
|
|
36
|
-
const letterSpacing = TRACKING[tracking] || TRACKING.tight
|
|
37
|
-
const styles = [
|
|
38
|
-
`max-width:${maxWidth}ch`,
|
|
39
|
-
`letter-spacing:${letterSpacing}`,
|
|
40
|
-
balance && 'text-wrap:balance',
|
|
41
|
-
].filter(Boolean).join(';')
|
|
42
|
-
|
|
43
|
-
return `<${tag} class="${e(classes)}" style="${styles}">${e(text)}</${tag}>`
|
|
44
|
-
}
|