@invisibleloop/pulse 0.9.1 → 0.10.0
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/docs/public/.pulse-ui-version +1 -1
- package/docs/public/docs.css +12 -0
- package/docs/public/pulse-ui.css +128 -5
- package/docs/public/themes/lumio.css +50 -0
- package/docs/server.js +4 -0
- package/docs/src/lib/component-page.js +2 -1
- package/docs/src/lib/nav.js +8 -0
- package/docs/src/pages/components/hero.js +33 -4
- package/docs/src/pages/components/icons.js +7 -0
- package/docs/src/pages/components/phone-frame.js +54 -0
- package/docs/src/pages/templates/mobile-app.js +343 -0
- package/docs/src/pages/templates.js +156 -0
- package/package.json +1 -1
- package/public/.pulse-ui-version +1 -1
- package/public/pulse-ui.css +118 -1
- package/scripts/build.js +6 -2
- package/src/agent/guide-components.md +3 -2
- package/src/ui/hero.js +33 -4
- package/src/ui/icons.js +10 -0
- package/src/ui/index.js +3 -0
- package/src/ui/nav.js +10 -1
- package/src/ui/phone-frame.js +39 -0
package/scripts/build.js
CHANGED
|
@@ -617,9 +617,13 @@ async function purgeCssStep(pages, manifest, root, outDir) {
|
|
|
617
617
|
// Scan pulse UI package source — class names generated by UI components
|
|
618
618
|
// (breadcrumbs, avatar, container, etc.) only appear in the package source,
|
|
619
619
|
// not in the project's own pages, so they'd otherwise be purged.
|
|
620
|
+
// Also scan the parent src/ui in case root is a subdirectory (e.g. docs/) that
|
|
621
|
+
// imports UI components via relative paths from the parent project.
|
|
620
622
|
const scanUiDirs = [
|
|
621
|
-
path.join(root, 'src', 'ui'),
|
|
622
|
-
path.join(root, '
|
|
623
|
+
path.join(root, 'src', 'ui'), // project's own source
|
|
624
|
+
path.join(root, '..', 'src', 'ui'), // parent project source (e.g. pulse2/src/ui when root=docs)
|
|
625
|
+
path.join(root, 'node_modules', '@invisibleloop', 'pulse', 'src', 'ui'), // installed package
|
|
626
|
+
path.join(root, '..', 'node_modules', '@invisibleloop', 'pulse', 'src', 'ui'), // parent node_modules
|
|
623
627
|
]
|
|
624
628
|
for (const dir of scanUiDirs) {
|
|
625
629
|
if (!fs.existsSync(dir)) continue
|
|
@@ -146,8 +146,9 @@ Use `prose()` for HTML from external sources (CMS, markdown). Use `heading()`/`l
|
|
|
146
146
|
|
|
147
147
|
| Component | Key props |
|
|
148
148
|
|-----------|-----------|
|
|
149
|
-
| `nav` | `logo` (HTML slot), `logoHref`, `links` ([{label,href}]), `action` (HTML slot), `sticky` |
|
|
150
|
-
| `hero` | `eyebrow`, `title`, `subtitle`, `actions` (HTML slot), `align` (center/left), `size` (md/sm) |
|
|
149
|
+
| `nav` | `logo` (HTML slot), `logoHref`, `links` ([{label,href}]), `action` (HTML slot), `sticky`, `background` (any CSS value — overrides default surface colour), `color` (foreground colour — set when custom background doesn't contrast with default muted text) |
|
|
150
|
+
| `hero` | `eyebrow`, `title`, `subtitle`, `actions` (HTML slot), `image` (HTML slot — activates split layout), `imageAlign` (right/left), `align` (center/left), `size` (md/sm), `gradient` (true/false or preset: 'purple'\|'blue'\|'green'\|'rose'\|'orange'), `background` (any CSS value — solid colour or custom gradient, overrides gradient preset), `eyebrowColor` (overrides eyebrow text colour — always set when using a custom background to ensure WCAG contrast) |
|
|
151
|
+
| `phoneFrame` | `content` (HTML slot — rendered inside the phone screen). Pure CSS phone mockup with dynamic island, side buttons, home indicator. Use as the `image` slot in `hero()` for app landing pages. |
|
|
151
152
|
| `feature` | `icon` (HTML slot), `title`, `level` (1–6, default 3), `description`, `center` |
|
|
152
153
|
| `testimonial` | `quote`, `name`, `role`, `src`, `rating` (1–5) |
|
|
153
154
|
| `pricing` | `name`, `level` (1–6, default 3), `price`, `period`, `features` ([strings]), `action` (HTML slot), `highlighted` |
|
package/src/ui/hero.js
CHANGED
|
@@ -13,11 +13,19 @@
|
|
|
13
13
|
* @param {'right'|'left'} opts.imageAlign - Which side the image sits on (default: 'right')
|
|
14
14
|
* @param {'center'|'left'} opts.align - Text alignment when no image (default: 'center')
|
|
15
15
|
* @param {'md'|'sm'} opts.size - Vertical padding: 'md' (default, 5rem) or 'sm' (2.5rem)
|
|
16
|
+
* @param {boolean|'purple'|'blue'|'green'|'rose'|'orange'} opts.gradient
|
|
17
|
+
* Gradient background preset. true/'purple' = deep indigo, 'blue' = navy,
|
|
18
|
+
* 'green' = forest, 'rose' = crimson, 'orange' = amber. (default: false)
|
|
19
|
+
* @param {string} opts.background - Arbitrary CSS background value — overrides gradient preset.
|
|
20
|
+
* Accepts any valid CSS: solid colours ('#1a1a2e'), gradients ('linear-gradient(...)'), etc.
|
|
21
|
+
* @param {string} opts.eyebrowColor - Overrides the eyebrow text colour — use when the default accent colour lacks contrast against a custom background
|
|
16
22
|
* @param {string} opts.class
|
|
17
23
|
*/
|
|
18
24
|
|
|
19
25
|
import { escHtml as e } from '../html.js'
|
|
20
26
|
|
|
27
|
+
const GRADIENT_PRESETS = new Set(['purple', 'blue', 'green', 'rose', 'orange'])
|
|
28
|
+
|
|
21
29
|
export function hero({
|
|
22
30
|
eyebrow = '',
|
|
23
31
|
title = '',
|
|
@@ -27,25 +35,46 @@ export function hero({
|
|
|
27
35
|
imageAlign = 'right',
|
|
28
36
|
align = 'center',
|
|
29
37
|
size = 'md',
|
|
30
|
-
|
|
38
|
+
gradient = false,
|
|
39
|
+
background = '',
|
|
40
|
+
eyebrowColor = '',
|
|
41
|
+
color = '',
|
|
42
|
+
class: cls = '',
|
|
31
43
|
} = {}) {
|
|
32
44
|
const split = Boolean(image)
|
|
45
|
+
|
|
46
|
+
// Resolve gradient class — true/'purple' share the base class; others get their own modifier
|
|
47
|
+
const gradientClass = gradient
|
|
48
|
+
? (gradient === true || gradient === 'purple')
|
|
49
|
+
? 'ui-hero--gradient'
|
|
50
|
+
: GRADIENT_PRESETS.has(gradient) ? `ui-hero--gradient--${gradient}` : ''
|
|
51
|
+
: ''
|
|
52
|
+
|
|
33
53
|
const classes = [
|
|
34
54
|
'ui-hero',
|
|
35
55
|
split ? 'ui-hero--split' : align === 'left' && 'ui-hero--left',
|
|
36
56
|
split && imageAlign === 'left' && 'ui-hero--media-left',
|
|
37
57
|
size === 'sm' && 'ui-hero--sm',
|
|
58
|
+
gradientClass,
|
|
38
59
|
cls,
|
|
39
60
|
].filter(Boolean).join(' ')
|
|
40
61
|
|
|
62
|
+
// Inline style — strip quotes to prevent attribute injection
|
|
63
|
+
const styles = [
|
|
64
|
+
background && `background:${background.replace(/"/g, "'")}`,
|
|
65
|
+
color && `color:${color.replace(/"/g, "'")};--ui-muted:${color.replace(/"/g, "'")}`,
|
|
66
|
+
].filter(Boolean).join(';')
|
|
67
|
+
const bgStyle = styles ? ` style="${styles}"` : ''
|
|
68
|
+
|
|
69
|
+
const eyebrowStyle = eyebrowColor ? ` style="color:${eyebrowColor.replace(/"/g, "'")}"` : ''
|
|
41
70
|
const content = `
|
|
42
|
-
${eyebrow ? `<p class="ui-hero-eyebrow">${e(eyebrow)}</p>` : ''}
|
|
71
|
+
${eyebrow ? `<p class="ui-hero-eyebrow"${eyebrowStyle}>${e(eyebrow)}</p>` : ''}
|
|
43
72
|
${title ? `<h1 class="ui-hero-title">${e(title)}</h1>` : ''}
|
|
44
73
|
${subtitle ? `<p class="ui-hero-subtitle">${e(subtitle)}</p>` : ''}
|
|
45
74
|
${actions ? `<div class="ui-hero-actions">${actions}</div>` : ''}`
|
|
46
75
|
|
|
47
76
|
if (split) {
|
|
48
|
-
return `<section class="${e(classes)}">
|
|
77
|
+
return `<section class="${e(classes)}"${bgStyle}>
|
|
49
78
|
<div class="ui-hero-inner">
|
|
50
79
|
<div class="ui-hero-content">${content}
|
|
51
80
|
</div>
|
|
@@ -54,7 +83,7 @@ export function hero({
|
|
|
54
83
|
</section>`
|
|
55
84
|
}
|
|
56
85
|
|
|
57
|
-
return `<section class="${e(classes)}">
|
|
86
|
+
return `<section class="${e(classes)}"${bgStyle}>
|
|
58
87
|
<div class="ui-hero-inner">${content}
|
|
59
88
|
</div>
|
|
60
89
|
</section>`
|
package/src/ui/icons.js
CHANGED
|
@@ -173,6 +173,16 @@ export const iconGrid = (o) => s('<rect x="3" y="3" width="7" height="
|
|
|
173
173
|
export const iconBug = (o) => s('<rect x="8" y="6" width="8" height="14" rx="4"/><path d="m19 7-3 2"/><path d="m5 7 3 2"/><path d="m19 19-3-2"/><path d="m5 19 3-2"/><path d="M20 13h-4"/><path d="M4 13h4"/><path d="m10 4 1 2"/><path d="m14 4-1 2"/>', opts(o))
|
|
174
174
|
export const iconMapPin = (o) => s('<path d="M20 10c0 6-8 13-8 13s-8-7-8-13a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/>', opts(o))
|
|
175
175
|
|
|
176
|
+
// ─── AI & Data ───────────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
export const iconAi = (o) => s('<path d="M12 2a2 2 0 012 2v1a7 7 0 017 7 7 7 0 01-7 7v1a2 2 0 01-4 0v-1A7 7 0 012 12a7 7 0 017-7V4a2 2 0 012-2z"/><circle cx="12" cy="12" r="2"/>', opts(o))
|
|
179
|
+
export const iconSmile = (o) => s('<circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><line x1="9" y1="9" x2="9.01" y2="9"/><line x1="15" y1="9" x2="15.01" y2="9"/>', opts(o))
|
|
180
|
+
export const iconBarChart = (o) => s('<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>', opts(o))
|
|
181
|
+
|
|
182
|
+
// ─── Writing ─────────────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
export const iconFeather = (o) => s('<path d="M20.24 12.24a6 6 0 0 0-8.49-8.49L5 10.5V19h8.5z"/><line x1="16" y1="8" x2="2" y2="22"/><line x1="17.5" y1="15" x2="9" y2="15"/>', opts(o))
|
|
185
|
+
|
|
176
186
|
// ─── Ecommerce ───────────────────────────────────────────────────────────────
|
|
177
187
|
|
|
178
188
|
export const iconShoppingCart = (o) => s('<circle cx="8" cy="21" r="1"/><circle cx="19" cy="21" r="1"/><path d="M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12"/>', opts(o))
|
package/src/ui/index.js
CHANGED
|
@@ -32,6 +32,7 @@ export { avatar } from './avatar.js'
|
|
|
32
32
|
export { empty } from './empty.js'
|
|
33
33
|
export { table } from './table.js'
|
|
34
34
|
export { hero } from './hero.js'
|
|
35
|
+
export { phoneFrame } from './phone-frame.js'
|
|
35
36
|
export { testimonial } from './testimonial.js'
|
|
36
37
|
export { feature } from './feature.js'
|
|
37
38
|
export { pricing } from './pricing.js'
|
|
@@ -86,6 +87,8 @@ export {
|
|
|
86
87
|
iconHandPointUp, iconHandPointDown, iconHandPointLeft, iconHandPointRight,
|
|
87
88
|
iconGlobe, iconShield, iconZap, iconTrendingUp, iconTrendingDown, iconLoader, iconGrid, iconBug, iconMapPin,
|
|
88
89
|
iconSun, iconMoon,
|
|
90
|
+
iconFeather,
|
|
91
|
+
iconAi, iconSmile, iconBarChart,
|
|
89
92
|
iconShoppingCart, iconShoppingBag, iconCreditCard, iconPackage, iconGift,
|
|
90
93
|
iconWallet, iconTruck, iconReceipt, iconStore, iconPercent, iconTicket, iconBanknote,
|
|
91
94
|
iconUtensils, iconCoffee, iconPizza, iconApple, iconCarrot,
|
package/src/ui/nav.js
CHANGED
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
* @param {string} opts.action - Raw HTML slot — typically a button()
|
|
25
25
|
* @param {boolean} opts.sticky - Position sticky with backdrop blur
|
|
26
26
|
* @param {'right'|'left'} opts.burgerAlign - Mobile burger position (default: 'right')
|
|
27
|
+
* @param {string} opts.background - Any CSS background value — overrides the default surface colour
|
|
28
|
+
* @param {string} opts.color - Foreground colour for links and text — use when background has low contrast with defaults
|
|
27
29
|
* @param {string} opts.class
|
|
28
30
|
*/
|
|
29
31
|
|
|
@@ -79,10 +81,17 @@ export function nav({
|
|
|
79
81
|
action = '',
|
|
80
82
|
sticky = false,
|
|
81
83
|
burgerAlign = 'right',
|
|
84
|
+
background = '',
|
|
85
|
+
color = '',
|
|
82
86
|
class: cls = '',
|
|
83
87
|
} = {}) {
|
|
84
88
|
const id = `ui-nav-${++_navId}`
|
|
85
89
|
const classes = ['ui-nav', sticky && 'ui-nav--sticky', burgerAlign === 'left' && 'ui-nav--burger-left', cls].filter(Boolean).join(' ')
|
|
90
|
+
const styles = [
|
|
91
|
+
background && `background:${background.replace(/"/g, "'")}`,
|
|
92
|
+
color && `color:${color.replace(/"/g, "'")};--ui-muted:${color.replace(/"/g, "'")}`,
|
|
93
|
+
].filter(Boolean).join(';')
|
|
94
|
+
const bgStyle = styles ? ` style="${styles}"` : ''
|
|
86
95
|
|
|
87
96
|
const linksHtml = links.map((l, i) => renderLink(l, id, i)).join('')
|
|
88
97
|
const mobileLinksHtml = links.map(renderMobileLink).join('')
|
|
@@ -98,7 +107,7 @@ export function nav({
|
|
|
98
107
|
<nav>${mobileLinksHtml}</nav>
|
|
99
108
|
</div>` : ''
|
|
100
109
|
|
|
101
|
-
return `<header class="${e(classes)}">
|
|
110
|
+
return `<header class="${e(classes)}"${bgStyle}>
|
|
102
111
|
<div class="ui-nav-inner">
|
|
103
112
|
<a href="${e(logoHref)}" class="ui-nav-logo">${logo}</a>
|
|
104
113
|
${links.length ? `<nav class="ui-nav-links" aria-label="Site navigation">${linksHtml}</nav>` : ''}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse UI — Phone Frame
|
|
3
|
+
*
|
|
4
|
+
* A realistic CSS mobile phone frame. Pass screen content in the `content` slot.
|
|
5
|
+
* Renders the phone body, dynamic island, side buttons, and home indicator.
|
|
6
|
+
* No images — pure CSS/HTML.
|
|
7
|
+
*
|
|
8
|
+
* @param {object} opts
|
|
9
|
+
* @param {string} opts.content - HTML to render inside the screen area
|
|
10
|
+
* @param {string} opts.class
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export function phoneFrame({ content = '', class: cls = '' } = {}) {
|
|
14
|
+
const classes = ['ui-phone', cls].filter(Boolean).join(' ')
|
|
15
|
+
|
|
16
|
+
return `<div class="${classes}" role="img" aria-label="Phone screen preview">
|
|
17
|
+
<!-- Left buttons: volume up, volume down, mute -->
|
|
18
|
+
<div class="ui-phone-btn ui-phone-btn--vol-up" aria-hidden="true"></div>
|
|
19
|
+
<div class="ui-phone-btn ui-phone-btn--vol-down" aria-hidden="true"></div>
|
|
20
|
+
<div class="ui-phone-btn ui-phone-btn--mute" aria-hidden="true"></div>
|
|
21
|
+
<!-- Right button: power -->
|
|
22
|
+
<div class="ui-phone-btn ui-phone-btn--power" aria-hidden="true"></div>
|
|
23
|
+
|
|
24
|
+
<div class="ui-phone-inner">
|
|
25
|
+
<!-- Dynamic island -->
|
|
26
|
+
<div class="ui-phone-island" aria-hidden="true">
|
|
27
|
+
<span class="ui-phone-camera"></span>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<!-- Screen -->
|
|
31
|
+
<div class="ui-phone-screen">
|
|
32
|
+
${content}
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<!-- Home indicator -->
|
|
36
|
+
<div class="ui-phone-home" aria-hidden="true"></div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>`
|
|
39
|
+
}
|