@invisibleloop/pulse 0.9.1 → 0.10.1

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.
@@ -1 +1 @@
1
- 0.9.0
1
+ 0.9.1
@@ -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
  }
@@ -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, #ffffff);
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 1.5rem; text-align: center; }
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: 1200px;
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 1.25rem; }
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>
@@ -12,7 +12,7 @@ import pkg from '../../../package.json' with { type: 'json' }
12
12
  // Populated by initLayoutManifest() in server.js before the server starts.
13
13
  let _manifest = {}
14
14
  export function initLayoutManifest(m) { _manifest = m }
15
- const asset = href => _manifest[href] || href
15
+ export const asset = href => _manifest[href] || href
16
16
 
17
17
  const version = typeof pkg !== 'undefined' ? (pkg?.version ?? '') : ''
18
18
 
@@ -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 },
@@ -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
+ }