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.
- package/gsp/skills/gsp-accessibility/SKILL.md +50 -0
- package/gsp/skills/gsp-accessibility/motion-effects.md +43 -0
- package/gsp/skills/gsp-brand-apply/SKILL.md +213 -0
- package/gsp/skills/gsp-brand-apply/bin/serve-preset.js +71 -0
- package/gsp/skills/gsp-brand-guidelines/SKILL.md +64 -4
- package/{bin → gsp/skills/gsp-brand-guidelines/bin}/theme-css.js +124 -10
- package/gsp/skills/gsp-brand-guidelines/methodology/gsp-brand-engineer.md +1 -1
- package/gsp/skills/gsp-brand-guidelines/token-mapping.md +2 -2
- package/gsp/skills/gsp-brand-identity/SKILL.md +5 -4
- package/gsp/skills/gsp-brand-refine/SKILL.md +33 -3
- package/gsp/skills/gsp-brand-research/SKILL.md +1 -1
- package/gsp/skills/gsp-brand-research/methodology/gsp-brand-researcher.md +9 -0
- package/gsp/skills/gsp-brand-strategy/methodology/gsp-brand-strategist.md +3 -3
- package/gsp/skills/gsp-brand-sync/SKILL.md +0 -1
- package/gsp/skills/gsp-project-brief/SKILL.md +6 -1
- package/gsp/skills/gsp-project-build/SKILL.md +44 -108
- package/gsp/skills/gsp-project-build/agent-rules.md +36 -0
- package/gsp/skills/gsp-project-build/brand-feedback.md +12 -0
- package/gsp/skills/gsp-project-build/component-classification.md +36 -0
- package/gsp/skills/gsp-project-build/extraction-review.md +31 -0
- package/gsp/skills/gsp-project-build/methodology/gsp-project-builder.md +17 -10
- package/gsp/skills/gsp-project-build/visual-effects.md +1 -16
- package/gsp/skills/gsp-project-critique/SKILL.md +5 -1
- package/gsp/skills/gsp-project-critique/methodology/gsp-project-critic.md +46 -89
- package/gsp/skills/gsp-project-design/SKILL.md +1 -1
- package/gsp/skills/gsp-project-design/methodology/gsp-project-designer.md +56 -118
- package/gsp/skills/gsp-project-research/SKILL.md +13 -0
- package/gsp/skills/gsp-project-review/SKILL.md +1 -1
- package/gsp/skills/gsp-project-review/methodology/gsp-project-reviewer.md +5 -4
- package/gsp/skills/gsp-scaffold/SKILL.md +13 -0
- package/gsp/skills/gsp-scaffold/shadcn-rules.md +7 -11
- package/gsp/skills/gsp-style/SKILL.md +3 -3
- package/gsp/skills/gsp-style/style-preset-schema.md +1 -1
- package/gsp/skills/gsp-visuals/domains/imagery.md +10 -16
- package/gsp/templates/branding/config.json +2 -2
- package/gsp/templates/phases/patterns.md +2 -2
- package/gsp/templates/projects/config.json +4 -3
- package/package.json +1 -1
- 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}/
|
|
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**
|
|
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
|
|
3
|
+
* theme-css.js — GSP deterministic token generator
|
|
4
4
|
*
|
|
5
|
-
* Reads a GSP style preset `.yml` file and
|
|
6
|
-
* CSS variables block
|
|
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 <
|
|
10
|
-
* node bin/theme-css.js <
|
|
11
|
-
* node bin/theme-css.js <
|
|
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
|
-
|
|
285
|
-
console.log(`
|
|
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
|
|
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');
|