get-shit-pretty 0.8.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/gsp/skills/gsp-accessibility/SKILL.md +50 -0
  2. package/gsp/skills/gsp-accessibility/motion-effects.md +43 -0
  3. package/gsp/skills/gsp-brand-apply/SKILL.md +213 -0
  4. package/gsp/skills/gsp-brand-apply/bin/serve-preset.js +71 -0
  5. package/gsp/skills/gsp-brand-guidelines/SKILL.md +64 -4
  6. package/{bin → gsp/skills/gsp-brand-guidelines/bin}/theme-css.js +124 -10
  7. package/gsp/skills/gsp-brand-guidelines/methodology/gsp-brand-engineer.md +1 -1
  8. package/gsp/skills/gsp-brand-guidelines/token-mapping.md +2 -2
  9. package/gsp/skills/gsp-brand-identity/SKILL.md +5 -4
  10. package/gsp/skills/gsp-brand-refine/SKILL.md +33 -3
  11. package/gsp/skills/gsp-brand-research/SKILL.md +1 -1
  12. package/gsp/skills/gsp-brand-research/methodology/gsp-brand-researcher.md +9 -0
  13. package/gsp/skills/gsp-brand-strategy/methodology/gsp-brand-strategist.md +3 -3
  14. package/gsp/skills/gsp-brand-sync/SKILL.md +0 -1
  15. package/gsp/skills/gsp-project-brief/SKILL.md +6 -1
  16. package/gsp/skills/gsp-project-build/SKILL.md +44 -108
  17. package/gsp/skills/gsp-project-build/agent-rules.md +36 -0
  18. package/gsp/skills/gsp-project-build/brand-feedback.md +12 -0
  19. package/gsp/skills/gsp-project-build/component-classification.md +36 -0
  20. package/gsp/skills/gsp-project-build/extraction-review.md +31 -0
  21. package/gsp/skills/gsp-project-build/methodology/gsp-project-builder.md +17 -10
  22. package/gsp/skills/gsp-project-build/visual-effects.md +1 -16
  23. package/gsp/skills/gsp-project-critique/SKILL.md +5 -1
  24. package/gsp/skills/gsp-project-critique/methodology/gsp-project-critic.md +46 -89
  25. package/gsp/skills/gsp-project-design/SKILL.md +1 -1
  26. package/gsp/skills/gsp-project-design/methodology/gsp-project-designer.md +56 -118
  27. package/gsp/skills/gsp-project-research/SKILL.md +13 -0
  28. package/gsp/skills/gsp-project-review/SKILL.md +1 -1
  29. package/gsp/skills/gsp-project-review/methodology/gsp-project-reviewer.md +5 -4
  30. package/gsp/skills/gsp-scaffold/SKILL.md +13 -0
  31. package/gsp/skills/gsp-scaffold/shadcn-rules.md +7 -11
  32. package/gsp/skills/gsp-style/SKILL.md +3 -3
  33. package/gsp/skills/gsp-style/style-preset-schema.md +1 -1
  34. package/gsp/skills/gsp-visuals/domains/imagery.md +10 -16
  35. package/gsp/templates/branding/config.json +2 -2
  36. package/gsp/templates/phases/patterns.md +2 -2
  37. package/gsp/templates/projects/config.json +4 -3
  38. package/package.json +1 -1
  39. package/gsp/skills/gsp-brand-guidelines/design-tokens.md +0 -184
@@ -46,6 +46,7 @@ Read `$ARGUMENTS` to determine the mode:
46
46
  |-------|------|--------|
47
47
  | `--check #FG #BG` | Quick contrast check | Display only |
48
48
  | `--tokens` | Token-only: contrast pairs, sizing, spacing | `critique/accessibility-token-audit.md` |
49
+ | `--validate <yml-path>` | Pre-emit gate: validate a brand `.yml` for WCAG compliance | Exit 0 (pass) / exit 1 (fail) — failures to stdout, no file writes |
49
50
  | (no args) | Mode picker | Prompt user |
50
51
 
51
52
  Additional flag: `--level AAA` overrides conformance level (default: AA).
@@ -71,6 +72,10 @@ If args contain `--check`, extract the two hex color values and skip to Step 3.
71
72
 
72
73
  Skip to Step 4.
73
74
 
75
+ ### Validate mode (`--validate <yml-path>`)
76
+
77
+ Skip to Step 4.5.
78
+
74
79
  ## Step 3: Quick check mode (`--check #FG #BG`)
75
80
 
76
81
  Calculate WCAG 2.x contrast ratio between the two hex colors.
@@ -205,6 +210,51 @@ Display result and use `AskUserQuestion`:
205
210
  - **Run code audit** — "run `/gsp-accessibility-audit --code` to check the codebase"
206
211
  - **Done** — "that's all for now"
207
212
 
