@invisibleloop/pulse 0.12.3 → 0.14.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/build-page.md +5 -1
- package/.claude/commands/verify.md +31 -17
- package/.claude/pulse-checklist.md +21 -1
- package/.github/workflows/publish.yml +1 -0
- package/.pulse-verified +1 -0
- package/CLAUDE.md +56 -15
- package/docs/.claude/commands/verify.md +4 -0
- package/docs/.claude/pulse-checklist.md +20 -2
- package/docs/public/.pulse-ui-version +1 -1
- package/docs/public/andypixel.css +237 -0
- package/docs/public/bookworms.css +422 -0
- package/docs/public/coutumes.css +584 -0
- package/docs/public/images/bookworms/hero-books.jpg +0 -0
- package/docs/public/images/bookworms/how-we-read-now.jpg +0 -0
- package/docs/public/images/bookworms/lantern-keepers.jpg +0 -0
- package/docs/public/images/bookworms/provincial-bookseller.jpg +0 -0
- package/docs/public/images/journey/178.jpg +0 -0
- package/docs/public/images/journey/29.jpg +0 -0
- package/docs/public/images/retrowheels/hero.jpg +0 -0
- package/docs/public/images/shop/devbook-pro.jpg +0 -0
- package/docs/public/images/suzukibento/1015.jpg +0 -0
- package/docs/public/images/suzukibento/1025.jpg +0 -0
- package/docs/public/images/suzukibento/1043.jpg +0 -0
- package/docs/public/images/suzukibento/1059.jpg +0 -0
- package/docs/public/images/suzukibento/1062.jpg +0 -0
- package/docs/public/images/suzukibento/1084.jpg +0 -0
- package/docs/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
- package/docs/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
- package/docs/public/images/swish/1485518882345-15568b007407.jpg +0 -0
- package/docs/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
- package/docs/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
- package/docs/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
- package/docs/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
- package/docs/public/images/swish/1551803091-e20673f15770.jpg +0 -0
- package/docs/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
- package/docs/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
- package/docs/public/journey.css +528 -0
- package/docs/public/pulse-ui.css +1036 -22
- package/docs/public/retrowheels.css +402 -0
- package/docs/public/shop.css +235 -0
- package/docs/public/sparkys.css +349 -0
- package/docs/public/suzukibento.css +517 -0
- package/docs/public/swish.css +394 -0
- package/docs/server.js +19 -4
- package/docs/src/lib/nav.js +14 -3
- package/docs/src/pages/components/decorate.js +74 -0
- package/docs/src/pages/components/gallery.js +102 -0
- package/docs/src/pages/components/hero.js +1 -0
- package/docs/src/pages/components/marquee.js +76 -0
- package/docs/src/pages/components/photo-card.js +74 -0
- package/docs/src/pages/components/section.js +40 -1
- package/docs/src/pages/meta.js +26 -0
- package/docs/src/pages/templates/andypixel.js +260 -0
- package/docs/src/pages/templates/bookworms.js +227 -0
- package/docs/src/pages/templates/coutumes.js +226 -0
- package/docs/src/pages/templates/journey.js +186 -0
- package/docs/src/pages/templates/retrowheels.js +342 -0
- package/docs/src/pages/templates/shop.js +257 -0
- package/docs/src/pages/templates/sparkys.js +249 -0
- package/docs/src/pages/templates/suzukibento.js +200 -0
- package/docs/src/pages/templates/swish.js +313 -0
- package/docs/src/pages/templates.js +313 -2
- package/docs/src/pages/testing.js +2 -1
- package/examples/.claude/commands/pulse-build.md +7 -0
- package/examples/.claude/commands/pulse-dev.md +7 -0
- package/examples/.claude/commands/pulse-start.md +9 -0
- package/examples/.claude/commands/pulse-stop.md +5 -0
- package/examples/.claude/commands/verify.md +84 -0
- package/examples/.claude/coverage-check.js +127 -0
- package/examples/.claude/pulse-checklist.md +177 -0
- package/examples/andypixel.css +237 -0
- package/examples/andypixel.js +258 -0
- package/examples/bookworms.css +422 -0
- package/examples/bookworms.js +225 -0
- package/examples/contact.js +1 -0
- package/examples/counter.js +1 -0
- package/examples/dev.server.js +17 -0
- package/examples/journey.css +528 -0
- package/examples/journey.js +186 -0
- package/examples/pricing.js +1 -0
- package/examples/public/.pulse-ui-version +1 -0
- package/examples/public/pulse-ui.css +4031 -0
- package/examples/public/pulse-ui.js +257 -0
- package/examples/quiz.js +1 -0
- package/examples/retrowheels.css +402 -0
- package/examples/retrowheels.js +340 -0
- package/examples/shared.js +6 -0
- package/examples/suzukibento.css +513 -0
- package/examples/suzukibento.js +200 -0
- package/examples/swish.css +394 -0
- package/examples/swish.js +311 -0
- package/examples/todos.js +1 -0
- package/package.json +2 -3
- package/public/.pulse-ui-version +1 -1
- package/public/images/bookworms/hero-books.jpg +0 -0
- package/public/images/bookworms/how-we-read-now.jpg +0 -0
- package/public/images/bookworms/lantern-keepers.jpg +0 -0
- package/public/images/bookworms/provincial-bookseller.jpg +0 -0
- package/public/images/journey/178.jpg +0 -0
- package/public/images/journey/29.jpg +0 -0
- package/public/images/retrowheels/hero.jpg +0 -0
- package/public/images/shop/devbook-pro.jpg +0 -0
- package/public/images/suzukibento/1015.jpg +0 -0
- package/public/images/suzukibento/1025.jpg +0 -0
- package/public/images/suzukibento/1043.jpg +0 -0
- package/public/images/suzukibento/1059.jpg +0 -0
- package/public/images/suzukibento/1062.jpg +0 -0
- package/public/images/suzukibento/1084.jpg +0 -0
- package/public/images/swish/1469334031218-e382a71b716b.jpg +0 -0
- package/public/images/swish/1483985988355-763728e1935b.jpg +0 -0
- package/public/images/swish/1485518882345-15568b007407.jpg +0 -0
- package/public/images/swish/1490481651871-ab68de25d43d.jpg +0 -0
- package/public/images/swish/1496747611176-843222e1e57c.jpg +0 -0
- package/public/images/swish/1502716119720-b23a93e5fe1b.jpg +0 -0
- package/public/images/swish/1539109136881-3be0616acf4b.jpg +0 -0
- package/public/images/swish/1551803091-e20673f15770.jpg +0 -0
- package/public/images/swish/1558769132-cb1aea458c5e.jpg +0 -0
- package/public/images/swish/1572804013309-59a88b7e92f1.jpg +0 -0
- package/public/pulse-ui.css +1080 -22
- package/scripts/build.js +72 -18
- package/src/agent/checklist.md +22 -2
- package/src/agent/commands/verify.md +4 -0
- package/src/agent/guide-components.md +146 -8
- package/src/agent/guide-design-gallery.md +288 -0
- package/src/agent/guide-design-references.md +172 -0
- package/src/agent/guide-explore.md +313 -0
- package/src/agent/guide-styles.md +212 -11
- package/src/agent/guide-templates.md +223 -10
- package/src/agent/guide.md +9 -4
- package/src/agent/identity.md +2 -1
- package/src/agent/skills/verify/SKILL.md +64 -28
- package/src/agent/workflow.md +247 -61
- package/src/cli/dev.js +39 -14
- package/src/cli/fmt.js +113 -0
- package/src/cli/index.js +60 -33
- package/src/cli/logger.js +145 -0
- package/src/cli/scaffold.js +57 -3
- package/src/html.js +6 -1
- package/src/mcp/server.js +2561 -99
- package/src/mcp/validate-worker.js +49 -5
- package/src/runtime/ssr.js +4 -1
- package/src/server/index.js +29 -5
- package/src/spec/schema.js +21 -0
- package/src/spec/schema.test.js +28 -0
- package/src/testing/html.js +130 -4
- package/src/testing/testing.test.js +58 -0
- package/src/ui/banner.js +24 -9
- package/src/ui/brand-statement.js +58 -0
- package/src/ui/button.js +13 -2
- package/src/ui/card.js +20 -1
- package/src/ui/colophon.js +29 -0
- package/src/ui/contact-row.js +41 -0
- package/src/ui/cta.js +18 -4
- package/src/ui/decorate.js +55 -0
- package/src/ui/display-heading.js +44 -0
- package/src/ui/drop-cap.js +29 -0
- package/src/ui/editorial-hero.js +63 -0
- package/src/ui/entry-list.js +36 -0
- package/src/ui/feature.js +20 -3
- package/src/ui/footer.js +78 -9
- package/src/ui/gallery.js +56 -0
- package/src/ui/hero.js +68 -18
- package/src/ui/image-pair.js +72 -0
- package/src/ui/index.js +20 -0
- package/src/ui/input.js +1 -0
- package/src/ui/marquee.js +48 -0
- package/src/ui/nav.js +19 -9
- package/src/ui/photo-card.js +44 -0
- package/src/ui/product-card.js +65 -0
- package/src/ui/pullquote.js +14 -3
- package/src/ui/search.js +1 -0
- package/src/ui/section-label.js +38 -0
- package/src/ui/section.js +1 -1
- package/src/ui/service-card.js +47 -0
- package/src/ui/stat-grid.js +40 -0
- package/src/ui/stat.js +16 -2
- package/src/ui/testimonial.js +17 -6
- package/src/ui/textarea.js +1 -0
- package/src/ui/top-strip.js +47 -0
- package/src/ui/trust-list.js +46 -0
- package/src/ui/ui.test.js +35 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
# Pulse Explore Guide — Blank-Canvas Layout Thinking
|
|
2
|
+
|
|
3
|
+
This guide is for when you want a site that feels **distinct**, not just correct. It teaches structure-first thinking: start with layout zones and emotional intent, not with which template to adapt.
|
|
4
|
+
|
|
5
|
+
> Read this when `pulse_sketch` suggests an unusual layout, when the user says "make it feel different", or when no template in `pulse://guide/templates` is a good fit.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## The Problem with Defaulting to Components
|
|
10
|
+
|
|
11
|
+
Every Pulse component is well-built. That's also their risk: they pull strongly toward a recognisable vocabulary — centred hero, three-column features, testimonial cards. A dog groomer and a hedge fund built with default components will look like cousins.
|
|
12
|
+
|
|
13
|
+
**The escape hatch is always available.** The `view` function is raw HTML. You can use:
|
|
14
|
+
- Zero components — just utility classes and inline HTML
|
|
15
|
+
- A mix of raw layout with components for specific elements (nav, footer, forms)
|
|
16
|
+
- Component structure with overridden CSS via app.css class rules
|
|
17
|
+
|
|
18
|
+
> **Note on inline styles:** The raw HTML patterns in this guide use `style=` attributes for clarity. In your own specs, move those rules into `app.css` as named classes — the `pulse_review` checklist will flag inline styles as a smell, and they can't be overridden cleanly. The pattern is: sketch with inline styles → extract to `app.css` classes → done.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Start with Intent, Not Components
|
|
23
|
+
|
|
24
|
+
Before opening `pulse://guide/components`, answer three questions:
|
|
25
|
+
|
|
26
|
+
### 1. What is the emotional job?
|
|
27
|
+
Not "what does the page contain" but "how should the user feel 30 seconds after landing?"
|
|
28
|
+
|
|
29
|
+
| Feeling | Layout signal |
|
|
30
|
+
|---|---|
|
|
31
|
+
| Trust / authority | Wide margins, generous whitespace, restrained type, no decoration |
|
|
32
|
+
| Energy / urgency | Full-bleed images, bold type, tight spacing, strong diagonals |
|
|
33
|
+
| Warmth / handmade | Irregular layout, slight tilts, texture, serif body text |
|
|
34
|
+
| Precision / technical | Monospace accents, grid-aligned everything, data tables, no fluff |
|
|
35
|
+
| Playful / surprising | Breaking the grid, overlapping elements, unexpected colour pops |
|
|
36
|
+
| Editorial / serious | Long reading width, pullquotes, photo captions, no eyebrow badges |
|
|
37
|
+
|
|
38
|
+
### 2. What is the dominant structural gesture?
|
|
39
|
+
One of these should describe the entire page:
|
|
40
|
+
|
|
41
|
+
- **Full-bleed** — images or colour fills edge-to-edge, type floats over them
|
|
42
|
+
- **Offset split** — persistent asymmetry: content always 60/40 or 70/30, never centered
|
|
43
|
+
- **Vertical rhythm** — everything stacked, each section separated by whitespace not dividers
|
|
44
|
+
- **Dense grid** — information-rich, small gutters, horizontal scannable
|
|
45
|
+
- **Long-form** — single narrow column, essay-like, designed for reading not scanning
|
|
46
|
+
- **Kinetic** — scrolling triggers layout shifts (parallax, sticky elements, revealed content)
|
|
47
|
+
|
|
48
|
+
### 3. What should it NOT look like?
|
|
49
|
+
Name something to avoid. "Not another SaaS dark card page." "Not a startup landing page." "Not like it was made with a template."
|
|
50
|
+
|
|
51
|
+
This negative constraint is often more useful than the positive one.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Layout Zones Approach
|
|
56
|
+
|
|
57
|
+
Design the page as zones before deciding what fills them:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
┌─────────────────────────────────────────┐
|
|
61
|
+
│ IDENTITY ZONE │ ← Name, logo, hook. First impression.
|
|
62
|
+
│ (full-bleed image? type only? split?) │
|
|
63
|
+
├─────────────────────────────────────────┤
|
|
64
|
+
│ PROOF ZONE │ ← Why believe it? Numbers, names, photos.
|
|
65
|
+
│ (stats? logos? photos? quote?) │
|
|
66
|
+
├─────────────────────────────────────────┤
|
|
67
|
+
│ DETAIL ZONE │ ← What exactly do you offer?
|
|
68
|
+
│ (features? services? work? prices?) │
|
|
69
|
+
├─────────────────────────────────────────┤
|
|
70
|
+
│ TRUST ZONE │ ← Social proof, credentials, story.
|
|
71
|
+
│ (testimonials? about? process?) │
|
|
72
|
+
├─────────────────────────────────────────┤
|
|
73
|
+
│ ACTION ZONE │ ← The ask. Form, CTA, booking, purchase.
|
|
74
|
+
└─────────────────────────────────────────┘
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The *order* and *proportion* of these zones is a design decision. A personal trainer might lead with PROOF (transformation photos) before IDENTITY. A law firm might bury the ACTION ZONE beneath extensive TRUST ZONE content.
|
|
78
|
+
|
|
79
|
+
**Reordering zones is one of the cheapest ways to feel distinctive.**
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Raw HTML Patterns (Zero Components)
|
|
84
|
+
|
|
85
|
+
These are layouts that deliberately use no Pulse components except `nav` and `footer`. They demonstrate what's possible.
|
|
86
|
+
|
|
87
|
+
### Full-Bleed Typography Hero
|
|
88
|
+
```js
|
|
89
|
+
const typographicHero = `
|
|
90
|
+
<section style="
|
|
91
|
+
min-height: 100svh;
|
|
92
|
+
display: grid;
|
|
93
|
+
place-items: center;
|
|
94
|
+
background: var(--ui-bg);
|
|
95
|
+
padding: var(--ui-space-8);
|
|
96
|
+
">
|
|
97
|
+
<div style="max-width: 900px; text-align: center;">
|
|
98
|
+
<p style="
|
|
99
|
+
font-size: clamp(0.75rem, 1.5vw, 0.875rem);
|
|
100
|
+
letter-spacing: 0.15em;
|
|
101
|
+
text-transform: uppercase;
|
|
102
|
+
color: var(--ui-muted);
|
|
103
|
+
margin-bottom: var(--ui-space-6);
|
|
104
|
+
">Est. 2024 · London</p>
|
|
105
|
+
<h1 style="
|
|
106
|
+
font-size: clamp(3rem, 10vw, 8rem);
|
|
107
|
+
font-weight: 800;
|
|
108
|
+
line-height: 0.9;
|
|
109
|
+
letter-spacing: -0.04em;
|
|
110
|
+
color: var(--ui-text);
|
|
111
|
+
">The Work<br>Speaks for<br>Itself.</h1>
|
|
112
|
+
<a href="#work" style="
|
|
113
|
+
display: inline-block;
|
|
114
|
+
margin-top: var(--ui-space-10);
|
|
115
|
+
padding: var(--ui-space-4) var(--ui-space-8);
|
|
116
|
+
border: 1px solid var(--ui-border);
|
|
117
|
+
color: var(--ui-text);
|
|
118
|
+
text-decoration: none;
|
|
119
|
+
font-size: 0.875rem;
|
|
120
|
+
letter-spacing: 0.05em;
|
|
121
|
+
">View Work ↓</a>
|
|
122
|
+
</div>
|
|
123
|
+
</section>
|
|
124
|
+
`
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Asymmetric Split (Persistent Offset)
|
|
128
|
+
```js
|
|
129
|
+
const asymmetricHero = `
|
|
130
|
+
<section style="
|
|
131
|
+
min-height: 100svh;
|
|
132
|
+
display: grid;
|
|
133
|
+
grid-template-columns: 1fr 1.6fr;
|
|
134
|
+
">
|
|
135
|
+
<div style="
|
|
136
|
+
background: var(--ui-bg-alt);
|
|
137
|
+
padding: var(--ui-space-16) var(--ui-space-8);
|
|
138
|
+
display: flex;
|
|
139
|
+
flex-direction: column;
|
|
140
|
+
justify-content: flex-end;
|
|
141
|
+
">
|
|
142
|
+
<p style="color:var(--ui-muted);font-size:0.75rem;letter-spacing:0.1em;text-transform:uppercase;margin-bottom:var(--ui-space-4)">Dog grooming · Maplewood</p>
|
|
143
|
+
<h1 style="font-size:clamp(2rem,4vw,3.5rem);font-weight:700;line-height:1.1;letter-spacing:-0.02em;color:var(--ui-text)">Your dog,<br>beautifully<br>groomed.</h1>
|
|
144
|
+
<a href="#book" style="display:inline-flex;align-items:center;gap:0.5rem;margin-top:var(--ui-space-8);color:var(--ui-text);text-decoration:none;font-weight:500">
|
|
145
|
+
Book now <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
|
146
|
+
</a>
|
|
147
|
+
</div>
|
|
148
|
+
<div style="overflow:hidden;">
|
|
149
|
+
<img src="${imageUrl}" alt="${imageAlt}" style="width:100%;height:100%;object-fit:cover;">
|
|
150
|
+
</div>
|
|
151
|
+
</section>
|
|
152
|
+
`
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Stacked Long-Form (Essay / Blog)
|
|
156
|
+
```js
|
|
157
|
+
const essayLayout = `
|
|
158
|
+
<article style="
|
|
159
|
+
max-width: 680px;
|
|
160
|
+
margin: 0 auto;
|
|
161
|
+
padding: var(--ui-space-16) var(--ui-space-6);
|
|
162
|
+
">
|
|
163
|
+
<header style="margin-bottom: var(--ui-space-12);">
|
|
164
|
+
<p style="font-size:0.75rem;letter-spacing:0.1em;text-transform:uppercase;color:var(--ui-muted);margin-bottom:var(--ui-space-3)">
|
|
165
|
+
${category} · ${date} · ${readTime}
|
|
166
|
+
</p>
|
|
167
|
+
<h1 style="font-size:clamp(2rem,4vw,3rem);font-weight:700;line-height:1.15;letter-spacing:-0.02em;margin-bottom:var(--ui-space-4)">
|
|
168
|
+
${title}
|
|
169
|
+
</h1>
|
|
170
|
+
<p style="font-size:1.125rem;color:var(--ui-muted);line-height:1.6;">${deck}</p>
|
|
171
|
+
<div style="display:flex;align-items:center;gap:1rem;margin-top:var(--ui-space-6);padding-top:var(--ui-space-6);border-top:1px solid var(--ui-border);">
|
|
172
|
+
<img src="${authorAvatar}" alt="${authorName}" style="width:40px;height:40px;border-radius:50%;object-fit:cover;">
|
|
173
|
+
<div>
|
|
174
|
+
<p style="font-weight:500;font-size:0.875rem;">${authorName}</p>
|
|
175
|
+
<p style="color:var(--ui-muted);font-size:0.75rem;">${authorRole}</p>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</header>
|
|
179
|
+
<img src="${heroImage}" alt="" style="width:100%;aspect-ratio:16/9;object-fit:cover;margin-bottom:var(--ui-space-10);">
|
|
180
|
+
<div style="font-size:1.0625rem;line-height:1.75;color:var(--ui-text);">
|
|
181
|
+
${bodyHtml}
|
|
182
|
+
</div>
|
|
183
|
+
</article>
|
|
184
|
+
`
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Dense Grid (Portfolio / Gallery)
|
|
188
|
+
```js
|
|
189
|
+
const denseGrid = `
|
|
190
|
+
<section style="
|
|
191
|
+
padding: var(--ui-space-4);
|
|
192
|
+
display: grid;
|
|
193
|
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
194
|
+
gap: var(--ui-space-3);
|
|
195
|
+
">
|
|
196
|
+
${items.map(item => `
|
|
197
|
+
<a href="${item.href}" style="
|
|
198
|
+
display: block;
|
|
199
|
+
overflow: hidden;
|
|
200
|
+
position: relative;
|
|
201
|
+
aspect-ratio: 4/3;
|
|
202
|
+
background: var(--ui-bg-alt);
|
|
203
|
+
">
|
|
204
|
+
<img src="${item.img}" alt="${item.title}" style="width:100%;height:100%;object-fit:cover;transition:transform 0.4s ease;">
|
|
205
|
+
<div style="
|
|
206
|
+
position:absolute;inset:0;
|
|
207
|
+
background:linear-gradient(to top, rgba(0,0,0,0.6) 0%, transparent 50%);
|
|
208
|
+
display:flex;align-items:flex-end;padding:var(--ui-space-4);
|
|
209
|
+
opacity:0;transition:opacity 0.3s ease;
|
|
210
|
+
" class="grid-overlay">
|
|
211
|
+
<p style="color:#fff;font-weight:500;font-size:0.875rem;">${item.title}</p>
|
|
212
|
+
</div>
|
|
213
|
+
</a>
|
|
214
|
+
`).join('')}
|
|
215
|
+
</section>
|
|
216
|
+
`
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Mixing Raw Layout with Pulse Components
|
|
222
|
+
|
|
223
|
+
You don't have to choose all-or-nothing. Common hybrid pattern:
|
|
224
|
+
|
|
225
|
+
```js
|
|
226
|
+
view: () => `
|
|
227
|
+
${uiNav({ ... })} ← Use nav — it handles mobile burger, sticky, a11y
|
|
228
|
+
|
|
229
|
+
<section style="...raw layout..."> ← Raw hero with your own structure
|
|
230
|
+
...
|
|
231
|
+
</section>
|
|
232
|
+
|
|
233
|
+
${section({ ← Use section for padding + background variant
|
|
234
|
+
variant: 'alt',
|
|
235
|
+
content: grid({ ... }) ← Use grid for the responsive column system
|
|
236
|
+
})}
|
|
237
|
+
|
|
238
|
+
${cta({ ... })} ← Use cta — saves 10 lines and is a11y correct
|
|
239
|
+
|
|
240
|
+
${footer({ ... })} ← Use footer — handles all the footer layout
|
|
241
|
+
`
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**Rule of thumb:** Use Pulse components for global chrome (nav, footer), repeating patterns (cards, testimonials, features), and interactive elements (buttons, forms, modals). Use raw HTML for hero sections, custom layouts, and any zone where the default shape doesn't serve the design.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## CSS Custom Properties Available Everywhere
|
|
249
|
+
|
|
250
|
+
Even in raw HTML, you have the full design token system:
|
|
251
|
+
|
|
252
|
+
```css
|
|
253
|
+
/* Colours */
|
|
254
|
+
var(--ui-bg) /* page background */
|
|
255
|
+
var(--ui-bg-alt) /* slightly offset background (cards, alternating sections) */
|
|
256
|
+
var(--ui-surface) /* elevated surface (modals, dropdowns) */
|
|
257
|
+
var(--ui-text) /* primary text */
|
|
258
|
+
var(--ui-muted) /* secondary / caption text */
|
|
259
|
+
var(--ui-border) /* divider / outline color */
|
|
260
|
+
var(--ui-accent) /* brand accent (buttons, links, highlights) */
|
|
261
|
+
|
|
262
|
+
/* Spacing */
|
|
263
|
+
var(--ui-space-1) /* 0.25rem */
|
|
264
|
+
var(--ui-space-2) /* 0.5rem */
|
|
265
|
+
var(--ui-space-3) /* 0.75rem */
|
|
266
|
+
var(--ui-space-4) /* 1rem */
|
|
267
|
+
var(--ui-space-6) /* 1.5rem */
|
|
268
|
+
var(--ui-space-8) /* 2rem */
|
|
269
|
+
var(--ui-space-10) /* 2.5rem */
|
|
270
|
+
var(--ui-space-12) /* 3rem */
|
|
271
|
+
var(--ui-space-16) /* 4rem */
|
|
272
|
+
|
|
273
|
+
/* Typography */
|
|
274
|
+
var(--ui-font) /* body font stack */
|
|
275
|
+
var(--ui-radius) /* base border-radius (affected by vibe) */
|
|
276
|
+
var(--ui-radius-lg) /* larger radius */
|
|
277
|
+
|
|
278
|
+
/* Shadows */
|
|
279
|
+
var(--ui-shadow-sm) /* subtle card shadow */
|
|
280
|
+
var(--ui-shadow) /* standard shadow */
|
|
281
|
+
var(--ui-shadow-lg) /* large shadow */
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
Use `clamp()` for fluid type: `font-size: clamp(2rem, 5vw, 4rem)` — scales smoothly across breakpoints.
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## The 3-Layout Rule
|
|
289
|
+
|
|
290
|
+
Before writing any code, sketch 3 structurally different options for the hero / above-the-fold zone. Even one sentence each:
|
|
291
|
+
|
|
292
|
+
> Option A: Full-bleed photo, title in bottom-left corner, text reversed out of image
|
|
293
|
+
> Option B: 60/40 split — headline left, product photo right, earthy cream background
|
|
294
|
+
> Option C: Typography-only, massive headline, minimal, no image — lets the copy breathe
|
|
295
|
+
|
|
296
|
+
Then choose the one that matches the emotional intent. If you can't decide, pick the most unusual one.
|
|
297
|
+
|
|
298
|
+
**The `pulse_sketch` tool automates this** — call it with a brief description and it returns 3 structured layout directions ready to choose from.
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## Anti-Pattern Checklist
|
|
303
|
+
|
|
304
|
+
These are the signs a design has defaulted to template mode rather than intent:
|
|
305
|
+
|
|
306
|
+
- [ ] Hero is centred with eyebrow badge + large title + subtitle + two buttons
|
|
307
|
+
- [ ] Three-column feature grid with icon + heading + body
|
|
308
|
+
- [ ] Testimonials are all in cards with quote + author name
|
|
309
|
+
- [ ] Every section alternates white/light-grey backgrounds
|
|
310
|
+
- [ ] The nav has a logo on the left, links in the middle, CTA on the right
|
|
311
|
+
- [ ] The footer has the same logo, same links, and a copyright line
|
|
312
|
+
|
|
313
|
+
None of these are wrong. But if they're all there together, the page will look like every other Pulse page. Consider swapping at least 2 of the 6 for something more distinctive.
|
|
@@ -16,8 +16,37 @@ Non-interactive components (nav, hero, button, card, etc.) only need '/pulse-ui.
|
|
|
16
16
|
|
|
17
17
|
pulse-ui.css exposes CSS custom properties for every token. app.css MUST use these tokens — never hardcode colour hex values.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
### How theming works — two-layer token system
|
|
20
|
+
|
|
21
|
+
Pulse uses a **two-layer token system**:
|
|
22
|
+
|
|
23
|
+
1. **Input tokens** (unprefixed: `--bg`, `--text`, `--accent`, etc.) — you define these in `:root` or `[data-theme="light"]` to set your palette
|
|
24
|
+
2. **Output tokens** (prefixed: `--ui-bg`, `--ui-text`, `--ui-accent`, etc.) — pulse-ui components read these; you reference them in app.css
|
|
25
|
+
|
|
26
|
+
pulse-ui.css sets `--ui-*` tokens to `var(--your-input-token, fallback)`. When you override `--accent`, components automatically pick it up via `--ui-accent`.
|
|
27
|
+
|
|
28
|
+
**Which token names to use where:**
|
|
29
|
+
|
|
30
|
+
| File | Define input tokens | Reference output tokens |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `public/theme.css` | ✓ `--accent: #e25;` | — |
|
|
33
|
+
| `app.css` | — | ✓ `color: var(--ui-accent);` |
|
|
34
|
+
| Pulse components | — | ✓ (already done internally) |
|
|
35
|
+
|
|
36
|
+
**Common mistake:** defining `--color-accent` or `--brand-primary` expecting components to use them. Components only read the canonical unprefixed names: `--accent`, `--text`, `--bg`, `--surface`, `--border`, `--muted`, `--radius`. Define those, and everything updates.
|
|
37
|
+
|
|
38
|
+
### Dark by default
|
|
39
|
+
|
|
40
|
+
**The library is dark by default.** `pulse-ui.css` applies `background-color: var(--ui-bg)` and `color: var(--ui-text)` to `html, body` automatically using the dark palette defined in `:root` — you do NOT need to add these to app.css.
|
|
41
|
+
|
|
42
|
+
For a **light theme**, set `meta.theme: 'light'` in the spec — this adds `data-theme="light"` to the `<body>` and activates the built-in light token set (accessible contrast for badges, alerts, and all semantic colours).
|
|
43
|
+
|
|
44
|
+
### Overriding tokens in your theme
|
|
45
|
+
|
|
46
|
+
Override input tokens to retheme all components at once. The pattern is the same for both dark and light themes — set the unprefixed input token in the appropriate selector:
|
|
47
|
+
|
|
20
48
|
```css
|
|
49
|
+
/* Dark theme overrides — in :root */
|
|
21
50
|
:root {
|
|
22
51
|
--bg: #0d0d10; /* page background */
|
|
23
52
|
--surface: #111116; /* card / panel background */
|
|
@@ -29,8 +58,17 @@ Override tokens in :root inside app.css to retheme all components at once:
|
|
|
29
58
|
--accent-hover: #b5aaff;
|
|
30
59
|
--radius: 8px;
|
|
31
60
|
}
|
|
61
|
+
|
|
62
|
+
/* Light theme overrides — in [data-theme="light"] */
|
|
63
|
+
[data-theme="light"] {
|
|
64
|
+
--bg: #ffffff;
|
|
65
|
+
--surface: #f5f5fa;
|
|
66
|
+
--accent: #e25; /* overrides the light default #6b5ce7 */
|
|
67
|
+
}
|
|
32
68
|
```
|
|
33
69
|
|
|
70
|
+
pulse-ui.css maps input → output via `var()` in **both** the dark (`:root`) and light (`[data-theme="light"]`) blocks, so the same two-layer pattern works in both themes.
|
|
71
|
+
|
|
34
72
|
Then use the computed --ui-* tokens everywhere in app.css:
|
|
35
73
|
```css
|
|
36
74
|
/* pulse-ui.css already sets background-color and color on html/body — do not repeat them */
|
|
@@ -40,28 +78,64 @@ a { color: var(--ui-accent); }
|
|
|
40
78
|
code { background: var(--ui-surface-2); color: var(--ui-accent); border: 1px solid var(--ui-border); }
|
|
41
79
|
```
|
|
42
80
|
|
|
43
|
-
**
|
|
81
|
+
> **Light theme escape hatch:** if you need to override a specific output token that has no input equivalent, you can set `--ui-accent` directly inside `[data-theme="light"]` — but prefer input tokens wherever possible.
|
|
82
|
+
|
|
83
|
+
```js
|
|
84
|
+
meta: {
|
|
85
|
+
theme: 'light',
|
|
86
|
+
styles: ['/pulse-ui.css', '/app.css'],
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Colour tokens:** `--ui-bg`, `--ui-surface`, `--ui-surface-2`, `--ui-border`, `--ui-text`, `--ui-muted`, `--ui-accent`, `--ui-accent-hover`, `--ui-accent-dim`, `--ui-accent-text`, `--ui-green`, `--ui-red`, `--ui-yellow`, `--ui-blue`, `--ui-radius`, `--ui-radius-sm`, `--ui-font`, `--ui-font-display`, `--ui-mono`. Never hardcode hex values — override the tokens.
|
|
91
|
+
|
|
92
|
+
> **⚠ `--ui-muted` contrast on warm/light palettes** — The default `--ui-muted` value (`#60607a` in light theme) achieves 4.5:1 on the default white `--ui-bg`. When you set a warm or coloured background (`--bg: #f5e6d3` etc.) the luminance difference shrinks and `--ui-muted` can drop below the WCAG AA threshold of 4.5:1. **Always override `--muted` when changing `--bg`**, and run `pulse_check_contrast` immediately after writing your `theme.css`. A common failure mode: stat labels, nav links, and feature descriptions all use `--ui-muted` and will fail contrast silently if this token is not rechecked against your actual background colour.
|
|
44
93
|
|
|
45
|
-
|
|
94
|
+
> **⚠ `section--dark` in light theme** — `section({ variant: 'dark' })` uses `--ui-surface-2` as its background, which in the default light theme resolves to `#f0f0f8` — a light grey, not a dark background. The framework now auto-corrects this via a `[data-theme="light"] .ui-section--dark` override (navy `#1a1a2e`), and ghost buttons inside it get automatic light-text treatment. If you use a custom `--surface-2` override in your theme, set `--ui-dark-bg` and `--ui-dark-text` to pin the dark section to specific values:
|
|
95
|
+
> ```css
|
|
96
|
+
> [data-theme="light"] { --ui-dark-bg: #1a1a2e; --ui-dark-text: #e8e0d0; }
|
|
97
|
+
> ```
|
|
46
98
|
|
|
47
|
-
**
|
|
99
|
+
> **Ghost buttons on dark backgrounds** — use `variant: 'ghost-light'` instead of `variant: 'ghost'` whenever the button sits on a dark background (custom hero div, `section--dark`, navy sections). `ghost-light` renders white text with a subtle white border, maintaining 4.5:1 contrast regardless of the surrounding theme. `ghost` uses `--ui-muted` text which fails on dark backgrounds in light theme.
|
|
48
100
|
|
|
101
|
+
**Display font token:** `--ui-font-display` is used for hero titles and section headings. By default it inherits `--ui-font`. Override it separately to get a display/heading face that differs from body text:
|
|
49
102
|
```css
|
|
50
|
-
/*
|
|
51
|
-
:root { --
|
|
103
|
+
/* app.css */
|
|
104
|
+
:root { --font-display: 'Playfair Display', Georgia, serif; }
|
|
105
|
+
```
|
|
52
106
|
|
|
53
|
-
|
|
54
|
-
|
|
107
|
+
**Letter-spacing token:** `--ui-letter-spacing-display` (default `-0.025em`) controls heading tightness. Override in `:root` to match your typeface:
|
|
108
|
+
```css
|
|
109
|
+
:root { --letter-spacing-display: -0.04em; } /* tighter for heavy condensed fonts */
|
|
55
110
|
```
|
|
56
111
|
|
|
112
|
+
## Visual personality — meta.vibe
|
|
113
|
+
|
|
114
|
+
`meta.vibe` sets `data-vibe` on `<body>` and adjusts geometry + type tokens as a preset. It doesn't touch colour — pair it with a `theme.css` for full personality.
|
|
115
|
+
|
|
57
116
|
```js
|
|
58
117
|
meta: {
|
|
59
|
-
|
|
60
|
-
|
|
118
|
+
vibe: 'warm', // warm | editorial | playful | minimal | bold
|
|
119
|
+
theme: 'light',
|
|
120
|
+
styles: ['/pulse-ui.css', '/theme.css', '/app.css'],
|
|
61
121
|
}
|
|
62
122
|
```
|
|
63
123
|
|
|
64
|
-
|
|
124
|
+
| Vibe | Effect | Pairs well with |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| `warm` | radius 14px, softer letter-spacing | rounded photo cards, `section: paper`, warm palette |
|
|
127
|
+
| `editorial` | radius 0, serif display font, tight letter-spacing | `hero layout: overlap`, `section: diagonal`, `pullquote` |
|
|
128
|
+
| `playful` | radius 22px, neutral letter-spacing | `gallery` with rounded images, `marquee`, bright accent |
|
|
129
|
+
| `minimal` | radius 2px, open letter-spacing, no shadows | left-aligned hero, `section: spotlight`, monochrome |
|
|
130
|
+
| `bold` | radius 6px, very tight letter-spacing | `section: dark`, large `stat` components, gradient hero |
|
|
131
|
+
|
|
132
|
+
Vibes affect `--ui-radius`, `--ui-font-display`, and `--ui-letter-spacing-display`. All component shapes, borders, and heading appearance change automatically.
|
|
133
|
+
|
|
134
|
+
**Custom override:** Set vibe first, then fine-tune with CSS variable overrides in `public/theme.css`:
|
|
135
|
+
```css
|
|
136
|
+
/* Fine-tune the warm vibe */
|
|
137
|
+
[data-vibe="warm"] { --ui-radius: 18px; }
|
|
138
|
+
```
|
|
65
139
|
|
|
66
140
|
**Spacing tokens** (`--ui-space-N`): `--ui-space-1` (4px), `--ui-space-2` (8px), `--ui-space-3` (12px), `--ui-space-4` (16px), `--ui-space-5` (20px), `--ui-space-6` (24px), `--ui-space-8` (32px), `--ui-space-10` (40px), `--ui-space-12` (48px), `--ui-space-16` (64px), `--ui-space-20` (80px), `--ui-space-24` (96px). Use these for `padding`, `margin`, and `gap`.
|
|
67
141
|
|
|
@@ -119,6 +193,49 @@ Then in app.css:
|
|
|
119
193
|
|
|
120
194
|
If you cannot modify `createServer` (e.g. the project uses auto-discovery), use a self-hosted font in `public/fonts/` with `@font-face` in `app.css` — no CSP changes needed.
|
|
121
195
|
|
|
196
|
+
### External images (img-src)
|
|
197
|
+
|
|
198
|
+
The default CSP does **not** restrict `img-src` — it inherits `default-src 'self'`, which means images served from the same origin load fine. But if you load images from an external host (e.g. `https://images.unsplash.com`, `https://picsum.photos`, a CDN), you must add the host to `img-src`.
|
|
199
|
+
|
|
200
|
+
**Do this at build time, before writing any image tags.** If you know you'll use external images, add the CSP entry to `pulse.config.js` first — discovering the block at Lighthouse time costs an extra round-trip.
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
// pulse.config.js
|
|
204
|
+
export default {
|
|
205
|
+
csp: {
|
|
206
|
+
'img-src': ['https://picsum.photos', 'https://fastly.picsum.photos'],
|
|
207
|
+
},
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Or via `createServer`:
|
|
212
|
+
|
|
213
|
+
```js
|
|
214
|
+
createServer(specs, {
|
|
215
|
+
csp: {
|
|
216
|
+
'img-src': ['https://images.unsplash.com', 'https://picsum.photos'],
|
|
217
|
+
},
|
|
218
|
+
})
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Common hosts to add:**
|
|
222
|
+
| Source | `img-src` entry |
|
|
223
|
+
|---|---|
|
|
224
|
+
| picsum.photos | `https://picsum.photos https://fastly.picsum.photos` |
|
|
225
|
+
| Unsplash | `https://images.unsplash.com` |
|
|
226
|
+
| Cloudinary | `https://res.cloudinary.com` |
|
|
227
|
+
| Imgix | your subdomain, e.g. `https://mysite.imgix.net` |
|
|
228
|
+
|
|
229
|
+
> **picsum CDN note:** `picsum.photos` redirects image requests through `fastly.picsum.photos`. You must whitelist **both** domains — whitelisting only `https://picsum.photos` will still block the actual image bytes and the browser error will point to the `fastly.picsum.photos` URL, which is confusing to debug.
|
|
230
|
+
|
|
231
|
+
> **⚠ Unsplash and third-party cookies — Lighthouse Best Practices failure:** `images.unsplash.com` sets tracking cookies (`azk`, `azk-ss`) on every image request. These are classified as third-party cookies and cause Lighthouse Best Practices to drop to ~77, failing the 100 pass bar. **For production builds, download images locally instead of referencing Unsplash URLs directly.** Use `curl` to save images to `public/images/`:
|
|
232
|
+
> ```bash
|
|
233
|
+
> curl -sL "https://images.unsplash.com/photo-ID?w=900&q=85" -o public/images/hero.jpg
|
|
234
|
+
> ```
|
|
235
|
+
> Then reference `/images/hero.jpg` in your spec. This also improves performance (no external DNS lookup) and avoids rate limiting. `picsum.photos` does **not** set tracking cookies and is safe to use via CDN for prototypes.
|
|
236
|
+
|
|
237
|
+
Without this, external images will be blocked in production and Lighthouse will flag a Best Practices failure. Use the same pattern for other external resource types (`media-src` for video, `connect-src` for fetch/XHR to external APIs).
|
|
238
|
+
|
|
122
239
|
For multiple weights or italic variants, separate them with a semicolon in the URL:
|
|
123
240
|
```
|
|
124
241
|
?family=Inter:ital,wght@0,400;0,700;1,400&display=swap
|
|
@@ -171,6 +288,33 @@ For multi-brand sites, keep `@font-face` declarations (or the font service URL)
|
|
|
171
288
|
|
|
172
289
|
## CSS rules — where to put styles and when to use utilities
|
|
173
290
|
|
|
291
|
+
### The hex colour rule — know this upfront
|
|
292
|
+
|
|
293
|
+
There are three places you write colour values. The rules are different for each:
|
|
294
|
+
|
|
295
|
+
| Where | Hex/raw values | `var(--ui-*)` tokens | Inline `style=""` |
|
|
296
|
+
|---|---|---|---|
|
|
297
|
+
| `public/theme.css` or `public/themes/*.css` | **✓ allowed** — this is where palette values live | ✓ allowed | — |
|
|
298
|
+
| `app.css` or any `.css` file in `public/` | **✗ blocked by lint** | **✓ required** | — |
|
|
299
|
+
| JS spec files (the `view` function) | **✓ allowed** (e.g. `color: '#fff'` on component props) | ✓ allowed | **✗ avoid** |
|
|
300
|
+
|
|
301
|
+
The rule in plain English: **CSS files reference tokens; theme files define them.** JS spec files are exempt because component props like `hero({ background: '#1a1a2e' })` are intentional design overrides, not CSS authoring.
|
|
302
|
+
|
|
303
|
+
**Common mistake:** Writing `--ui-accent: #e25;` directly in `app.css`. This trips the lint. Move it to `public/theme.css` (dark theme in `:root`) or `public/themes/brand.css` (light theme in `[data-theme="light"]`).
|
|
304
|
+
|
|
305
|
+
```css
|
|
306
|
+
/* public/theme.css — hex values live here ✓ */
|
|
307
|
+
[data-theme="light"] { --ui-accent: #e25; }
|
|
308
|
+
|
|
309
|
+
/* app.css — var() only ✓ */
|
|
310
|
+
.hero { color: var(--ui-accent); }
|
|
311
|
+
|
|
312
|
+
/* app.css — this will be blocked ✗ */
|
|
313
|
+
.hero { color: #e25; }
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Load order in `meta.styles` matters: `pulse-ui.css` → `theme.css` → `app.css`. Theme tokens must exist before `app.css` references them.
|
|
317
|
+
|
|
174
318
|
RULE: Never use inline style attributes (style="...") in HTML. Always use classes.
|
|
175
319
|
|
|
176
320
|
RULE: For spacing, typography, layout, and colour, always prefer pulse-ui utility classes first. Only add to app.css if you need something the utilities cannot provide (e.g. a unique component style, a keyframe animation, a custom grid).
|
|
@@ -218,3 +362,60 @@ Example — a centred hero block using only utilities, no custom CSS:
|
|
|
218
362
|
```
|
|
219
363
|
|
|
220
364
|
When you DO need to write CSS, add it to public/app.css — never inline.
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
## Placeholder images for prototypes
|
|
368
|
+
|
|
369
|
+
**Use `picsum.photos` with numeric IDs for anything Lighthouse-audited.** Unsplash direct IDs rotate and 404 without warning. Picsum seeds are convenient for development but can return `ERR_CONNECTION_CLOSED` under Lighthouse's burst-load pattern — use numeric IDs for pages you'll audit:
|
|
370
|
+
|
|
371
|
+
```html
|
|
372
|
+
<!-- Best for Lighthouse: numeric IDs — stable under burst load -->
|
|
373
|
+
<img src="https://picsum.photos/id/10/1200/600" alt="..." width="1200" height="600">
|
|
374
|
+
<img src="https://picsum.photos/id/64/80/80" alt="..." width="80" height="80">
|
|
375
|
+
|
|
376
|
+
<!-- OK for dev / visual work only: seeds can fail under Lighthouse burst -->
|
|
377
|
+
<img src="https://picsum.photos/seed/hero/1200/600" alt="..." width="1200" height="600">
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
The numeric ID is sequential (1–1000+). Browse options at `https://picsum.photos/images`. Use numeric IDs whenever you're running Lighthouse — seeds are fine for screenshots and dev iteration.
|
|
381
|
+
|
|
382
|
+
**Avoid:** `https://images.unsplash.com/photo-LONGID?...` — these are unstable for prototypes. If you use Unsplash, add `https://images.unsplash.com` to `csp.img-src` in `pulse.config.js`, and expect some IDs to rot.
|
|
383
|
+
|
|
384
|
+
> **Unsplash photo IDs must be the full hash.** The format is `photo-` followed by an 11-character alphanumeric hash, e.g. `photo-1506905925346-21bda4d32df4`. A truncated or partial ID (e.g. `photo-1506905925346`) returns a 404 silently — the image is missing with no obvious error. Always copy the full ID from the Unsplash URL.
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
## CSS gotchas
|
|
388
|
+
|
|
389
|
+
### calc() with + and − requires whitespace
|
|
390
|
+
|
|
391
|
+
The CSS specification **requires** whitespace around `+` and `−` inside `calc()` when either operand is a value that starts with a sign (including CSS custom properties, since their resolved value may start with `−`):
|
|
392
|
+
|
|
393
|
+
```css
|
|
394
|
+
/* ✓ CORRECT — spaces around + */
|
|
395
|
+
padding-top: calc(64px + var(--ui-space-16));
|
|
396
|
+
|
|
397
|
+
/* ✗ WRONG — will silently fail in browsers, padding becomes 0 */
|
|
398
|
+
padding-top: calc(64px+var(--ui-space-16));
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
The Pulse production CSS minifier preserves these spaces. Never manually collapse `calc()` expressions.
|
|
402
|
+
|
|
403
|
+
### Padding shorthand vs. longhand ordering
|
|
404
|
+
|
|
405
|
+
CSS processes declarations top-to-bottom. A shorthand that appears **after** a longhand will silently override it:
|
|
406
|
+
|
|
407
|
+
```css
|
|
408
|
+
/* ✗ WRONG — padding-top: 0 because shorthand resets it */
|
|
409
|
+
.block {
|
|
410
|
+
padding-top: var(--ui-space-8); /* set */
|
|
411
|
+
padding: 0 var(--ui-space-6); /* shorthand resets padding-top to 0 */
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/* ✓ CORRECT — shorthand first, longhand overrides */
|
|
415
|
+
.block {
|
|
416
|
+
padding: 0 var(--ui-space-6);
|
|
417
|
+
padding-top: var(--ui-space-8);
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
This produces no browser error and is invisible at a glance — the expected spacing just doesn't appear. Always write shorthand before longhand in the same declaration block.
|