Skip to content

Commit ec03728

Browse files
authored
Merge pull request #2 from marcusgoll/claude/review-init-brand-tokens-01623xBJ5hD6pX7yTPoLYFvb
Review init-brand-tokens command implementation
2 parents c40b986 + 31f6bd0 commit ec03728

File tree

3 files changed

+644
-2
lines changed

3 files changed

+644
-2
lines changed

.claude/commands/project/init-brand-tokens.md

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,16 @@ export default {
460460
--color-error-border: oklch(85% 0.08 27);
461461
--color-error-icon: oklch(40% 0.20 27);
462462

463+
--color-warning-bg: oklch(95% 0.02 90);
464+
--color-warning-fg: oklch(35% 0.15 90);
465+
--color-warning-border: oklch(85% 0.08 90);
466+
--color-warning-icon: oklch(45% 0.16 90);
467+
468+
--color-info-bg: oklch(95% 0.02 240);
469+
--color-info-fg: oklch(30% 0.12 240);
470+
--color-info-border: oklch(85% 0.05 240);
471+
--color-info-icon: oklch(40% 0.14 240);
472+
463473
/* Neutral palette */
464474
--color-neutral-50: oklch(98% 0 0);
465475
--color-neutral-100: oklch(96% 0 0);
@@ -473,22 +483,67 @@ export default {
473483
--color-neutral-900: oklch(15% 0 0);
474484
--color-neutral-950: oklch(11% 0 0);
475485

476-
/* Typography */
486+
/* Typography - Families */
477487
--font-sans: Inter, system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
478488
--font-mono: Fira Code, Menlo, Monaco, Consolas, monospace;
479489
--font-serif: Georgia, Cambria, 'Times New Roman', Times, serif;
480490

491+
/* Typography - Sizes */
492+
--font-size-xs: 0.75rem;
493+
--font-size-sm: 0.875rem;
494+
--font-size-base: 1rem;
495+
--font-size-lg: 1.125rem;
496+
--font-size-xl: 1.25rem;
497+
--font-size-2xl: 1.5rem;
498+
--font-size-3xl: 1.875rem;
499+
--font-size-4xl: 2.25rem;
500+
501+
/* Typography - Weights */
502+
--font-weight-normal: 400;
503+
--font-weight-medium: 500;
504+
--font-weight-semibold: 600;
505+
--font-weight-bold: 700;
506+
507+
/* Typography - Line Heights */
508+
--line-height-tight: 1.25;
509+
--line-height-normal: 1.5;
510+
--line-height-relaxed: 1.75;
511+
512+
/* Typography - Letter Spacing */
513+
--letter-spacing-display: -0.025em;
514+
--letter-spacing-body: 0em;
515+
--letter-spacing-cta: 0.025em;
516+
517+
/* Spacing scale (4px grid) */
518+
--spacing-0: 0px;
519+
--spacing-1: 4px;
520+
--spacing-2: 8px;
521+
--spacing-3: 12px;
522+
--spacing-4: 16px;
523+
--spacing-5: 20px;
524+
--spacing-6: 24px;
525+
--spacing-8: 32px;
526+
--spacing-10: 40px;
527+
--spacing-12: 48px;
528+
--spacing-16: 64px;
529+
--spacing-20: 80px;
530+
--spacing-24: 96px;
531+
481532
/* Shadows - Light mode */
482533
--shadow-sm: 0 1px 2px oklch(0% 0 0 / 0.05);
483534
--shadow-md: 0 4px 6px oklch(0% 0 0 / 0.07), 0 2px 4px oklch(0% 0 0 / 0.06);
484535
--shadow-lg: 0 10px 15px oklch(0% 0 0 / 0.10), 0 4px 6px oklch(0% 0 0 / 0.05);
485536

486-
/* Motion */
537+
/* Motion - Duration */
487538
--motion-duration-fast: 150ms;
488539
--motion-duration-base: 200ms;
489540
--motion-duration-slow: 300ms;
541+
--motion-duration-slower: 500ms;
542+
543+
/* Motion - Easing */
490544
--motion-easing-standard: cubic-bezier(0.4, 0.0, 0.2, 1);
491545
--motion-easing-decelerate: cubic-bezier(0.0, 0.0, 0.2, 1);
546+
--motion-easing-accelerate: cubic-bezier(0.4, 0.0, 1.0, 1.0);
492547

493548
/* Data visualization - Okabe-Ito (color-blind-safe) */
494549
--dataviz-okabe-ito-orange: oklch(68.29% 0.151 58.43);
@@ -499,6 +554,26 @@ export default {
499554
--dataviz-okabe-ito-vermillion: oklch(57.50% 0.199 37.70);
500555
--dataviz-okabe-ito-reddish-purple: oklch(50.27% 0.159 328.36);
501556
--dataviz-okabe-ito-black: oklch(0% 0 0);
557+
558+
/* Data visualization - Sequential scales */
559+
--dataviz-sequential-blue-1: oklch(95% 0.02 240);
560+
--dataviz-sequential-blue-2: oklch(75% 0.08 240);
561+
--dataviz-sequential-blue-3: oklch(55% 0.12 240);
562+
--dataviz-sequential-blue-4: oklch(35% 0.14 240);
563+
--dataviz-sequential-blue-5: oklch(25% 0.16 240);
564+
565+
--dataviz-sequential-green-1: oklch(95% 0.02 145);
566+
--dataviz-sequential-green-2: oklch(75% 0.08 145);
567+
--dataviz-sequential-green-3: oklch(55% 0.12 145);
568+
--dataviz-sequential-green-4: oklch(35% 0.14 145);
569+
--dataviz-sequential-green-5: oklch(25% 0.16 145);
570+
571+
/* Data visualization - Diverging scales */
572+
--dataviz-diverging-red-blue-1: oklch(95% 0.02 27);
573+
--dataviz-diverging-red-blue-2: oklch(75% 0.08 27);
574+
--dataviz-diverging-red-blue-3: oklch(90% 0 0);
575+
--dataviz-diverging-red-blue-4: oklch(75% 0.08 240);
576+
--dataviz-diverging-red-blue-5: oklch(95% 0.02 240);
502577
}
503578

504579
/* Dark mode shadows (4-6x opacity increase) */
@@ -516,7 +591,10 @@ export default {
516591
--motion-duration-fast: 0ms;
517592
--motion-duration-base: 0ms;
518593
--motion-duration-slow: 0ms;
594+
--motion-duration-slower: 0ms;
519595
--motion-easing-standard: linear;
596+
--motion-easing-decelerate: linear;
597+
--motion-easing-accelerate: linear;
520598
}
521599
}
522600

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Emit Module - Generate tokens.json and tokens.css
5+
*
6+
* Transforms consolidated token data into:
7+
* 1. tokens.json - Machine-readable source of truth
8+
* 2. tokens.css - Browser-consumable CSS variables
9+
*
10+
* Ensures complete parity between JSON and CSS output.
11+
*/
12+
13+
import fs from 'fs/promises';
14+
import path from 'path';
15+
16+
/**
17+
* Emit tokens.json file
18+
* @param {Object} options
19+
* @param {Object} options.tokens - Consolidated token object
20+
* @param {string} options.out - Output file path
21+
*/
22+
export async function emitJSON({ tokens, out }) {
23+
await fs.mkdir(path.dirname(out), { recursive: true });
24+
await fs.writeFile(out, JSON.stringify(tokens, null, 2), 'utf8');
25+
return out;
26+
}
27+
28+
/**
29+
* Emit tokens.css file with complete coverage
30+
* @param {Object} options
31+
* @param {Object} options.tokens - Consolidated token object
32+
* @param {string} options.out - Output file path
33+
*/
34+
export async function emitCSS({ tokens, out }) {
35+
const lines = [];
36+
37+
lines.push(':root {');
38+
39+
// Brand colors
40+
if (tokens.colors?.brand) {
41+
lines.push(' /* Brand colors (OKLCH with sRGB fallback) */');
42+
for (const [key, value] of Object.entries(tokens.colors.brand)) {
43+
lines.push(` --color-${key}: ${value.oklch};`);
44+
lines.push(` --color-${key}-fallback: ${value.fallback};`);
45+
}
46+
lines.push('');
47+
}
48+
49+
// Semantic colors (all 4: success, error, warning, info)
50+
if (tokens.colors?.semantic) {
51+
lines.push(' /* Semantic colors (bg/fg/border/icon structure) */');
52+
for (const [semantic, states] of Object.entries(tokens.colors.semantic)) {
53+
for (const [state, value] of Object.entries(states)) {
54+
lines.push(` --color-${semantic}-${state}: ${value.oklch};`);
55+
}
56+
lines.push('');
57+
}
58+
}
59+
60+
// Neutral palette
61+
if (tokens.colors?.neutral) {
62+
lines.push(' /* Neutral palette */');
63+
for (const [shade, value] of Object.entries(tokens.colors.neutral)) {
64+
lines.push(` --color-neutral-${shade}: ${value.oklch};`);
65+
}
66+
lines.push('');
67+
}
68+
69+
// Typography - Families
70+
if (tokens.typography?.families) {
71+
lines.push(' /* Typography - Families */');
72+
for (const [key, value] of Object.entries(tokens.typography.families)) {
73+
lines.push(` --font-${key}: ${value};`);
74+
}
75+
lines.push('');
76+
}
77+
78+
// Typography - Sizes
79+
if (tokens.typography?.sizes) {
80+
lines.push(' /* Typography - Sizes */');
81+
for (const [key, value] of Object.entries(tokens.typography.sizes)) {
82+
lines.push(` --font-size-${key}: ${value};`);
83+
}
84+
lines.push('');
85+
}
86+
87+
// Typography - Weights
88+
if (tokens.typography?.weights) {
89+
lines.push(' /* Typography - Weights */');
90+
for (const [key, value] of Object.entries(tokens.typography.weights)) {
91+
lines.push(` --font-weight-${key}: ${value};`);
92+
}
93+
lines.push('');
94+
}
95+
96+
// Typography - Line Heights
97+
if (tokens.typography?.lineHeights) {
98+
lines.push(' /* Typography - Line Heights */');
99+
for (const [key, value] of Object.entries(tokens.typography.lineHeights)) {
100+
lines.push(` --line-height-${key}: ${value};`);
101+
}
102+
lines.push('');
103+
}
104+
105+
// Typography - Letter Spacing
106+
if (tokens.typography?.letterSpacing) {
107+
lines.push(' /* Typography - Letter Spacing */');
108+
for (const [key, value] of Object.entries(tokens.typography.letterSpacing)) {
109+
lines.push(` --letter-spacing-${key}: ${value};`);
110+
}
111+
lines.push('');
112+
}
113+
114+
// Spacing scale
115+
if (tokens.spacing) {
116+
lines.push(' /* Spacing scale (4px grid) */');
117+
for (const [key, value] of Object.entries(tokens.spacing)) {
118+
lines.push(` --spacing-${key}: ${value};`);
119+
}
120+
lines.push('');
121+
}
122+
123+
// Shadows - Light mode
124+
if (tokens.shadows?.light) {
125+
lines.push(' /* Shadows - Light mode */');
126+
for (const [key, value] of Object.entries(tokens.shadows.light)) {
127+
const shadowValue = value.value || value;
128+
lines.push(` --shadow-${key}: ${shadowValue};`);
129+
}
130+
lines.push('');
131+
}
132+
133+
// Motion - Duration
134+
if (tokens.motion?.duration) {
135+
lines.push(' /* Motion - Duration */');
136+
for (const [key, value] of Object.entries(tokens.motion.duration)) {
137+
lines.push(` --motion-duration-${key}: ${value};`);
138+
}
139+
lines.push('');
140+
}
141+
142+
// Motion - Easing
143+
if (tokens.motion?.easing) {
144+
lines.push(' /* Motion - Easing */');
145+
for (const [key, value] of Object.entries(tokens.motion.easing)) {
146+
lines.push(` --motion-easing-${key}: ${value};`);
147+
}
148+
lines.push('');
149+
}
150+
151+
// Data visualization - Okabe-Ito
152+
if (tokens.dataViz?.categorical?.['okabe-ito']) {
153+
lines.push(' /* Data visualization - Okabe-Ito (color-blind-safe) */');
154+
for (const [key, value] of Object.entries(tokens.dataViz.categorical['okabe-ito'])) {
155+
const varName = key.replace(/([A-Z])/g, '-$1').toLowerCase();
156+
lines.push(` --dataviz-okabe-ito-${varName}: ${value.oklch};`);
157+
}
158+
lines.push('');
159+
}
160+
161+
// Data visualization - Sequential scales
162+
if (tokens.dataViz?.sequential) {
163+
lines.push(' /* Data visualization - Sequential scales */');
164+
for (const [palette, colors] of Object.entries(tokens.dataViz.sequential)) {
165+
colors.forEach((color, index) => {
166+
lines.push(` --dataviz-sequential-${palette}-${index + 1}: ${color};`);
167+
});
168+
lines.push('');
169+
}
170+
}
171+
172+
// Data visualization - Diverging scales
173+
if (tokens.dataViz?.diverging) {
174+
lines.push(' /* Data visualization - Diverging scales */');
175+
for (const [palette, colors] of Object.entries(tokens.dataViz.diverging)) {
176+
colors.forEach((color, index) => {
177+
lines.push(` --dataviz-diverging-${palette}-${index + 1}: ${color};`);
178+
});
179+
lines.push('');
180+
}
181+
}
182+
183+
lines.push('}');
184+
lines.push('');
185+
186+
// Dark mode shadows
187+
if (tokens.shadows?.dark) {
188+
lines.push('/* Dark mode shadows (4-6x opacity increase) */');
189+
lines.push('@media (prefers-color-scheme: dark) {');
190+
lines.push(' :root {');
191+
for (const [key, value] of Object.entries(tokens.shadows.dark)) {
192+
const shadowValue = value.value || value;
193+
lines.push(` --shadow-${key}: ${shadowValue};`);
194+
}
195+
lines.push(' }');
196+
lines.push('}');
197+
lines.push('');
198+
}
199+
200+
// Reduced motion (accessibility)
201+
if (tokens.motion) {
202+
lines.push('/* Reduced motion (accessibility) */');
203+
lines.push('@media (prefers-reduced-motion: reduce) {');
204+
lines.push(' :root {');
205+
206+
if (tokens.motion.duration) {
207+
for (const key of Object.keys(tokens.motion.duration)) {
208+
lines.push(` --motion-duration-${key}: 0ms;`);
209+
}
210+
}
211+
212+
if (tokens.motion.easing) {
213+
for (const key of Object.keys(tokens.motion.easing)) {
214+
lines.push(` --motion-easing-${key}: linear;`);
215+
}
216+
}
217+
218+
lines.push(' }');
219+
lines.push('}');
220+
lines.push('');
221+
}
222+
223+
// OKLCH fallback for legacy browsers
224+
if (tokens.colors?.brand) {
225+
lines.push('/* OKLCH fallback for legacy browsers (~8%) */');
226+
lines.push('@supports not (color: oklch(0% 0 0)) {');
227+
lines.push(' :root {');
228+
for (const key of Object.keys(tokens.colors.brand)) {
229+
lines.push(` --color-${key}: var(--color-${key}-fallback);`);
230+
}
231+
lines.push(' }');
232+
lines.push('}');
233+
}
234+
235+
const css = lines.join('\n');
236+
await fs.mkdir(path.dirname(out), { recursive: true });
237+
await fs.writeFile(out, css, 'utf8');
238+
return out;
239+
}
240+
241+
/**
242+
* Emit both JSON and CSS files
243+
* @param {Object} options
244+
* @param {Object} options.tokens - Consolidated token object
245+
* @param {string} options.jsonOut - Path for tokens.json
246+
* @param {string} options.cssOut - Path for tokens.css
247+
*/
248+
export async function emitAll({ tokens, jsonOut, cssOut }) {
249+
const [jsonPath, cssPath] = await Promise.all([
250+
emitJSON({ tokens, out: jsonOut }),
251+
emitCSS({ tokens, out: cssOut })
252+
]);
253+
254+
return { jsonPath, cssPath };
255+
}

0 commit comments

Comments
 (0)