213
+ ## Step 4.5: Validate mode (`--validate <yml-path>`)
214
+
215
+ Pre-emit gate for `gsp-brand-guidelines` (and any caller that needs a yes/no contrast verdict on a brand `.yml` without project context).
216
+
217
+ **Differs from `--tokens`:** no project resolution, no file writes, returns exit code instead of writing a chunk. Use when you need a hard PASS/FAIL gate before downstream emission.
218
+
219
+ ### Inputs
220
+
221
+ - `<yml-path>` — absolute or relative path to a brand `.yml` preset (e.g. `.design/branding/{brand}/patterns/{brand-name}.yml`)
222
+ - `--level AA|AAA` — optional conformance override (default: AA)
223
+
224
+ ### Checks (subset of `--tokens`, contrast-only)
225
+
226
+ Reuse the contrast logic from Step 4 (sections 4.1, 4.2, 4.3):
227
+
228
+ 1. **Contrast pairs** — every semantic foreground/background pair from the `.yml`. Flag failures: normal text < 4.5:1 (AA) or < 7:1 (AAA), large text < 3:1 (AA) or < 4.5:1 (AAA), non-text < 3:1
229
+ 2. **Interactive states** — hover/active/focus/disabled pairs. Disabled states still need 3:1 non-text contrast
230
+ 3. **Focus ring** — `--ring` token vs adjacent backgrounds, ≥ 3:1
231
+ 4. **Dark mode** — if `dark_mode.color` exists, re-verify all pairs
232
+
233
+ Skip the `--tokens` extras (touch targets, typography minimums) — those are not contrast gates.
234
+
235
+ ### Output
236
+
237
+ **On pass** — print one line to stdout, exit 0:
238
+
239
+ ```
240
+ ✓ /gsp-accessibility --validate {yml-name} — N pairs checked, all WCAG 2.2 {level} compliant
241
+ ```
242
+
243
+ **On fail** — print failing pairs + exit 1:
244
+
245
+ ```
246
+ ✗ /gsp-accessibility --validate {yml-name} — {M} contrast failure(s) (WCAG 2.2 {level})
247
+
248
+ Failures:
249
+ {token-pair} ratio {N.N}:1 required {required}:1 ({use-case})
250
+ {token-pair} ratio {N.N}:1 required {required}:1 ({use-case})
251
+ ...
252
+
253
+ Fix via: /gsp-brand-refine "{token-name} contrast"
254
+ ```
255
+
256
+ **No file writes.** This mode is a callable gate — output goes to stdout, exit code carries the verdict. Stop here; no AskUserQuestion routing.
257
+
208
258
  ## Step 5: Update STATE.md
209
259
 
210
260
  If within a project and files were written:
