@invisibleloop/pulse 0.10.2 → 0.12.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/img/heic1501a.jpg +0 -0
- package/docs/public/pulse-ui.css +4 -7
- package/docs/public/themes/observatory.css +103 -0
- package/docs/server.js +1 -0
- package/docs/src/lib/nav.js +1 -0
- package/docs/src/pages/components/icons.js +4 -1
- package/docs/src/pages/templates/blog-post.js +226 -0
- package/package.json +1 -1
- package/public/.pulse-ui-version +1 -1
- package/src/agent/guide-templates.md +246 -0
- package/src/mcp/server.js +10 -1
- package/src/ui/icons.js +5 -1
- package/src/ui/index.js +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.10.2
|
|
Binary file
|
package/docs/public/pulse-ui.css
CHANGED
|
@@ -759,7 +759,7 @@ hr.ui-divider {
|
|
|
759
759
|
}
|
|
760
760
|
|
|
761
761
|
/* ─── Hero ───────────────────────────────────────────────────────────────── */
|
|
762
|
-
.ui-hero { padding: 5rem
|
|
762
|
+
.ui-hero { padding: 5rem 1.5rem; text-align: center; }
|
|
763
763
|
.ui-hero--left { text-align: left; }
|
|
764
764
|
.ui-hero--sm { padding-top: 2.5rem; padding-bottom: 0; }
|
|
765
765
|
.ui-hero--gradient,
|
|
@@ -798,7 +798,7 @@ hr.ui-divider {
|
|
|
798
798
|
#1e0e04;
|
|
799
799
|
}
|
|
800
800
|
|
|
801
|
-
.ui-hero-inner { max-width: 720px; margin: 0 auto;
|
|
801
|
+
.ui-hero-inner { max-width: 720px; margin: 0 auto; }
|
|
802
802
|
.ui-hero--left .ui-hero-inner { margin: 0; }
|
|
803
803
|
|
|
804
804
|
.ui-hero-eyebrow {
|
|
@@ -837,9 +837,8 @@ hr.ui-divider {
|
|
|
837
837
|
/* Split (text + image) */
|
|
838
838
|
.ui-hero--split { text-align: left; }
|
|
839
839
|
.ui-hero--split .ui-hero-inner {
|
|
840
|
-
max-width:
|
|
840
|
+
max-width: 1200px;
|
|
841
841
|
margin: 0 auto;
|
|
842
|
-
padding: 0 1.5rem;
|
|
843
842
|
display: grid;
|
|
844
843
|
grid-template-columns: 1fr 1fr;
|
|
845
844
|
gap: 3rem;
|
|
@@ -1462,8 +1461,7 @@ hr.ui-divider {
|
|
|
1462
1461
|
.ui-media { grid-template-columns: 1fr; }
|
|
1463
1462
|
.ui-media--reverse { direction: ltr; }
|
|
1464
1463
|
|
|
1465
|
-
.ui-hero { padding: 3rem
|
|
1466
|
-
.ui-hero-inner { padding: 0 1.25rem; }
|
|
1464
|
+
.ui-hero { padding: 3rem 1.25rem; }
|
|
1467
1465
|
.ui-hero-title { font-size: 2rem; }
|
|
1468
1466
|
.ui-hero-subtitle { font-size: 1rem; }
|
|
1469
1467
|
.ui-hero--split .ui-hero-inner { grid-template-columns: 1fr; }
|
|
@@ -1865,7 +1863,6 @@ hr.ui-divider {
|
|
|
1865
1863
|
/* Screen */
|
|
1866
1864
|
.ui-phone-screen {
|
|
1867
1865
|
flex: 1;
|
|
1868
|
-
min-height: 440px;
|
|
1869
1866
|
overflow: hidden;
|
|
1870
1867
|
}
|
|
1871
1868
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Observatory theme — dark astronomy palette
|
|
3
|
+
*
|
|
4
|
+
* Contrast notes (all checked against WCAG AA 4.5:1 for small text on --ui-bg):
|
|
5
|
+
* --pacific-blue (#53a2be) on --prussian-blue (#0a2239) → 5.29:1 ✓
|
|
6
|
+
* --ocean-blue (#1d84b5) on --prussian-blue (#0a2239) → 3.68:1 (large text / UI only)
|
|
7
|
+
* Default --ui-text (#e2e2ea) on --prussian-blue (#0a2239) ✓ (very high)
|
|
8
|
+
* --ui-muted rgba(180,215,230,0.85) on --prussian-blue (#0a2239) → ~7.4:1 ✓
|
|
9
|
+
* --ui-muted rgba(180,215,230,0.85) on --jet-black (#132e32) → ~6.4:1 ✓
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/* Palette tokens */
|
|
13
|
+
:root {
|
|
14
|
+
--prussian-blue: #0a2239;
|
|
15
|
+
--pacific-blue: #53a2be;
|
|
16
|
+
--ocean-blue: #1d84b5;
|
|
17
|
+
--jet-black: #132e32;
|
|
18
|
+
--baltic-blue: #176087;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/* Dark theme overrides — target :root (dark is the default pulse-ui theme) */
|
|
22
|
+
:root {
|
|
23
|
+
--ui-bg: var(--prussian-blue);
|
|
24
|
+
--ui-surface: var(--jet-black);
|
|
25
|
+
--ui-surface-2: #0d2b38;
|
|
26
|
+
--ui-border: rgba(83, 162, 190, 0.18);
|
|
27
|
+
--ui-accent: var(--pacific-blue);
|
|
28
|
+
--ui-accent-hover:#6bbcd4;
|
|
29
|
+
--ui-accent-dim: rgba(83, 162, 190, 0.12);
|
|
30
|
+
--ui-accent-text: var(--prussian-blue);
|
|
31
|
+
--ui-muted: rgba(180, 215, 230, 0.85);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Reading progress bar — CSS scroll-driven animation, no JS required */
|
|
35
|
+
.obs-reading-progress {
|
|
36
|
+
position: fixed;
|
|
37
|
+
top: 0;
|
|
38
|
+
left: 0;
|
|
39
|
+
width: 100%;
|
|
40
|
+
height: 3px;
|
|
41
|
+
z-index: 10000;
|
|
42
|
+
pointer-events: none;
|
|
43
|
+
}
|
|
44
|
+
.obs-reading-progress-bar {
|
|
45
|
+
height: 100%;
|
|
46
|
+
width: 100%;
|
|
47
|
+
background: var(--ui-accent);
|
|
48
|
+
transform: scaleX(0);
|
|
49
|
+
transform-origin: left;
|
|
50
|
+
animation: obs-reading-progress linear both;
|
|
51
|
+
animation-timeline: scroll(root block);
|
|
52
|
+
}
|
|
53
|
+
@keyframes obs-reading-progress {
|
|
54
|
+
from { transform: scaleX(0); }
|
|
55
|
+
to { transform: scaleX(1); }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* Article hero — restore bottom padding (ui-hero--sm removes it) */
|
|
59
|
+
.ui-hero--sm { padding-bottom: 3rem; }
|
|
60
|
+
|
|
61
|
+
/* Article layout — prose + aside side by side */
|
|
62
|
+
.obs-article {
|
|
63
|
+
display: grid;
|
|
64
|
+
grid-template-columns: 1fr 260px;
|
|
65
|
+
gap: 3rem;
|
|
66
|
+
align-items: start;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@media (max-width: 767px) {
|
|
70
|
+
.obs-article { grid-template-columns: 1fr; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* Aside sections */
|
|
74
|
+
.obs-aside-section {
|
|
75
|
+
margin-bottom: 2rem;
|
|
76
|
+
}
|
|
77
|
+
.obs-aside-section:last-child {
|
|
78
|
+
margin-bottom: 0;
|
|
79
|
+
}
|
|
80
|
+
.obs-aside-nav-link {
|
|
81
|
+
display: block;
|
|
82
|
+
padding: 0.375rem 0;
|
|
83
|
+
font-size: 0.875rem;
|
|
84
|
+
color: var(--ui-muted);
|
|
85
|
+
text-decoration: none;
|
|
86
|
+
border-bottom: 1px solid var(--ui-border);
|
|
87
|
+
transition: color .15s;
|
|
88
|
+
}
|
|
89
|
+
.obs-aside-nav-link:hover { color: var(--ui-text); }
|
|
90
|
+
.obs-aside-nav-link:last-child { border-bottom: none; }
|
|
91
|
+
|
|
92
|
+
/* Nav — frosted glass effect */
|
|
93
|
+
.ui-nav {
|
|
94
|
+
backdrop-filter: blur(14px);
|
|
95
|
+
-webkit-backdrop-filter: blur(14px);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* Mobile nav — ensure readable text on the expanded panel */
|
|
99
|
+
.ui-nav-mobile .ui-nav-link,
|
|
100
|
+
.ui-nav-mobile .ui-nav-mega-item-label,
|
|
101
|
+
.ui-nav-mobile-section {
|
|
102
|
+
color: var(--ui-text);
|
|
103
|
+
}
|
package/docs/server.js
CHANGED
|
@@ -106,6 +106,7 @@ await createServer(
|
|
|
106
106
|
// Templates
|
|
107
107
|
new URL('./src/pages/templates.js', import.meta.url),
|
|
108
108
|
new URL('./src/pages/templates/mobile-app.js', import.meta.url),
|
|
109
|
+
new URL('./src/pages/templates/blog-post.js', import.meta.url),
|
|
109
110
|
// Layout Components
|
|
110
111
|
new URL('./src/pages/components/banner.js', import.meta.url),
|
|
111
112
|
new URL('./src/pages/components/cluster.js', import.meta.url),
|
package/docs/src/lib/nav.js
CHANGED
|
@@ -19,7 +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
|
+
iconFeather, iconTelescope, iconAi, iconSmile, iconBarChart,
|
|
23
23
|
iconShoppingCart, iconShoppingBag, iconCreditCard, iconPackage, iconGift,
|
|
24
24
|
iconWallet, iconTruck, iconReceipt, iconStore, iconPercent, iconTicket, iconBanknote,
|
|
25
25
|
iconUtensils, iconCoffee, iconPizza, iconApple, iconCarrot,
|
|
@@ -59,6 +59,9 @@ const ICONS = [
|
|
|
59
59
|
{ name: 'iconAi', fn: iconAi },
|
|
60
60
|
{ name: 'iconBarChart', fn: iconBarChart },
|
|
61
61
|
]},
|
|
62
|
+
{ group: 'Astronomy', icons: [
|
|
63
|
+
{ name: 'iconTelescope', fn: iconTelescope },
|
|
64
|
+
]},
|
|
62
65
|
{ group: 'Actions', icons: [
|
|
63
66
|
{ name: 'iconPlus', fn: iconPlus },
|
|
64
67
|
{ name: 'iconMinus', fn: iconMinus },
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template: Blog Post — Observatory
|
|
3
|
+
*
|
|
4
|
+
* A full blog post page for an astronomy publication.
|
|
5
|
+
* Includes sticky nav, article hero, prose body, newsletter signup, and footer.
|
|
6
|
+
* Dark theme using the Observatory palette (prussian blue / pacific blue).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { asset } from '../../lib/layout.js'
|
|
10
|
+
import {
|
|
11
|
+
nav as uiNav,
|
|
12
|
+
hero,
|
|
13
|
+
section,
|
|
14
|
+
container,
|
|
15
|
+
prose,
|
|
16
|
+
heading,
|
|
17
|
+
card,
|
|
18
|
+
input,
|
|
19
|
+
button,
|
|
20
|
+
footer,
|
|
21
|
+
iconTelescope,
|
|
22
|
+
iconCheckCircle,
|
|
23
|
+
iconCalendar,
|
|
24
|
+
iconMapPin,
|
|
25
|
+
} from '../../../../src/ui/index.js'
|
|
26
|
+
|
|
27
|
+
// ── Navigation ───────────────────────────────────���────────────────────────────
|
|
28
|
+
|
|
29
|
+
const pageNav = uiNav({
|
|
30
|
+
logo: `${iconTelescope({ size: 20 })}<span class="u-font-bold" style="letter-spacing:-0.02em">Observatory</span>`,
|
|
31
|
+
links: [
|
|
32
|
+
{ label: 'Articles', href: '#' },
|
|
33
|
+
{ label: 'Topics', href: '#' },
|
|
34
|
+
{ label: 'Archive', href: '#' },
|
|
35
|
+
],
|
|
36
|
+
sticky: true,
|
|
37
|
+
background: 'rgba(10, 34, 57, 0.85)',
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// ── Article hero ──────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
const articleHero = hero({
|
|
43
|
+
eyebrow: 'Deep Sky Objects',
|
|
44
|
+
title: 'The Pillars of Creation: Stellar Nurseries Caught in the Act',
|
|
45
|
+
subtitle: 'Six trillion miles tall and sculpted by the light of newborn stars — the Eagle Nebula\'s most famous feature is one of the most photographed objects in the cosmos. Here\'s what three decades of observation have revealed.',
|
|
46
|
+
actions: `<p class="u-text-sm u-text-muted">By Dr. Elena Vasquez · 14 January 2026 · 9 min read</p>`,
|
|
47
|
+
align: 'center',
|
|
48
|
+
size: 'sm',
|
|
49
|
+
background: 'linear-gradient(160deg, #0a2239 0%, #132e32 60%, #0d2b38 100%)',
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// ── Article body ──────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
const articleBody = `
|
|
55
|
+
<p>In April 1995, astronomers pointed the Hubble Space Telescope at a seemingly unremarkable patch of sky in the constellation Serpens. What they captured would become one of the most iconic images in the history of science: three towering columns of gas and dust, backlit by the fierce radiation of newly-formed stars.</p>
|
|
56
|
+
|
|
57
|
+
<p>These are the Pillars of Creation — a region of active star formation nestled inside the Eagle Nebula, some 6,500 light-years from Earth. The tallest pillar stretches four light-years from base to tip, roughly equivalent to the distance between our Sun and its nearest stellar neighbour, Proxima Centauri.</p>
|
|
58
|
+
|
|
59
|
+
<figure>
|
|
60
|
+
<img
|
|
61
|
+
src="/img/heic1501a.jpg"
|
|
62
|
+
alt="Three towering columns of gas and dust, backlit by brilliant blue stars, rise from a field of glowing hydrogen in the Eagle Nebula — the Pillars of Creation as seen by Hubble."
|
|
63
|
+
loading="lazy">
|
|
64
|
+
<figcaption>The Pillars of Creation, Eagle Nebula (M16) — Hubble Space Telescope, 2015. Credit: NASA, ESA/Hubble and the Hubble Heritage Team</figcaption>
|
|
65
|
+
</figure>
|
|
66
|
+
|
|
67
|
+
<h2 id="what-are-the-pillars">What are the Pillars, exactly?</h2>
|
|
68
|
+
|
|
69
|
+
<p>The Pillars are a type of structure known as an evaporating gaseous globule, or EGG. They form when intense ultraviolet radiation from hot, massive young stars bombards surrounding clouds of molecular hydrogen. The radiation ionises the outer layers of the cloud, causing them to evaporate and stream away. But denser knots of gas resist this erosion, acting as shields that protect the material behind them.</p>
|
|
70
|
+
|
|
71
|
+
<p>The result is a column: a finger of denser gas pointing away from the radiation source, its tip rounded and hardened by the very light trying to destroy it. The pillars we see in the Eagle Nebula are essentially the remnants of a much larger cloud, sculpted over millions of years by the stars that formed within it.</p>
|
|
72
|
+
|
|
73
|
+
<h2 id="nursery-in-destruction">A nursery in the act of destruction</h2>
|
|
74
|
+
|
|
75
|
+
<p>Look closely at the tips of the pillars and you'll see small, dark bumps — protrusions that break away from the column like droplets from a slow-moving waterfall. Each of these is a potential solar system in the making. Inside every bump, gravity is pulling gas and dust into an ever-tighter knot. Given time — another hundred thousand years, perhaps — each one may collapse under its own weight and ignite as a new star.</p>
|
|
76
|
+
|
|
77
|
+
<p>The irony is profound. The very stars that are destroying the pillars through photoevaporation were themselves born from the same cloud. The Pillars of Creation are, simultaneously, a stellar nursery and a funeral pyre.</p>
|
|
78
|
+
|
|
79
|
+
<h2 id="how-spitzer-changed">How Spitzer changed the story</h2>
|
|
80
|
+
|
|
81
|
+
<p>In 2007, the Spitzer Space Telescope — sensitive to infrared light invisible to human eyes — revealed something unexpected. The Eagle Nebula appeared to have already been hit by the shockwave from a supernova explosion. If correct, the pillars we see in visible light may already be gone. Light from their destruction would still be en route to us, not expected to arrive for another thousand years.</p>
|
|
82
|
+
|
|
83
|
+
<p>When it does, the Eagle Nebula will briefly outshine everything in the night sky except the Moon. Then, as the light-years between us absorb the news, it will fade — and one of the most recognisable objects in the cosmos will exist only in our archives.</p>
|
|
84
|
+
|
|
85
|
+
<h2 id="the-webb-revision">The Webb revision</h2>
|
|
86
|
+
|
|
87
|
+
<p>In 2022, the James Webb Space Telescope turned its golden mirror on the Pillars and returned images that made 1995 look like a rough draft. Webb's near-infrared camera penetrated the dust to reveal thousands of previously hidden young stars, some still wrapped in their accretion discs. Orange jets of superheated material punch out from several of the protostars, signatures of the violent magnetic fields that accompany stellar birth.</p>
|
|
88
|
+
|
|
89
|
+
<p>Webb also confirmed that the Pillars are not as serene as they appear. Turbulence ripples through the gas. Temperature gradients steepen where radiation meets denser material. The whole structure is in slow, continuous motion — a three-dimensional sculpture being revised in real time by forces we are only beginning to understand.</p>
|
|
90
|
+
|
|
91
|
+
<h2 id="observing-from-earth">Observing from Earth</h2>
|
|
92
|
+
|
|
93
|
+
<p>The Eagle Nebula is visible from Earth with modest equipment. From mid-northern latitudes, it rises in the southeast during summer evenings and reaches its highest point around midnight in July. A modest telescope at low magnification will reveal the nebulosity surrounding the cluster M16. The pillars themselves, however, require long-exposure imaging — they lie within the brighter core of the nebula and are impossible to resolve visually.</p>
|
|
94
|
+
|
|
95
|
+
<p>If you point a camera at M16 on a dark night and stack several hours of exposures, the pillars will emerge from the noise — dim, dark columns against a field of glowing hydrogen. You'll be photographing light that left the nebula when the Western Roman Empire was still intact. Every image you take is a time machine.</p>
|
|
96
|
+
`
|
|
97
|
+
|
|
98
|
+
// ── Article aside ────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
const articleAside = `
|
|
101
|
+
<aside aria-label="Article sidebar">
|
|
102
|
+
<div class="obs-aside-section">
|
|
103
|
+
<p class="u-text-sm u-font-semibold u-mb-3">In this article</p>
|
|
104
|
+
<nav aria-label="Article sections">
|
|
105
|
+
<a class="obs-aside-nav-link" href="#what-are-the-pillars">What are the Pillars?</a>
|
|
106
|
+
<a class="obs-aside-nav-link" href="#nursery-in-destruction">A nursery in the act of destruction</a>
|
|
107
|
+
<a class="obs-aside-nav-link" href="#how-spitzer-changed">How Spitzer changed the story</a>
|
|
108
|
+
<a class="obs-aside-nav-link" href="#the-webb-revision">The Webb revision</a>
|
|
109
|
+
<a class="obs-aside-nav-link" href="#observing-from-earth">Observing from Earth</a>
|
|
110
|
+
</nav>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="obs-aside-section">
|
|
113
|
+
${card({
|
|
114
|
+
content: `
|
|
115
|
+
<p class="u-text-xs u-font-semibold u-text-accent u-mb-2">UPCOMING EVENT</p>
|
|
116
|
+
${heading({ level: 3, text: 'Dark Sky Night 2026' })}
|
|
117
|
+
<p class="u-text-sm u-text-muted u-mt-2 u-mb-4">A guided tour of the summer sky with expert astronomers. Telescopes provided. Suitable for all ages.</p>
|
|
118
|
+
<div class="u-flex u-flex-col u-gap-1 u-mb-4">
|
|
119
|
+
<span class="u-flex u-items-center u-gap-2 u-text-sm u-text-muted">
|
|
120
|
+
${iconCalendar({ size: 14 })} 19 July 2026
|
|
121
|
+
</span>
|
|
122
|
+
<span class="u-flex u-items-center u-gap-2 u-text-sm u-text-muted">
|
|
123
|
+
${iconMapPin({ size: 14 })} Galloway Forest Park, Scotland
|
|
124
|
+
</span>
|
|
125
|
+
</div>
|
|
126
|
+
${button({ label: 'Book a place', href: '#', size: 'sm', full: true })}
|
|
127
|
+
`,
|
|
128
|
+
})}
|
|
129
|
+
</div>
|
|
130
|
+
</aside>`
|
|
131
|
+
|
|
132
|
+
// ── Newsletter ────────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
const newsletterSection = (state) => section({
|
|
135
|
+
id: 'newsletter',
|
|
136
|
+
variant: 'alt',
|
|
137
|
+
content: container({
|
|
138
|
+
size: 'sm',
|
|
139
|
+
content: `
|
|
140
|
+
<div class="u-text-center">
|
|
141
|
+
${heading({ level: 2, text: 'Never miss a discovery' })}
|
|
142
|
+
<p class="u-text-muted u-mt-2 u-mb-6">Weekly dispatches from the cosmos — deep sky objects, new research, and what to observe this month.</p>
|
|
143
|
+
${state.status === 'success'
|
|
144
|
+
? `<div class="u-flex u-items-center u-justify-center u-gap-2">
|
|
145
|
+
${iconCheckCircle({ size: 20 })}
|
|
146
|
+
<span>You're subscribed. Clear skies ahead.</span>
|
|
147
|
+
</div>`
|
|
148
|
+
: `<form data-action="subscribe" aria-label="Newsletter signup" novalidate>
|
|
149
|
+
<div class="u-flex u-gap-3 u-flex-wrap u-justify-center u-items-end">
|
|
150
|
+
${input({ label: 'Email address', name: 'email', type: 'email', placeholder: 'your@email.com', required: true })}
|
|
151
|
+
${button({
|
|
152
|
+
label: state.status === 'loading' ? 'Subscribing\u2026' : 'Subscribe',
|
|
153
|
+
type: 'submit',
|
|
154
|
+
attrs: state.status === 'loading' ? { 'aria-busy': 'true', disabled: '' } : {},
|
|
155
|
+
})}
|
|
156
|
+
</div>
|
|
157
|
+
</form>`
|
|
158
|
+
}
|
|
159
|
+
</div>
|
|
160
|
+
`,
|
|
161
|
+
}),
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
// ── Footer ────────────────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
const pageFooter = footer({
|
|
167
|
+
logo: `${iconTelescope({ size: 18 })}<span class="u-font-bold" style="letter-spacing:-0.02em">Observatory</span>`,
|
|
168
|
+
logoHref: '/',
|
|
169
|
+
links: [
|
|
170
|
+
{ label: 'About', href: '#' },
|
|
171
|
+
{ label: 'Archive', href: '#' },
|
|
172
|
+
{ label: 'Topics', href: '#' },
|
|
173
|
+
{ label: 'Privacy', href: '#' },
|
|
174
|
+
{ label: 'RSS', href: '#' },
|
|
175
|
+
],
|
|
176
|
+
legal: `\u00a9 ${new Date().getFullYear()} Observatory. All rights reserved.`,
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
// ── Spec ─────────────────────────────────────────────────────────────────��────
|
|
180
|
+
|
|
181
|
+
export default {
|
|
182
|
+
route: '/templates/blog-post',
|
|
183
|
+
|
|
184
|
+
meta: {
|
|
185
|
+
title: 'The Pillars of Creation \u2014 Observatory',
|
|
186
|
+
description: 'Six trillion miles tall and sculpted by the light of newborn stars \u2014 the Eagle Nebula\'s Pillars of Creation remain one of the most studied and photographed objects in the cosmos.',
|
|
187
|
+
styles: ['/pulse-ui.css', '/themes/observatory.css'],
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
state: {
|
|
191
|
+
status: 'idle',
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
actions: {
|
|
195
|
+
subscribe: {
|
|
196
|
+
onStart: (state) => ({ status: 'loading' }),
|
|
197
|
+
run: async (state, serverState, formData) => {
|
|
198
|
+
const email = formData.get('email')
|
|
199
|
+
if (!email) throw new Error('Email address is required')
|
|
200
|
+
// Replace with your email API call
|
|
201
|
+
await new Promise(r => setTimeout(r, 600))
|
|
202
|
+
return { email }
|
|
203
|
+
},
|
|
204
|
+
onSuccess: (state) => ({
|
|
205
|
+
status: 'success',
|
|
206
|
+
_toast: { message: 'Welcome to Observatory! Clear skies ahead.', variant: 'success' },
|
|
207
|
+
}),
|
|
208
|
+
onError: (state, err) => ({
|
|
209
|
+
status: 'error',
|
|
210
|
+
_toast: { message: err.message || 'Something went wrong. Please try again.', variant: 'error' },
|
|
211
|
+
}),
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
view: (state) => `
|
|
216
|
+
<a href="#main-content" class="skip-link" style="position:absolute;left:-9999px;top:1rem;z-index:9999;padding:0.5rem 1rem;background:var(--ui-accent);color:var(--ui-accent-text);border-radius:0.25rem;text-decoration:none">Skip to main content</a>
|
|
217
|
+
<div class="obs-reading-progress" aria-hidden="true"><div class="obs-reading-progress-bar"></div></div>
|
|
218
|
+
${pageNav}
|
|
219
|
+
<main id="main-content">
|
|
220
|
+
${articleHero}
|
|
221
|
+
${section({ content: container({ content: `<div class="obs-article">${prose({ content: articleBody })}${articleAside}</div>` }) })}
|
|
222
|
+
${newsletterSection(state)}
|
|
223
|
+
</main>
|
|
224
|
+
${pageFooter}
|
|
225
|
+
<script src="${asset('/pulse-ui.js')}" defer></script>`,
|
|
226
|
+
}
|
package/package.json
CHANGED
package/public/.pulse-ui-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.12.0
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# Pulse Templates — Scaffold Guide
|
|
2
|
+
|
|
3
|
+
Templates are complete, production-ready page specs you can adapt for a new brand. Always start from a template when the user's request matches one — do not build from scratch.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When to use a template
|
|
8
|
+
|
|
9
|
+
Read the user's request and match against the triggers below. If it matches, use the template.
|
|
10
|
+
|
|
11
|
+
| Template | Route | Triggers |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| Mobile App Landing | `docs/src/pages/templates/mobile-app.js` | "landing page for a mobile app", "app marketing site", "app homepage", "SaaS landing page" |
|
|
14
|
+
| Blog Post | `docs/src/pages/templates/blog-post.js` | "blog post", "article page", "publication", "editorial site", "newsletter with blog" |
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Before writing any code — mandatory pre-build questions
|
|
19
|
+
|
|
20
|
+
Ask these **before** presenting a plan. Do not skip them.
|
|
21
|
+
|
|
22
|
+
1. **Colour palette** — "Do you have a colour palette, or should I choose one?"
|
|
23
|
+
2. **Brand fonts** — "Any brand fonts, or system-ui is fine?"
|
|
24
|
+
3. **Theme** — "Light or dark theme preference?"
|
|
25
|
+
|
|
26
|
+
Record the answers. If the user provides palette colours, note which are light/mid/dark toned so you can plan contrast strategy before writing any CSS.
|
|
27
|
+
|
|
28
|
+
**Why this matters:** Building with placeholder colours and retrofitting a palette requires rebuilding components, fixing WCAG contrast failures, and touching multiple files. Getting the palette upfront avoids that loop entirely.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Mobile App Landing Page
|
|
33
|
+
|
|
34
|
+
### What it includes
|
|
35
|
+
|
|
36
|
+
| Section | Component(s) |
|
|
37
|
+
|---|---|
|
|
38
|
+
| Sticky nav | `nav` with logo, links, CTA button |
|
|
39
|
+
| Hero | `hero` (split layout) with `phoneFrame` mockup |
|
|
40
|
+
| Stats bar | `stat` × 4 in a `grid` |
|
|
41
|
+
| Features | `feature` × 6 in a `grid` |
|
|
42
|
+
| Testimonials | `testimonial` × 3 in a `grid` |
|
|
43
|
+
| Pricing | `pricing` × 3 (Free / Pro / Family) in a `grid` |
|
|
44
|
+
| FAQ | `accordion` |
|
|
45
|
+
| Download CTA | `cta` with `appBadge` |
|
|
46
|
+
| Footer | `footer` with logo, links, legal |
|
|
47
|
+
|
|
48
|
+
### Files to create
|
|
49
|
+
|
|
50
|
+
| File | Purpose |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `src/pages/[name].js` | The page spec — copy and adapt the mobile-app template |
|
|
53
|
+
| `public/themes/[name].css` | Brand theme — palette tokens + light-theme overrides |
|
|
54
|
+
|
|
55
|
+
### Adapting the template
|
|
56
|
+
|
|
57
|
+
1. **Brand name** — replace every instance of "Lumio" with the new app name.
|
|
58
|
+
2. **Copy** — update eyebrows, titles, subtitles, feature descriptions, testimonials, pricing names/features, FAQ items, and footer links to match the new product.
|
|
59
|
+
3. **Phone screen** — the `phoneScreen` HTML inside `phoneFrame` is a bespoke mockup. Update colours, labels, and data to match the new brand. Inline styles inside `phoneScreen` are intentional — the mockup uses sub-utility-scale sizes that utility classes cannot express.
|
|
60
|
+
4. **Colour palette** — create `public/themes/[name].css` (see CSS rules below).
|
|
61
|
+
5. **Icons** — the logo uses `iconFeather` for Lumio. Choose an appropriate icon from `src/ui/icons.js` or add a new one if nothing fits.
|
|
62
|
+
6. **Route** — set `route: '/[name]'` or `route: '/'` as appropriate.
|
|
63
|
+
7. **`pulse-ui.js`** — always load via `asset('/pulse-ui.js')` (import `asset` from the docs layout lib). Never hardcode `/pulse-ui.js` — the unhashed path gets a 1h cache header, not the immutable cache the hashed dist path gets.
|
|
64
|
+
|
|
65
|
+
### Theme CSS rules
|
|
66
|
+
|
|
67
|
+
Create `public/themes/[name].css`. Structure it like this:
|
|
68
|
+
|
|
69
|
+
```css
|
|
70
|
+
/* Token definitions — raw palette values */
|
|
71
|
+
:root {
|
|
72
|
+
--color-primary: #hex;
|
|
73
|
+
--color-secondary: #hex;
|
|
74
|
+
/* ... other palette colours */
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Light theme overrides — must target [data-theme="light"], NOT :root */
|
|
78
|
+
/* [data-theme="light"] has higher specificity than :root in pulse-ui.css */
|
|
79
|
+
[data-theme="light"], .ui-theme-light {
|
|
80
|
+
--ui-bg: #hex; /* page background */
|
|
81
|
+
--ui-surface: #hex; /* card/panel surface */
|
|
82
|
+
--ui-surface-2: #hex; /* nested surface */
|
|
83
|
+
--ui-border: #hex; /* border colour */
|
|
84
|
+
--ui-accent: #hex; /* MUST pass WCAG AA (4.5:1) on --ui-bg for small text */
|
|
85
|
+
--ui-accent-hover: #hex;
|
|
86
|
+
--ui-accent-dim: rgba(...); /* subtle tint for hover states */
|
|
87
|
+
--ui-accent-text: #fff; /* text on accent background */
|
|
88
|
+
--ui-muted: #hex; /* secondary text */
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Primary button text fix for light theme (framework default uses var(--ui-bg)) */
|
|
92
|
+
[data-theme="light"] .ui-btn--primary {
|
|
93
|
+
color: #fff;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Nav — frosted glass effect */
|
|
97
|
+
[data-theme="light"] .ui-nav {
|
|
98
|
+
backdrop-filter: blur(14px);
|
|
99
|
+
-webkit-backdrop-filter: blur(14px);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* Mobile nav — inherit dark text, not the white nav bar colour */
|
|
103
|
+
[data-theme="light"] .ui-nav-mobile .ui-nav-link,
|
|
104
|
+
[data-theme="light"] .ui-nav-mobile .ui-nav-mega-item-label,
|
|
105
|
+
[data-theme="light"] .ui-nav-mobile-section {
|
|
106
|
+
color: var(--ui-text);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Pricing — show Pro card first on mobile */
|
|
110
|
+
@media (max-width: 639px) {
|
|
111
|
+
#pricing .ui-grid > div:nth-child(2) { order: -1; }
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Load order in `meta.styles`: `['/pulse-ui.css', '/themes/[name].css']`
|
|
116
|
+
|
|
117
|
+
**WCAG contrast rule for `--ui-accent`:** The palette's mid-tones are often too light to pass 4.5:1 on a near-white background. If none of the palette colours pass, derive a darker tonal variant (e.g. darken the primary colour) for `--ui-accent`. Keep the palette colours for large background areas where dark text is used — those only need 3:1 (large text / non-text).
|
|
118
|
+
|
|
119
|
+
### Spec meta
|
|
120
|
+
|
|
121
|
+
```js
|
|
122
|
+
meta: {
|
|
123
|
+
title: '[App name] — [tagline]',
|
|
124
|
+
description: '...',
|
|
125
|
+
theme: 'light',
|
|
126
|
+
styles: ['/pulse-ui.css', '/themes/[name].css'],
|
|
127
|
+
},
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`theme: 'light'` sets `data-theme="light"` on `<body>`, which activates the light theme rules in `pulse-ui.css` and your theme file.
|
|
131
|
+
|
|
132
|
+
### Utility classes — rule
|
|
133
|
+
|
|
134
|
+
All HTML outside `phoneScreen` must use utility classes, not inline `style=""` attributes. The only legitimate inline styles are:
|
|
135
|
+
- Inside `phoneScreen` (sub-utility-scale phone mockup)
|
|
136
|
+
- `letter-spacing` values (no utility equivalent)
|
|
137
|
+
- The skip-link off-screen positioning (`left: -9999px`)
|
|
138
|
+
- `color:#fff` on the nav logo span (overrides the nav's cascaded colour)
|
|
139
|
+
- `background` and `color` props on `hero()` and `nav()` — these are component props, not raw attributes
|
|
140
|
+
|
|
141
|
+
### Accessibility checklist for this template
|
|
142
|
+
|
|
143
|
+
- Nav and footer logo links must go to the same `href` (both `/`). Identical accessible name + different destination = WCAG failure.
|
|
144
|
+
- Skip link: `<a href="#main-content" class="skip-link" style="position:absolute;left:-9999px;...">Skip to main content</a>` — always include it at the top of the view.
|
|
145
|
+
- `phoneFrame` has `role="img" aria-label="Phone screen preview"` built in.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Blog Post
|
|
150
|
+
|
|
151
|
+
### What it includes
|
|
152
|
+
|
|
153
|
+
| Section | Component(s) |
|
|
154
|
+
|---|---|
|
|
155
|
+
| Sticky nav | `nav` with logo, links |
|
|
156
|
+
| Article header | `hero` (size `sm`, centered) — eyebrow category, title, subtitle/deck, byline in `actions` slot |
|
|
157
|
+
| Article body | `section` + `container(size: 'sm')` + `prose` |
|
|
158
|
+
| Newsletter signup | `section(variant: 'alt')` + `input` + `button` — subscribe action with loading/success states |
|
|
159
|
+
| Footer | `footer` with logo, links, legal |
|
|
160
|
+
|
|
161
|
+
### Files to create
|
|
162
|
+
|
|
163
|
+
| File | Purpose |
|
|
164
|
+
|---|---|
|
|
165
|
+
| `src/pages/[name].js` | The page spec |
|
|
166
|
+
| `public/themes/[name].css` | Brand theme |
|
|
167
|
+
|
|
168
|
+
### State and action
|
|
169
|
+
|
|
170
|
+
The page needs minimal state — only the newsletter form requires hydration:
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
state: { status: 'idle' },
|
|
174
|
+
|
|
175
|
+
actions: {
|
|
176
|
+
subscribe: {
|
|
177
|
+
onStart: (state) => ({ status: 'loading' }),
|
|
178
|
+
run: async (state, serverState, formData) => {
|
|
179
|
+
const email = formData.get('email')
|
|
180
|
+
if (!email) throw new Error('Email address is required')
|
|
181
|
+
await yourEmailApi(email)
|
|
182
|
+
return { email }
|
|
183
|
+
},
|
|
184
|
+
onSuccess: (state) => ({
|
|
185
|
+
status: 'success',
|
|
186
|
+
_toast: { message: 'Subscribed! Welcome aboard.', variant: 'success' },
|
|
187
|
+
}),
|
|
188
|
+
onError: (state, err) => ({
|
|
189
|
+
status: 'error',
|
|
190
|
+
_toast: { message: err.message || 'Something went wrong.', variant: 'error' },
|
|
191
|
+
}),
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Newsletter form — view pattern
|
|
197
|
+
|
|
198
|
+
The form is state-driven. Show success confirmation once subscribed:
|
|
199
|
+
|
|
200
|
+
```js
|
|
201
|
+
state.status === 'success'
|
|
202
|
+
? `<div class="u-flex u-items-center u-justify-center u-gap-2">
|
|
203
|
+
${iconCheckCircle({ size: 20 })}
|
|
204
|
+
<span>You're subscribed. Clear skies ahead.</span>
|
|
205
|
+
</div>`
|
|
206
|
+
: `<form data-action="subscribe" novalidate>
|
|
207
|
+
<div class="u-flex u-gap-3 u-flex-wrap u-justify-center u-items-end">
|
|
208
|
+
${input({ label: 'Email address', name: 'email', type: 'email', placeholder: 'your@email.com', required: true })}
|
|
209
|
+
${button({
|
|
210
|
+
label: state.status === 'loading' ? 'Subscribing\u2026' : 'Subscribe',
|
|
211
|
+
type: 'submit',
|
|
212
|
+
attrs: state.status === 'loading' ? { 'aria-busy': 'true', disabled: '' } : {},
|
|
213
|
+
})}
|
|
214
|
+
</div>
|
|
215
|
+
</form>`
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Dark theme CSS pattern
|
|
219
|
+
|
|
220
|
+
Dark theme overrides go in `:root` (not `[data-theme="light"]`):
|
|
221
|
+
|
|
222
|
+
```css
|
|
223
|
+
:root {
|
|
224
|
+
--ui-bg: #hex; /* darkest background */
|
|
225
|
+
--ui-surface: #hex; /* card / panel background */
|
|
226
|
+
--ui-surface-2: #hex; /* nested surface */
|
|
227
|
+
--ui-border: rgba(..., 0.18);
|
|
228
|
+
--ui-accent: #hex; /* MUST pass 4.5:1 on --ui-bg */
|
|
229
|
+
--ui-accent-hover:#hex;
|
|
230
|
+
--ui-accent-dim: rgba(..., 0.12);
|
|
231
|
+
--ui-accent-text: #hex; /* text on accent-coloured backgrounds */
|
|
232
|
+
--ui-muted: rgba(..., 0.7);
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Do NOT set `meta.theme: 'light'` on dark templates — the default pulse-ui theme is already dark.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Adding a new template
|
|
241
|
+
|
|
242
|
+
When you build a new template and want it available for future scaffolding:
|
|
243
|
+
|
|
244
|
+
1. Add it to this guide under its own `##` section following the pattern above.
|
|
245
|
+
2. Add a row to the "When to use a template" table.
|
|
246
|
+
3. Update the MCP resource (`pulse://guide/templates`) — it is loaded from this file.
|
package/src/mcp/server.js
CHANGED
|
@@ -103,6 +103,13 @@ const GUIDE_RESOURCES = [
|
|
|
103
103
|
description: 'Full working page examples including contact form with actions, validation, and error handling.',
|
|
104
104
|
content: () => GUIDE_EXAMPLES,
|
|
105
105
|
},
|
|
106
|
+
{
|
|
107
|
+
name: 'guide-templates',
|
|
108
|
+
uri: 'pulse://guide/templates',
|
|
109
|
+
title: 'Pulse Guide — Templates & Scaffolding',
|
|
110
|
+
description: 'Ready-made page templates. Fetch this when the user asks to build a landing page, marketing site, or branded template — it lists triggers, pre-build questions, and adaptation rules for each template.',
|
|
111
|
+
content: () => GUIDE_TEMPLATES,
|
|
112
|
+
},
|
|
106
113
|
]
|
|
107
114
|
|
|
108
115
|
for (const { name, uri, title, description, content } of GUIDE_RESOURCES) {
|
|
@@ -887,7 +894,8 @@ const GUIDE_SERVER = fs.readFileSync(new URL('../agent/guide-server.md',
|
|
|
887
894
|
const GUIDE_STYLES = fs.readFileSync(new URL('../agent/guide-styles.md', import.meta.url), 'utf8')
|
|
888
895
|
const GUIDE_ROUTING = fs.readFileSync(new URL('../agent/guide-routing.md', import.meta.url), 'utf8')
|
|
889
896
|
const GUIDE_COMPONENTS = fs.readFileSync(new URL('../agent/guide-components.md', import.meta.url), 'utf8')
|
|
890
|
-
const GUIDE_EXAMPLES
|
|
897
|
+
const GUIDE_EXAMPLES = fs.readFileSync(new URL('../agent/guide-examples.md', import.meta.url), 'utf8')
|
|
898
|
+
const GUIDE_TEMPLATES = fs.readFileSync(new URL('../agent/guide-templates.md', import.meta.url), 'utf8')
|
|
891
899
|
|
|
892
900
|
// ---------------------------------------------------------------------------
|
|
893
901
|
// The Persona — CLI-specific identity header + shared identity content
|
|
@@ -927,6 +935,7 @@ const PULSE_GUIDE_INDEX = `# Pulse Framework Guide
|
|
|
927
935
|
| \`pulse://guide/routing\` | Navigation, page discovery, dynamic routes |
|
|
928
936
|
| \`pulse://guide/components\` | All UI components, icons, charts, composition patterns |
|
|
929
937
|
| \`pulse://guide/examples\` | Complete working page examples |
|
|
938
|
+
| \`pulse://guide/templates\` | **Fetch when asked to build a landing page or branded template.** Pre-build questions, template inventory, adaptation rules, theme CSS patterns. |
|
|
930
939
|
|
|
931
940
|
## Tools available
|
|
932
941
|
|
package/src/ui/icons.js
CHANGED
|
@@ -181,7 +181,11 @@ export const iconBarChart = (o) => s('<line x1="18" y1="20" x2="18" y2="10"/><li
|
|
|
181
181
|
|
|
182
182
|
// ─── Writing ─────────────────────────────────────────────────────────────────
|
|
183
183
|
|
|
184
|
-
export const iconFeather
|
|
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
|
+
|
|
186
|
+
// ─── Astronomy ───────────────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
export const iconTelescope = (o) => s('<path d="M3 17L20 6 22 8 5 19z"/><line x1="13" y1="14" x2="13" y2="21"/><line x1="9" y1="21" x2="17" y2="21"/>', opts(o))
|
|
185
189
|
|
|
186
190
|
// ─── Ecommerce ───────────────────────────────────────────────────────────────
|
|
187
191
|
|
package/src/ui/index.js
CHANGED
|
@@ -88,6 +88,7 @@ export {
|
|
|
88
88
|
iconGlobe, iconShield, iconZap, iconTrendingUp, iconTrendingDown, iconLoader, iconGrid, iconBug, iconMapPin,
|
|
89
89
|
iconSun, iconMoon,
|
|
90
90
|
iconFeather,
|
|
91
|
+
iconTelescope,
|
|
91
92
|
iconAi, iconSmile, iconBarChart,
|
|
92
93
|
iconShoppingCart, iconShoppingBag, iconCreditCard, iconPackage, iconGift,
|
|
93
94
|
iconWallet, iconTruck, iconReceipt, iconStore, iconPercent, iconTicket, iconBanknote,
|