@invisibleloop/pulse 0.12.3 → 0.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/build-page.md +5 -1
- package/.claude/commands/verify.md +31 -17
- package/.claude/pulse-checklist.md +21 -1
- package/.github/workflows/publish.yml +1 -0
- package/.pulse-verified +1 -0
- package/CLAUDE.md +56 -15
- package/docs/.claude/commands/verify.md +4 -0
- package/docs/.claude/pulse-checklist.md +20 -2
- package/docs/public/.pulse-ui-version +1 -1
- package/docs/public/andypixel.css +237 -0
- package/docs/public/bookworms.css +422 -0
- package/docs/public/coutumes.css +584 -0
- package/docs/public/images/bookworms/hero-books.jpg +0 -0
- package/docs/public/images/bookworms/how-we-read-now.jpg +0 -0
- package/docs/public/images/bookworms/lantern-keepers.jpg +0 -0
- package/docs/public/images/bookworms/provincial-bookseller.jpg +0 -0
- package/docs/public/images/journey/178.jpg +0 -0
- package/docs/public/images/journey/29.jpg +0 -0
- package/docs/public/images/retrowheels/hero.jpg +0 -0
- package/docs/public/images/shop/devbook-pro.jpg +0 -0
- package/docs/public/images/suzukibento/1015.jpg +0 -0
- package/docs/public/images/suzukibento/1025.jpg +0 -0
- package/docs/public/images/suzukibento/1043.jpg +0 -0
- package/docs/public/images/suzukibento/1059.jpg +0 -0
- package/docs/public/images/suzukibento/1062.jpg +0 -0
- package/docs/public/images/suzukibento/1084.jpg +0 -0
- package/docs/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
- package/docs/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
- package/docs/public/images/swish/1485518882345-15568b007407.jpg +0 -0
- package/docs/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
- package/docs/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
- package/docs/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
- package/docs/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
- package/docs/public/images/swish/1551803091-e20673f15770.jpg +0 -0
- package/docs/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
- package/docs/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
- package/docs/public/journey.css +528 -0
- package/docs/public/pulse-ui.css +1036 -22
- package/docs/public/retrowheels.css +402 -0
- package/docs/public/shop.css +235 -0
- package/docs/public/sparkys.css +349 -0
- package/docs/public/suzukibento.css +517 -0
- package/docs/public/swish.css +394 -0
- package/docs/server.js +19 -4
- package/docs/src/lib/nav.js +14 -3
- package/docs/src/pages/components/decorate.js +74 -0
- package/docs/src/pages/components/gallery.js +102 -0
- package/docs/src/pages/components/hero.js +1 -0
- package/docs/src/pages/components/marquee.js +76 -0
- package/docs/src/pages/components/photo-card.js +74 -0
- package/docs/src/pages/components/section.js +40 -1
- package/docs/src/pages/meta.js +26 -0
- package/docs/src/pages/templates/andypixel.js +260 -0
- package/docs/src/pages/templates/bookworms.js +227 -0
- package/docs/src/pages/templates/coutumes.js +226 -0
- package/docs/src/pages/templates/journey.js +186 -0
- package/docs/src/pages/templates/retrowheels.js +342 -0
- package/docs/src/pages/templates/shop.js +257 -0
- package/docs/src/pages/templates/sparkys.js +249 -0
- package/docs/src/pages/templates/suzukibento.js +200 -0
- package/docs/src/pages/templates/swish.js +313 -0
- package/docs/src/pages/templates.js +313 -2
- package/docs/src/pages/testing.js +2 -1
- package/examples/.claude/commands/pulse-build.md +7 -0
- package/examples/.claude/commands/pulse-dev.md +7 -0
- package/examples/.claude/commands/pulse-start.md +9 -0
- package/examples/.claude/commands/pulse-stop.md +5 -0
- package/examples/.claude/commands/verify.md +84 -0
- package/examples/.claude/coverage-check.js +127 -0
- package/examples/.claude/pulse-checklist.md +177 -0
- package/examples/andypixel.css +237 -0
- package/examples/andypixel.js +258 -0
- package/examples/bookworms.css +422 -0
- package/examples/bookworms.js +225 -0
- package/examples/contact.js +1 -0
- package/examples/counter.js +1 -0
- package/examples/dev.server.js +17 -0
- package/examples/journey.css +528 -0
- package/examples/journey.js +186 -0
- package/examples/pricing.js +1 -0
- package/examples/public/.pulse-ui-version +1 -0
- package/examples/public/pulse-ui.css +4031 -0
- package/examples/public/pulse-ui.js +257 -0
- package/examples/quiz.js +1 -0
- package/examples/retrowheels.css +402 -0
- package/examples/retrowheels.js +340 -0
- package/examples/shared.js +6 -0
- package/examples/suzukibento.css +513 -0
- package/examples/suzukibento.js +200 -0
- package/examples/swish.css +394 -0
- package/examples/swish.js +311 -0
- package/examples/todos.js +1 -0
- package/package.json +2 -3
- package/public/.pulse-ui-version +1 -1
- package/public/images/bookworms/hero-books.jpg +0 -0
- package/public/images/bookworms/how-we-read-now.jpg +0 -0
- package/public/images/bookworms/lantern-keepers.jpg +0 -0
- package/public/images/bookworms/provincial-bookseller.jpg +0 -0
- package/public/images/journey/178.jpg +0 -0
- package/public/images/journey/29.jpg +0 -0
- package/public/images/retrowheels/hero.jpg +0 -0
- package/public/images/shop/devbook-pro.jpg +0 -0
- package/public/images/suzukibento/1015.jpg +0 -0
- package/public/images/suzukibento/1025.jpg +0 -0
- package/public/images/suzukibento/1043.jpg +0 -0
- package/public/images/suzukibento/1059.jpg +0 -0
- package/public/images/suzukibento/1062.jpg +0 -0
- package/public/images/suzukibento/1084.jpg +0 -0
- package/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
- package/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
- package/public/images/swish/1485518882345-15568b007407.jpg +0 -0
- package/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
- package/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
- package/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
- package/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
- package/public/images/swish/1551803091-e20673f15770.jpg +0 -0
- package/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
- package/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
- package/public/pulse-ui.css +1080 -22
- package/scripts/build.js +72 -18
- package/src/agent/checklist.md +22 -2
- package/src/agent/commands/verify.md +4 -0
- package/src/agent/guide-components.md +146 -8
- package/src/agent/guide-design-gallery.md +288 -0
- package/src/agent/guide-design-references.md +172 -0
- package/src/agent/guide-explore.md +313 -0
- package/src/agent/guide-styles.md +212 -11
- package/src/agent/guide-templates.md +223 -10
- package/src/agent/guide.md +9 -4
- package/src/agent/identity.md +2 -1
- package/src/agent/skills/verify/SKILL.md +64 -28
- package/src/agent/workflow.md +247 -61
- package/src/cli/dev.js +39 -14
- package/src/cli/fmt.js +113 -0
- package/src/cli/index.js +60 -33
- package/src/cli/logger.js +145 -0
- package/src/cli/scaffold.js +57 -3
- package/src/html.js +6 -1
- package/src/mcp/server.js +2561 -99
- package/src/mcp/validate-worker.js +49 -5
- package/src/runtime/ssr.js +4 -1
- package/src/server/index.js +29 -5
- package/src/spec/schema.js +21 -0
- package/src/spec/schema.test.js +28 -0
- package/src/testing/html.js +130 -4
- package/src/testing/testing.test.js +58 -0
- package/src/ui/banner.js +24 -9
- package/src/ui/brand-statement.js +58 -0
- package/src/ui/button.js +13 -2
- package/src/ui/card.js +20 -1
- package/src/ui/colophon.js +29 -0
- package/src/ui/contact-row.js +41 -0
- package/src/ui/cta.js +18 -4
- package/src/ui/decorate.js +55 -0
- package/src/ui/display-heading.js +44 -0
- package/src/ui/drop-cap.js +29 -0
- package/src/ui/editorial-hero.js +63 -0
- package/src/ui/entry-list.js +36 -0
- package/src/ui/feature.js +20 -3
- package/src/ui/footer.js +78 -9
- package/src/ui/gallery.js +56 -0
- package/src/ui/hero.js +68 -18
- package/src/ui/image-pair.js +72 -0
- package/src/ui/index.js +20 -0
- package/src/ui/input.js +1 -0
- package/src/ui/marquee.js +48 -0
- package/src/ui/nav.js +19 -9
- package/src/ui/photo-card.js +44 -0
- package/src/ui/product-card.js +65 -0
- package/src/ui/pullquote.js +14 -3
- package/src/ui/search.js +1 -0
- package/src/ui/section-label.js +38 -0
- package/src/ui/section.js +1 -1
- package/src/ui/service-card.js +47 -0
- package/src/ui/stat-grid.js +40 -0
- package/src/ui/stat.js +16 -2
- package/src/ui/testimonial.js +17 -6
- package/src/ui/textarea.js +1 -0
- package/src/ui/top-strip.js +47 -0
- package/src/ui/trust-list.js +46 -0
- package/src/ui/ui.test.js +35 -0
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
}
|
package/src/ui/stat.js
CHANGED
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
* @param {string} opts.value - Formatted value string (e.g. "2.4k", "98%")
|
|
9
9
|
* @param {string} opts.change - Change label (e.g. "+12%", "−3")
|
|
10
10
|
* @param {'up'|'down'|'neutral'} opts.trend
|
|
11
|
+
* @param {string} opts.suffix - Raw HTML slot appended after the value — use for icons, ratings, units
|
|
12
|
+
* @param {'sm'|'md'|'lg'} opts.size
|
|
13
|
+
* - 'sm' — compact, for dense grids
|
|
14
|
+
* - 'md' — default
|
|
15
|
+
* - 'lg' — hero-scale, for top-of-page KPIs
|
|
11
16
|
* @param {boolean} opts.center - Centre-align all text
|
|
12
17
|
* @param {string} opts.class
|
|
13
18
|
*/
|
|
@@ -16,6 +21,7 @@ import { escHtml as e } from '../html.js'
|
|
|
16
21
|
import { iconTrendingUp, iconTrendingDown, iconMinus } from './icons.js'
|
|
17
22
|
|
|
18
23
|
const TRENDS = new Set(['up', 'down', 'neutral'])
|
|
24
|
+
const SIZES = new Set(['sm', 'md', 'lg'])
|
|
19
25
|
|
|
20
26
|
const TREND_ICONS = {
|
|
21
27
|
up: iconTrendingUp({ size: 13 }),
|
|
@@ -30,12 +36,20 @@ export function stat({
|
|
|
30
36
|
value = '',
|
|
31
37
|
change = '',
|
|
32
38
|
trend = 'neutral',
|
|
39
|
+
suffix = '',
|
|
40
|
+
size = 'md',
|
|
33
41
|
center = false,
|
|
34
42
|
class: cls = '',
|
|
35
43
|
} = {}) {
|
|
36
44
|
if (!TRENDS.has(trend)) trend = 'neutral'
|
|
45
|
+
if (!SIZES.has(size)) size = 'md'
|
|
37
46
|
|
|
38
|
-
const classes = [
|
|
47
|
+
const classes = [
|
|
48
|
+
'ui-stat',
|
|
49
|
+
size !== 'md' ? `ui-stat--${size}` : '',
|
|
50
|
+
center ? 'ui-stat--center' : '',
|
|
51
|
+
cls,
|
|
52
|
+
].filter(Boolean).join(' ')
|
|
39
53
|
|
|
40
54
|
const changeHtml = change
|
|
41
55
|
? `<p class="ui-stat-change ui-stat-change--${e(trend)}">
|
|
@@ -46,7 +60,7 @@ export function stat({
|
|
|
46
60
|
|
|
47
61
|
return `<div class="${e(classes)}">
|
|
48
62
|
<p class="ui-stat-label">${e(label)}</p>
|
|
49
|
-
<p class="ui-stat-value">${e(value)}</p>
|
|
63
|
+
<p class="ui-stat-value">${e(value)}${suffix ? `<span class="ui-stat-suffix" aria-hidden="true">${suffix}</span>` : ''}</p>
|
|
50
64
|
${changeHtml}
|
|
51
65
|
</div>`
|
|
52
66
|
}
|
package/src/ui/testimonial.js
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
* A customer quote with optional star rating, avatar, and attribution.
|
|
5
5
|
*
|
|
6
6
|
* @param {object} opts
|
|
7
|
-
* @param {string} opts.quote
|
|
8
|
-
* @param {string} opts.name
|
|
9
|
-
* @param {string} opts.role
|
|
10
|
-
* @param {string} opts.src
|
|
11
|
-
* @param {
|
|
7
|
+
* @param {string} opts.quote - The testimonial text
|
|
8
|
+
* @param {string} opts.name - Author full name
|
|
9
|
+
* @param {string} opts.role - Author role/company (e.g. "CEO at Acme")
|
|
10
|
+
* @param {string} opts.src - Avatar image URL; falls back to initials when omitted
|
|
11
|
+
* @param {string} opts.initials - Override auto-derived initials (useful for names containing & or unusual characters)
|
|
12
|
+
* @param {number} opts.rating - Star rating 1–5; omit to hide stars
|
|
12
13
|
* @param {string} opts.class
|
|
13
14
|
*/
|
|
14
15
|
|
|
@@ -19,6 +20,7 @@ export function testimonial({
|
|
|
19
20
|
name = '',
|
|
20
21
|
role = '',
|
|
21
22
|
src = '',
|
|
23
|
+
initials: initialsOverride = '',
|
|
22
24
|
rating = 0,
|
|
23
25
|
class: cls = '',
|
|
24
26
|
} = {}) {
|
|
@@ -28,7 +30,16 @@ export function testimonial({
|
|
|
28
30
|
? `<p class="ui-testimonial-rating" aria-label="${Math.round(rating)} out of 5 stars">${'★'.repeat(Math.min(5, Math.max(1, Math.round(rating))))}</p>`
|
|
29
31
|
: ''
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
// Filter to letter chars only so names like "Tomás & Ren Okabe" produce "TR" not "T&"
|
|
34
|
+
const derivedInitials = name
|
|
35
|
+
.split(/\s+/)
|
|
36
|
+
.map(w => w.replace(/[^a-zA-ZÀ-ÿ]/g, ''))
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.map(w => w[0])
|
|
39
|
+
.join('')
|
|
40
|
+
.slice(0, 2)
|
|
41
|
+
.toUpperCase()
|
|
42
|
+
const initials = initialsOverride || derivedInitials
|
|
32
43
|
|
|
33
44
|
const avatarHtml = src
|
|
34
45
|
? `<img src="${e(src)}" alt="${e(name)}" class="ui-testimonial-avatar" loading="lazy" width="40" height="40">`
|
package/src/ui/textarea.js
CHANGED
|
@@ -42,6 +42,7 @@ export function textarea({
|
|
|
42
42
|
const wrapClasses = ['ui-field', error ? 'ui-field--error' : '', cls].filter(Boolean).join(' ')
|
|
43
43
|
|
|
44
44
|
const attrsStr = Object.entries(attrs)
|
|
45
|
+
.filter(([, v]) => v !== undefined)
|
|
45
46
|
.map(([k, v]) => ` ${e(k)}="${e(String(v))}"`)
|
|
46
47
|
.join('')
|
|
47
48
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse UI — TopStrip
|
|
3
|
+
*
|
|
4
|
+
* A slim utility bar that sits above the nav — phone numbers, trading hours,
|
|
5
|
+
* social links, or any short announcements. Distinct from banner() which is
|
|
6
|
+
* for promotional/dismissible messages.
|
|
7
|
+
*
|
|
8
|
+
* Two layout modes:
|
|
9
|
+
* - left + right slots: `topStrip({ left: '...', right: '...' })`
|
|
10
|
+
* - single content: `topStrip({ content: '...' })`
|
|
11
|
+
*
|
|
12
|
+
* @param {object} opts
|
|
13
|
+
* @param {string} opts.content - Single centred content (overrides left/right)
|
|
14
|
+
* @param {string} opts.left - Left-aligned slot
|
|
15
|
+
* @param {string} opts.right - Right-aligned slot
|
|
16
|
+
* @param {string} opts.background - CSS background value
|
|
17
|
+
* @param {string} opts.color - Foreground/text colour
|
|
18
|
+
* @param {string} opts.class
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { escHtml as e } from '../html.js'
|
|
22
|
+
|
|
23
|
+
export function topStrip({
|
|
24
|
+
content = '',
|
|
25
|
+
left = '',
|
|
26
|
+
right = '',
|
|
27
|
+
background = '',
|
|
28
|
+
color = '',
|
|
29
|
+
class: cls = '',
|
|
30
|
+
} = {}) {
|
|
31
|
+
const classes = ['ui-top-strip', cls].filter(Boolean).join(' ')
|
|
32
|
+
|
|
33
|
+
const styles = [
|
|
34
|
+
background && `background:${background.replace(/"/g, "'")}`,
|
|
35
|
+
color && `color:${color.replace(/"/g, "'")}`,
|
|
36
|
+
].filter(Boolean).join(';')
|
|
37
|
+
const styleAttr = styles ? ` style="${styles}"` : ''
|
|
38
|
+
|
|
39
|
+
if (content) {
|
|
40
|
+
return `<div class="${e(classes)}"${styleAttr}><div class="ui-top-strip-content">${content}</div></div>`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return `<div class="${e(classes)}"${styleAttr}>
|
|
44
|
+
${left ? `<div class="ui-top-strip-left">${left}</div>` : ''}
|
|
45
|
+
${right ? `<div class="ui-top-strip-right">${right}</div>` : ''}
|
|
46
|
+
</div>`
|
|
47
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse UI — TrustList
|
|
3
|
+
*
|
|
4
|
+
* A compact row of trust/compliance badges — "Part P registered · NICEIC · £10m insured".
|
|
5
|
+
* Each item gets a leading icon (default: check mark). Common on trades, services,
|
|
6
|
+
* and professional-services sites.
|
|
7
|
+
*
|
|
8
|
+
* @param {object} opts
|
|
9
|
+
* @param {string[]} opts.items - List of labels
|
|
10
|
+
* @param {string} opts.icon - Raw HTML slot — icon shown before each item (default: ✓ check SVG)
|
|
11
|
+
* @param {string} opts.color - Icon colour CSS value (default: inherits --ui-accent)
|
|
12
|
+
* @param {'sm'|'md'} opts.size
|
|
13
|
+
* @param {string} opts.class
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { escHtml as e } from '../html.js'
|
|
17
|
+
import { iconCheck } from './icons.js'
|
|
18
|
+
|
|
19
|
+
const SIZES = new Set(['sm', 'md'])
|
|
20
|
+
|
|
21
|
+
export function trustList({
|
|
22
|
+
items = [],
|
|
23
|
+
icon = iconCheck({ size: 14 }),
|
|
24
|
+
color = '',
|
|
25
|
+
size = 'sm',
|
|
26
|
+
class: cls = '',
|
|
27
|
+
} = {}) {
|
|
28
|
+
if (!SIZES.has(size)) size = 'sm'
|
|
29
|
+
|
|
30
|
+
const classes = [
|
|
31
|
+
'ui-trust-list',
|
|
32
|
+
size !== 'sm' && `ui-trust-list--${size}`,
|
|
33
|
+
cls,
|
|
34
|
+
].filter(Boolean).join(' ')
|
|
35
|
+
|
|
36
|
+
const iconStyle = color ? ` style="color:${color.replace(/"/g, "'")}"` : ''
|
|
37
|
+
|
|
38
|
+
const itemsHtml = items.map(label =>
|
|
39
|
+
`<li class="ui-trust-list-item">
|
|
40
|
+
<span class="ui-trust-list-icon" aria-hidden="true"${iconStyle}>${icon}</span>
|
|
41
|
+
<span>${e(label)}</span>
|
|
42
|
+
</li>`
|
|
43
|
+
).join('')
|
|
44
|
+
|
|
45
|
+
return `<ul class="${e(classes)}">${itemsHtml}</ul>`
|
|
46
|
+
}
|
package/src/ui/ui.test.js
CHANGED
|
@@ -113,6 +113,17 @@ test('button: renders icon HTML inside element', () => {
|
|
|
113
113
|
assert.match(html, /<svg\/>/)
|
|
114
114
|
})
|
|
115
115
|
|
|
116
|
+
test('button: attrs with undefined values are omitted', () => {
|
|
117
|
+
const html = button({ label: 'Minus', attrs: { disabled: undefined, 'data-count': '3' } })
|
|
118
|
+
assert.doesNotMatch(html, /disabled="undefined"/)
|
|
119
|
+
assert.match(html, /data-count="3"/)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('button: attrs with defined values are rendered', () => {
|
|
123
|
+
const html = button({ label: 'Add', attrs: { 'data-action': 'add' } })
|
|
124
|
+
assert.match(html, /data-action="add"/)
|
|
125
|
+
})
|
|
126
|
+
|
|
116
127
|
// ─── badge ──────────────────────────────────────────────────────────────────
|
|
117
128
|
|
|
118
129
|
test('badge: renders with default variant', () => {
|
|
@@ -656,6 +667,30 @@ test('nav: escapes link labels and hrefs', () => {
|
|
|
656
667
|
assert.doesNotMatch(html, /<b>X<\/b>/)
|
|
657
668
|
})
|
|
658
669
|
|
|
670
|
+
test('nav: aria-current="page" on current link', () => {
|
|
671
|
+
const html = nav({ logo: 'x', links: [
|
|
672
|
+
{ label: 'Home', href: '/', current: true },
|
|
673
|
+
{ label: 'About', href: '/about' },
|
|
674
|
+
] })
|
|
675
|
+
assert.match(html, /aria-current="page"/)
|
|
676
|
+
// Appears in both desktop and mobile nav — once per nav, so 2 total
|
|
677
|
+
const matches = html.match(/aria-current="page"/g)
|
|
678
|
+
assert.ok(matches && matches.length >= 1, 'At least one link should have aria-current')
|
|
679
|
+
assert.doesNotMatch(html, /href="\/about"[^>]*aria-current/)
|
|
680
|
+
})
|
|
681
|
+
|
|
682
|
+
test('nav: no aria-current when current not set', () => {
|
|
683
|
+
const html = nav({ logo: 'x', links: [{ label: 'Home', href: '/' }] })
|
|
684
|
+
assert.doesNotMatch(html, /aria-current/)
|
|
685
|
+
})
|
|
686
|
+
|
|
687
|
+
test('nav: aria-current on utility link', () => {
|
|
688
|
+
const html = nav({ logo: 'x', links: [], utilityLinks: [
|
|
689
|
+
{ label: 'Sign in', href: '/login', current: true },
|
|
690
|
+
] })
|
|
691
|
+
assert.match(html, /aria-current="page"/)
|
|
692
|
+
})
|
|
693
|
+
|
|
659
694
|
// ─── appBadge ────────────────────────────────────────────────────────────────
|
|
660
695
|
|
|
661
696
|
test('appBadge: renders apple badge by default', () => {
|