@invisibleloop/pulse 0.9.0 → 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/.claude/pulse-checklist.md +11 -0
- package/docs/public/.pulse-ui-version +1 -1
- package/docs/public/docs.css +12 -0
- package/docs/public/menu.js +25 -15
- 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 +37 -19
- 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/runtime/index.js +8 -2
- package/src/runtime/tt.js +22 -2
- package/src/server/index.js +7 -4
- 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
|
@@ -130,3 +130,14 @@ Before finishing any spec, verify every point below. Fix anything that fails.
|
|
|
130
130
|
- Interactive elements without visible text have an `aria-label`.
|
|
131
131
|
- Disabled state is reflected with the `disabled` attribute, not just CSS.
|
|
132
132
|
- The page has a `<main id="main-content">` landmark.
|
|
133
|
+
- **Every element with `data-event`, `data-store-event`, `data-dialog-open`, or `data-dialog-close` is natively interactive (`button`, `a`, `input`, `select`, `textarea`, `summary`) or has `tabindex="0"`.** Non-interactive tags (`div`, `span`, `li`, `td`, etc.) with these attributes are not keyboard reachable. Prefer `<button>` — it is focusable, fires on Enter/Space, and carries the correct ARIA role for free. Only use `tabindex="0"` on a non-interactive element when a `<button>` genuinely cannot be used.
|
|
134
|
+
- **Every interactive element communicates its purpose and state:**
|
|
135
|
+
- Buttons have visible text or `aria-label`. Links use descriptive text — "click here", "here", "read more" are failures.
|
|
136
|
+
- Form inputs have an associated `<label for="id">` or `aria-label`. `placeholder` alone is not a label.
|
|
137
|
+
- Toggle controls (open/close, show/hide, expand/collapse) carry `aria-expanded="true|false"` or `aria-pressed="true|false"`.
|
|
138
|
+
- While an action is in flight, the trigger button has `aria-busy="true"` or its label changes (e.g. "Saving…"). A spinner alone is not enough.
|
|
139
|
+
- Active nav items have `aria-current="page"`. Selected items in a list, tab set, or option group have `aria-selected="true"`.
|
|
140
|
+
- **The page has a logical tab order:**
|
|
141
|
+
- No `tabindex` value greater than 0. Positive tabindex values override the natural DOM order and create a broken tab sequence. Remove them and reorder the DOM instead.
|
|
142
|
+
- Visually hidden interactive content (off-canvas menus, collapsed panels, `opacity:0` elements) is removed from the tab order with `tabindex="-1"` or the `inert` attribute — CSS alone does not remove an element from tab order.
|
|
143
|
+
- DOM order matches the visual reading order. When flexbox `order` or CSS grid placement visually reposition elements, the tab sequence still follows the DOM — author the DOM in the order a user would read and interact with the page.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.9.1
|
package/docs/public/docs.css
CHANGED
|
@@ -1160,6 +1160,18 @@ a:hover {
|
|
|
1160
1160
|
width: 100%;
|
|
1161
1161
|
}
|
|
1162
1162
|
|
|
1163
|
+
.demo-preview--bleed {
|
|
1164
|
+
padding-inline: 0;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
.demo-preview--bleed .demo-preview-inner {
|
|
1168
|
+
gap: 0;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
.demo-preview--bleed .demo-preview-inner > * {
|
|
1172
|
+
width: 100%;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1163
1175
|
.demo-preview--col .demo-preview-inner {
|
|
1164
1176
|
display: block;
|
|
1165
1177
|
}
|
package/docs/public/menu.js
CHANGED
|
@@ -67,20 +67,30 @@
|
|
|
67
67
|
input.style.setProperty('--slider-fill', pct)
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
// ── Demo theme toggles ────────────────────────────────────────────────────
|
|
75
|
-
document.querySelectorAll('.demo-theme-toggle').forEach(function (toggle) {
|
|
76
|
-
var preview = toggle.closest('.demo-preview')
|
|
77
|
-
if (!preview) return
|
|
78
|
-
toggle.addEventListener('click', function () {
|
|
79
|
-
var isLight = preview.classList.toggle('is-light')
|
|
80
|
-
var inner = preview.querySelector('.demo-preview-inner')
|
|
81
|
-
if (inner) {
|
|
82
|
-
inner.classList.toggle('ui-theme-light', isLight)
|
|
83
|
-
}
|
|
70
|
+
function bindDemoControls() {
|
|
71
|
+
document.querySelectorAll('.ui-slider').forEach(function (input) {
|
|
72
|
+
input.addEventListener('input', function () { updateSliderFill(this) })
|
|
84
73
|
})
|
|
85
|
-
|
|
74
|
+
|
|
75
|
+
// ── Demo theme toggles ──────────────────────────────────────────────────
|
|
76
|
+
document.querySelectorAll('.demo-theme-toggle').forEach(function (toggle) {
|
|
77
|
+
var preview = toggle.closest('.demo-preview')
|
|
78
|
+
if (!preview) return
|
|
79
|
+
toggle.addEventListener('click', function () {
|
|
80
|
+
var isLight = preview.classList.toggle('is-light')
|
|
81
|
+
var inner = preview.querySelector('.demo-preview-inner')
|
|
82
|
+
if (inner) {
|
|
83
|
+
inner.classList.toggle('ui-theme-light', isLight)
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
document.addEventListener('pulse:navigate', bindDemoControls)
|
|
90
|
+
|
|
91
|
+
if (document.readyState === 'complete') {
|
|
92
|
+
setTimeout(bindDemoControls, 50)
|
|
93
|
+
} else {
|
|
94
|
+
window.addEventListener('load', bindDemoControls)
|
|
95
|
+
}
|
|
86
96
|
})()
|
package/docs/public/pulse-ui.css
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
--ui-accent: var(--accent, #9b8dff);
|
|
25
25
|
--ui-accent-hover: var(--accent-hover, #b5aaff);
|
|
26
26
|
--ui-accent-dim: var(--accent-dim, rgba(155,141,255,.12));
|
|
27
|
-
--ui-accent-text: var(--accent-text, #
|
|
27
|
+
--ui-accent-text: var(--accent-text, #0d0a20);
|
|
28
28
|
--ui-green: var(--green, #3dd68c);
|
|
29
29
|
--ui-green-dim: rgba(61, 214, 140, .12);
|
|
30
30
|
--ui-red: var(--red, #ff6b6b);
|
|
@@ -525,6 +525,7 @@ html, body {
|
|
|
525
525
|
.ui-stat-change--neutral { color: var(--ui-muted); }
|
|
526
526
|
|
|
527
527
|
.ui-stat--center { align-items: center; text-align: center; }
|
|
528
|
+
.ui-stat--center .ui-stat-change { margin-inline: auto; }
|
|
528
529
|
|
|
529
530
|
/* ─── Avatar ─────────────────────────────────────────────────────────────── */
|
|
530
531
|
.ui-avatar {
|
|
@@ -758,11 +759,46 @@ hr.ui-divider {
|
|
|
758
759
|
}
|
|
759
760
|
|
|
760
761
|
/* ─── Hero ───────────────────────────────────────────────────────────────── */
|
|
761
|
-
.ui-hero { padding: 5rem
|
|
762
|
+
.ui-hero { padding: 5rem 0; text-align: center; }
|
|
762
763
|
.ui-hero--left { text-align: left; }
|
|
763
764
|
.ui-hero--sm { padding-top: 2.5rem; padding-bottom: 0; }
|
|
765
|
+
.ui-hero--gradient,
|
|
766
|
+
.ui-hero--gradient--purple {
|
|
767
|
+
background:
|
|
768
|
+
radial-gradient(ellipse 70% 80% at 75% 50%, #2d1b6e 0%, #1a0f45 45%, transparent 75%),
|
|
769
|
+
radial-gradient(ellipse 60% 60% at 20% 80%, #1c1060 0%, transparent 65%),
|
|
770
|
+
#0d0a1e;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
.ui-hero--gradient--blue {
|
|
774
|
+
background:
|
|
775
|
+
radial-gradient(ellipse 70% 80% at 75% 50%, #0e2d6b 0%, #071840 45%, transparent 75%),
|
|
776
|
+
radial-gradient(ellipse 60% 60% at 20% 80%, #0a1f5c 0%, transparent 65%),
|
|
777
|
+
#060d1e;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
.ui-hero--gradient--green {
|
|
781
|
+
background:
|
|
782
|
+
radial-gradient(ellipse 70% 80% at 75% 50%, #0d3d28 0%, #052218 45%, transparent 75%),
|
|
783
|
+
radial-gradient(ellipse 60% 60% at 20% 80%, #083020 0%, transparent 65%),
|
|
784
|
+
#020e09;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
.ui-hero--gradient--rose {
|
|
788
|
+
background:
|
|
789
|
+
radial-gradient(ellipse 70% 80% at 75% 50%, #6b0e35 0%, #400720 45%, transparent 75%),
|
|
790
|
+
radial-gradient(ellipse 60% 60% at 20% 80%, #5c0a2a 0%, transparent 65%),
|
|
791
|
+
#1e0610;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
.ui-hero--gradient--orange {
|
|
795
|
+
background:
|
|
796
|
+
radial-gradient(ellipse 70% 80% at 75% 50%, #6b2e0e 0%, #401807 45%, transparent 75%),
|
|
797
|
+
radial-gradient(ellipse 60% 60% at 20% 80%, #5c2208 0%, transparent 65%),
|
|
798
|
+
#1e0e04;
|
|
799
|
+
}
|
|
764
800
|
|
|
765
|
-
.ui-hero-inner { max-width: 720px; margin: 0 auto; }
|
|
801
|
+
.ui-hero-inner { max-width: 720px; margin: 0 auto; padding: 0 1.5rem; }
|
|
766
802
|
.ui-hero--left .ui-hero-inner { margin: 0; }
|
|
767
803
|
|
|
768
804
|
.ui-hero-eyebrow {
|
|
@@ -801,8 +837,9 @@ hr.ui-divider {
|
|
|
801
837
|
/* Split (text + image) */
|
|
802
838
|
.ui-hero--split { text-align: left; }
|
|
803
839
|
.ui-hero--split .ui-hero-inner {
|
|
804
|
-
max-width:
|
|
840
|
+
max-width: 1100px;
|
|
805
841
|
margin: 0 auto;
|
|
842
|
+
padding: 0 1.5rem;
|
|
806
843
|
display: grid;
|
|
807
844
|
grid-template-columns: 1fr 1fr;
|
|
808
845
|
gap: 3rem;
|
|
@@ -1425,7 +1462,8 @@ hr.ui-divider {
|
|
|
1425
1462
|
.ui-media { grid-template-columns: 1fr; }
|
|
1426
1463
|
.ui-media--reverse { direction: ltr; }
|
|
1427
1464
|
|
|
1428
|
-
.ui-hero { padding: 3rem
|
|
1465
|
+
.ui-hero { padding: 3rem 0; }
|
|
1466
|
+
.ui-hero-inner { padding: 0 1.25rem; }
|
|
1429
1467
|
.ui-hero-title { font-size: 2rem; }
|
|
1430
1468
|
.ui-hero-subtitle { font-size: 1rem; }
|
|
1431
1469
|
.ui-hero--split .ui-hero-inner { grid-template-columns: 1fr; }
|
|
@@ -1767,6 +1805,88 @@ hr.ui-divider {
|
|
|
1767
1805
|
|
|
1768
1806
|
.ui-code-window-code { font-family: inherit; }
|
|
1769
1807
|
|
|
1808
|
+
/* ─── Phone Frame ────────────────────────────────────────────────────────────
|
|
1809
|
+
Realistic CSS mobile phone frame — body, dynamic island, side buttons,
|
|
1810
|
+
home indicator. Drop any HTML into the content slot. */
|
|
1811
|
+
.ui-phone {
|
|
1812
|
+
position: relative;
|
|
1813
|
+
display: inline-flex;
|
|
1814
|
+
width: 260px;
|
|
1815
|
+
background: linear-gradient(160deg, #1e1e28 0%, #141418 100%);
|
|
1816
|
+
border-radius: 3rem;
|
|
1817
|
+
border: 1.5px solid rgba(255, 255, 255, 0.1);
|
|
1818
|
+
box-shadow:
|
|
1819
|
+
0 0 0 1px rgba(0, 0, 0, 0.8),
|
|
1820
|
+
0 2px 0 1px rgba(255, 255, 255, 0.05) inset,
|
|
1821
|
+
0 48px 96px rgba(0, 0, 0, 0.55),
|
|
1822
|
+
0 12px 32px rgba(0, 0, 0, 0.4);
|
|
1823
|
+
padding: 12px;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
/* Side buttons */
|
|
1827
|
+
.ui-phone-btn {
|
|
1828
|
+
position: absolute;
|
|
1829
|
+
background: linear-gradient(90deg, #1a1a22 0%, #26262e 100%);
|
|
1830
|
+
border-radius: 2px;
|
|
1831
|
+
}
|
|
1832
|
+
.ui-phone-btn--mute { left: -3px; top: 88px; width: 3px; height: 28px; }
|
|
1833
|
+
.ui-phone-btn--vol-up { left: -3px; top: 128px; width: 3px; height: 48px; }
|
|
1834
|
+
.ui-phone-btn--vol-down { left: -3px; top: 186px; width: 3px; height: 48px; }
|
|
1835
|
+
.ui-phone-btn--power { right: -3px; top: 140px; width: 3px; height: 64px; }
|
|
1836
|
+
|
|
1837
|
+
/* Phone inner — holds screen + chrome */
|
|
1838
|
+
.ui-phone-inner {
|
|
1839
|
+
width: 100%;
|
|
1840
|
+
background: #000;
|
|
1841
|
+
border-radius: 2.5rem;
|
|
1842
|
+
overflow: hidden;
|
|
1843
|
+
display: flex;
|
|
1844
|
+
flex-direction: column;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
/* Dynamic island */
|
|
1848
|
+
.ui-phone-island {
|
|
1849
|
+
flex-shrink: 0;
|
|
1850
|
+
display: flex;
|
|
1851
|
+
align-items: center;
|
|
1852
|
+
justify-content: center;
|
|
1853
|
+
height: 32px;
|
|
1854
|
+
background: #000;
|
|
1855
|
+
}
|
|
1856
|
+
.ui-phone-camera {
|
|
1857
|
+
display: block;
|
|
1858
|
+
width: 10px;
|
|
1859
|
+
height: 10px;
|
|
1860
|
+
background: radial-gradient(circle at 35% 35%, #2a2a3a, #0a0a0a);
|
|
1861
|
+
border-radius: 50%;
|
|
1862
|
+
box-shadow: 0 0 0 1px rgba(255,255,255,0.06);
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
/* Screen */
|
|
1866
|
+
.ui-phone-screen {
|
|
1867
|
+
flex: 1;
|
|
1868
|
+
min-height: 440px;
|
|
1869
|
+
overflow: hidden;
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
/* Home indicator */
|
|
1873
|
+
.ui-phone-home {
|
|
1874
|
+
flex-shrink: 0;
|
|
1875
|
+
display: flex;
|
|
1876
|
+
align-items: center;
|
|
1877
|
+
justify-content: center;
|
|
1878
|
+
height: 24px;
|
|
1879
|
+
background: #000;
|
|
1880
|
+
}
|
|
1881
|
+
.ui-phone-home::after {
|
|
1882
|
+
content: '';
|
|
1883
|
+
display: block;
|
|
1884
|
+
width: 80px;
|
|
1885
|
+
height: 4px;
|
|
1886
|
+
background: rgba(255, 255, 255, 0.25);
|
|
1887
|
+
border-radius: 2px;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1770
1890
|
/* ─── Footer ─────────────────────────────────────────────────────────────────
|
|
1771
1891
|
Site footer — logo, nav links, legal text. Stacks on mobile. */
|
|
1772
1892
|
.ui-footer {
|
|
@@ -1789,6 +1909,9 @@ hr.ui-divider {
|
|
|
1789
1909
|
color: var(--ui-text);
|
|
1790
1910
|
text-decoration: none;
|
|
1791
1911
|
flex-shrink: 0;
|
|
1912
|
+
display: flex;
|
|
1913
|
+
align-items: center;
|
|
1914
|
+
gap: .5rem;
|
|
1792
1915
|
transition: color .15s;
|
|
1793
1916
|
}
|
|
1794
1917
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/* ─── Lumio brand theme ───────────────────────────────────────────────────────
|
|
2
|
+
Palette tokens + light-theme overrides for the Lumio mobile-app template.
|
|
3
|
+
Load after pulse-ui.css so these override the framework defaults. */
|
|
4
|
+
|
|
5
|
+
/* Palette tokens — available in any rule */
|
|
6
|
+
:root {
|
|
7
|
+
--cotton-rose: #e5c1bd;
|
|
8
|
+
--bone: #d2d0ba;
|
|
9
|
+
--dry-sage: #b6be9c;
|
|
10
|
+
--muted-teal: #7b9e87;
|
|
11
|
+
--blue-slate: #5e747f;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Override light-theme semantic tokens */
|
|
15
|
+
[data-theme="light"],
|
|
16
|
+
.ui-theme-light {
|
|
17
|
+
--ui-bg: #f7f4ed;
|
|
18
|
+
--ui-surface: #edeae0;
|
|
19
|
+
--ui-surface-2: #e4e1d6;
|
|
20
|
+
--ui-border: #cbc8b8;
|
|
21
|
+
/* Deep teal — darker than --muted-teal so it passes WCAG AA on both bg surfaces */
|
|
22
|
+
--ui-accent: #3d6650;
|
|
23
|
+
--ui-accent-hover: #2e5040;
|
|
24
|
+
--ui-accent-dim: rgba(61, 102, 80, 0.14);
|
|
25
|
+
--ui-accent-text: #ffffff;
|
|
26
|
+
--ui-muted: #5a574e;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Nav: frosted glass effect */
|
|
30
|
+
[data-theme="light"] .ui-nav {
|
|
31
|
+
backdrop-filter: blur(14px);
|
|
32
|
+
-webkit-backdrop-filter: blur(14px);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Mobile menu: dark text on light dropdown background */
|
|
36
|
+
[data-theme="light"] .ui-nav-mobile .ui-nav-link,
|
|
37
|
+
[data-theme="light"] .ui-nav-mobile .ui-nav-mega-item-label,
|
|
38
|
+
[data-theme="light"] .ui-nav-mobile-section {
|
|
39
|
+
color: var(--ui-text);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Pricing: show the Pro (highlighted) card first on mobile */
|
|
43
|
+
@media (max-width: 639px) {
|
|
44
|
+
#pricing .ui-grid > div:nth-child(2) { order: -1; }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Primary buttons: white text on the deep teal accent background (6.5:1) */
|
|
48
|
+
[data-theme="light"] .ui-btn--primary {
|
|
49
|
+
color: #fff;
|
|
50
|
+
}
|
package/docs/server.js
CHANGED
|
@@ -100,8 +100,12 @@ await createServer(
|
|
|
100
100
|
new URL('./src/pages/components/feature.js', import.meta.url),
|
|
101
101
|
new URL('./src/pages/components/hero.js', import.meta.url),
|
|
102
102
|
new URL('./src/pages/components/nav.js', import.meta.url),
|
|
103
|
+
new URL('./src/pages/components/phone-frame.js', import.meta.url),
|
|
103
104
|
new URL('./src/pages/components/pricing.js', import.meta.url),
|
|
104
105
|
new URL('./src/pages/components/testimonial.js', import.meta.url),
|
|
106
|
+
// Templates
|
|
107
|
+
new URL('./src/pages/templates.js', import.meta.url),
|
|
108
|
+
new URL('./src/pages/templates/mobile-app.js', import.meta.url),
|
|
105
109
|
// Layout Components
|
|
106
110
|
new URL('./src/pages/components/banner.js', import.meta.url),
|
|
107
111
|
new URL('./src/pages/components/cluster.js', import.meta.url),
|
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
import { renderLayout, h1, lead, codeBlock } from './layout.js'
|
|
9
9
|
import { highlight } from './highlight.js'
|
|
10
10
|
|
|
11
|
-
export function demo(previewHtml, codeStr, { col = false, scroll = false } = {}) {
|
|
11
|
+
export function demo(previewHtml, codeStr, { col = false, scroll = false, bleed = false } = {}) {
|
|
12
12
|
const previewClass = [
|
|
13
13
|
'demo-preview',
|
|
14
14
|
col ? 'demo-preview--col' : '',
|
|
15
15
|
scroll ? 'demo-preview--scroll' : '',
|
|
16
|
+
bleed ? 'demo-preview--bleed' : '',
|
|
16
17
|
].filter(Boolean).join(' ')
|
|
17
18
|
const toggle = `<button class="demo-theme-toggle" aria-label="Toggle light/dark theme" title="Toggle theme">
|
|
18
19
|
<svg class="demo-theme-toggle__dark" width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M21 12.79A9 9 0 1111.21 3a7 7 0 109.79 9.79z"/></svg>
|
package/docs/src/lib/nav.js
CHANGED
|
@@ -121,6 +121,7 @@ export const NAV = [
|
|
|
121
121
|
{ label: 'Feature', href: '/components/feature' },
|
|
122
122
|
{ label: 'Hero', href: '/components/hero' },
|
|
123
123
|
{ label: 'Nav', href: '/components/nav' },
|
|
124
|
+
{ label: 'Phone Frame', href: '/components/phone-frame' },
|
|
124
125
|
{ label: 'Pricing', href: '/components/pricing' },
|
|
125
126
|
{ label: 'Testimonial', href: '/components/testimonial' },
|
|
126
127
|
],
|
|
@@ -148,6 +149,13 @@ export const NAV = [
|
|
|
148
149
|
{ label: 'Payments (Stripe)', href: '/stripe' },
|
|
149
150
|
],
|
|
150
151
|
},
|
|
152
|
+
{
|
|
153
|
+
section: 'Templates',
|
|
154
|
+
items: [
|
|
155
|
+
{ label: 'All Templates', href: '/templates' },
|
|
156
|
+
{ label: 'Mobile App Landing', href: '/templates/mobile-app' },
|
|
157
|
+
],
|
|
158
|
+
},
|
|
151
159
|
]
|
|
152
160
|
|
|
153
161
|
export function prevNext(currentHref) {
|
|
@@ -51,7 +51,8 @@ export default {
|
|
|
51
51
|
subtitle: 'Beautifully simple. Ridiculously fast.',
|
|
52
52
|
actions: appBadge({ store: 'apple', href: appStoreUrl }) +
|
|
53
53
|
appBadge({ store: 'google', href: playStoreUrl }),
|
|
54
|
-
})
|
|
54
|
+
})`,
|
|
55
|
+
{ bleed: true }
|
|
55
56
|
)}
|
|
56
57
|
|
|
57
58
|
${demo(
|
|
@@ -69,7 +70,8 @@ export default {
|
|
|
69
70
|
actions: appBadge({ store: 'apple', href: appStoreUrl }) +
|
|
70
71
|
appBadge({ store: 'google', href: playStoreUrl }),
|
|
71
72
|
image: \`<img src="\${screenshotUrl}" alt="Product screenshot">\`,
|
|
72
|
-
})
|
|
73
|
+
})`,
|
|
74
|
+
{ bleed: true }
|
|
73
75
|
)}
|
|
74
76
|
|
|
75
77
|
${demo(
|
|
@@ -86,7 +88,31 @@ export default {
|
|
|
86
88
|
subtitle: 'Set imageAlign to swap sides.',
|
|
87
89
|
image: \`<img src="\${screenshotUrl}" alt="Product screenshot">\`,
|
|
88
90
|
imageAlign: 'left',
|
|
89
|
-
})
|
|
91
|
+
})`,
|
|
92
|
+
{ bleed: true }
|
|
93
|
+
)}
|
|
94
|
+
|
|
95
|
+
${demo(
|
|
96
|
+
`<div style="display:flex;flex-direction:column;gap:0">` +
|
|
97
|
+
hero({ eyebrow: 'Purple (default)', title: 'gradient: true', gradient: 'purple', size: 'sm' }) +
|
|
98
|
+
hero({ eyebrow: 'Blue', title: "gradient: 'blue'", gradient: 'blue', size: 'sm' }) +
|
|
99
|
+
hero({ eyebrow: 'Green', title: "gradient: 'green'", gradient: 'green', size: 'sm' }) +
|
|
100
|
+
hero({ eyebrow: 'Rose', title: "gradient: 'rose'", gradient: 'rose', size: 'sm' }) +
|
|
101
|
+
hero({ eyebrow: 'Orange', title: "gradient: 'orange'", gradient: 'orange', size: 'sm' }) +
|
|
102
|
+
`</div>`,
|
|
103
|
+
`hero({ title: 'Headline', gradient: 'purple' }) // or 'blue' | 'green' | 'rose' | 'orange'`,
|
|
104
|
+
{ bleed: true }
|
|
105
|
+
)}
|
|
106
|
+
|
|
107
|
+
${demo(
|
|
108
|
+
`<div style="display:flex;flex-direction:column;gap:0">` +
|
|
109
|
+
hero({ eyebrow: 'Solid colour', title: 'background: color', background: '#0f172a', size: 'sm' }) +
|
|
110
|
+
hero({ eyebrow: 'Custom gradient', title: 'background: CSS gradient', background: 'linear-gradient(135deg, #1e1b4b 0%, #312e81 50%, #1e1b4b 100%)', size: 'sm' }) +
|
|
111
|
+
`</div>`,
|
|
112
|
+
`// Any valid CSS background value
|
|
113
|
+
hero({ title: 'Headline', background: '#0f172a' })
|
|
114
|
+
hero({ title: 'Headline', background: 'linear-gradient(135deg, #1e1b4b, #312e81)' })`,
|
|
115
|
+
{ bleed: true }
|
|
90
116
|
)}
|
|
91
117
|
|
|
92
118
|
${demo(
|
|
@@ -99,7 +125,8 @@ export default {
|
|
|
99
125
|
title: 'Blog',
|
|
100
126
|
subtitle: 'Thoughts on building for the web.',
|
|
101
127
|
size: 'sm',
|
|
102
|
-
})
|
|
128
|
+
})`,
|
|
129
|
+
{ bleed: true }
|
|
103
130
|
)}
|
|
104
131
|
|
|
105
132
|
${table(
|
|
@@ -113,6 +140,8 @@ export default {
|
|
|
113
140
|
['<code>imageAlign</code>', 'string', "'right'", "'right' (text left) or 'left' (text right) — only applies when image is set"],
|
|
114
141
|
['<code>align</code>', 'string', "'center'", "'center' or 'left' — text alignment when no image"],
|
|
115
142
|
['<code>size</code>', 'string', "'md'", "'md' (5rem padding) or 'sm' (2.5rem top, no bottom) — use sm for inner-page headers"],
|
|
143
|
+
['<code>gradient</code>', "boolean | 'purple' | 'blue' | 'green' | 'rose' | 'orange'", 'false', 'Named gradient preset. true and \'purple\' are identical.'],
|
|
144
|
+
['<code>background</code>', 'string', '—', 'Any CSS background value — solid colour or custom gradient. Overrides gradient preset.'],
|
|
116
145
|
]
|
|
117
146
|
)}
|
|
118
147
|
`,
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
iconHandPointUp, iconHandPointDown, iconHandPointLeft, iconHandPointRight,
|
|
20
20
|
iconGlobe, iconShield, iconZap, iconTrendingUp, iconTrendingDown, iconLoader, iconGrid, iconBug,
|
|
21
21
|
iconMapPin, iconSun, iconMoon,
|
|
22
|
+
iconFeather, iconAi, iconSmile, iconBarChart,
|
|
22
23
|
iconShoppingCart, iconShoppingBag, iconCreditCard, iconPackage, iconGift,
|
|
23
24
|
iconWallet, iconTruck, iconReceipt, iconStore, iconPercent, iconTicket, iconBanknote,
|
|
24
25
|
iconUtensils, iconCoffee, iconPizza, iconApple, iconCarrot,
|
|
@@ -52,6 +53,11 @@ const ICONS = [
|
|
|
52
53
|
{ name: 'iconAlertCircle', fn: iconAlertCircle },
|
|
53
54
|
{ name: 'iconAlertTriangle', fn: iconAlertTriangle },
|
|
54
55
|
{ name: 'iconInfo', fn: iconInfo },
|
|
56
|
+
{ name: 'iconSmile', fn: iconSmile },
|
|
57
|
+
]},
|
|
58
|
+
{ group: 'AI & Data', icons: [
|
|
59
|
+
{ name: 'iconAi', fn: iconAi },
|
|
60
|
+
{ name: 'iconBarChart', fn: iconBarChart },
|
|
55
61
|
]},
|
|
56
62
|
{ group: 'Actions', icons: [
|
|
57
63
|
{ name: 'iconPlus', fn: iconPlus },
|
|
@@ -87,6 +93,7 @@ const ICONS = [
|
|
|
87
93
|
{ name: 'iconLogIn', fn: iconLogIn },
|
|
88
94
|
]},
|
|
89
95
|
{ group: 'Content & Files', icons: [
|
|
96
|
+
{ name: 'iconFeather', fn: iconFeather },
|
|
90
97
|
{ name: 'iconFile', fn: iconFile },
|
|
91
98
|
{ name: 'iconImage', fn: iconImage },
|
|
92
99
|
{ name: 'iconLink', fn: iconLink },
|
|
@@ -238,21 +245,7 @@ function iconModals_() {
|
|
|
238
245
|
return ICONS.flatMap(({ icons }) => icons).map(iconPreviewModal).join('')
|
|
239
246
|
}
|
|
240
247
|
|
|
241
|
-
|
|
242
|
-
route: '/components/icons',
|
|
243
|
-
meta: {
|
|
244
|
-
title: 'Icons — Pulse Docs',
|
|
245
|
-
description: 'Built-in icon set for Pulse UI.',
|
|
246
|
-
styles: ['/pulse-ui.css', '/docs.css'],
|
|
247
|
-
},
|
|
248
|
-
view: () => renderComponentPage({
|
|
249
|
-
currentHref: '/components/icons',
|
|
250
|
-
prev,
|
|
251
|
-
next,
|
|
252
|
-
name: 'icons',
|
|
253
|
-
description: '100 curated icons. All are pure functions returning an SVG string — no external library, no DOM dependency, tree-shakeable. Style: 24×24 viewBox, <code>stroke="currentColor"</code>, compatible with any colour token.',
|
|
254
|
-
content: `
|
|
255
|
-
|
|
248
|
+
const pageContent = `
|
|
256
249
|
<h2 class="doc-h2" id="usage">Usage</h2>
|
|
257
250
|
<p>Import the icon functions you need alongside other components. Call each as a function — optionally pass <code>size</code> and <code>class</code>.</p>
|
|
258
251
|
${demo(
|
|
@@ -370,8 +363,6 @@ feature({ icon: iconCode({ size: 22, bg: 'square', bgColor: 'muted' }), titl
|
|
|
370
363
|
|
|
371
364
|
${iconGrid_()}
|
|
372
365
|
|
|
373
|
-
${iconModals_()}
|
|
374
|
-
|
|
375
366
|
${table(
|
|
376
367
|
['Prop', 'Type', 'Default', 'Description'],
|
|
377
368
|
[
|
|
@@ -383,6 +374,33 @@ feature({ icon: iconCode({ size: 22, bg: 'square', bgColor: 'muted' }), titl
|
|
|
383
374
|
)}
|
|
384
375
|
|
|
385
376
|
<p style="margin-top:2rem;font-size:.8rem;color:var(--muted)">SVG paths derived from <a href="https://lucide.dev" target="_blank" rel="noopener noreferrer">Lucide</a> (ISC License, Copyright © 2022 Lucide Contributors) and <a href="https://feathericons.com" target="_blank" rel="noopener noreferrer">Feather Icons</a> (MIT License, Copyright © 2013–2017 Cole Bemis). Phosphor hand-pointer icons MIT License, Copyright © 2020 Phosphor Icons.</p>
|
|
386
|
-
|
|
387
|
-
|
|
377
|
+
`
|
|
378
|
+
|
|
379
|
+
export default {
|
|
380
|
+
route: '/components/icons',
|
|
381
|
+
meta: {
|
|
382
|
+
title: 'Icons — Pulse Docs',
|
|
383
|
+
description: 'Built-in icon set for Pulse UI.',
|
|
384
|
+
styles: ['/pulse-ui.css', '/docs.css'],
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
// Shell: all visible content (nav, examples, icon grid) — sends immediately.
|
|
388
|
+
// Modals: 100 hidden <dialog> elements — streamed after. Users never see them
|
|
389
|
+
// arrive since dialogs are display:none until opened.
|
|
390
|
+
stream: {
|
|
391
|
+
shell: ['page'],
|
|
392
|
+
deferred: ['modals'],
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
view: {
|
|
396
|
+
page: () => renderComponentPage({
|
|
397
|
+
currentHref: '/components/icons',
|
|
398
|
+
prev,
|
|
399
|
+
next,
|
|
400
|
+
name: 'icons',
|
|
401
|
+
description: '100 curated icons. All are pure functions returning an SVG string — no external library, no DOM dependency, tree-shakeable. Style: 24×24 viewBox, <code>stroke="currentColor"</code>, compatible with any colour token.',
|
|
402
|
+
content: pageContent,
|
|
403
|
+
}),
|
|
404
|
+
modals: () => iconModals_(),
|
|
405
|
+
},
|
|
388
406
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
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 { phoneFrame } from '../../../../src/ui/index.js'
|
|
5
|
+
|
|
6
|
+
const { prev, next } = prevNext('/components/phone-frame')
|
|
7
|
+
|
|
8
|
+
const sampleScreen = `
|
|
9
|
+
<div style="background:#1a2c24;height:100%;padding:0.75rem;display:flex;flex-direction:column;gap:0.625rem;font-family:system-ui,sans-serif;">
|
|
10
|
+
<div style="display:flex;justify-content:space-between;align-items:center;padding:0 0.25rem;font-size:0.6rem;color:rgba(255,255,255,0.5)">
|
|
11
|
+
<span>9:41</span>
|
|
12
|
+
<svg width="20" height="8" viewBox="0 0 20 8" fill="none" aria-hidden="true"><rect x="0.5" y="0.5" width="16" height="7" rx="2" stroke="rgba(255,255,255,0.35)" stroke-width="1"/><rect x="1.5" y="1.5" width="12" height="5" rx="1" fill="rgba(255,255,255,0.7)"/><path d="M18 3v2c.8-.3.8-1.7 0-2z" fill="rgba(255,255,255,0.6)"/></svg>
|
|
13
|
+
</div>
|
|
14
|
+
<div style="font-size:0.85rem;font-weight:700;color:#fff">Good morning, Sarah</div>
|
|
15
|
+
<div style="background:rgba(255,255,255,0.06);border-radius:0.625rem;padding:0.625rem;flex:1">
|
|
16
|
+
<div style="font-size:0.6rem;color:rgba(255,255,255,0.5);margin-bottom:0.3rem">TODAY'S ENTRY</div>
|
|
17
|
+
<div style="font-size:0.72rem;color:rgba(255,255,255,0.85);line-height:1.5">Feeling grateful today. The early morning light…</div>
|
|
18
|
+
</div>
|
|
19
|
+
</div>`
|
|
20
|
+
|
|
21
|
+
export default {
|
|
22
|
+
route: '/components/phone-frame',
|
|
23
|
+
meta: {
|
|
24
|
+
title: 'Phone Frame — Pulse Docs',
|
|
25
|
+
description: 'Phone Frame component for Pulse UI.',
|
|
26
|
+
styles: ['/pulse-ui.css', '/docs.css'],
|
|
27
|
+
},
|
|
28
|
+
state: {},
|
|
29
|
+
view: () => renderComponentPage({
|
|
30
|
+
currentHref: '/components/phone-frame',
|
|
31
|
+
prev,
|
|
32
|
+
next,
|
|
33
|
+
name: 'phoneFrame',
|
|
34
|
+
description: 'A realistic CSS mobile phone frame — device chrome, dynamic island, side buttons, and home indicator. Pass arbitrary HTML into the <code>content</code> slot to fill the screen area. No images, pure CSS/HTML.',
|
|
35
|
+
content: `
|
|
36
|
+
${demo(
|
|
37
|
+
phoneFrame({ content: sampleScreen }),
|
|
38
|
+
`phoneFrame({
|
|
39
|
+
content: \`<div style="background:#1a2c24;height:100%;padding:0.75rem;">
|
|
40
|
+
<!-- your app screen UI -->
|
|
41
|
+
</div>\`,
|
|
42
|
+
})`,
|
|
43
|
+
)}
|
|
44
|
+
|
|
45
|
+
${table(
|
|
46
|
+
['Prop', 'Type', 'Default', 'Description'],
|
|
47
|
+
[
|
|
48
|
+
['<code>content</code>', 'string (HTML)', "''", 'HTML rendered inside the screen area'],
|
|
49
|
+
['<code>class</code>', 'string', "''", 'Extra CSS classes on the root element'],
|
|
50
|
+
]
|
|
51
|
+
)}
|
|
52
|
+
`,
|
|
53
|
+
}),
|
|
54
|
+
}
|