@invisibleloop/pulse 0.20.5 → 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.
Files changed (79) hide show
  1. package/.claude/pulse-checklist.md +1 -2
  2. package/CLAUDE.md +1 -2
  3. package/docs/.claude/pulse-checklist.md +1 -2
  4. package/docs/public/coutumes.css +21 -44
  5. package/docs/public/docs.css +67 -0
  6. package/docs/public/pulse-ui.css +8 -1037
  7. package/docs/public/retrowheels.css +23 -0
  8. package/docs/public/shop.css +35 -1
  9. package/docs/public/sparkys.css +56 -23
  10. package/docs/server.js +0 -11
  11. package/docs/src/lib/nav.js +0 -11
  12. package/docs/src/pages/components/charts.js +2 -1
  13. package/docs/src/pages/components/grid.js +9 -5
  14. package/docs/src/pages/components.js +0 -4
  15. package/docs/src/pages/home.js +16 -1
  16. package/docs/src/pages/how-it-works.js +20 -14
  17. package/docs/src/pages/templates/coutumes.js +83 -72
  18. package/docs/src/pages/templates/mobile-app.js +36 -9
  19. package/docs/src/pages/templates/retrowheels.js +17 -4
  20. package/docs/src/pages/templates/shop.js +21 -10
  21. package/docs/src/pages/templates/sparkys.js +35 -20
  22. package/examples/.claude/pulse-checklist.md +1 -2
  23. package/examples/examples.test.js +4 -4
  24. package/examples/pricing.js +21 -3
  25. package/examples/public/pulse-ui.css +8 -1037
  26. package/examples/retrowheels.css +23 -0
  27. package/examples/retrowheels.js +17 -4
  28. package/package.json +5 -1
  29. package/public/.pulse-ui-version +1 -1
  30. package/public/pulse-ui.css +8 -1037
  31. package/src/agent/checklist.md +1 -2
  32. package/src/agent/guide-components.md +43 -67
  33. package/src/agent/guide-design-gallery.md +14 -14
  34. package/src/agent/guide-design-references.md +21 -21
  35. package/src/agent/guide-styles.md +2 -2
  36. package/src/agent/guide-templates.md +6 -6
  37. package/src/agent/identity.md +1 -1
  38. package/src/agent/workflow.md +1 -2
  39. package/src/mcp/server.js +31 -24
  40. package/src/ui/index.js +0 -27
  41. package/src/ui/ui.test.js +0 -137
  42. package/types/charts.d.ts +61 -0
  43. package/types/ui.d.ts +1 -129
  44. package/docs/src/pages/components/banner.js +0 -44
  45. package/docs/src/pages/components/carousel.js +0 -71
  46. package/docs/src/pages/components/code-window.js +0 -56
  47. package/docs/src/pages/components/decorate.js +0 -74
  48. package/docs/src/pages/components/gallery.js +0 -102
  49. package/docs/src/pages/components/list.js +0 -111
  50. package/docs/src/pages/components/marquee.js +0 -76
  51. package/docs/src/pages/components/photo-card.js +0 -74
  52. package/docs/src/pages/components/pricing.js +0 -67
  53. package/docs/src/pages/components/testimonial.js +0 -48
  54. package/docs/src/pages/components/timeline.js +0 -226
  55. package/src/ui/banner.js +0 -41
  56. package/src/ui/brand-statement.js +0 -58
  57. package/src/ui/carousel.js +0 -59
  58. package/src/ui/code-window.js +0 -39
  59. package/src/ui/colophon.js +0 -29
  60. package/src/ui/contact-row.js +0 -41
  61. package/src/ui/decorate.js +0 -55
  62. package/src/ui/display-heading.js +0 -44
  63. package/src/ui/drop-cap.js +0 -29
  64. package/src/ui/editorial-hero.js +0 -63
  65. package/src/ui/entry-list.js +0 -36
  66. package/src/ui/gallery.js +0 -60
  67. package/src/ui/image-pair.js +0 -72
  68. package/src/ui/list.js +0 -36
  69. package/src/ui/marquee.js +0 -48
  70. package/src/ui/photo-card.js +0 -44
  71. package/src/ui/pricing.js +0 -56
  72. package/src/ui/product-card.js +0 -65
  73. package/src/ui/section-label.js +0 -38
  74. package/src/ui/service-card.js +0 -47
  75. package/src/ui/stat-grid.js +0 -40
  76. package/src/ui/testimonial.js +0 -59
  77. package/src/ui/timeline.js +0 -72
  78. package/src/ui/top-strip.js +0 -47
  79. 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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }