@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
package/src/ui/drop-cap.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse UI — Drop Cap
|
|
3
|
-
*
|
|
4
|
-
* First-letter drop cap for editorial openings. Wraps the first
|
|
5
|
-
* letter or word of a paragraph.
|
|
6
|
-
*
|
|
7
|
-
* @param {object} opts
|
|
8
|
-
* @param {string} opts.letter - The drop cap letter or word
|
|
9
|
-
* @param {string} opts.content - The rest of the paragraph text
|
|
10
|
-
* @param {number} opts.lines - How many lines tall (default 3)
|
|
11
|
-
* @param {string} opts.class
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { escHtml as e } from '../html.js'
|
|
15
|
-
|
|
16
|
-
export function dropCap({
|
|
17
|
-
letter = '',
|
|
18
|
-
content = '',
|
|
19
|
-
lines = 3,
|
|
20
|
-
class: cls = '',
|
|
21
|
-
} = {}) {
|
|
22
|
-
if (!letter) return content ? `<p>${e(content)}</p>` : ''
|
|
23
|
-
|
|
24
|
-
const classes = ['ui-drop-cap', cls].filter(Boolean).join(' ')
|
|
25
|
-
|
|
26
|
-
return `<p class="${e(classes)}">
|
|
27
|
-
<span class="ui-drop-cap-letter" style="font-size:${+(lines * 1.2).toFixed(2)}em" aria-hidden="true">${e(letter)}</span>${e(content)}
|
|
28
|
-
</p>`
|
|
29
|
-
}
|
package/src/ui/editorial-hero.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse UI — Editorial Hero
|
|
3
|
-
*
|
|
4
|
-
* Full-bleed hero with two side-by-side images and display text that
|
|
5
|
-
* overlaps both columns simultaneously. The text uses mix-blend-mode: multiply
|
|
6
|
-
* so it visually "lives inside" the photography — a signature technique in
|
|
7
|
-
* fashion and luxury editorial design.
|
|
8
|
-
*
|
|
9
|
-
* The overlay text is decorative (aria-hidden) with a separate accessible
|
|
10
|
-
* heading provided via the `heading` param.
|
|
11
|
-
*
|
|
12
|
-
* @param {object} opts
|
|
13
|
-
* @param {string} opts.leftImage - Raw HTML slot for the left image (<img>, uiImage, etc.)
|
|
14
|
-
* @param {string} opts.rightImage - Raw HTML slot for the right image
|
|
15
|
-
* @param {string|string[]} opts.lines - Headline text — string or array of lines
|
|
16
|
-
* @param {string} opts.heading - Accessible heading text (sr-only, replaces lines for AT)
|
|
17
|
-
* @param {string} opts.cta - Raw HTML slot — optional CTA link at the bottom
|
|
18
|
-
* @param {string} opts.color - Overlay text colour (default: var(--ui-color-accent))
|
|
19
|
-
* @param {'multiply'|'screen'|'overlay'|'none'} opts.blend - blend-mode for overlay text (default: 'multiply')
|
|
20
|
-
* @param {string} opts.ratio - Image aspect-ratio CSS string (default: '3/4')
|
|
21
|
-
* @param {string} opts.class
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import { escHtml as e } from '../html.js'
|
|
25
|
-
|
|
26
|
-
const BLENDS = new Set(['multiply', 'screen', 'overlay', 'none'])
|
|
27
|
-
|
|
28
|
-
export function editorialHero({
|
|
29
|
-
leftImage = '',
|
|
30
|
-
rightImage = '',
|
|
31
|
-
lines = [],
|
|
32
|
-
heading = '',
|
|
33
|
-
cta = '',
|
|
34
|
-
color = '',
|
|
35
|
-
blend = 'multiply',
|
|
36
|
-
ratio = '3/4',
|
|
37
|
-
class: cls = '',
|
|
38
|
-
} = {}) {
|
|
39
|
-
if (!BLENDS.has(blend)) blend = 'multiply'
|
|
40
|
-
|
|
41
|
-
const classes = ['ui-editorial-hero', cls].filter(Boolean).join(' ')
|
|
42
|
-
const lineArray = Array.isArray(lines) ? lines : [lines]
|
|
43
|
-
|
|
44
|
-
const overlayStyle = [
|
|
45
|
-
color ? `color:${color.replace(/"/g, "'")}` : '',
|
|
46
|
-
blend !== 'none' ? `mix-blend-mode:${blend}` : '',
|
|
47
|
-
].filter(Boolean).join(';')
|
|
48
|
-
|
|
49
|
-
const overlayAttr = overlayStyle ? ` style="${overlayStyle}"` : ''
|
|
50
|
-
const imageStyle = ratio ? ` style="aspect-ratio:${e(ratio)}"` : ''
|
|
51
|
-
|
|
52
|
-
const linesHtml = lineArray.map(l => `<span>${e(l)}</span>`).join('')
|
|
53
|
-
|
|
54
|
-
return `<section class="${e(classes)}">
|
|
55
|
-
${heading ? `<h1 class="ui-editorial-hero-heading">${e(heading)}</h1>` : ''}
|
|
56
|
-
<div class="ui-editorial-hero-images"${imageStyle}>
|
|
57
|
-
<div class="ui-editorial-hero-img">${leftImage}</div>
|
|
58
|
-
<div class="ui-editorial-hero-img">${rightImage}</div>
|
|
59
|
-
</div>
|
|
60
|
-
${linesHtml ? `<div class="ui-editorial-hero-overlay" aria-hidden="true"${overlayAttr}>${linesHtml}</div>` : ''}
|
|
61
|
-
${cta ? `<div class="ui-editorial-hero-cta">${cta}</div>` : ''}
|
|
62
|
-
</section>`
|
|
63
|
-
}
|
package/src/ui/entry-list.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse UI — Entry List
|
|
3
|
-
*
|
|
4
|
-
* Tabular list of items with leading metadata (year, number, date),
|
|
5
|
-
* title, and trailing description. For: backlists, release notes,
|
|
6
|
-
* project archives, talk logs, changelogs.
|
|
7
|
-
*
|
|
8
|
-
* @param {object} opts
|
|
9
|
-
* @param {Array<{meta: string, title: string, description: string}>} opts.items
|
|
10
|
-
* @param {string} opts.metaLabel - Accessible label for metadata column (e.g. "Year", "Date")
|
|
11
|
-
* @param {string} opts.class
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { escHtml as e } from '../html.js'
|
|
15
|
-
|
|
16
|
-
export function entryList({
|
|
17
|
-
items = [],
|
|
18
|
-
metaLabel = 'Date',
|
|
19
|
-
class: cls = '',
|
|
20
|
-
} = {}) {
|
|
21
|
-
if (!Array.isArray(items) || items.length === 0) return ''
|
|
22
|
-
|
|
23
|
-
const classes = ['ui-entry-list', cls].filter(Boolean).join(' ')
|
|
24
|
-
|
|
25
|
-
const rows = items.map(item => `
|
|
26
|
-
<div class="ui-entry-list-item">
|
|
27
|
-
<div class="ui-entry-list-meta" aria-label="${e(metaLabel)}">${e(item.meta || '')}</div>
|
|
28
|
-
<div class="ui-entry-list-content">
|
|
29
|
-
<h3 class="ui-entry-list-title">${e(item.title || '')}</h3>
|
|
30
|
-
${item.description ? `<p class="ui-entry-list-description">${e(item.description)}</p>` : ''}
|
|
31
|
-
</div>
|
|
32
|
-
</div>
|
|
33
|
-
`).join('')
|
|
34
|
-
|
|
35
|
-
return `<div class="${e(classes)}">${rows}</div>`
|
|
36
|
-
}
|
package/src/ui/gallery.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse UI — Gallery
|
|
3
|
-
*
|
|
4
|
-
* Responsive image grid for photos, illustrations, or any visual content.
|
|
5
|
-
* Three layout modes: grid (equal cells), strip (single scrollable row), masonry (Pinterest-style).
|
|
6
|
-
*
|
|
7
|
-
* @param {object} opts
|
|
8
|
-
* @param {Array<{src:string,alt:string,caption?:string,href?:string}>} opts.images
|
|
9
|
-
* @param {'grid'|'strip'|'masonry'} opts.layout - Grid layout (default: 'grid')
|
|
10
|
-
* @param {1|2|3|4} opts.cols - Columns in grid/masonry layout (default: 3)
|
|
11
|
-
* @param {'sm'|'md'|'lg'} opts.gap - Gap between images (default: 'md')
|
|
12
|
-
* @param {boolean} opts.rounded - Rounded corners on images
|
|
13
|
-
* @param {string} opts.ratio - CSS aspect-ratio for grid/strip cells e.g. '4/3', '1/1', '16/9'
|
|
14
|
-
* @param {boolean} opts.lazy - loading="lazy" on images (default: true). Set false for
|
|
15
|
-
* above-the-fold galleries or when full-page screenshots
|
|
16
|
-
* during development need every image rendered eagerly.
|
|
17
|
-
* @param {string} opts.class
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { escHtml as e } from '../html.js'
|
|
21
|
-
|
|
22
|
-
const LAYOUTS = new Set(['grid', 'strip', 'masonry'])
|
|
23
|
-
const GAPS = new Set(['sm', 'md', 'lg'])
|
|
24
|
-
|
|
25
|
-
export function gallery({
|
|
26
|
-
images = [],
|
|
27
|
-
layout = 'grid',
|
|
28
|
-
cols = 3,
|
|
29
|
-
gap = 'md',
|
|
30
|
-
rounded = false,
|
|
31
|
-
ratio = '4/3',
|
|
32
|
-
lazy = true,
|
|
33
|
-
class: cls = '',
|
|
34
|
-
} = {}) {
|
|
35
|
-
if (!LAYOUTS.has(layout)) layout = 'grid'
|
|
36
|
-
if (!GAPS.has(gap)) gap = 'md'
|
|
37
|
-
const safeC = Math.min(Math.max(Math.floor(cols), 1), 4)
|
|
38
|
-
|
|
39
|
-
const classes = [
|
|
40
|
-
'ui-gallery',
|
|
41
|
-
`ui-gallery--${layout}`,
|
|
42
|
-
`ui-gallery--cols-${safeC}`,
|
|
43
|
-
gap !== 'md' && `ui-gallery--gap-${gap}`,
|
|
44
|
-
rounded && 'ui-gallery--rounded',
|
|
45
|
-
cls,
|
|
46
|
-
].filter(Boolean).join(' ')
|
|
47
|
-
|
|
48
|
-
const items = images.map(img => {
|
|
49
|
-
const imgTag = `<img src="${e(img.src)}" alt="${e(img.alt || '')}" class="ui-gallery-img"${lazy ? ' loading="lazy"' : ''} decoding="async">`
|
|
50
|
-
const cap = img.caption ? `<figcaption class="ui-gallery-caption">${e(img.caption)}</figcaption>` : ''
|
|
51
|
-
const inner = img.href
|
|
52
|
-
? `<a href="${e(img.href)}" class="ui-gallery-link">${imgTag}</a>`
|
|
53
|
-
: imgTag
|
|
54
|
-
const cropStyle = (layout !== 'masonry' && ratio) ? ` style="aspect-ratio:${e(ratio)}"` : ''
|
|
55
|
-
const crop = `<div class="ui-gallery-crop"${cropStyle}>${inner}</div>`
|
|
56
|
-
return `<figure class="ui-gallery-item">${crop}${cap}</figure>`
|
|
57
|
-
}).join('\n')
|
|
58
|
-
|
|
59
|
-
return `<div class="${e(classes)}">\n${items}\n</div>`
|
|
60
|
-
}
|
package/src/ui/image-pair.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse UI — Image Pair
|
|
3
|
-
*
|
|
4
|
-
* Two images side by side — no text. A pure editorial layout used in fashion,
|
|
5
|
-
* portfolio, and magazine contexts to present photography at full scale.
|
|
6
|
-
* Both images share the same aspect ratio and fill equal columns.
|
|
7
|
-
*
|
|
8
|
-
* For image + text layouts, use the media() component instead.
|
|
9
|
-
*
|
|
10
|
-
* @param {object} opts
|
|
11
|
-
* @param {string} opts.leftSrc - Left image URL
|
|
12
|
-
* @param {string} opts.leftAlt - Left image alt text
|
|
13
|
-
* @param {string} opts.rightSrc - Right image URL
|
|
14
|
-
* @param {string} opts.rightAlt - Right image alt text
|
|
15
|
-
* @param {string} opts.ratio - CSS aspect-ratio for both images (default: '3/4')
|
|
16
|
-
* @param {'none'|'sm'|'md'} opts.gap - Gap between images (default: 'none')
|
|
17
|
-
* @param {string} opts.label - Optional aria-label for the section
|
|
18
|
-
* @param {string} opts.leftLabel - Link label below left image (e.g. 'Craftmanship')
|
|
19
|
-
* @param {string} opts.leftHref - href for left link
|
|
20
|
-
* @param {string} opts.rightLabel - Link label below right image (e.g. 'Store')
|
|
21
|
-
* @param {string} opts.rightHref - href for right link
|
|
22
|
-
* @param {string} opts.class
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import { escHtml as e } from '../html.js'
|
|
26
|
-
|
|
27
|
-
const GAPS = new Set(['none', 'sm', 'md'])
|
|
28
|
-
|
|
29
|
-
export function imagePair({
|
|
30
|
-
leftSrc = '',
|
|
31
|
-
leftAlt = '',
|
|
32
|
-
rightSrc = '',
|
|
33
|
-
rightAlt = '',
|
|
34
|
-
ratio = '3/4',
|
|
35
|
-
gap = 'none',
|
|
36
|
-
label = '',
|
|
37
|
-
leftLabel = '',
|
|
38
|
-
leftHref = '#',
|
|
39
|
-
rightLabel = '',
|
|
40
|
-
rightHref = '#',
|
|
41
|
-
class: cls = '',
|
|
42
|
-
} = {}) {
|
|
43
|
-
if (!GAPS.has(gap)) gap = 'none'
|
|
44
|
-
|
|
45
|
-
const classes = [
|
|
46
|
-
'ui-image-pair',
|
|
47
|
-
gap !== 'none' && `ui-image-pair--gap-${gap}`,
|
|
48
|
-
(leftLabel || rightLabel) && 'ui-image-pair--captioned',
|
|
49
|
-
cls,
|
|
50
|
-
].filter(Boolean).join(' ')
|
|
51
|
-
|
|
52
|
-
const labelAttr = label ? ` aria-label="${e(label)}"` : ''
|
|
53
|
-
const cropStyle = ratio ? ` style="aspect-ratio:${e(ratio)}"` : ''
|
|
54
|
-
|
|
55
|
-
const leftCaption = leftLabel ? `<a href="${e(leftHref)}" class="ui-image-pair-link">${e(leftLabel)} →</a>` : ''
|
|
56
|
-
const rightCaption = rightLabel ? `<a href="${e(rightHref)}" class="ui-image-pair-link">${e(rightLabel)} →</a>` : ''
|
|
57
|
-
|
|
58
|
-
return `<div class="${e(classes)}"${labelAttr}>
|
|
59
|
-
<div class="ui-image-pair-col">
|
|
60
|
-
<div class="ui-image-pair-crop"${cropStyle}>
|
|
61
|
-
<img src="${e(leftSrc)}" alt="${e(leftAlt)}" loading="lazy" decoding="async">
|
|
62
|
-
</div>
|
|
63
|
-
${leftCaption}
|
|
64
|
-
</div>
|
|
65
|
-
<div class="ui-image-pair-col">
|
|
66
|
-
<div class="ui-image-pair-crop"${cropStyle}>
|
|
67
|
-
<img src="${e(rightSrc)}" alt="${e(rightAlt)}" loading="lazy" decoding="async">
|
|
68
|
-
</div>
|
|
69
|
-
${rightCaption}
|
|
70
|
-
</div>
|
|
71
|
-
</div>`
|
|
72
|
-
}
|
package/src/ui/list.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse UI — List
|
|
3
|
-
*
|
|
4
|
-
* Styled ordered or unordered list. Use this instead of raw <ul>/<ol> to get
|
|
5
|
-
* consistent spacing, bullets, and colour tokens.
|
|
6
|
-
*
|
|
7
|
-
* Items are HTML strings — escape user data before passing them in.
|
|
8
|
-
*
|
|
9
|
-
* @param {object} opts
|
|
10
|
-
* @param {string[]} opts.items - Array of HTML strings for each list item
|
|
11
|
-
* @param {boolean} [opts.ordered] - true for <ol>, false for <ul> (default: false)
|
|
12
|
-
* @param {string} [opts.gap] - Item spacing: 'xs'|'sm'|'md' (default: 'sm')
|
|
13
|
-
* @param {string} [opts.class] - Extra classes on the list element
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
export function list({
|
|
17
|
-
items = [],
|
|
18
|
-
ordered = false,
|
|
19
|
-
gap = 'sm',
|
|
20
|
-
class: cls = '',
|
|
21
|
-
} = {}) {
|
|
22
|
-
if (!items.length) return ''
|
|
23
|
-
|
|
24
|
-
const tag = ordered ? 'ol' : 'ul'
|
|
25
|
-
const gapMap = { xs: 'u-gap-1', sm: 'u-gap-2', md: 'u-gap-3' }
|
|
26
|
-
const classes = [
|
|
27
|
-
'ui-list',
|
|
28
|
-
ordered ? 'ui-list--ordered' : 'ui-list--unordered',
|
|
29
|
-
gapMap[gap] || 'u-gap-2',
|
|
30
|
-
cls,
|
|
31
|
-
].filter(Boolean).join(' ')
|
|
32
|
-
|
|
33
|
-
const itemsHtml = items.map(item => `<li class="ui-list-item">${item}</li>`).join('')
|
|
34
|
-
|
|
35
|
-
return `<${tag} class="${classes}">${itemsHtml}</${tag}>`
|
|
36
|
-
}
|
package/src/ui/marquee.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse UI — Marquee
|
|
3
|
-
*
|
|
4
|
-
* CSS-only infinite scrolling strip. Ideal for logo clouds, trust badges,
|
|
5
|
-
* press mentions, or tag lists. No JavaScript — pure CSS animation.
|
|
6
|
-
*
|
|
7
|
-
* @param {object} opts
|
|
8
|
-
* @param {string[]} opts.items - Array of raw HTML strings (logos, badges, text)
|
|
9
|
-
* @param {number} opts.speed - Duration in seconds for one full loop (default: 30)
|
|
10
|
-
* @param {'sm'|'md'|'lg'} opts.gap - Gap between items (default: 'lg')
|
|
11
|
-
* @param {'left'|'right'} opts.direction - Scroll direction (default: 'left')
|
|
12
|
-
* @param {boolean} opts.pause - Pause animation on hover (default: true)
|
|
13
|
-
* @param {boolean} opts.fade - Fade edges with gradient mask (default: true)
|
|
14
|
-
* @param {string} opts.class
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { escHtml as e } from '../html.js'
|
|
18
|
-
|
|
19
|
-
const GAPS = new Set(['sm', 'md', 'lg'])
|
|
20
|
-
|
|
21
|
-
export function marquee({
|
|
22
|
-
items = [],
|
|
23
|
-
speed = 30,
|
|
24
|
-
gap = 'lg',
|
|
25
|
-
direction = 'left',
|
|
26
|
-
pause = true,
|
|
27
|
-
fade = true,
|
|
28
|
-
class: cls = '',
|
|
29
|
-
} = {}) {
|
|
30
|
-
if (!GAPS.has(gap)) gap = 'lg'
|
|
31
|
-
const safeSpeed = Math.max(5, parseFloat(speed) || 30)
|
|
32
|
-
const dir = direction === 'right' ? 'reverse' : 'normal'
|
|
33
|
-
|
|
34
|
-
const classes = [
|
|
35
|
-
'ui-marquee',
|
|
36
|
-
gap !== 'lg' && `ui-marquee--gap-${gap}`,
|
|
37
|
-
pause && 'ui-marquee--pause',
|
|
38
|
-
fade && 'ui-marquee--fade',
|
|
39
|
-
cls,
|
|
40
|
-
].filter(Boolean).join(' ')
|
|
41
|
-
|
|
42
|
-
// Items are duplicated so the loop is seamless
|
|
43
|
-
const itemsHtml = items.map(item => `<li class="ui-marquee-item">${item}</li>`).join('')
|
|
44
|
-
|
|
45
|
-
return `<div class="${e(classes)}" style="--marquee-speed:${safeSpeed}s;--marquee-dir:${dir}" aria-hidden="true">
|
|
46
|
-
<ul class="ui-marquee-track">${itemsHtml}${itemsHtml}</ul>
|
|
47
|
-
</div>`
|
|
48
|
-
}
|
package/src/ui/photo-card.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse UI — PhotoCard
|
|
3
|
-
*
|
|
4
|
-
* Polaroid-style photo card with optional caption and CSS tilt.
|
|
5
|
-
* Ideal for "meet the team", testimonial portraits, or lifestyle photography.
|
|
6
|
-
*
|
|
7
|
-
* @param {object} opts
|
|
8
|
-
* @param {string} opts.src - Image source URL
|
|
9
|
-
* @param {string} opts.alt - Alt text
|
|
10
|
-
* @param {string} opts.caption - Caption text below the photo
|
|
11
|
-
* @param {number} opts.tilt - CSS rotate degrees, e.g. 2 or -1.5. 0 = no tilt (default)
|
|
12
|
-
* @param {boolean} opts.rounded - Adds corner radius to the image (default: false = square crop)
|
|
13
|
-
* @param {string} opts.ratio - CSS aspect-ratio for the image crop (default: '4/3')
|
|
14
|
-
* @param {string} opts.class
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { escHtml as e } from '../html.js'
|
|
18
|
-
|
|
19
|
-
export function photoCard({
|
|
20
|
-
src = '',
|
|
21
|
-
alt = '',
|
|
22
|
-
caption = '',
|
|
23
|
-
tilt = 0,
|
|
24
|
-
rounded = false,
|
|
25
|
-
ratio = '4/3',
|
|
26
|
-
class: cls = '',
|
|
27
|
-
} = {}) {
|
|
28
|
-
const safeTilt = parseFloat(tilt) || 0
|
|
29
|
-
const classes = [
|
|
30
|
-
'ui-photo-card',
|
|
31
|
-
rounded && 'ui-photo-card--rounded',
|
|
32
|
-
cls,
|
|
33
|
-
].filter(Boolean).join(' ')
|
|
34
|
-
|
|
35
|
-
const tiltStyle = safeTilt ? ` style="--photo-tilt:${safeTilt}deg"` : ''
|
|
36
|
-
const cropStyle = ratio ? ` style="aspect-ratio:${e(ratio)}"` : ''
|
|
37
|
-
|
|
38
|
-
return `<figure class="${e(classes)}"${tiltStyle}>
|
|
39
|
-
<div class="ui-photo-card-crop"${cropStyle}>
|
|
40
|
-
<img src="${e(src)}" alt="${e(alt)}" class="ui-photo-card-img" loading="lazy" decoding="async">
|
|
41
|
-
</div>
|
|
42
|
-
${caption ? `<figcaption class="ui-photo-card-caption">${e(caption)}</figcaption>` : ''}
|
|
43
|
-
</figure>`
|
|
44
|
-
}
|
package/src/ui/pricing.js
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse UI — Pricing
|
|
3
|
-
*
|
|
4
|
-
* A single pricing plan card with feature list and CTA.
|
|
5
|
-
* Compose multiple in a flex/grid container for a full pricing section.
|
|
6
|
-
*
|
|
7
|
-
* @param {object} opts
|
|
8
|
-
* @param {string} opts.name - Plan name (e.g. "Pro")
|
|
9
|
-
* @param {number} opts.level - Heading level 1–6 for the plan name (default 3). Visual style is always ui-pricing-name.
|
|
10
|
-
* @param {string} opts.price - Price string (e.g. "$9", "Free")
|
|
11
|
-
* @param {string} opts.period - Billing period (e.g. "/month")
|
|
12
|
-
* @param {string} opts.description - One-line plan description
|
|
13
|
-
* @param {string[]} opts.features - Array of feature strings
|
|
14
|
-
* @param {string} opts.action - Raw HTML slot — typically a button()
|
|
15
|
-
* @param {boolean} opts.highlighted - Accent border + elevated appearance
|
|
16
|
-
* @param {string} opts.badge - Floating label above the card (e.g. "Most popular")
|
|
17
|
-
* @param {string} opts.class
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { escHtml as e } from '../html.js'
|
|
21
|
-
|
|
22
|
-
export function pricing({
|
|
23
|
-
name = '',
|
|
24
|
-
level = 3,
|
|
25
|
-
price = '',
|
|
26
|
-
period = '',
|
|
27
|
-
description = '',
|
|
28
|
-
features = [],
|
|
29
|
-
action = '',
|
|
30
|
-
highlighted = false,
|
|
31
|
-
badge = '',
|
|
32
|
-
class: cls = '',
|
|
33
|
-
} = {}) {
|
|
34
|
-
const classes = ['ui-pricing', highlighted && 'ui-pricing--highlighted', cls].filter(Boolean).join(' ')
|
|
35
|
-
const tag = `h${Math.min(Math.max(Math.floor(level), 1), 6)}`
|
|
36
|
-
|
|
37
|
-
const featureList = features.length
|
|
38
|
-
? `<ul class="ui-pricing-features">
|
|
39
|
-
${features.map(f => `<li class="ui-pricing-feature"><span class="ui-pricing-check" aria-hidden="true">✓</span>${e(f)}</li>`).join('\n ')}
|
|
40
|
-
</ul>`
|
|
41
|
-
: ''
|
|
42
|
-
|
|
43
|
-
return `<div class="${e(classes)}">
|
|
44
|
-
${badge ? `<p class="ui-pricing-badge">${e(badge)}</p>` : ''}
|
|
45
|
-
<div class="ui-pricing-header">
|
|
46
|
-
${name ? `<${tag} class="ui-pricing-name">${e(name)}</${tag}>` : ''}
|
|
47
|
-
${description ? `<p class="ui-pricing-desc">${e(description)}</p>` : ''}
|
|
48
|
-
</div>
|
|
49
|
-
<div class="ui-pricing-price">
|
|
50
|
-
<span class="ui-pricing-amount">${e(price)}</span>
|
|
51
|
-
${period ? `<span class="ui-pricing-period">${e(period)}</span>` : ''}
|
|
52
|
-
</div>
|
|
53
|
-
${featureList}
|
|
54
|
-
${action ? `<div class="ui-pricing-action">${action}</div>` : ''}
|
|
55
|
-
</div>`
|
|
56
|
-
}
|
package/src/ui/product-card.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse UI — Product Card
|
|
3
|
-
*
|
|
4
|
-
* Full-bleed editorial product card — the image IS the card. The product
|
|
5
|
-
* name (and optional price) overlays at the bottom on hover. Designed for
|
|
6
|
-
* fashion/e-commerce grids where photography is the primary communication.
|
|
7
|
-
*
|
|
8
|
-
* Unlike photoCard (polaroid frame + caption always visible), this card is
|
|
9
|
-
* frameless: the image fills 100% of the card, and metadata is revealed on
|
|
10
|
-
* hover via a gradient overlay.
|
|
11
|
-
*
|
|
12
|
-
* @param {object} opts
|
|
13
|
-
* @param {string} opts.src - Image source URL
|
|
14
|
-
* @param {string} opts.alt - Alt text
|
|
15
|
-
* @param {string} opts.name - Product name
|
|
16
|
-
* @param {string} opts.price - Optional price string
|
|
17
|
-
* @param {string} opts.href - Link destination (default: '#')
|
|
18
|
-
* @param {string} opts.badge - Short badge text ("NEW", "SALE", "−20%")
|
|
19
|
-
* @param {'left'|'right'} opts.badgeAlign - Badge corner (default: 'left')
|
|
20
|
-
* @param {string} opts.ratio - CSS aspect-ratio (default: '4/5')
|
|
21
|
-
* @param {'overlay'|'below'} opts.layout - 'overlay' (hover reveal) or 'below' (caption under image)
|
|
22
|
-
* @param {string} opts.class
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import { escHtml as e } from '../html.js'
|
|
26
|
-
|
|
27
|
-
export function productCard({
|
|
28
|
-
src = '',
|
|
29
|
-
alt = '',
|
|
30
|
-
name = '',
|
|
31
|
-
price = '',
|
|
32
|
-
href = '#',
|
|
33
|
-
badge = '',
|
|
34
|
-
badgeAlign = 'left',
|
|
35
|
-
ratio = '4/5',
|
|
36
|
-
layout = 'overlay',
|
|
37
|
-
class: cls = '',
|
|
38
|
-
} = {}) {
|
|
39
|
-
const classes = [
|
|
40
|
-
'ui-product-card',
|
|
41
|
-
badgeAlign === 'right' && 'ui-product-card--badge-right',
|
|
42
|
-
layout === 'below' && 'ui-product-card--below',
|
|
43
|
-
cls,
|
|
44
|
-
].filter(Boolean).join(' ')
|
|
45
|
-
|
|
46
|
-
const cropStyle = ratio ? ` style="aspect-ratio:${e(ratio)}"` : ''
|
|
47
|
-
|
|
48
|
-
const namePrice = name ? (layout === 'below'
|
|
49
|
-
? `<div class="ui-product-card-caption">
|
|
50
|
-
<p class="ui-product-card-name">${e(name)}</p>
|
|
51
|
-
${price ? `<p class="ui-product-card-price">${e(price)}</p>` : ''}
|
|
52
|
-
</div>`
|
|
53
|
-
: `<div class="ui-product-card-overlay" aria-hidden="true">
|
|
54
|
-
<p class="ui-product-card-name">${e(name)}</p>
|
|
55
|
-
${price ? `<p class="ui-product-card-price">${e(price)}</p>` : ''}
|
|
56
|
-
</div>`) : ''
|
|
57
|
-
|
|
58
|
-
return `<a href="${e(href)}" class="${e(classes)}">
|
|
59
|
-
<div class="ui-product-card-crop"${cropStyle}>
|
|
60
|
-
<img src="${e(src)}" alt="${e(alt)}" class="ui-product-card-img" loading="lazy" decoding="async">
|
|
61
|
-
</div>
|
|
62
|
-
${badge ? `<span class="ui-product-card-badge">${e(badge)}</span>` : ''}
|
|
63
|
-
${namePrice}
|
|
64
|
-
</a>`
|
|
65
|
-
}
|
package/src/ui/section-label.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse UI — Section Label
|
|
3
|
-
*
|
|
4
|
-
* Eyebrow + heading + horizontal rule. The editorial equivalent of feature().
|
|
5
|
-
* For: chapter openings, section dividers, content blocks.
|
|
6
|
-
*
|
|
7
|
-
* @param {object} opts
|
|
8
|
-
* @param {string} opts.eyebrow - Small label above heading
|
|
9
|
-
* @param {string} opts.heading - Main heading text
|
|
10
|
-
* @param {number} opts.level - Heading level 1–6 (default 2)
|
|
11
|
-
* @param {boolean} opts.rule - Show horizontal rule after heading (default true)
|
|
12
|
-
* @param {'left'|'center'} opts.align - Text alignment (default 'left')
|
|
13
|
-
* @param {string} opts.class
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { escHtml as e } from '../html.js'
|
|
17
|
-
|
|
18
|
-
export function sectionLabel({
|
|
19
|
-
eyebrow = '',
|
|
20
|
-
heading = '',
|
|
21
|
-
level = 2,
|
|
22
|
-
rule = true,
|
|
23
|
-
align = 'left',
|
|
24
|
-
class: cls = '',
|
|
25
|
-
} = {}) {
|
|
26
|
-
const tag = `h${Math.min(Math.max(Math.floor(level), 1), 6)}`
|
|
27
|
-
const classes = [
|
|
28
|
-
'ui-section-label',
|
|
29
|
-
align === 'center' && 'ui-section-label--center',
|
|
30
|
-
cls,
|
|
31
|
-
].filter(Boolean).join(' ')
|
|
32
|
-
|
|
33
|
-
return `<div class="${e(classes)}">
|
|
34
|
-
${eyebrow ? `<p class="ui-section-label-eyebrow">${e(eyebrow)}</p>` : ''}
|
|
35
|
-
${heading ? `<${tag} class="ui-section-label-heading">${e(heading)}</${tag}>` : ''}
|
|
36
|
-
${rule ? '<hr class="ui-section-label-rule" aria-hidden="true">' : ''}
|
|
37
|
-
</div>`
|
|
38
|
-
}
|
package/src/ui/service-card.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse UI — ServiceCard
|
|
3
|
-
*
|
|
4
|
-
* A bordered, left-aligned tile with icon, title, and description.
|
|
5
|
-
* The most common pattern for trades/services grids — an unboxed, centred
|
|
6
|
-
* feature() is the wrong tool for these; serviceCard() is built for it.
|
|
7
|
-
*
|
|
8
|
-
* Use inside a grid() or css grid for multi-column layouts.
|
|
9
|
-
*
|
|
10
|
-
* @param {object} opts
|
|
11
|
-
* @param {string} opts.icon - Raw HTML slot — SVG icon displayed in an accent-tinted box
|
|
12
|
-
* @param {string} opts.title
|
|
13
|
-
* @param {number} opts.level - Heading level 1–6 (default 3)
|
|
14
|
-
* @param {string} opts.description
|
|
15
|
-
* @param {string} opts.href - When set, wraps the card in an <a> making it fully clickable
|
|
16
|
-
* @param {string} opts.class
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { escHtml as e } from '../html.js'
|
|
20
|
-
|
|
21
|
-
export function serviceCard({
|
|
22
|
-
icon = '',
|
|
23
|
-
title = '',
|
|
24
|
-
level = 3,
|
|
25
|
-
description = '',
|
|
26
|
-
href = '',
|
|
27
|
-
class: cls = '',
|
|
28
|
-
} = {}) {
|
|
29
|
-
const tag = `h${Math.min(Math.max(Math.floor(level), 1), 6)}`
|
|
30
|
-
const linked = Boolean(href)
|
|
31
|
-
const classes = [
|
|
32
|
-
'ui-service-card',
|
|
33
|
-
linked && 'ui-service-card--linked',
|
|
34
|
-
cls,
|
|
35
|
-
].filter(Boolean).join(' ')
|
|
36
|
-
|
|
37
|
-
const inner = `
|
|
38
|
-
${icon ? `<div class="ui-service-card-icon" aria-hidden="true">${icon}</div>` : ''}
|
|
39
|
-
${title ? `<${tag} class="ui-service-card-title">${e(title)}</${tag}>` : ''}
|
|
40
|
-
${description ? `<p class="ui-service-card-desc">${e(description)}</p>` : ''}
|
|
41
|
-
`
|
|
42
|
-
|
|
43
|
-
if (linked) {
|
|
44
|
-
return `<a href="${e(href)}" class="${e(classes)}">${inner}</a>`
|
|
45
|
-
}
|
|
46
|
-
return `<div class="${e(classes)}">${inner}</div>`
|
|
47
|
-
}
|
package/src/ui/stat-grid.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pulse UI — StatGrid
|
|
3
|
-
*
|
|
4
|
-
* A grid of stat tiles with optional coloured panel background.
|
|
5
|
-
* Use when you need multiple stat() tiles arranged together — e.g. a 2×2
|
|
6
|
-
* "at a glance" panel inside a hero or section.
|
|
7
|
-
*
|
|
8
|
-
* Each item is passed directly to stat(). The grid handles layout.
|
|
9
|
-
*
|
|
10
|
-
* @param {object} opts
|
|
11
|
-
* @param {Array} opts.stats - Array of stat() option objects (label, value, change, trend, size, suffix)
|
|
12
|
-
* @param {number} opts.columns - Number of grid columns (default: 2)
|
|
13
|
-
* @param {string} opts.background - CSS background value (e.g. 'var(--ui-surface-raised)')
|
|
14
|
-
* @param {string} opts.color - Foreground/text colour override
|
|
15
|
-
* @param {string} opts.class
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { stat } from './stat.js'
|
|
19
|
-
import { escHtml as e } from '../html.js'
|
|
20
|
-
|
|
21
|
-
export function statGrid({
|
|
22
|
-
stats = [],
|
|
23
|
-
columns = 2,
|
|
24
|
-
background = '',
|
|
25
|
-
color = '',
|
|
26
|
-
class: cls = '',
|
|
27
|
-
} = {}) {
|
|
28
|
-
const classes = ['ui-stat-grid', cls].filter(Boolean).join(' ')
|
|
29
|
-
|
|
30
|
-
const styles = [
|
|
31
|
-
`--stat-cols:${Number(columns) || 2}`,
|
|
32
|
-
background && `background:${background.replace(/"/g, "'")}`,
|
|
33
|
-
color && `color:${color.replace(/"/g, "'")}`,
|
|
34
|
-
].filter(Boolean).join(';')
|
|
35
|
-
const styleAttr = styles ? ` style="${styles}"` : ''
|
|
36
|
-
|
|
37
|
-
const tilesHtml = stats.map(s => stat(s)).join('')
|
|
38
|
-
|
|
39
|
-
return `<div class="${e(classes)}"${styleAttr}>${tilesHtml}</div>`
|
|
40
|
-
}
|