@invisibleloop/pulse 0.14.2 → 0.15.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.
- package/.claude/commands/verify.md +10 -4
- package/docs/.claude/pulse-checklist.md +5 -3
- package/docs/public/.pulse-ui-version +1 -1
- package/docs/public/docs.css +236 -0
- package/docs/public/home-brut.css +998 -0
- package/docs/public/menu.js +237 -6
- package/docs/public/pulse-ui.css +44 -0
- package/docs/public/theme.css +8 -0
- package/docs/server.js +24 -0
- package/docs/src/lib/layout.js +51 -3
- package/docs/src/pages/getting-started.js +1 -1
- package/docs/src/pages/home.js +231 -312
- package/docs/src/pages/how-it-works.js +67 -12
- package/package.json +1 -1
- package/public/.pulse-ui-version +1 -1
- package/scripts/build.js +127 -21
- package/src/agent/commands/verify.md +2 -6
- package/src/agent/guide-components.md +1 -1
- package/src/agent/guide-examples.md +310 -32
- package/src/agent/skills/verify/SKILL.md +2 -4
- package/src/agent/workflow.md +3 -1
- package/src/cli/dev.js +43 -0
- package/src/mcp/server.js +79 -27
- package/src/mcp/validate-worker.js +6 -0
- package/src/agent/guide.md +0 -1087
|
@@ -87,13 +87,20 @@ export default {
|
|
|
87
87
|
<p>Together these replace the need to paste documentation into a system prompt or rely on the agent's training data to know how Pulse works.</p>
|
|
88
88
|
|
|
89
89
|
${section('resources', 'Resources: what the agent knows')}
|
|
90
|
-
<p>The MCP server exposes
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
<
|
|
94
|
-
|
|
90
|
+
<p>The MCP server exposes read-only documents the agent fetches at any time. There are two top-level resources and a set of focused guide sections:</p>
|
|
91
|
+
|
|
92
|
+
<dl class="definition-list">
|
|
93
|
+
<dt><code>pulse://quickstart</code></dt>
|
|
94
|
+
<dd>Workflow phases, spec skeleton, component rules, and theming essentials combined into a single fetch. Used for targeted edits, bug fixes, or simple one-shot page builds — avoids loading the full guide for small tasks.</dd>
|
|
95
|
+
|
|
96
|
+
<dt><code>pulse://workflow</code></dt>
|
|
97
|
+
<dd>The full build workflow — every phase, pass gate, and ordering rule. The agent reads this before starting any new page or project.</dd>
|
|
98
|
+
|
|
99
|
+
<dt><code>pulse://persona</code></dt>
|
|
100
|
+
<dd>The quality bar the agent holds itself to: always write correct SSR, always handle errors, never skip empty states, never hardcode colours, never use <code>data-event</code> on text inputs. The persona defines what "done" means.</dd>
|
|
101
|
+
</dl>
|
|
95
102
|
|
|
96
|
-
<p>The guide sections
|
|
103
|
+
<p>The guide is split into topic sections the agent fetches as needed:</p>
|
|
97
104
|
<dl class="definition-list">
|
|
98
105
|
<dt><code>pulse://guide/spec</code></dt>
|
|
99
106
|
<dd>Spec format — state, mutations, actions, streaming SSR, validation, key rules, and form layout.</dd>
|
|
@@ -112,12 +119,51 @@ export default {
|
|
|
112
119
|
|
|
113
120
|
<dt><code>pulse://guide/examples</code></dt>
|
|
114
121
|
<dd>Complete working page examples covering common patterns.</dd>
|
|
122
|
+
|
|
123
|
+
<dt><code>pulse://guide/templates</code></dt>
|
|
124
|
+
<dd>Pre-built landing page templates, adaptation rules, and theme CSS patterns. Fetched when building a new branded site or landing page.</dd>
|
|
125
|
+
|
|
126
|
+
<dt><code>pulse://guide/design-references</code></dt>
|
|
127
|
+
<dd>Twelve design directions with vibes, component combinations, palette patterns, and signature moves. Fetched during intake to pick the right aesthetic rather than defaulting to a generic SaaS look.</dd>
|
|
128
|
+
|
|
129
|
+
<dt><code>pulse://guide/design-gallery</code></dt>
|
|
130
|
+
<dd>All templates with visual descriptions and key components. Includes critical prop-name references and component recipes for cards, stat strips, and forms. Fetched when adapting a template or combining components.</dd>
|
|
131
|
+
|
|
132
|
+
<dt><code>pulse://guide/explore</code></dt>
|
|
133
|
+
<dd>Zone-based layout thinking, seven structural gestures, raw HTML patterns without components, and an anti-pattern checklist. Fetched when a distinctive or unusual layout is needed.</dd>
|
|
115
134
|
</dl>
|
|
116
135
|
|
|
117
136
|
${callout('note', 'The guide is authoritative. When there is a question about how something should be structured — where state lives, how validation is wired, how streaming works — the guide answers it. The agent does not guess.')}
|
|
118
137
|
|
|
119
138
|
${section('tools', 'Tools: what the agent can do')}
|
|
120
|
-
<p>The MCP server provides tools across
|
|
139
|
+
<p>The MCP server provides tools across five categories:</p>
|
|
140
|
+
|
|
141
|
+
<h3 class="doc-h3">Design & intake</h3>
|
|
142
|
+
<dl class="definition-list">
|
|
143
|
+
<dt><code>pulse_extract_inspiration</code></dt>
|
|
144
|
+
<dd>Extracts a structured design brief from a URL or image shared as inspiration. Called before <code>pulse_intake</code> when the user says "I like the look of X" — it tells the agent exactly what to observe and capture.</dd>
|
|
145
|
+
|
|
146
|
+
<dt><code>pulse_intake</code></dt>
|
|
147
|
+
<dd>Captures the product name, pitch, features, palette, vibe, and anti-style before scaffolding any new page or branded template. Returns a structured brief with copy-ready content, early contrast warnings, and component suggestions. Always runs before <code>pulse_sketch</code>.</dd>
|
|
148
|
+
|
|
149
|
+
<dt><code>pulse_sketch</code></dt>
|
|
150
|
+
<dd>Generates three structurally distinct layout directions (full-bleed, asymmetric split, editorial flow, etc.) before any code is written. Called after <code>pulse_intake</code> — prevents every project from defaulting to the same centred-hero layout.</dd>
|
|
151
|
+
|
|
152
|
+
<dt><code>pulse_intent</code></dt>
|
|
153
|
+
<dd>Intent engine — describe what you want to build and get back a matched archetype, recommended components, a ready-to-adapt spec scaffold, and which guide sections to read first. An alternative entry point to <code>pulse://workflow</code> for targeted tasks.</dd>
|
|
154
|
+
|
|
155
|
+
<dt><code>pulse_design_review</code></dt>
|
|
156
|
+
<dd>Reviews the built page's visual design against the original product brief from <code>pulse_intake</code>. Checks whether the design looks and feels appropriate for the product, audience, and vibe. Runs after screenshot, before Lighthouse.</dd>
|
|
157
|
+
|
|
158
|
+
<dt><code>pulse_layout_review</code></dt>
|
|
159
|
+
<dd>Multi-viewport layout review — checks for overflow, collapsed sections, and broken images at different screen widths. Runs after design approval but before Lighthouse, catching layout issues that Lighthouse doesn't flag.</dd>
|
|
160
|
+
|
|
161
|
+
<dt><code>pulse_tokens</code></dt>
|
|
162
|
+
<dd>Lists all available <code>--ui-*</code> CSS custom property tokens from the installed <code>pulse-ui.css</code>. Used before writing theme overrides to avoid guessing token names.</dd>
|
|
163
|
+
|
|
164
|
+
<dt><code>pulse_check_contrast</code></dt>
|
|
165
|
+
<dd>Static WCAG contrast checker. Extracts colour variable definitions from a theme file and checks common foreground/background pairings for WCAG AA compliance. Run immediately after writing a theme file to catch palette mistakes before Lighthouse.</dd>
|
|
166
|
+
</dl>
|
|
121
167
|
|
|
122
168
|
<h3 class="doc-h3">Scaffolding</h3>
|
|
123
169
|
<dl class="definition-list">
|
|
@@ -136,11 +182,17 @@ export default {
|
|
|
136
182
|
|
|
137
183
|
<h3 class="doc-h3">Inspection</h3>
|
|
138
184
|
<dl class="definition-list">
|
|
185
|
+
<dt><code>pulse_status</code></dt>
|
|
186
|
+
<dd>Project health snapshot — pages, routes, server status, last build. Called at the start of a session to orient quickly without reading files individually.</dd>
|
|
187
|
+
|
|
139
188
|
<dt><code>pulse_list_structure</code></dt>
|
|
140
189
|
<dd>Lists every page, component, and store that already exists in the project. The agent calls this before creating anything — to avoid duplication and to understand what the codebase already provides.</dd>
|
|
141
190
|
|
|
142
191
|
<dt><code>pulse_fetch_page</code></dt>
|
|
143
192
|
<dd>Reads the full source of an existing spec file. Used when the agent needs to understand an existing page before editing it, or when a review of the current state is needed.</dd>
|
|
193
|
+
|
|
194
|
+
<dt><code>pulse_list_icons</code></dt>
|
|
195
|
+
<dd>Lists all available Pulse UI icon names, grouped by category. Called before importing icons — the agent never guesses a name.</dd>
|
|
144
196
|
</dl>
|
|
145
197
|
|
|
146
198
|
<h3 class="doc-h3">Validation & review</h3>
|
|
@@ -148,26 +200,29 @@ export default {
|
|
|
148
200
|
<dt><code>pulse_validate</code></dt>
|
|
149
201
|
<dd>Validates a spec against the Pulse schema. Returns errors (which block progress) and warnings (which must also be resolved). The agent calls this after writing or editing every spec file.</dd>
|
|
150
202
|
|
|
203
|
+
<dt><code>pulse_suggest</code></dt>
|
|
204
|
+
<dd>Draft-mode feedback — paste a partial or complete spec and get constructive suggestions before running the hard validator. Unlike <code>pulse_validate</code>, it is collaborative rather than a gate: it notices patterns, spots likely omissions, and offers ideas. Used mid-build for a second opinion.</dd>
|
|
205
|
+
|
|
151
206
|
<dt><code>pulse_review</code></dt>
|
|
152
207
|
<dd>Switches the agent into reviewer mode. Returns the spec source, the rendered HTML output, and a structured checklist covering accessibility, empty states, error handling, component usage, security, and correctness. The agent reads its own output critically and fixes every issue before continuing.</dd>
|
|
208
|
+
|
|
209
|
+
<dt><code>pulse_run_tests</code></dt>
|
|
210
|
+
<dd>Runs the project test suite (<code>npm test</code>) and returns the full output. Called after writing or editing specs to verify nothing is broken.</dd>
|
|
153
211
|
</dl>
|
|
154
212
|
|
|
155
|
-
<h3 class="doc-h3">Server</h3>
|
|
213
|
+
<h3 class="doc-h3">Server & maintenance</h3>
|
|
156
214
|
<dl class="definition-list">
|
|
157
215
|
<dt><code>pulse_restart_server</code></dt>
|
|
158
216
|
<dd>Restarts the Pulse dev server. Called after structural changes — adding a new page, modifying the server entry — to reload all specs and re-register routes.</dd>
|
|
159
217
|
|
|
160
218
|
<dt><code>pulse_build</code></dt>
|
|
161
219
|
<dd>Builds the project for production. Bundles specs, hashes assets, and writes the manifest. Called when the agent needs to verify the production build or prepare a release.</dd>
|
|
162
|
-
</dl>
|
|
163
220
|
|
|
164
|
-
<h3 class="doc-h3">Maintenance</h3>
|
|
165
|
-
<dl class="definition-list">
|
|
166
221
|
<dt><code>pulse_check_version</code></dt>
|
|
167
222
|
<dd>Reports the installed Pulse version and whether an update is available.</dd>
|
|
168
223
|
|
|
169
224
|
<dt><code>pulse_update</code></dt>
|
|
170
|
-
<dd>Updates the Pulse package to the latest version.</dd>
|
|
225
|
+
<dd>Updates the Pulse package to the latest version and copies the latest UI assets and agent files into the project.</dd>
|
|
171
226
|
</dl>
|
|
172
227
|
|
|
173
228
|
${section('flow', 'What happens when you ask for something')}
|
package/package.json
CHANGED
package/public/.pulse-ui-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.15.1
|
package/scripts/build.js
CHANGED
|
@@ -19,7 +19,7 @@ import path from 'path'
|
|
|
19
19
|
import { createHash } from 'crypto'
|
|
20
20
|
import { discoverPages } from '../src/cli/discover.js'
|
|
21
21
|
import { renderToString } from '../src/runtime/ssr.js'
|
|
22
|
-
import { c, header, table, ok, fail, info, elapsed, icon } from '../src/cli/fmt.js'
|
|
22
|
+
import { c, header, table, ok, fail, info, warn, elapsed, icon } from '../src/cli/fmt.js'
|
|
23
23
|
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
25
25
|
// Server-only property stripping
|
|
@@ -380,6 +380,23 @@ const ROOT = rootArg !== -1
|
|
|
380
380
|
const OUT_DIR = path.join(ROOT, 'public', 'dist')
|
|
381
381
|
const TMP_DIR = path.join(ROOT, '.pulse-build')
|
|
382
382
|
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
// Load pulse.config.js (optional)
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
|
|
387
|
+
let _projectConfig = {}
|
|
388
|
+
const _configPath = path.join(ROOT, 'pulse.config.js')
|
|
389
|
+
if (fs.existsSync(_configPath)) {
|
|
390
|
+
try { _projectConfig = (await import(_configPath)).default ?? {} } catch { /* ignore */ }
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// css.safelist — array of class name strings or RegExp patterns to always keep
|
|
394
|
+
// even if the purger would otherwise remove them.
|
|
395
|
+
// Example in pulse.config.js:
|
|
396
|
+
// export default { css: { safelist: ['my-class', /^js-/] } }
|
|
397
|
+
const CSS_SAFELIST = (_projectConfig.css?.safelist ?? [])
|
|
398
|
+
.map(entry => typeof entry === 'string' ? entry : entry)
|
|
399
|
+
|
|
383
400
|
// ---------------------------------------------------------------------------
|
|
384
401
|
// Discover pages
|
|
385
402
|
// ---------------------------------------------------------------------------
|
|
@@ -516,7 +533,7 @@ fs.rmSync(TMP_DIR, { recursive: true })
|
|
|
516
533
|
// CSS Purge — strip unused styles, write content-hashed CSS bundles
|
|
517
534
|
// ---------------------------------------------------------------------------
|
|
518
535
|
|
|
519
|
-
await purgeCssStep(pages, manifest, ROOT, OUT_DIR)
|
|
536
|
+
const cssPurgeStats = await purgeCssStep(pages, manifest, ROOT, OUT_DIR)
|
|
520
537
|
|
|
521
538
|
// ---------------------------------------------------------------------------
|
|
522
539
|
// JS static assets — minify + hash any .js files directly in public/
|
|
@@ -556,13 +573,17 @@ await jsAssetsStep(ROOT, OUT_DIR, manifest)
|
|
|
556
573
|
c.dim(bundle.replace('/dist/', '')),
|
|
557
574
|
`${sizeKb} kB`,
|
|
558
575
|
c.dim('js'),
|
|
576
|
+
'',
|
|
559
577
|
])
|
|
560
578
|
} else if (ext === '.css') {
|
|
579
|
+
const pct = cssPurgeStats?.[src]
|
|
580
|
+
const pctLabel = pct != null ? c.dim(`−${pct}%`) : ''
|
|
561
581
|
rows.push([
|
|
562
582
|
c.dim(src),
|
|
563
583
|
c.dim(bundle.replace('/dist/', '')),
|
|
564
584
|
`${sizeKb} kB`,
|
|
565
585
|
c.dim('css'),
|
|
586
|
+
pctLabel,
|
|
566
587
|
])
|
|
567
588
|
}
|
|
568
589
|
}
|
|
@@ -577,9 +598,9 @@ await jsAssetsStep(ROOT, OUT_DIR, manifest)
|
|
|
577
598
|
})
|
|
578
599
|
|
|
579
600
|
console.log('\n' + table(
|
|
580
|
-
['Route / Asset', 'Bundle', 'Size', 'Type'],
|
|
601
|
+
['Route / Asset', 'Bundle', 'Size', 'Type', 'Purged'],
|
|
581
602
|
rows,
|
|
582
|
-
{ align: ['left', 'left', 'right', 'left'] }
|
|
603
|
+
{ align: ['left', 'left', 'right', 'left', 'right'] }
|
|
583
604
|
))
|
|
584
605
|
|
|
585
606
|
const totalMs = Date.now() - BUILD_START
|
|
@@ -599,9 +620,28 @@ async function jsAssetsStep(root, outDir, manifest) {
|
|
|
599
620
|
|
|
600
621
|
console.log(info('Hashing static JS assets…'))
|
|
601
622
|
|
|
623
|
+
// Collect fetch/XHR origins used in static JS for CSP connect-src checking
|
|
624
|
+
const cspConnectSrc = new Set([
|
|
625
|
+
...(_projectConfig.csp?.['connect-src'] ?? []),
|
|
626
|
+
'self',
|
|
627
|
+
])
|
|
628
|
+
|
|
602
629
|
for (const file of jsFiles) {
|
|
603
630
|
const srcPath = path.join(publicDir, file)
|
|
604
631
|
const source = fs.readFileSync(srcPath, 'utf8')
|
|
632
|
+
|
|
633
|
+
// Warn if the file contains fetch/XMLHttpRequest calls to origins not in
|
|
634
|
+
// the project's CSP connect-src — these will silently fail in production.
|
|
635
|
+
const fetchUrls = [...source.matchAll(/fetch\s*\(\s*['"`](https?:\/\/[^'"`]+)['"`]/g)].map(m => m[1])
|
|
636
|
+
for (const url of fetchUrls) {
|
|
637
|
+
try {
|
|
638
|
+
const origin = new URL(url).origin
|
|
639
|
+
if (!cspConnectSrc.has(origin) && !cspConnectSrc.has('*')) {
|
|
640
|
+
console.log(warn(`${file}: fetch to ${origin} may be blocked — add to csp.connect-src in pulse.config.js`))
|
|
641
|
+
}
|
|
642
|
+
} catch { /* ignore malformed URLs */ }
|
|
643
|
+
}
|
|
644
|
+
|
|
605
645
|
const { code } = await esbuild.transform(source, { minify: true, target: 'es2018' })
|
|
606
646
|
const hash = createHash('sha256').update(code).digest('hex').slice(0, 8)
|
|
607
647
|
const name = path.basename(file, '.js')
|
|
@@ -610,8 +650,6 @@ async function jsAssetsStep(root, outDir, manifest) {
|
|
|
610
650
|
fs.writeFileSync(path.join(outDir, outName), code)
|
|
611
651
|
manifest[`/${file}`] = `/dist/${outName}`
|
|
612
652
|
|
|
613
|
-
const origKb = (Buffer.byteLength(source) / 1024).toFixed(1)
|
|
614
|
-
const minKb = (Buffer.byteLength(code) / 1024).toFixed(1)
|
|
615
653
|
// silent — summary table covers this
|
|
616
654
|
}
|
|
617
655
|
|
|
@@ -629,6 +667,7 @@ async function purgeCssStep(pages, manifest, root, outDir) {
|
|
|
629
667
|
const htmlContents = []
|
|
630
668
|
const jsContents = []
|
|
631
669
|
const cssFiles = new Set()
|
|
670
|
+
const cssStats = {}
|
|
632
671
|
|
|
633
672
|
for (const { filePath } of pages) {
|
|
634
673
|
try {
|
|
@@ -667,6 +706,16 @@ async function purgeCssStep(pages, manifest, root, outDir) {
|
|
|
667
706
|
}
|
|
668
707
|
}
|
|
669
708
|
|
|
709
|
+
// Scan public/*.js static assets — these often contain classList.toggle() or
|
|
710
|
+
// classList.add() calls that reference CSS classes not present in any page spec,
|
|
711
|
+
// which would otherwise be incorrectly purged from the built CSS.
|
|
712
|
+
const publicDir = path.join(root, 'public')
|
|
713
|
+
if (fs.existsSync(publicDir)) {
|
|
714
|
+
for (const f of fs.readdirSync(publicDir).filter(f => f.endsWith('.js'))) {
|
|
715
|
+
try { jsContents.push(fs.readFileSync(path.join(publicDir, f), 'utf8')) } catch {}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
670
719
|
// Scan pulse UI package source — class names generated by UI components
|
|
671
720
|
// (breadcrumbs, avatar, container, etc.) only appear in the package source,
|
|
672
721
|
// not in the project's own pages, so they'd otherwise be purged.
|
|
@@ -687,10 +736,10 @@ async function purgeCssStep(pages, manifest, root, outDir) {
|
|
|
687
736
|
|
|
688
737
|
if (cssFiles.size === 0) {
|
|
689
738
|
console.log(info('No CSS files referenced — skipping.'))
|
|
690
|
-
return
|
|
739
|
+
return cssStats
|
|
691
740
|
}
|
|
692
741
|
|
|
693
|
-
const usedClasses = extractUsedClasses(htmlContents, jsContents)
|
|
742
|
+
const usedClasses = extractUsedClasses(htmlContents, jsContents, CSS_SAFELIST)
|
|
694
743
|
|
|
695
744
|
for (const cssHref of cssFiles) {
|
|
696
745
|
// CSS path is relative to public/ (e.g. '/pulse-ui.css' → public/pulse-ui.css)
|
|
@@ -698,6 +747,21 @@ async function purgeCssStep(pages, manifest, root, outDir) {
|
|
|
698
747
|
if (!fs.existsSync(cssPath)) continue
|
|
699
748
|
|
|
700
749
|
const original = fs.readFileSync(cssPath, 'utf8')
|
|
750
|
+
|
|
751
|
+
// Warn about classes defined in this CSS file that weren't found in any
|
|
752
|
+
// scanned source — these will be purged and may indicate a missing scan source
|
|
753
|
+
// (e.g. a JS file that toggles classes via classList but wasn't picked up).
|
|
754
|
+
// Only warn for project-owned CSS files (not pulse-ui.css framework styles).
|
|
755
|
+
if (!cssHref.includes('pulse-ui')) {
|
|
756
|
+
const definedClasses = [...original.matchAll(/\.([a-zA-Z][a-zA-Z0-9_-]*)\s*[{,:\[]/g)]
|
|
757
|
+
.map(m => m[1])
|
|
758
|
+
.filter((cls, i, arr) => arr.indexOf(cls) === i) // dedupe
|
|
759
|
+
const purgedClasses = definedClasses.filter(cls => !usedClasses.has(cls))
|
|
760
|
+
if (purgedClasses.length > 0) {
|
|
761
|
+
console.log(warn(`${cssHref}: ${purgedClasses.length} class${purgedClasses.length === 1 ? '' : 'es'} purged (not found in any scanned source): ${purgedClasses.slice(0, 5).map(c => `.${c}`).join(', ')}${purgedClasses.length > 5 ? ` … +${purgedClasses.length - 5} more` : ''}`))
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
701
765
|
const purged = minifyCss(purgeCss(original, usedClasses))
|
|
702
766
|
|
|
703
767
|
const hash = createHash('sha256').update(purged).digest('hex').slice(0, 8)
|
|
@@ -707,25 +771,24 @@ async function purgeCssStep(pages, manifest, root, outDir) {
|
|
|
707
771
|
|
|
708
772
|
fs.writeFileSync(outPath, purged)
|
|
709
773
|
|
|
710
|
-
const
|
|
711
|
-
const
|
|
712
|
-
const pct
|
|
713
|
-
|
|
714
|
-
// silent — summary table covers this
|
|
774
|
+
const origBytes = Buffer.byteLength(original)
|
|
775
|
+
const purgBytes = Buffer.byteLength(purged)
|
|
776
|
+
const pct = Math.round((1 - purgBytes / origBytes) * 100)
|
|
777
|
+
cssStats[cssHref] = pct
|
|
715
778
|
|
|
716
779
|
manifest[cssHref] = `/dist/${outName}`
|
|
717
780
|
}
|
|
718
781
|
|
|
719
782
|
// Re-write manifest with CSS entries added
|
|
720
783
|
fs.writeFileSync(path.join(outDir, 'manifest.json'), JSON.stringify(manifest, null, 2))
|
|
721
|
-
|
|
784
|
+
return cssStats
|
|
722
785
|
}
|
|
723
786
|
|
|
724
787
|
// ---------------------------------------------------------------------------
|
|
725
788
|
// CSS class extractor
|
|
726
789
|
// ---------------------------------------------------------------------------
|
|
727
790
|
|
|
728
|
-
function extractUsedClasses(htmlContents, jsContents) {
|
|
791
|
+
function extractUsedClasses(htmlContents, jsContents, safelist = []) {
|
|
729
792
|
const used = new Set()
|
|
730
793
|
|
|
731
794
|
// Extract from class="..." in both rendered HTML and JS template literals
|
|
@@ -751,6 +814,21 @@ function extractUsedClasses(htmlContents, jsContents) {
|
|
|
751
814
|
}
|
|
752
815
|
}
|
|
753
816
|
|
|
817
|
+
// Apply safelist from pulse.config.js — css.safelist accepts strings or RegExp.
|
|
818
|
+
// String entries are added directly; RegExp entries are matched against all
|
|
819
|
+
// class names defined in the CSS (resolved later in purgeCss via selectorUsed
|
|
820
|
+
// because we don't have the CSS text here). To handle RegExp, we mark them as
|
|
821
|
+
// a sentinel that selectorUsed checks separately.
|
|
822
|
+
for (const entry of safelist) {
|
|
823
|
+
if (typeof entry === 'string') {
|
|
824
|
+
used.add(entry)
|
|
825
|
+
}
|
|
826
|
+
// RegExp entries are stored on the Set as a symbol property for selectorUsed to check
|
|
827
|
+
}
|
|
828
|
+
if (safelist.some(e => e instanceof RegExp)) {
|
|
829
|
+
used._safelistPatterns = safelist.filter(e => e instanceof RegExp)
|
|
830
|
+
}
|
|
831
|
+
|
|
754
832
|
return used
|
|
755
833
|
}
|
|
756
834
|
|
|
@@ -820,7 +898,17 @@ function parseCssBlocks(css) {
|
|
|
820
898
|
|
|
821
899
|
/**
|
|
822
900
|
* Return true if any selector in the rule should be kept.
|
|
823
|
-
*
|
|
901
|
+
*
|
|
902
|
+
* Matching strategy: check the *rightmost* class token in the selector (the
|
|
903
|
+
* subject of the rule). A rule like `.parent .child` should survive only when
|
|
904
|
+
* `.child` is used — keeping it whenever `.parent` is used would retain dead
|
|
905
|
+
* rules whose actual target class was never referenced.
|
|
906
|
+
*
|
|
907
|
+
* Pseudo-classes/elements and attribute selectors are stripped before checking
|
|
908
|
+
* so `.btn:hover` is treated as `.btn`, `.btn--active::before` as `.btn--active`.
|
|
909
|
+
*
|
|
910
|
+
* Element-only selectors (`a`, `*`, `:root`) and rules that contain no class
|
|
911
|
+
* tokens at all are always kept.
|
|
824
912
|
*/
|
|
825
913
|
function selectorUsed(selector, usedClasses) {
|
|
826
914
|
const selectors = selector.split(',').map(s => s.trim())
|
|
@@ -832,12 +920,21 @@ function selectorUsed(selector, usedClasses) {
|
|
|
832
920
|
.replace(/\[[^\]]*\]/g, '')
|
|
833
921
|
.trim()
|
|
834
922
|
|
|
835
|
-
// Keep element selectors
|
|
923
|
+
// Keep element-only selectors — no class tokens present
|
|
836
924
|
if (!base.includes('.')) return true
|
|
837
925
|
|
|
838
|
-
//
|
|
839
|
-
|
|
840
|
-
|
|
926
|
+
// Extract all class tokens and check the rightmost one (the rule subject).
|
|
927
|
+
// Using the rightmost class correctly handles descendant selectors:
|
|
928
|
+
// .nav .link → subject is .link
|
|
929
|
+
// .btn--active → subject is .btn--active
|
|
930
|
+
const classes = [...base.matchAll(/\.([a-zA-Z][a-zA-Z0-9_-]*)/g)].map(m => m[1])
|
|
931
|
+
if (classes.length === 0) return true
|
|
932
|
+
const subject = classes[classes.length - 1]
|
|
933
|
+
if (usedClasses.has(subject)) return true
|
|
934
|
+
|
|
935
|
+
// Check RegExp safelist patterns if any were provided
|
|
936
|
+
const patterns = usedClasses._safelistPatterns
|
|
937
|
+
if (patterns && patterns.some(re => re.test(subject))) return true
|
|
841
938
|
}
|
|
842
939
|
|
|
843
940
|
return false
|
|
@@ -868,9 +965,18 @@ function purgeCss(cssText, usedClasses) {
|
|
|
868
965
|
const p = block.prelude.toLowerCase()
|
|
869
966
|
if (/^@(?:keyframes|font-face|charset)/.test(p)) {
|
|
870
967
|
kept.push(block.raw)
|
|
871
|
-
} else if (/^@(?:media|supports
|
|
968
|
+
} else if (/^@(?:media|supports)/.test(p)) {
|
|
872
969
|
const innerPurged = purgeCss(block.inner, usedClasses)
|
|
873
970
|
if (innerPurged.trim()) kept.push(`${block.prelude} {\n${innerPurged}\n}`)
|
|
971
|
+
} else if (/^@layer/.test(p)) {
|
|
972
|
+
// Always preserve empty @layer declarations — they establish cascade order
|
|
973
|
+
// and removing them silently changes specificity even when no rules remain.
|
|
974
|
+
const innerPurged = purgeCss(block.inner, usedClasses)
|
|
975
|
+
if (innerPurged.trim()) {
|
|
976
|
+
kept.push(`${block.prelude} {\n${innerPurged}\n}`)
|
|
977
|
+
} else {
|
|
978
|
+
kept.push(`${block.prelude} {}`)
|
|
979
|
+
}
|
|
874
980
|
} else {
|
|
875
981
|
kept.push(block.raw) // unknown at-blocks — keep to be safe
|
|
876
982
|
}
|
|
@@ -54,13 +54,9 @@ Use `mcp__chrome-devtools__list_pages` to get all open pages, then call `mcp__ch
|
|
|
54
54
|
|
|
55
55
|
### 11. Write verification stamp
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
Call `pulse_stamp`. This writes `.pulse-verified` via the MCP server, which is more reliable than the `date +%s` shell command — the MCP write always lands after all spec edits, avoiding the mtime race condition that can cause the stop hook to block immediately after verification.
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
date +%s > .pulse-verified
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
**This must be the last write operation before stopping.** The stop hook compares each edited spec's modification time against this stamp — if any spec is newer than the stamp, the hook blocks. Do not edit any spec file after writing the stamp.
|
|
59
|
+
**This must be the last operation before stopping.** The stop hook compares each edited spec's mtime against this stamp — if any spec is newer than the stamp, the hook blocks. Do not edit any spec file after calling `pulse_stamp`.
|
|
64
60
|
|
|
65
61
|
### 12. Report
|
|
66
62
|
|
|
@@ -172,7 +172,7 @@ To add a missing icon: copy a Lucide path (MIT) into `src/ui/icons.js` using `s(
|
|
|
172
172
|
| `stat` | `label`, `value`, `change`, `trend` (up/down/neutral), `size` (sm/md/lg — lg for hero KPIs), `center` |
|
|
173
173
|
| `avatar` | `src`, `alt`, `size` (sm/md/lg/xl), `initials` |
|
|
174
174
|
| `empty` | `title`, `description`, `action` ({label,href,variant}) |
|
|
175
|
-
| `table` | `headers`, `rows` (2D array of HTML strings), `caption` |
|
|
175
|
+
| `table` | `headers`, `rows` (2D array of HTML strings), `caption` | ⚠ Each cell item must be **raw HTML content** — `table()` wraps each item in `<td>` itself. Do NOT pass `<td>...</td>` strings or you will get double-nested cells. |
|
|
176
176
|
| `spinner` | `size` (sm/md/lg), `color` (accent/muted/white), `label` |
|
|
177
177
|
| `progress` | `value`, `max` (100), `label`, `showLabel`, `showValue`, `variant`, `size` |
|
|
178
178
|
| `breadcrumbs` | `items` ([{label,href}] — last item has no href), `separator` |
|