@@ -0,0 +1,43 @@
1
+ # Motion + Effects Accessibility
2
+
3
+ Canonical accessibility guidance for visual effects. Read by `gsp-project-build`'s builder methodology and `gsp-project-build/visual-effects.md`. Owned by `gsp-accessibility` per the two-layer architecture (CLAUDE.md): expertise skills own domain knowledge.
4
+
5
+ ## prefers-reduced-motion
6
+
7
+ All visual effects must degrade gracefully:
8
+
9
+ ```css
10
+ @media (prefers-reduced-motion: reduce) {
11
+ *, *::before, *::after {
12
+ animation-duration: 0.01ms !important;
13
+ animation-iteration-count: 1 !important;
14
+ transition-duration: 0.01ms !important;
15
+ }
16
+ }
17
+ ```
18
+
19
+ This rule applies everywhere — entrance animations, hover transforms, scroll-driven reveals, parallax. No effect should fight the user's stated preference.
20
+
21
+ ## Contrast on effects
22
+
23
+ Effects must not compromise text/UI contrast:
24
+
25
+ - **Glow / shadow on text** — ensure text contrast meets WCAG AA *without* the effect (effects can fail in high-contrast mode or for users with backdrop-filter disabled)
26
+ - **Backdrop-blur** — pair with `@supports not (backdrop-filter: blur(1px))` solid-background fallback. The fallback must independently meet AA contrast against the foreground content
27
+ - **Gradient text** — test contrast ratio of *both endpoints*, not just the midpoint. A gradient from light-blue to dark-blue passes at one end and fails at the other
28
+ - **Translucent surfaces** — verify the layer behind (worst case: pure white or pure black if backdrop-filter is dropped) meets AA against the foreground
29
+
30
+ ## Hover / interaction magnitudes
31
+
32
+ Keep transforms small to avoid disorientation:
33
+
34
+ - Translate: 2-4px maximum
35
+ - Scale: 1.02-1.05 maximum (5% growth ceiling)
36
+ - Rotation: avoid except for explicit affordances (loading spinners, expand chevrons)
37
+ - Layout-property animation (width/height/padding) — don't; use transform instead
38
+
39
+ ## Cross-references
40
+
41
+ - `gsp-project-build/visual-effects.md` — CSS recipes for the effects this guidance constrains
42
+ - `gsp-accessibility-audit/wcag-checklist.md` — broader WCAG 2.2 AA criteria
43
+ - `gsp-accessibility/SKILL.md` — `--check`, `--tokens` modes for contrast validation
@@ -0,0 +1,213 @@
1
+ ---
2
+ name: gsp-brand-apply
3
+ description: Install a brand theme into a shadcn codebase — use when: apply the brand to the code, install the theme, switch to brand X, refresh the theme
4
+ user-invocable: true
5
+ allowed-tools:
6
+ - Read
7
+ - Write
8
+ - Bash
9
+ - Glob
10
+ - Grep
11
+ - AskUserQuestion
12
+ ---
13
+ <context>
14
+ The universal theme-install primitive. Reads a `{brand}.theme.json` (registry:theme) artifact and installs it into a shadcn codebase via `shadcn apply --only theme`. Surgical — only cssVars (and optionally fonts) change; components are untouched.
15
+
16
+ Spawns an ephemeral localhost HTTP server (`serve-preset.js`, colocated in this skill's `bin/`) because shadcn CLI's `--preset` flag accepts HTTP URLs only (`file://` and bare paths are rejected).
17
+
18
+ Called explicitly by the user, or invoked by `/gsp-brand-guidelines` (after generation, with consent) and `/gsp-brand-refine` (after token regen).
19
+ </context>
20
+
21
+ <objective>
22
+ Install a brand's `{brand}.theme.json` into a target shadcn project's `globals.css`.
23
+
24
+ **Input:** brand name (positional arg) + optional `--target <path>`
25
+ **Output:** Updated CSS file (path declared in `{target}/components.json` → `tailwind.css`) — cssVars only — plus log entry in `{BRAND_PATH}/STATE.md`
26
+ **Agent:** None — inline Bash
27
+ </objective>
28
+
29
+ <rules>
30
+ - Always use `AskUserQuestion` for user-facing questions — never raw text prompts
31
+ - One decision per question — never batch multiple questions in a single message
32
+ </rules>
33
+
34
+ <process>
35
+ ## Step 0: Resolve brand
36
+
37
+ Parse the user's argument:
38
+ - If a brand name was passed (e.g. `/gsp-brand-apply lyra`) → `BRAND={arg}`.
39
+ - If no name was given → scan `.design/branding/` for brand directories.
40
+ - If exactly one → use it.
41
+ - If multiple → use `AskUserQuestion`: "Which brand should I apply?" — list names as options.
42
+ - If zero → error: "No brand found. Run `/gsp-brand-guidelines` first." Stop.
43
+
44
+ Set `BRAND_PATH=.design/branding/{BRAND}`.
45
+ Set `THEME_JSON={BRAND_PATH}/patterns/{BRAND}.theme.json`.
46
+
47
+ If `THEME_JSON` does not exist → error: "Brand '{BRAND}' has no theme.json. Run `/gsp-brand-guidelines {BRAND}` to generate it." Stop.
48
+
49
+ ## Step 0.5: Pre-flight — preserve user work
50
+
51
+ `shadcn apply --only theme` is not strictly cssVars-only. Its preflight may also bump dependency versions in `package.json` (notably `shadcn` itself and `@base-ui/react`), regenerate `package-lock.json`, and rewrite `components.json`. We don't suppress this — there's no upstream flag (see issue tracker) — but we refuse to run when those files have uncommitted work, so the user can review the dep changes via `git diff` afterward.
52
+
53
+ If `{TARGET}` is inside a git repository, run:
54
+
55
+ ```bash
56
+ git -C {TARGET} diff --quiet -- package.json package-lock.json components.json 2>/dev/null
57
+ ```
58
+
59
+ If the exit code is non-zero (uncommitted changes exist in any of those three files), use `AskUserQuestion`:
60
+
61
+ - Question: "`{TARGET}` has uncommitted changes in `package.json`, `package-lock.json`, or `components.json`. `shadcn apply` may modify these (preflight bumps versions). Continue anyway?"
62
+ - Options:
63
+ - A: "Cancel — I'll commit/stash first"
64
+ - B: "Proceed anyway — I accept potential conflicts with my pending work"
65
+
66
+ If A → exit cleanly. If B → continue.
67
+
68
+ If `{TARGET}` is not a git repo, skip this check silently — there's nothing to compare against.
69
+
70
+ ## Step 1: Resolve target
71
+
72
+ Parse `--target` flag if present.
73
+
74
+ If no flag:
75
+ - Read project config at `.design/projects/*/config.json` and find `app_path`.
76
+ - If exactly one project config → use that `app_path`.
77
+ - If multiple → use `AskUserQuestion`: "Which project's codebase?" — list project names + paths.
78
+ - If zero project configs → error: "No project found. Pass `--target <path>` explicitly." Stop.
79
+
80
+ Set `TARGET={app_path}` (default `.` if empty).
81
+
82
+ Verify `{TARGET}/components.json` exists. If not → error: "{TARGET} is not a shadcn project (no components.json). Run `/gsp-scaffold` first." Stop.
83
+
84
+ ## Step 2: Detect currently-installed brand (informational)
85
+
86
+ Resolve the CSS path: read `{TARGET}/components.json` and extract the value at `.tailwind.css` (a relative path from `{TARGET}`). Open `{TARGET}/{cssPath}`. Look for OKLCH cssVars in `:root`. Compare the `--background` light value against `.design/branding/*/patterns/*.theme.json` files in the workspace.
87
+
88
+ - If a match is found → `CURRENT={matched-brand-name}`.
89
+ - If file exists but no match → `CURRENT="custom or shadcn defaults"`.
90
+ - If file missing → `CURRENT="(none — fresh project)"`.
91
+
92
+ This is informational only — surfaced in the confirmation message in Step 3.
93
+
94
+ ## Step 2.5: Detect custom token indirection
95
+
96
+ Scan the cssVars sections of `{TARGET}/{cssPath}` (`:root` and `.dark` blocks) for declarations of the form `--<name>: var(--<other>);` — these are custom indirection layers (e.g. `--background: var(--brand-bg);`).
97
+
98
+ `shadcn apply --only theme` will REPLACE those `var()` references with literal OKLCH values from the theme.json. The custom indirection is lost — the upstream tokens (e.g. `--brand-bg`) remain defined elsewhere in the file, but the shadcn cssVars no longer reference them.
99
+
100
+ If any `var(--*)` declarations are found in the cssVars blocks, use `AskUserQuestion`:
101
+ - Question: "`{cssPath}` has **{N} cssVar(s) using `var(--*)` indirection** (e.g. `{first-example}`). `shadcn apply` will replace these with literal OKLCH values, breaking the indirection layer. Continue?"
102
+ - Options:
103
+ - A: "Yes, replace with literal values"
104
+ - B: "No, cancel — preserve the indirection"
105
+
106
+ If B → append `- {ISO-8601 timestamp}: apply cancelled — would have broken indirection in {cssPath}` to `{BRAND_PATH}/STATE.md` under `## Apply log`. Output "Apply cancelled — preserve your indirection layer manually if needed." Exit cleanly.
107
+ If A → continue.
108
+
109
+ If no `var(--*)` indirection is found, skip this confirmation silently.
110
+
111
+ ## Step 3: Confirm (when overwriting a different installed brand)
112
+
113
+ If `CURRENT` is a recognized brand name AND it differs from `BRAND`:
114
+
115
+ Use `AskUserQuestion`:
116
+ - Question: "Currently installed: **{CURRENT}**. Replace with **{BRAND}**?"
117
+ - Options:
118
+ - A: "Yes, replace"
119
+ - B: "No, cancel"
120
+
121
+ If B → append `- {ISO-8601 timestamp}: apply cancelled by user (target: {TARGET})` to `{BRAND_PATH}/STATE.md` under `## Apply log`. Output "Apply cancelled" and exit cleanly.
122
+ If A → continue.
123
+
124
+ If `CURRENT` is unrecognized or fresh, skip this confirmation and proceed silently.
125
+
126
+ ## Steps 4–6: Spawn preset server, run apply, kill server
127
+
128
+ **Run Steps 4 through 6 as a single Bash call.** `SERVER_PID`, `PRESET_URL`, and `APPLY_EXIT` are shell variables — they do not survive across separate Bash tool invocations.
129
+
130
+ ```bash
131
+ # Step 4: spawn preset server, capture URL
132
+ node ${CLAUDE_SKILL_DIR}/bin/serve-preset.js {THEME_JSON} > /tmp/preset-server-url-$$.txt 2>/dev/null &
133
+ SERVER_PID=$!
134
+ # Wait up to 2s for the URL to be written
135
+ for i in 1 2 3 4 5 6 7 8 9 10; do sleep 0.2; [ -s /tmp/preset-server-url-$$.txt ] && break; done
136
+ PRESET_URL=$(head -1 /tmp/preset-server-url-$$.txt 2>/dev/null)
137
+ rm -f /tmp/preset-server-url-$$.txt
138
+
139
+ # Bail if the server didn't print a URL
140
+ if [[ -z "$PRESET_URL" || "$PRESET_URL" != http* ]]; then
141
+ kill "$SERVER_PID" 2>/dev/null
142
+ wait "$SERVER_PID" 2>/dev/null
143
+ echo "ERROR: preset server failed to start — serve-preset.js executable? Node available?"
144
+ exit 1
145
+ fi
146
+
147
+ # Step 5: run apply (quote {TARGET} — paths may contain spaces)
148
+ cd "{TARGET}" && npx shadcn@latest apply --only theme --preset "$PRESET_URL" --yes 2>&1
149
+ APPLY_EXIT=$?
150
+
151
+ # Step 6: kill preset server unconditionally
152
+ kill "$SERVER_PID" 2>/dev/null
153
+ wait "$SERVER_PID" 2>/dev/null
154
+
155
+ # Surface APPLY_EXIT for the verification step
156
+ echo "APPLY_EXIT=$APPLY_EXIT"
157
+ ```
158
+
159
+ Capture the bash output (especially the `APPLY_EXIT=N` line and any shadcn stderr) to use in Step 7.
160
+
161
+ ## Step 7: Verify or report failure
162
+
163
+ If `APPLY_EXIT != 0`:
164
+ - Surface the captured shadcn output to the user (concise — full stderr, plus a single-line summary).
165
+ - Append to `{BRAND_PATH}/STATE.md` under `## Apply log`: `- {ISO-timestamp}: apply FAILED — exit {code} on {TARGET}`.
166
+ - Stop. Do NOT auto-retry. Do NOT manually paste tokens as a fallback.
167
+
168
+ If `APPLY_EXIT == 0`:
169
+ - Resolve the CSS path: read `{TARGET}/components.json` and extract the value at `.tailwind.css` (a relative path from `{TARGET}`).
170
+ - Read `{BRAND_PATH}/patterns/{BRAND}.theme.json` and extract `cssVars.light.background` (the brand's signature OKLCH value).
171
+ - Open `{TARGET}/{cssPath}`. Verify:
172
+ - Contains `oklch(`
173
+ - Has both `:root {` and `.dark {` blocks
174
+ - Declares `--background`, `--foreground`, `--primary`, `--radius`
175
+ - **Contains the exact `cssVars.light.background` value from the theme.json** (this distinguishes a successful brand apply from shadcn's own nova defaults, which also satisfy the structural checks above)
176
+ - If any check fails → warn (do not error): "Apply reported success but expected tokens not found in `{cssPath}` — the apply may have fallen back to defaults. Inspect manually." Continue to Step 8.
177
+ - If all checks pass → continue to Step 8.
178
+
179
+ ## Step 8: Log success
180
+
181
+ Append to `{BRAND_PATH}/STATE.md` under a `## Apply log` section. If the file or section doesn't exist, create it.
182
+
183
+ ```
184
+ ## Apply log
185
+
186
+ - {ISO-8601 timestamp}: applied to `{TARGET}` (replaced: {CURRENT})
187
+ ```
188
+
189
+ ## Step 9: Output
190
+
191
+ After apply, check whether `package.json`, `package-lock.json`, or `components.json` were modified by shadcn's preflight:
192
+
193
+ ```bash
194
+ cd {TARGET} && git diff --name-only -- package.json package-lock.json components.json 2>/dev/null
195
+ ```
196
+
197
+ If the output is non-empty, list those files in the success block under a "Review" line so the user knows to inspect them. If empty (or the target is not a git repo), omit the Review line.
198
+
199
+ ```
200
+ ◆ brand applied — {BRAND} → {TARGET}
201
+
202
+ {cssPath} updated
203
+ Re-apply: /gsp-brand-apply {BRAND}
204
+ Refine: /gsp-brand-refine
205
+
206
+ [if dep files changed]
207
+ Review: git diff {file1} {file2} ...
208
+ (shadcn preflight may have bumped dep versions —
209
+ commit or revert as you prefer)
210
+
211
+ ──────────────────────────────
212
+ ```
213
+ </process>
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * serve-preset.js — ephemeral HTTP server for shadcn --preset URL fetch.
4
+ *
5
+ * Usage (from repo root):
6
+ * node gsp/skills/gsp-brand-apply/bin/serve-preset.js <path-to-registry-item.json>
7
+ *
8
+ * Behavior:
9
+ * - Listens on a random free port on 127.0.0.1.
10
+ * - Prints the URL ("http://127.0.0.1:<port>/<basename>") to stdout.
11
+ * - Serves the file as application/json on any request path.
12
+ * - Exits cleanly on SIGTERM/SIGINT.
13
+ * - Self-exits after 60s as a safety net.
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const http = require('http');
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ const TIMEOUT_MS = 60_000;
23
+
24
+ function main() {
25
+ const filePath = process.argv[2];
26
+ if (!filePath) {
27
+ console.error('Usage: node gsp/skills/gsp-brand-apply/bin/serve-preset.js <path-to-json>');
28
+ process.exit(1);
29
+ }
30
+ const abs = path.resolve(filePath);
31
+ if (!fs.existsSync(abs)) {
32
+ console.error(`File not found: ${abs}`);
33
+ process.exit(1);
34
+ }
35
+ const body = fs.readFileSync(abs, 'utf8');
36
+ const basename = path.basename(abs);
37
+
38
+ const server = http.createServer((req, res) => {
39
+ res.setHeader('Content-Type', 'application/json');
40
+ res.setHeader('Cache-Control', 'no-store');
41
+ res.end(body);
42
+ });
43
+
44
+ server.on('error', (err) => {
45
+ console.error(`Server error: ${err.message}`);
46
+ process.exit(1);
47
+ });
48
+
49
+ server.listen(0, '127.0.0.1', () => {
50
+ const { port } = server.address();
51
+ const url = `http://127.0.0.1:${port}/${basename}`;
52
+ process.stdout.write(url + '\n');
53
+ });
54
+
55
+ let shuttingDown = false;
56
+ const shutdown = () => {
57
+ if (shuttingDown) return;
58
+ shuttingDown = true;
59
+ server.close(() => process.exit(0));
60
+ setTimeout(() => process.exit(0), 1000).unref();
61
+ };
62
+
63
+ process.on('SIGTERM', shutdown);
64
+ process.on('SIGINT', shutdown);
65
+ setTimeout(() => {
66
+ console.error('serve-preset: 60s safety timeout reached, exiting');
67
+ shutdown();
68
+ }, TIMEOUT_MS).unref();
69
+ }
70
+
71
+ main();
@@ -20,7 +20,7 @@ Identity made the creative decisions. This phase makes them work in code.
20
20
  Operationalize brand identity into project-ready artifacts and complete the branding diamond.
21
21
 
22
22
  **Input:** Brand identity (enriched by domain skills) + strategy + BRIEF.md
23
- **Output:** `{brand}/patterns/` ({brand-name}.yml, STYLE.md, guidelines.html, components/, INDEX.md)
23
+ **Output:** `{brand}/patterns/` ({brand-name}.yml, {brand-name}.theme.json, STYLE.md, guidelines.html, components/, INDEX.md)
24
24
  **Agent:** `gsp-brand-engineer`
25
25
  </objective>
26
26
 
@@ -112,7 +112,7 @@ Redesign the system from the ground up, informed by what exists.
112
112
  ### Load references and agent methodology
113
113
  Read these files and hold their content for inlining into the agent prompt:
114
114
  - `${CLAUDE_SKILL_DIR}/../../templates/phases/patterns.md` — patterns output template
115
- - `${CLAUDE_SKILL_DIR}/design-tokens.md` — design tokens reference
115
+ - `${CLAUDE_SKILL_DIR}/../gsp-style/style-preset-schema.md` — canonical `.yml` schema (shadcn-flat, 1:1 CSS var mapping)
116
116
  - `${CLAUDE_SKILL_DIR}/guidelines-structure.md` — guidelines.html structure spec (shadcn tokens, sections, primitive classes)
117
117
  - `${CLAUDE_SKILL_DIR}/methodology/gsp-brand-engineer.md` — agent methodology
118
118
 
@@ -125,7 +125,7 @@ Pass in the agent prompt:
125
125
  - **Content of** style base preset `.yml` + `.md` (loaded in Step 1) — `.yml` as structural scaffold, `.md` as philosophy + implementation content for STYLE.md
126
126
  - **Agent methodology** (loaded above)
127
127
  - **Content of** patterns output template (loaded above)
128
- - **Content of** design tokens reference (loaded above)
128
+ - **Content of** style preset schema (loaded above) — the engineer assembles `{brand-name}.yml` matching this exact shape
129
129
  - **Content of** guidelines structure spec (loaded above) — follow this exactly for `guidelines.html`
130
130
  - The `system_strategy` and `tech_stack` values
131
131
  - **Output path:** `{BRAND_PATH}/patterns/`
@@ -216,6 +216,66 @@ Spawn the `gsp-brand-engineer` agent with (reuse **Agent methodology** loaded in
216
216
  >
217
217
  > The `.yml` and `STYLE.md` are confirmed — do not modify them. Focus on mapping tokens to the detected component library and specifying overrides.
218
218
 
219
+ ## Step 4.7: WCAG validation gate
220
+
221
+ Before emitting `theme.json` (the artifact that installs into real codebases), validate the assembled `.yml` against WCAG 2.2 AA contrast requirements. Inaccessible token pairs must not ship to production.
222
+
223
+ Invoke `/gsp-accessibility --validate {BRAND_PATH}/patterns/{brand-name}.yml` (use `--level AAA` if `accessibility_level` in the project config is set to AAA).
224
+
225
+ - **Pass (exit 0):** continue to Step 4.75
226
+ - **Fail (exit 1):** STOP. The skill prints failing token pairs + the recommended fix path. Surface the failures to the user with: `Theme emission blocked — {N} contrast failure(s). Run /gsp-brand-refine to fix the failing pairs, then re-run /gsp-brand-guidelines.` Do NOT emit theme.json. The pipeline is incomplete until validation passes
227
+
228
+ ## Step 4.75: Emit shadcn theme registry artifact
229
+
230
+ Generate `{brand-name}.theme.json` (registry:theme) alongside the existing patterns. This is the artifact `/gsp-brand-apply` installs into shadcn codebases.
231
+
232
+ ```bash
233
+ node ${CLAUDE_SKILL_DIR}/bin/theme-css.js \
234
+ {BRAND_PATH}/patterns/{brand-name}.yml \
235
+ --registry \
236
+ --output {BRAND_PATH}/patterns/{brand-name}.theme.json
237
+ ```
238
+
239
+ Verify the file was written and contains valid JSON:
240
+
241
+ ```bash
242
+ node -e "JSON.parse(require('fs').readFileSync('{BRAND_PATH}/patterns/{brand-name}.theme.json', 'utf8'))" \
243
+ && echo "✓ theme.json emitted"
244
+ ```
245
+
246
+ If either command fails, surface the error and stop — the brand pipeline is incomplete without this artifact.
247
+
248
+ ## Step 4.8: Offer to apply theme to codebase
249
+
250
+ Detect installable target. Read project config (`.design/projects/*/config.json`) and look for `preferences.app_path`:
251
+
252
+ - If no project config exists, or `app_path` is empty/missing → skip this step. Output a one-line note: `Apply later with /gsp-brand-apply {brand-name}`. Continue to Step 4.5.
253
+ - If `app_path` exists, check `{app_path}/components.json`:
254
+ - If missing → skip (no shadcn project to install into). Same one-line note.
255
+ - If present → continue.
256
+
257
+ Detect currently-installed brand (informational):
258
+ - Resolve the CSS path from `{app_path}/components.json` → `.tailwind.css` (a relative path).
259
+ - Read `{app_path}/{cssPath}` if it exists.
260
+ - Look for OKLCH `:root` declarations.
261
+ - Compare `--background` light value against other `.design/branding/*/patterns/*.theme.json` files in the workspace.
262
+ - Set `CURRENT={matched-brand-name}` or `CURRENT="shadcn defaults"` or `CURRENT="(none)"`.
263
+
264
+ Use `AskUserQuestion`:
265
+ - Question: "Apply **{brand-name}** to `{app_path}`? Currently installed: **{CURRENT}**. This replaces cssVars in the CSS file; components stay as-is."
266
+ - Options:
267
+ - A: "Apply now"
268
+ - B: "Skip — I'll apply later"
269
+ - C: "Apply to a different project"
270
+
271
+ On A: output `Run /gsp-brand-apply {brand-name}` as the next step the user should take.
272
+
273
+ On B: output `Skipped. Apply later with /gsp-brand-apply {brand-name}.`
274
+
275
+ On C: use `AskUserQuestion` to ask for the target path. Then output `Run /gsp-brand-apply {brand-name} --target {chosen-path}` as the next step.
276
+
277
+ Continue to Step 4.5 regardless of choice.
278
+
219
279
  ## Step 4.5: Update state
220
280
 
221
281
  Update `{BRAND_PATH}/STATE.md`:
@@ -228,7 +288,7 @@ Update `.design/CLAUDE.md` — replace the existing `### {brand-name}` entry (wr
228
288
  ```markdown
229
289
  ### {brand-name} · complete · {DATE}
230
290
  "{brand_heartbeat}"
231
- .design/branding/{brand-name}/patterns/ — guidelines.html · STYLE.md · {brand-name}.yml
291
+ .design/branding/{brand-name}/patterns/ — guidelines.html · STYLE.md · {brand-name}.yml · {brand-name}.theme.json
232
292
  ```
233
293
 
234
294
  ## Step 5: Phase transition output
@@ -1,14 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * theme-css.js — GSP deterministic token-to-CSS generator
3
+ * theme-css.js — GSP deterministic token generator
4
4
  *
5
- * Reads a GSP style preset `.yml` file and outputs a shadcn/ui-compatible
6
- * CSS variables block for `:root` and `.dark`.
5
+ * Reads a GSP style preset `.yml` file and emits either a shadcn-compatible
6
+ * CSS variables block (`:root` and `.dark`) or a `registry:theme`
7
+ * registry-item.json artifact for use with `shadcn apply --only theme`.
7
8
  *
8
- * Usage:
9
- * node bin/theme-css.js <path-to-preset.yml>
10
- * node bin/theme-css.js <path-to-preset.yml> --output globals.css
11
- * node bin/theme-css.js <path-to-preset.yml> --stdout
9
+ * Usage (from repo root):
10
+ * node gsp/skills/gsp-brand-guidelines/bin/theme-css.js <preset.yml> # CSS to stdout
11
+ * node gsp/skills/gsp-brand-guidelines/bin/theme-css.js <preset.yml> --output globals.css # CSS to file
12
+ * node gsp/skills/gsp-brand-guidelines/bin/theme-css.js <preset.yml> --stdout # CSS to stdout (explicit)
13
+ * node gsp/skills/gsp-brand-guidelines/bin/theme-css.js <preset.yml> --registry --output theme.json # registry-item.json
12
14
  *
13
15
  * Token → CSS var mapping is 1:1. No derivation, no LLM guesswork.
14
16
  * Hex values are converted to OKLCH. Alpha values (oklch with /) pass through.
@@ -274,6 +276,103 @@ function generateBlock(colorObj, shapeObj, typographyObj, selector) {
274
276
  return `${selector} {\n${lines.join('\n')}\n}`;
275
277
  }
276
278
 
279
+ // ---------------------------------------------------------------------------
280
+ // Registry-item.json builder
281
+ // ---------------------------------------------------------------------------
282
+
283
+ /**
284
+ * Walk a flat color object (e.g. preset.tokens.color) and convert every value
285
+ * through formatValue, returning a flat key→value map for cssVars.
286
+ * Keys are the same as in the YAML (background, primary, etc.) — NOT prefixed
287
+ * with "--" because shadcn's registry-item.json schema uses bare names.
288
+ */
289
+ // Note: unlike generateBlock (CSS path), this does NOT synthesize chart-1..5
290
+ // from primary/secondary/etc. when not explicitly defined in the YAML. All
291
+ // current GSP presets define all five chart vars explicitly, so the paths
292
+ // stay symmetric in practice; if a future preset omits chart vars, add the
293
+ // fallback logic here too.
294
+ function colorObjToCssVars(colorObj) {
295
+ if (!colorObj) return {};
296
+ const out = {};
297
+ // Walk all known CSS var groups: core, sidebar, chart, extras
298
+ const allKeys = [...CORE_VARS, ...SIDEBAR_VARS, ...EXTRA_VARS];
299
+ for (const key of allKeys) {
300
+ if (colorObj[key] !== undefined) {
301
+ out[key] = formatValue(colorObj[key]);
302
+ }
303
+ }
304
+ // Chart vars
305
+ const chartSources = [
306
+ colorObj['chart-1'],
307
+ colorObj['chart-2'],
308
+ colorObj['chart-3'],
309
+ colorObj['chart-4'],
310
+ colorObj['chart-5'],
311
+ ];
312
+ chartSources.forEach((c, i) => {
313
+ if (c !== undefined) {
314
+ out[`chart-${i + 1}`] = formatValue(c);
315
+ }
316
+ });
317
+ return out;
318
+ }
319
+
320
+ /**
321
+ * Build a shadcn registry-item.json object from a parsed preset.
322
+ */
323
+ function buildRegistryItem(preset, inputPath) {
324
+ const name = preset.name || path.basename(inputPath, '.yml');
325
+ const title = preset.title || name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
326
+ const description = preset.description || '';
327
+
328
+ const colorLight = (preset.tokens && preset.tokens.color) || {};
329
+ const colorDark = (preset.dark_mode && preset.dark_mode.color) || {};
330
+ const shape = (preset.tokens && preset.tokens.shape) || {};
331
+ const typography = (preset.tokens && preset.tokens.typography) || {};
332
+
333
+ // Build cssVars.theme from typography font mappings
334
+ const theme = {};
335
+ const fontMappings = [
336
+ ['font-family-primary', 'font-sans'],
337
+ ['font-family-mono', 'font-mono'],
338
+ ['font-family-display', 'font-display'],
339
+ ['font-family-secondary', 'font-secondary'],
340
+ ];
341
+ for (const [ymlKey, cssKey] of fontMappings) {
342
+ if (typography[ymlKey] !== undefined) {
343
+ theme[cssKey] = typography[ymlKey];
344
+ }
345
+ }
346
+
347
+ // Build cssVars.light — colors + radius
348
+ const light = colorObjToCssVars(colorLight);
349
+ const lg = shape['border-radius-lg'];
350
+ if (lg !== undefined) {
351
+ light['radius'] = String(lg);
352
+ }
353
+
354
+ // Build cssVars.dark — dark_mode color overrides
355
+ const dark = colorObjToCssVars(colorDark);
356
+
357
+ const registryItem = {
358
+ $schema: 'https://ui.shadcn.com/schema/registry-item.json',
359
+ name,
360
+ type: 'registry:theme',
361
+ title,
362
+ cssVars: {
363
+ ...(Object.keys(theme).length ? { theme } : {}),
364
+ light,
365
+ dark,
366
+ },
367
+ };
368
+
369
+ if (description) {
370
+ registryItem.description = description;
371
+ }
372
+
373
+ return registryItem;
374
+ }
375
+
277
376
  // ---------------------------------------------------------------------------
278
377
  // Main
279
378
  // ---------------------------------------------------------------------------
@@ -281,8 +380,10 @@ function generateBlock(colorObj, shapeObj, typographyObj, selector) {
281
380
  function main() {
282
381
  const args = process.argv.slice(2);
283
382
  if (!args.length || args.includes('--help') || args.includes('-h')) {
284
- console.log(`Usage: node bin/theme-css.js <preset.yml> [--output <file>] [--stdout]`);
285
- console.log(` node bin/theme-css.js gsp/skills/gsp-style/styles/saas.yml`);
383
+ const cmd = 'node gsp/skills/gsp-brand-guidelines/bin/theme-css.js';
384
+ console.log(`Usage: ${cmd} <preset.yml> [--output <file>] [--stdout] [--registry]`);
385
+ console.log(` ${cmd} gsp/skills/gsp-style/styles/saas.yml`);
386
+ console.log(` ${cmd} gsp/skills/gsp-style/styles/saas.yml --registry --output saas.theme.json`);
286
387
  process.exit(0);
287
388
  }
288
389
 
@@ -295,10 +396,23 @@ function main() {
295
396
  const outputIdx = args.indexOf('--output');
296
397
  const outputPath = outputIdx !== -1 ? path.resolve(args[outputIdx + 1]) : null;
297
398
  const toStdout = args.includes('--stdout') || !outputPath;
399
+ const asRegistry = args.includes('--registry');
298
400
 
299
401
  const raw = fs.readFileSync(inputPath, 'utf8');
300
402
  const preset = parseYaml(raw);
301
403
 
404
+ if (asRegistry) {
405
+ const registryItem = buildRegistryItem(preset, inputPath);
406
+ const output = JSON.stringify(registryItem, null, 2);
407
+ if (toStdout) {
408
+ process.stdout.write(output + '\n');
409
+ } else {
410
+ fs.writeFileSync(outputPath, output + '\n', 'utf8');
411
+ console.log(`Written to ${outputPath}`);
412
+ }
413
+ return;
414
+ }
415
+
302
416
  const colorLight = (preset.tokens && preset.tokens.color) || {};
303
417
  const colorDark = (preset.dark_mode && preset.dark_mode.color) || {};
304
418
  const shape = (preset.tokens && preset.tokens.shape) || {};
@@ -313,7 +427,7 @@ function main() {
313
427
  const header = [
314
428
  `/* GSP theme: ${presetName} */`,
315
429
  presetDesc ? `/* ${presetDesc} */` : null,
316
- `/* Generated by bin/theme-css.js from ${path.basename(inputPath)} */`,
430
+ `/* Generated by theme-css.js from ${path.basename(inputPath)} */`,
317
431
  `/* Edit the .yml file, not this output */`,
318
432
  '',
319
433
  ].filter(Boolean).join('\n');