@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
package/src/agent/workflow.md
CHANGED
|
@@ -1,57 +1,193 @@
|
|
|
1
1
|
# Build Workflow
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## Quick decision tree
|
|
6
|
+
|
|
7
|
+
| Situation | Start here |
|
|
8
|
+
|---|---|
|
|
9
|
+
| New site, new branded page, or "build me X from scratch" | `pulse_intake` → `pulse_sketch` → `pulse_intent` → build |
|
|
10
|
+
| Editing an existing page, adding a section, fixing a bug | Go straight to Step 1 — Understand |
|
|
11
|
+
| "Add X to the existing Y" (one-liner) | Read the relevant file, make the change, run `/verify` |
|
|
12
|
+
| Stuck on which component to use | `pulse_intent("describe what I'm building")` |
|
|
13
|
+
| Colours failing contrast / unsure about palette | `pulse_check_contrast` |
|
|
14
|
+
| Something looks wrong in the browser | `pulse_restart_server`, then `/verify` |
|
|
15
|
+
| Lighthouse < 100 | Fix the flagged audit, run `/verify` again — do not skip to commit |
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## New project or new page? Start with Intake
|
|
20
|
+
|
|
21
|
+
For any **new page, landing page, or branded site**, run the intake sequence first:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
0. ASK: "Do you have any design inspiration — a site you love, a screenshot, or a mood board
|
|
25
|
+
image? Drop images into public/intake/ or paste a URL and I'll extract the design
|
|
26
|
+
intent before we start." ← MANDATORY. Ask this before doing anything else.
|
|
27
|
+
0b. pulse_extract_inspiration(url/image) → structured design brief [if user supplies reference]
|
|
28
|
+
1. pulse_intake(name, pitch, features, ...) → product brief + contrast check
|
|
29
|
+
2. pulse_sketch(brief, vibe?, antiStyle?) → 3 layout directions to choose from
|
|
30
|
+
3. pulse_intent(description) → archetype + scaffold + guide list
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- **Step 0 — Ask for inspiration first (mandatory):** Before calling any tool or presenting any plan, ask the user: *"Do you have any design inspiration — a site you love, a screenshot, or a mood board image? Drop images into `public/intake/` or paste a URL and I'll extract the design intent before we start."* Wait for the answer. Do not skip this question, even if the user's brief already seems detailed. Check `public/intake/` for any images already dropped there.
|
|
34
|
+
- **`pulse_extract_inspiration`** — call this after step 0 if the user supplies a URL, site name, or image they like. Also call it for any images already present in `public/intake/`. Gives you a structured extraction template: visit the URL or analyse the image with your vision tools, extract colours, layout, typography, and feel, then feed those findings into pulse_intake. If the user explicitly says they have no reference, skip this step.
|
|
35
|
+
- **`pulse_intake`** — captures the real content (name, pitch, features, palette, vibe, what it should NOT look like). Returns a brief with copy-ready content and early contrast warnings. Always ask the user one question at a time — never multi-choice for open-ended questions.
|
|
36
|
+
- **`pulse_sketch`** — generates 3 structurally distinct layout directions (full-bleed, asymmetric split, typography-only, editorial, dense grid, story scroll, content-first). **Call this before writing any code.** Prevents defaulting to centred hero + three-column features every time.
|
|
37
|
+
- **Skip `pulse_sketch` only** when the vibe is `corporate` or `minimal` AND the user hasn't expressed any structural preference. For `playful`, `bold`, `brutalist`, `retro`, or `neon` vibes, `pulse_sketch` is mandatory — templates have default section orders that actively fight these vibes.
|
|
38
|
+
- **`pulse_intent`** — maps the chosen direction to a spec scaffold and tells you which guides to read.
|
|
39
|
+
|
|
40
|
+
For **small edits, bug fixes, or "add X to existing Y"** — skip intake/sketch and go straight to Step 0 below.
|
|
4
41
|
|
|
5
42
|
---
|
|
6
43
|
|
|
7
|
-
##
|
|
44
|
+
## Step 0 — Intent (always first for non-trivial builds)
|
|
45
|
+
|
|
46
|
+
After intake + sketch (or directly for targeted tasks), call `pulse_intent`:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
pulse_intent("a settings page with profile editing and a save action")
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
It returns a matched archetype, recommended components, a starter spec scaffold, and which guide sections to read. This replaces hunting through guide sections manually.
|
|
53
|
+
|
|
54
|
+
If the user's request is already very specific (a one-liner edit, a bug fix, "add X to the existing Y"), skip this and go straight to Understand.
|
|
8
55
|
|
|
9
|
-
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Step 1 — Understand
|
|
10
59
|
|
|
11
|
-
|
|
12
|
-
2. Fetch any topic sections you need (`pulse://guide/spec`, `pulse://guide/components`, etc.).
|
|
13
|
-
3. Call `pulse_list_structure` to see what pages and components already exist.
|
|
60
|
+
Call `pulse_list_structure` to see what already exists. Fetch only the guide sections relevant to your intent (the `pulse_intent` response tells you which ones).
|
|
14
61
|
|
|
15
|
-
Do not
|
|
62
|
+
Do not fetch every guide section for every task. Fetch what you need.
|
|
16
63
|
|
|
17
64
|
---
|
|
18
65
|
|
|
19
|
-
##
|
|
66
|
+
## Step 2 — Plan
|
|
20
67
|
|
|
21
|
-
|
|
68
|
+
Decide the complexity tier (see below) and confirm with the user if the task is non-trivial. Output a concise plan:
|
|
22
69
|
|
|
23
|
-
-
|
|
24
|
-
- The route, state shape, mutations/actions, and server fetchers
|
|
70
|
+
- Route, state shape, mutations/actions, server fetchers
|
|
25
71
|
- Which UI components you will use
|
|
26
|
-
-
|
|
72
|
+
- Files you will create or modify
|
|
73
|
+
|
|
74
|
+
**Skip confirmation for trivially small tasks** (add a button, fix a label, swap a component). When in doubt, confirm.
|
|
75
|
+
|
|
76
|
+
**When asking the user questions:** ask one question at a time. If you present choices, use at most 4 options — the `ask_user` tool enforces this limit and will error if exceeded. For open-ended questions (names, copy, features, hex colours), ask as plain prose with no choices at all.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Complexity tiers
|
|
81
|
+
|
|
82
|
+
Not every task needs all 8 phases. Match the tier to the task:
|
|
83
|
+
|
|
84
|
+
### Tier 1 — Simple (static or read-only content)
|
|
85
|
+
|
|
86
|
+
*Applies to:* landing pages, blog posts, profile pages, any page with no mutations or actions.
|
|
87
|
+
|
|
88
|
+
| Phase | Action | Gate |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| 1 | Intent + Understand | — |
|
|
91
|
+
| 2 | Build | announce brief → write files |
|
|
92
|
+
| 3 | Validate | `pulse_validate` clean |
|
|
93
|
+
| 4a | Design approval | screenshot → show user → **wait for approval** *(new-build only)* |
|
|
94
|
+
| 4b | Verify | `pulse_design_review` (if intake ran) → `pulse_layout_review` → Lighthouse desktop + mobile (100/100/100) |
|
|
95
|
+
|
|
96
|
+
No tests required for pure view specs with no logic. Add tests if the view has non-trivial helper functions.
|
|
97
|
+
|
|
98
|
+
### Tier 2 — Standard (interactive, single-purpose)
|
|
99
|
+
|
|
100
|
+
*Applies to:* forms, CRUD pages, settings, auth flows, dashboards without complex state machines.
|
|
101
|
+
|
|
102
|
+
| Phase | Action | Gate |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| 1 | Intent + Understand | — |
|
|
105
|
+
| 2 | Plan | user confirmation (skip if unambiguous) |
|
|
106
|
+
| 3 | Build | announce brief → write files |
|
|
107
|
+
| 4 | Validate | `pulse_validate` clean |
|
|
108
|
+
| 5a | Design approval | screenshot → show user → **wait for approval** *(new-build only)* |
|
|
109
|
+
| 5b | Verify | `pulse_design_review` (if intake ran) → `pulse_layout_review` → Lighthouse desktop + mobile (100/100/100) + CLS 0.00 |
|
|
110
|
+
| 6 | Tests | mutations, view landmarks, any utility functions |
|
|
111
|
+
| 7 | Code review | `pulse_review` — only after 5–6 pass |
|
|
112
|
+
|
|
113
|
+
### Tier 3 — Complex (multi-step, shared state, or security-sensitive)
|
|
114
|
+
|
|
115
|
+
*Applies to:* multi-step wizards, pages with guards and auth, store-connected pages, anything touching sensitive data or complex branching state.
|
|
116
|
+
|
|
117
|
+
All phases as originally defined — full flow with every gate.
|
|
27
118
|
|
|
28
|
-
|
|
119
|
+
| Phase | Action | Gate |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| 1 | Intent + Understand | — |
|
|
122
|
+
| 2 | Plan | **user confirmation required** |
|
|
123
|
+
| 3 | Build | announce brief → write files |
|
|
124
|
+
| 4 | Validate | `pulse_validate` clean |
|
|
125
|
+
| 5a | Design approval | screenshot → show user → **wait for approval** *(new-build only)* |
|
|
126
|
+
| 5b | Verify | `pulse_design_review` (if intake ran) → `pulse_layout_review` → Lighthouse 100/100/100 + CLS 0.00, desktop + mobile |
|
|
127
|
+
| 6 | Tests | full coverage: mutations, actions, guards, view |
|
|
128
|
+
| 7 | Code review | `pulse_review` |
|
|
129
|
+
| 8 | Fix + re-verify | re-run all gates |
|
|
29
130
|
|
|
30
|
-
**
|
|
131
|
+
**If in doubt, default to Tier 2.** Choosing a lower tier than the task warrants means you'll hit problems at gates — the tiers are a shortcut, not a way to skip quality.
|
|
31
132
|
|
|
32
133
|
---
|
|
33
134
|
|
|
34
135
|
## Phase 3 — Build
|
|
35
136
|
|
|
36
|
-
### 3a —
|
|
137
|
+
### 3a — Decide: components or creative override
|
|
37
138
|
|
|
38
|
-
|
|
139
|
+
**Two valid modes — choose one and state it in the build brief:**
|
|
140
|
+
|
|
141
|
+
**Mode A — Components first (default)**
|
|
142
|
+
Use Pulse UI components for all standard UI patterns. Before writing any view HTML, grep `src/ui/index.js` for the components you need:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
grep -E "^export" src/ui/index.js | head -20
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Or use the guide-components table. Never write these patterns from scratch in Mode A:
|
|
149
|
+
|
|
150
|
+
- Hero section → `hero()`
|
|
151
|
+
- Product/service cards → `card()`
|
|
152
|
+
- Image + text layouts → `media()`
|
|
153
|
+
- Horizontal strips → `banner()`
|
|
154
|
+
- Feature tiles → `feature()`
|
|
155
|
+
- Testimonials → `testimonial()`
|
|
156
|
+
- CTAs → `cta()`
|
|
157
|
+
|
|
158
|
+
If you're about to type `class="hero"` or `class="product-card"` in Mode A, stop — use the component instead.
|
|
159
|
+
|
|
160
|
+
**Mode B — Creative override (raw HTML throughout)**
|
|
161
|
+
When the design intent calls for a more expressive, unconventional, or typographically-driven layout that components would constrain, you may build with raw HTML throughout. This is a deliberate design decision, not a shortcut.
|
|
162
|
+
|
|
163
|
+
Rules for Mode B:
|
|
164
|
+
- Load `/pulse-ui.css` in `meta.styles` — the token system is still required
|
|
165
|
+
- Functional atoms (`button`, `input`, `badge`, `modal`) should still come from components unless there is a specific design reason
|
|
166
|
+
- The quality gate is 100/100/100/100 Lighthouse on both desktop and mobile — this replaces the component checklist as the pass bar
|
|
167
|
+
|
|
168
|
+
Announce Mode B explicitly in the build brief: *"Mode B — component-free, raw HTML for creative control."*
|
|
169
|
+
|
|
170
|
+
### 3b — Announce before writing
|
|
171
|
+
|
|
172
|
+
Before writing any file, output a build brief:
|
|
39
173
|
|
|
40
174
|
```
|
|
41
|
-
Building:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
175
|
+
Building: <page name> (<route>)
|
|
176
|
+
Mode: A — components first | B — creative override (raw HTML, reason: ...)
|
|
177
|
+
State: <fields and types, or "none">
|
|
178
|
+
Mutations: <list, or "none">
|
|
179
|
+
Actions: <list with brief description, or "none">
|
|
180
|
+
Server: <fetcher names and what they fetch, or "none">
|
|
181
|
+
Components: <which UI components you will use, or "none — Mode B">
|
|
182
|
+
View: <key sections / landmarks>
|
|
183
|
+
Files: <list of files that will be written>
|
|
48
184
|
```
|
|
49
185
|
|
|
50
|
-
|
|
186
|
+
One-liners are fine for simple pages, but always output something. **Declaring the mode upfront** makes the design intent explicit and prevents unnecessary component-reinvention warnings during review.
|
|
51
187
|
|
|
52
|
-
###
|
|
188
|
+
### 3c — Write
|
|
53
189
|
|
|
54
|
-
Write each file using the **Write tool** — not `pulse_create_page`. This shows the user a readable diff. Before each file write, output a one-line status (present-progressive
|
|
190
|
+
Write each file using the **Write tool** — not `pulse_create_page`. This shows the user a readable diff. Before each file write, output a one-line status (present-progressive):
|
|
55
191
|
|
|
56
192
|
```
|
|
57
193
|
Writing src/pages/my-page.js...
|
|
@@ -63,9 +199,11 @@ After the Write tool completes, call `pulse_create_page(name)` to register the p
|
|
|
63
199
|
✓ src/pages/my-page.js written and registered.
|
|
64
200
|
```
|
|
65
201
|
|
|
66
|
-
|
|
202
|
+
### 3d — Suggest (optional but recommended)
|
|
203
|
+
|
|
204
|
+
After writing the first draft, call `pulse_suggest(content)` before running the hard validator. It catches obvious omissions collaboratively — a second opinion, not a gate.
|
|
67
205
|
|
|
68
|
-
|
|
206
|
+
**Rule: never output `✓` before the tool call that confirms the result.**
|
|
69
207
|
|
|
70
208
|
---
|
|
71
209
|
|
|
@@ -74,11 +212,11 @@ If multiple files are needed (spec + shared component, spec + test stub, etc.),
|
|
|
74
212
|
Run `pulse_validate` on the spec file.
|
|
75
213
|
|
|
76
214
|
- **If it passes:** output `✓ Validation clean — checking browser...` and continue.
|
|
77
|
-
- **If it fails:** fix every error and every warning, then re-run. Repeat until clean.
|
|
215
|
+
- **If it fails:** fix every error and every warning, then re-run. Repeat until clean.
|
|
78
216
|
|
|
79
217
|
---
|
|
80
218
|
|
|
81
|
-
## Phase 5 — Browser check
|
|
219
|
+
## Phase 5 — Browser check
|
|
82
220
|
|
|
83
221
|
**After every file edit, restart the server and reload the browser before checking anything.** The dev server auto-restarts on file changes but the browser tab stays stale. Always:
|
|
84
222
|
1. Call `pulse_restart_server` — waits for the server to be ready
|
|
@@ -86,65 +224,113 @@ Run `pulse_validate` on the spec file.
|
|
|
86
224
|
|
|
87
225
|
Do this before every screenshot, console check, or visual inspection. Never debug a problem that might just be a stale tab.
|
|
88
226
|
|
|
89
|
-
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
### Phase 5a — Design approval *(new builds from `pulse_intake` only)*
|
|
230
|
+
|
|
231
|
+
**This phase applies when `pulse_intake` ran** — i.e. you are building a new page or site from scratch, not editing an existing one. For edits, bug fixes, or "add X to existing Y", skip to Phase 5b immediately.
|
|
232
|
+
|
|
233
|
+
1. Take a screenshot of the built page
|
|
234
|
+
2. Describe what you see in 2–3 plain sentences — layout structure, visual tone, key sections. Do NOT list features or code details.
|
|
235
|
+
3. **Ask the user directly:** *"Happy with the layout and direction, or any changes before I run tests and Lighthouse?"*
|
|
236
|
+
4. **Wait for their response before doing anything else.** Do not run design_review, layout_review, or Lighthouse until you have a clear go-ahead.
|
|
237
|
+
|
|
238
|
+
**If the user requests changes:**
|
|
239
|
+
- Make the edits
|
|
240
|
+
- Take a new screenshot
|
|
241
|
+
- Describe what changed
|
|
242
|
+
- Ask again
|
|
243
|
+
- Repeat until approved — each iteration is design-only, no Lighthouse between rounds
|
|
244
|
+
|
|
245
|
+
**If the user approves (or says "looks good", "go ahead", "yes", etc.):**
|
|
246
|
+
- Continue to Phase 5b
|
|
90
247
|
|
|
91
|
-
|
|
248
|
+
> The point of this gate is to let the user redirect the design before expensive, slow checks run. A Lighthouse build takes ~90 seconds — do not start it until the user has seen and approved the visual result.
|
|
92
249
|
|
|
93
|
-
|
|
94
|
-
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
### Phase 5b — Full verification (pass gate)
|
|
253
|
+
|
|
254
|
+
**Run `/verify` now.** Do not execute these steps manually.
|
|
255
|
+
|
|
256
|
+
`/verify` validates, screenshots, runs Lighthouse (desktop + mobile), runs the performance trace (LCP + CLS), checks console errors, runs `pulse_review`, and writes the `.pulse-verified` stamp. Running steps manually does not write the stamp.
|
|
257
|
+
|
|
258
|
+
> **Lighthouse pre-flight:** `/verify` calls `pulse_build` automatically before running Lighthouse, and audits against the production server on port 3001. If you ever call `lighthouse_audit` manually outside `/verify`, you must first run `pulse_build` and navigate to `http://localhost:3001/your-route`. Auditing the dev server (port 3000) will produce misleading results.
|
|
259
|
+
|
|
260
|
+
### ⛔ After design approval — mandatory steps before continuing
|
|
261
|
+
|
|
262
|
+
1. `pulse_design_review` (if `pulse_intake` ran) — work through all 7 signals, fix any Fail
|
|
263
|
+
2. `pulse_layout_review <url>` — 390/768/1280px overflow, broken images, collapsed sections
|
|
264
|
+
3. `/verify` — Lighthouse desktop + mobile (100/100/100) + CLS 0.00 + `pulse_review`
|
|
265
|
+
|
|
266
|
+
**Do not report the page as done until `/verify` writes the `.pulse-verified` stamp.** Skipping Lighthouse means broken contrast, missing landmarks, and Best Practices failures ship to the user.
|
|
267
|
+
|
|
268
|
+
Pass gates:
|
|
269
|
+
- Design approval: user has explicitly said they're happy with the layout
|
|
270
|
+
- Design review: all 7 signals pass (or intake didn't run)
|
|
271
|
+
- Layout review: no overflow, broken images, or collapsed sections at any viewport
|
|
95
272
|
- Lighthouse desktop: Accessibility, Best Practices, SEO all 100
|
|
96
|
-
- Lighthouse mobile: same
|
|
97
|
-
- **CLS: 0.00** — any layout shift is a blocker
|
|
98
|
-
- LCP: measured and reported; flag any fixable insight (render-blocking resources, large image delay)
|
|
273
|
+
- Lighthouse mobile: same
|
|
274
|
+
- **CLS: 0.00** — any layout shift is a blocker
|
|
99
275
|
- No console errors
|
|
100
276
|
|
|
101
|
-
**If any gate fails:** fix
|
|
277
|
+
**If any gate fails:** fix and run `/verify` again.
|
|
102
278
|
|
|
103
|
-
|
|
279
|
+
**Troubleshooting browser profile lock:** If you see "browser already running" or chrome profile errors, another session is using the same Chrome profile. This is a `chrome-devtools-mcp` limitation — only one session can use the profile at a time. Ask the user to close other Claude sessions, or wait for the other session to finish.
|
|
280
|
+
|
|
281
|
+
**CSS-only changes after verification:** If you've already run `/verify` successfully and the user asks for minor CSS tweaks (margins, colours, spacing), you must run the full `/verify` workflow again — the stop hook checks the file mtime against the stamp. There is no lighter CSS-only check. This is by design: even CSS changes can affect CLS, colour contrast, or readability.
|
|
104
282
|
|
|
105
283
|
---
|
|
106
284
|
|
|
107
|
-
## Phase 6 — Tests (pass gate)
|
|
285
|
+
## Phase 6 — Tests (pass gate, Tier 2+)
|
|
108
286
|
|
|
109
287
|
Write tests for every spec you created or modified. At minimum:
|
|
110
288
|
|
|
111
|
-
- Each mutation as a pure function
|
|
289
|
+
- Each mutation as a pure function
|
|
112
290
|
- View renders expected HTML landmarks
|
|
113
291
|
- Any utility functions defined in the spec
|
|
114
292
|
|
|
115
|
-
Run the tests. Fix every failure.
|
|
116
|
-
|
|
117
|
-
Output after passing: `✓ Tests passing — ready for review.`
|
|
293
|
+
Run the tests. Fix every failure.
|
|
118
294
|
|
|
119
295
|
---
|
|
120
296
|
|
|
121
|
-
## Phase 7 — Review
|
|
297
|
+
## Phase 7 — Review (Tier 2+)
|
|
122
298
|
|
|
123
|
-
|
|
299
|
+
Only invoke `pulse_review` after phases 4–6 all pass. It checks the code for correctness, security, accessibility, DRY violations, and checklist adherence.
|
|
124
300
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
> Note: `/verify` includes the review step (`pulse_review`) — if you ran `/verify` and it passed cleanly, the Review Agent has already run. You do not need to invoke it again separately.
|
|
301
|
+
> Note: `/verify` includes the review step — if you ran `/verify` and it passed, the Review Agent has already run.
|
|
128
302
|
|
|
129
303
|
---
|
|
130
304
|
|
|
131
|
-
## Phase 8 — Fix review issues
|
|
305
|
+
## Phase 8 — Fix review issues (Tier 3)
|
|
132
306
|
|
|
133
|
-
Fix every issue raised
|
|
307
|
+
Fix every issue raised. If fixes touch the spec or view, run `/verify` again.
|
|
134
308
|
|
|
135
|
-
Only declare the task done after
|
|
309
|
+
Only declare the task done after all gates still pass.
|
|
136
310
|
|
|
137
311
|
---
|
|
138
312
|
|
|
139
|
-
##
|
|
313
|
+
## Summary
|
|
140
314
|
|
|
141
315
|
```
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
316
|
+
New project / new page:
|
|
317
|
+
ASK first → "Do you have any design inspiration — a site, screenshot, or
|
|
318
|
+
mood board? Drop into public/intake/ or paste a URL."
|
|
319
|
+
pulse_extract_inspiration → (if user supplies a URL or image, or images found in public/intake/)
|
|
320
|
+
pulse_intake → capture real content, palette, vibe, anti-style
|
|
321
|
+
pulse_sketch → choose 1 of 3 structural layout directions
|
|
322
|
+
pulse_intent → get scaffold + guide list
|
|
323
|
+
|
|
324
|
+
Targeted edit / bug fix:
|
|
325
|
+
pulse_intent → (or skip entirely for trivial changes)
|
|
326
|
+
|
|
327
|
+
Step 1 Understand pulse_list_structure + relevant guides
|
|
328
|
+
Step 2 Plan confirm with user if non-trivial
|
|
329
|
+
|
|
330
|
+
Tier 1 Simple → phases 3 → 4 → 5a (design approval) → 5b (verify)
|
|
331
|
+
Tier 2 Standard → phases 3 → 4 → 5a (design approval) → 5b (verify) → 6 → 7
|
|
332
|
+
Tier 3 Complex → phases 3 → 4 → 5a (design approval) → 5b (verify) → 6 → 7 → 8
|
|
333
|
+
|
|
334
|
+
Phase 5a is skipped for edits, bug fixes, and "add X to existing Y".
|
|
150
335
|
```
|
|
336
|
+
|
package/src/cli/dev.js
CHANGED
|
@@ -12,6 +12,7 @@ import path from 'path'
|
|
|
12
12
|
import fs from 'fs'
|
|
13
13
|
import { createServer } from '../server/index.js'
|
|
14
14
|
import { loadPages } from './discover.js'
|
|
15
|
+
import * as log from './logger.js'
|
|
15
16
|
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
17
18
|
// Args
|
|
@@ -57,7 +58,15 @@ const PUBLIC_DIR = path.join(ROOT, 'public')
|
|
|
57
58
|
const stampPath = path.join(PUBLIC_DIR, '.pulse-ui-version')
|
|
58
59
|
const stamp = fs.existsSync(stampPath) ? fs.readFileSync(stampPath, 'utf8').trim() : null
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
// Compare CSS byte-length so any content change triggers a re-copy, even
|
|
62
|
+
// within the same version (e.g. when iterating locally with npm link).
|
|
63
|
+
const srcCss = path.join(pkgPublic, 'pulse-ui.css')
|
|
64
|
+
const dstCss = path.join(PUBLIC_DIR, 'pulse-ui.css')
|
|
65
|
+
const srcSize = fs.existsSync(srcCss) ? fs.statSync(srcCss).size : -1
|
|
66
|
+
const dstSize = fs.existsSync(dstCss) ? fs.statSync(dstCss).size : -2
|
|
67
|
+
const cssStale = srcSize !== dstSize
|
|
68
|
+
|
|
69
|
+
if (stamp === pkgVersion && !cssStale) return
|
|
61
70
|
|
|
62
71
|
fs.mkdirSync(PUBLIC_DIR, { recursive: true })
|
|
63
72
|
for (const asset of ['pulse-ui.css', 'pulse-ui.js']) {
|
|
@@ -98,19 +107,13 @@ const PUBLIC_DIR = path.join(ROOT, 'public')
|
|
|
98
107
|
// Start
|
|
99
108
|
// ---------------------------------------------------------------------------
|
|
100
109
|
|
|
101
|
-
console.log(`⚡ Pulse dev server starting...\n`)
|
|
102
|
-
|
|
103
110
|
const specs = await loadPages(ROOT)
|
|
104
111
|
|
|
105
112
|
if (specs.length === 0) {
|
|
106
|
-
|
|
113
|
+
log.error('No pages found in src/pages/. Create a page to get started.')
|
|
107
114
|
process.exit(1)
|
|
108
115
|
}
|
|
109
116
|
|
|
110
|
-
console.log('Pages:\n')
|
|
111
|
-
specs.forEach(spec => console.log(` ${spec.route}`))
|
|
112
|
-
console.log()
|
|
113
|
-
|
|
114
117
|
// ---------------------------------------------------------------------------
|
|
115
118
|
// Hot reload — SSE clients + file watcher
|
|
116
119
|
// ---------------------------------------------------------------------------
|
|
@@ -135,16 +138,37 @@ process.on('SIGTERM', closeReloadClients)
|
|
|
135
138
|
process.on('SIGINT', closeReloadClients)
|
|
136
139
|
|
|
137
140
|
let reloadTimer = null
|
|
138
|
-
|
|
141
|
+
let lastReload = 0
|
|
142
|
+
|
|
143
|
+
async function triggerReload(label = 'File changed') {
|
|
139
144
|
clearTimeout(reloadTimer)
|
|
140
145
|
reloadTimer = setTimeout(async () => {
|
|
146
|
+
const now = Date.now()
|
|
147
|
+
if (now - lastReload < 100) return
|
|
148
|
+
lastReload = now
|
|
149
|
+
|
|
141
150
|
try {
|
|
142
|
-
const fresh = await loadPages(ROOT,
|
|
151
|
+
const fresh = await loadPages(ROOT, now)
|
|
143
152
|
updateSpecs(fresh)
|
|
144
|
-
|
|
153
|
+
log.info(`${label} — specs reloaded`)
|
|
154
|
+
} catch (err) {
|
|
155
|
+
log.error(`Spec reload failed: ${err.message}`)
|
|
156
|
+
}
|
|
145
157
|
notifyReload()
|
|
146
|
-
},
|
|
147
|
-
}
|
|
158
|
+
}, 200)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Watch src/ for changes to existing files (spec edits, component edits)
|
|
162
|
+
fs.watch(path.join(ROOT, 'src'), { recursive: true }, () => triggerReload('File changed'))
|
|
163
|
+
|
|
164
|
+
// Also watch the pages directory explicitly so macOS reliably fires on new file creation.
|
|
165
|
+
// On macOS, fs.watch with recursive=true watches subdirectory contents but may not fire
|
|
166
|
+
// for the parent dir entry when a brand-new file is created. Watching the dir non-recursively
|
|
167
|
+
// catches the "new entry added to this directory" event.
|
|
168
|
+
const pagesDir = path.join(ROOT, 'src', 'pages')
|
|
169
|
+
if (fs.existsSync(pagesDir)) {
|
|
170
|
+
fs.watch(pagesDir, { recursive: false }, () => triggerReload('New page detected'))
|
|
171
|
+
}
|
|
148
172
|
|
|
149
173
|
// Tiny script injected into every page — connects to SSE and reloads on change
|
|
150
174
|
// Passed as a function so the server can inject the per-request CSP nonce
|
|
@@ -160,13 +184,14 @@ const reloadScript = (nonce) => `<script nonce="${nonce}">
|
|
|
160
184
|
})();
|
|
161
185
|
</script>`
|
|
162
186
|
|
|
163
|
-
const { updateSpecs } = createServer(specs, {
|
|
187
|
+
const { updateSpecs } = await createServer(specs, {
|
|
164
188
|
port: PORT,
|
|
165
189
|
stream: true,
|
|
166
190
|
staticDir: fs.existsSync(PUBLIC_DIR) ? PUBLIC_DIR : null,
|
|
167
191
|
manifest: {}, // never use a build manifest in dev — always serve source files
|
|
168
192
|
extraBody: reloadScript,
|
|
169
193
|
dev: true,
|
|
194
|
+
agentMode: !!process.env.PULSE_AGENT_MODE,
|
|
170
195
|
...(_config.csp ? { csp: _config.csp } : {}),
|
|
171
196
|
|
|
172
197
|
onRequest(req, res) {
|
package/src/cli/fmt.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse CLI — terminal formatting helpers
|
|
3
|
+
*
|
|
4
|
+
* Respects NO_COLOR and CI env vars — falls back to plain text automatically.
|
|
5
|
+
* Zero dependencies, Node built-ins only.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const USE_COLOR = !process.env.NO_COLOR && !process.env.CI && process.stdout.isTTY
|
|
9
|
+
|
|
10
|
+
// ANSI escape helpers — no-ops when colour is disabled
|
|
11
|
+
const c = {
|
|
12
|
+
reset: s => USE_COLOR ? `\x1b[0m${s}\x1b[0m` : s,
|
|
13
|
+
bold: s => USE_COLOR ? `\x1b[1m${s}\x1b[0m` : s,
|
|
14
|
+
dim: s => USE_COLOR ? `\x1b[2m${s}\x1b[0m` : s,
|
|
15
|
+
green: s => USE_COLOR ? `\x1b[32m${s}\x1b[0m` : s,
|
|
16
|
+
yellow: s => USE_COLOR ? `\x1b[33m${s}\x1b[0m` : s,
|
|
17
|
+
blue: s => USE_COLOR ? `\x1b[34m${s}\x1b[0m` : s,
|
|
18
|
+
cyan: s => USE_COLOR ? `\x1b[36m${s}\x1b[0m` : s,
|
|
19
|
+
white: s => USE_COLOR ? `\x1b[97m${s}\x1b[0m` : s,
|
|
20
|
+
red: s => USE_COLOR ? `\x1b[31m${s}\x1b[0m` : s,
|
|
21
|
+
purple: s => USE_COLOR ? `\x1b[35m${s}\x1b[0m` : s,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Strip ANSI codes to measure true visual width
|
|
25
|
+
function visLen(s) {
|
|
26
|
+
// eslint-disable-next-line no-control-regex
|
|
27
|
+
return s.replace(/\x1b\[[0-9;]*m/g, '').length
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Pad a string to a visual width (accounting for ANSI codes)
|
|
31
|
+
function pad(s, width, align = 'left') {
|
|
32
|
+
const len = visLen(s)
|
|
33
|
+
const fill = Math.max(0, width - len)
|
|
34
|
+
return align === 'right' ? ' '.repeat(fill) + s : s + ' '.repeat(fill)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── Box ─────────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
export function box(lines, opts = {}) {
|
|
40
|
+
const { title = '', style = 'single' } = opts
|
|
41
|
+
const chars = style === 'double'
|
|
42
|
+
? { tl: '╔', tr: '╗', bl: '╚', br: '╝', h: '═', v: '║' }
|
|
43
|
+
: { tl: '┌', tr: '┐', bl: '└', br: '┘', h: '─', v: '│' }
|
|
44
|
+
|
|
45
|
+
const inner = Array.isArray(lines) ? lines : [lines]
|
|
46
|
+
const maxW = Math.max(...inner.map(visLen), title ? visLen(title) + 2 : 0)
|
|
47
|
+
const bar = chars.h.repeat(maxW + 2)
|
|
48
|
+
|
|
49
|
+
const top = title
|
|
50
|
+
? `${chars.tl}${chars.h} ${c.bold(title)} ${chars.h.repeat(maxW - visLen(title) - 1)}${chars.tr}`
|
|
51
|
+
: `${chars.tl}${bar}${chars.tr}`
|
|
52
|
+
|
|
53
|
+
const rows = inner.map(l => `${chars.v} ${pad(l, maxW)} ${chars.v}`)
|
|
54
|
+
return [top, ...rows, `${chars.bl}${bar}${chars.br}`].join('\n')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Header (branded section label) ──────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
export function header(text) {
|
|
60
|
+
const line = `${c.purple('⚡')} ${c.bold(c.white(text))}`
|
|
61
|
+
return `\n${line}\n${c.dim('─'.repeat(visLen('⚡ ' + text) - (USE_COLOR ? 0 : 0)))}`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ─── Table ───────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Render an aligned table.
|
|
68
|
+
* @param {string[]} cols - Column headers
|
|
69
|
+
* @param {string[][]} rows - Data rows (array of arrays)
|
|
70
|
+
* @param {{align?: ('left'|'right')[]}} opts
|
|
71
|
+
*/
|
|
72
|
+
export function table(cols, rows, opts = {}) {
|
|
73
|
+
const aligns = opts.align || cols.map(() => 'left')
|
|
74
|
+
const allRows = [cols, ...rows]
|
|
75
|
+
const widths = cols.map((_, i) => Math.max(...allRows.map(r => visLen(r[i] ?? ''))))
|
|
76
|
+
|
|
77
|
+
const sep = ' '
|
|
78
|
+
|
|
79
|
+
const headerRow = cols.map((h, i) => pad(c.dim(h), widths[i], aligns[i])).join(sep)
|
|
80
|
+
const divider = c.dim('─'.repeat(widths.reduce((s, w, i) => s + w + (i < widths.length - 1 ? sep.length : 0), 0)))
|
|
81
|
+
const body = rows.map(row =>
|
|
82
|
+
row.map((cell, i) => pad(cell ?? '', widths[i], aligns[i])).join(sep)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return [headerRow, divider, ...body].join('\n')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── Status line helpers ──────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
export const icon = {
|
|
91
|
+
ok: () => c.green('✓'),
|
|
92
|
+
fail: () => c.red('✗'),
|
|
93
|
+
warn: () => c.yellow('⚠'),
|
|
94
|
+
info: () => c.cyan('·'),
|
|
95
|
+
run: () => c.yellow('›'),
|
|
96
|
+
bolt: () => c.purple('⚡'),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function ok(msg) { return ` ${icon.ok()} ${msg}` }
|
|
100
|
+
export function fail(msg) { return ` ${icon.fail()} ${c.red(msg)}` }
|
|
101
|
+
export function warn(msg) { return ` ${icon.warn()} ${c.yellow(msg)}` }
|
|
102
|
+
export function info(msg) { return ` ${icon.info()} ${c.dim(msg)}` }
|
|
103
|
+
|
|
104
|
+
// ─── Timing ───────────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
export function elapsed(ms) {
|
|
107
|
+
if (ms < 1000) return `${Math.round(ms)}ms`
|
|
108
|
+
return `${(ms / 1000).toFixed(1)}s`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── Exports ──────────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
export { c, pad, visLen }
|