Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 80 additions & 2 deletions .claude/commands/project/init-brand-tokens.md
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,16 @@ export default {
--color-error-border: oklch(85% 0.08 27);
--color-error-icon: oklch(40% 0.20 27);

--color-warning-bg: oklch(95% 0.02 90);
--color-warning-fg: oklch(35% 0.15 90);
--color-warning-border: oklch(85% 0.08 90);
--color-warning-icon: oklch(45% 0.16 90);

--color-info-bg: oklch(95% 0.02 240);
--color-info-fg: oklch(30% 0.12 240);
--color-info-border: oklch(85% 0.05 240);
--color-info-icon: oklch(40% 0.14 240);

/* Neutral palette */
--color-neutral-50: oklch(98% 0 0);
--color-neutral-100: oklch(96% 0 0);
Expand All @@ -473,22 +483,67 @@ export default {
--color-neutral-900: oklch(15% 0 0);
--color-neutral-950: oklch(11% 0 0);

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

/* Typography - Sizes */
--font-size-xs: 0.75rem;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.25rem;
--font-size-2xl: 1.5rem;
--font-size-3xl: 1.875rem;
--font-size-4xl: 2.25rem;

/* Typography - Weights */
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;

/* Typography - Line Heights */
--line-height-tight: 1.25;
--line-height-normal: 1.5;
--line-height-relaxed: 1.75;

/* Typography - Letter Spacing */
--letter-spacing-display: -0.025em;
--letter-spacing-body: 0em;
--letter-spacing-cta: 0.025em;

/* Spacing scale (4px grid) */
--spacing-0: 0px;
--spacing-1: 4px;
--spacing-2: 8px;
--spacing-3: 12px;
--spacing-4: 16px;
--spacing-5: 20px;
--spacing-6: 24px;
--spacing-8: 32px;
--spacing-10: 40px;
--spacing-12: 48px;
--spacing-16: 64px;
--spacing-20: 80px;
--spacing-24: 96px;

/* Shadows - Light mode */
--shadow-sm: 0 1px 2px oklch(0% 0 0 / 0.05);
--shadow-md: 0 4px 6px oklch(0% 0 0 / 0.07), 0 2px 4px oklch(0% 0 0 / 0.06);
--shadow-lg: 0 10px 15px oklch(0% 0 0 / 0.10), 0 4px 6px oklch(0% 0 0 / 0.05);

/* Motion */
/* Motion - Duration */
--motion-duration-fast: 150ms;
--motion-duration-base: 200ms;
--motion-duration-slow: 300ms;
--motion-duration-slower: 500ms;

/* Motion - Easing */
--motion-easing-standard: cubic-bezier(0.4, 0.0, 0.2, 1);
--motion-easing-decelerate: cubic-bezier(0.0, 0.0, 0.2, 1);
--motion-easing-accelerate: cubic-bezier(0.4, 0.0, 1.0, 1.0);

/* Data visualization - Okabe-Ito (color-blind-safe) */
--dataviz-okabe-ito-orange: oklch(68.29% 0.151 58.43);
Expand All @@ -499,6 +554,26 @@ export default {
--dataviz-okabe-ito-vermillion: oklch(57.50% 0.199 37.70);
--dataviz-okabe-ito-reddish-purple: oklch(50.27% 0.159 328.36);
--dataviz-okabe-ito-black: oklch(0% 0 0);

/* Data visualization - Sequential scales */
--dataviz-sequential-blue-1: oklch(95% 0.02 240);
--dataviz-sequential-blue-2: oklch(75% 0.08 240);
--dataviz-sequential-blue-3: oklch(55% 0.12 240);
--dataviz-sequential-blue-4: oklch(35% 0.14 240);
--dataviz-sequential-blue-5: oklch(25% 0.16 240);

--dataviz-sequential-green-1: oklch(95% 0.02 145);
--dataviz-sequential-green-2: oklch(75% 0.08 145);
--dataviz-sequential-green-3: oklch(55% 0.12 145);
--dataviz-sequential-green-4: oklch(35% 0.14 145);
--dataviz-sequential-green-5: oklch(25% 0.16 145);

/* Data visualization - Diverging scales */
--dataviz-diverging-red-blue-1: oklch(95% 0.02 27);
--dataviz-diverging-red-blue-2: oklch(75% 0.08 27);
--dataviz-diverging-red-blue-3: oklch(90% 0 0);
--dataviz-diverging-red-blue-4: oklch(75% 0.08 240);
--dataviz-diverging-red-blue-5: oklch(95% 0.02 240);
}

/* Dark mode shadows (4-6x opacity increase) */
Expand All @@ -516,7 +591,10 @@ export default {
--motion-duration-fast: 0ms;
--motion-duration-base: 0ms;
--motion-duration-slow: 0ms;
--motion-duration-slower: 0ms;
--motion-easing-standard: linear;
--motion-easing-decelerate: linear;
--motion-easing-accelerate: linear;
}
}

Expand Down
255 changes: 255 additions & 0 deletions .spec-flow/scripts/modules/emit.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
#!/usr/bin/env node

/**
* Emit Module - Generate tokens.json and tokens.css
*
* Transforms consolidated token data into:
* 1. tokens.json - Machine-readable source of truth
* 2. tokens.css - Browser-consumable CSS variables
*
* Ensures complete parity between JSON and CSS output.
*/

import fs from 'fs/promises';
import path from 'path';

/**
* Emit tokens.json file
* @param {Object} options
* @param {Object} options.tokens - Consolidated token object
* @param {string} options.out - Output file path
*/
export async function emitJSON({ tokens, out }) {
await fs.mkdir(path.dirname(out), { recursive: true });
await fs.writeFile(out, JSON.stringify(tokens, null, 2), 'utf8');
return out;
}

/**
* Emit tokens.css file with complete coverage
* @param {Object} options
* @param {Object} options.tokens - Consolidated token object
* @param {string} options.out - Output file path
*/
export async function emitCSS({ tokens, out }) {
const lines = [];

lines.push(':root {');

// Brand colors
if (tokens.colors?.brand) {
lines.push(' /* Brand colors (OKLCH with sRGB fallback) */');
for (const [key, value] of Object.entries(tokens.colors.brand)) {
lines.push(` --color-${key}: ${value.oklch};`);
lines.push(` --color-${key}-fallback: ${value.fallback};`);
}
lines.push('');
}

// Semantic colors (all 4: success, error, warning, info)
if (tokens.colors?.semantic) {
lines.push(' /* Semantic colors (bg/fg/border/icon structure) */');
for (const [semantic, states] of Object.entries(tokens.colors.semantic)) {
for (const [state, value] of Object.entries(states)) {
lines.push(` --color-${semantic}-${state}: ${value.oklch};`);
}
lines.push('');
}
}

// Neutral palette
if (tokens.colors?.neutral) {
lines.push(' /* Neutral palette */');
for (const [shade, value] of Object.entries(tokens.colors.neutral)) {
lines.push(` --color-neutral-${shade}: ${value.oklch};`);
}
lines.push('');
}

// Typography - Families
if (tokens.typography?.families) {
lines.push(' /* Typography - Families */');
for (const [key, value] of Object.entries(tokens.typography.families)) {
lines.push(` --font-${key}: ${value};`);
}
lines.push('');
}

// Typography - Sizes
if (tokens.typography?.sizes) {
lines.push(' /* Typography - Sizes */');
for (const [key, value] of Object.entries(tokens.typography.sizes)) {
lines.push(` --font-size-${key}: ${value};`);
}
lines.push('');
}

// Typography - Weights
if (tokens.typography?.weights) {
lines.push(' /* Typography - Weights */');
for (const [key, value] of Object.entries(tokens.typography.weights)) {
lines.push(` --font-weight-${key}: ${value};`);
}
lines.push('');
}

// Typography - Line Heights
if (tokens.typography?.lineHeights) {
lines.push(' /* Typography - Line Heights */');
for (const [key, value] of Object.entries(tokens.typography.lineHeights)) {
lines.push(` --line-height-${key}: ${value};`);
}
lines.push('');
}

// Typography - Letter Spacing
if (tokens.typography?.letterSpacing) {
lines.push(' /* Typography - Letter Spacing */');
for (const [key, value] of Object.entries(tokens.typography.letterSpacing)) {
lines.push(` --letter-spacing-${key}: ${value};`);
}
lines.push('');
}

// Spacing scale
if (tokens.spacing) {
lines.push(' /* Spacing scale (4px grid) */');
for (const [key, value] of Object.entries(tokens.spacing)) {
lines.push(` --spacing-${key}: ${value};`);
}
lines.push('');
}

// Shadows - Light mode
if (tokens.shadows?.light) {
lines.push(' /* Shadows - Light mode */');
for (const [key, value] of Object.entries(tokens.shadows.light)) {
const shadowValue = value.value || value;
lines.push(` --shadow-${key}: ${shadowValue};`);
}
lines.push('');
}

// Motion - Duration
if (tokens.motion?.duration) {
lines.push(' /* Motion - Duration */');
for (const [key, value] of Object.entries(tokens.motion.duration)) {
lines.push(` --motion-duration-${key}: ${value};`);
}
lines.push('');
}

// Motion - Easing
if (tokens.motion?.easing) {
lines.push(' /* Motion - Easing */');
for (const [key, value] of Object.entries(tokens.motion.easing)) {
lines.push(` --motion-easing-${key}: ${value};`);
}
lines.push('');
}

// Data visualization - Okabe-Ito
if (tokens.dataViz?.categorical?.['okabe-ito']) {
lines.push(' /* Data visualization - Okabe-Ito (color-blind-safe) */');
for (const [key, value] of Object.entries(tokens.dataViz.categorical['okabe-ito'])) {
const varName = key.replace(/([A-Z])/g, '-$1').toLowerCase();
lines.push(` --dataviz-okabe-ito-${varName}: ${value.oklch};`);
}
lines.push('');
}

// Data visualization - Sequential scales
if (tokens.dataViz?.sequential) {
lines.push(' /* Data visualization - Sequential scales */');
for (const [palette, colors] of Object.entries(tokens.dataViz.sequential)) {
colors.forEach((color, index) => {
lines.push(` --dataviz-sequential-${palette}-${index + 1}: ${color};`);
});
lines.push('');
}
}

// Data visualization - Diverging scales
if (tokens.dataViz?.diverging) {
lines.push(' /* Data visualization - Diverging scales */');
for (const [palette, colors] of Object.entries(tokens.dataViz.diverging)) {
colors.forEach((color, index) => {
lines.push(` --dataviz-diverging-${palette}-${index + 1}: ${color};`);
});
lines.push('');
}
}

lines.push('}');
lines.push('');

// Dark mode shadows
if (tokens.shadows?.dark) {
lines.push('/* Dark mode shadows (4-6x opacity increase) */');
lines.push('@media (prefers-color-scheme: dark) {');
lines.push(' :root {');
for (const [key, value] of Object.entries(tokens.shadows.dark)) {
const shadowValue = value.value || value;
lines.push(` --shadow-${key}: ${shadowValue};`);
}
lines.push(' }');
lines.push('}');
lines.push('');
}

// Reduced motion (accessibility)
if (tokens.motion) {
lines.push('/* Reduced motion (accessibility) */');
lines.push('@media (prefers-reduced-motion: reduce) {');
lines.push(' :root {');

if (tokens.motion.duration) {
for (const key of Object.keys(tokens.motion.duration)) {
lines.push(` --motion-duration-${key}: 0ms;`);
}
}

if (tokens.motion.easing) {
for (const key of Object.keys(tokens.motion.easing)) {
lines.push(` --motion-easing-${key}: linear;`);
}
}

lines.push(' }');
lines.push('}');
lines.push('');
}

// OKLCH fallback for legacy browsers
if (tokens.colors?.brand) {
lines.push('/* OKLCH fallback for legacy browsers (~8%) */');
lines.push('@supports not (color: oklch(0% 0 0)) {');
lines.push(' :root {');
for (const key of Object.keys(tokens.colors.brand)) {
lines.push(` --color-${key}: var(--color-${key}-fallback);`);
}
lines.push(' }');
lines.push('}');
}

const css = lines.join('\n');
await fs.mkdir(path.dirname(out), { recursive: true });
await fs.writeFile(out, css, 'utf8');
return out;
}

/**
* Emit both JSON and CSS files
* @param {Object} options
* @param {Object} options.tokens - Consolidated token object
* @param {string} options.jsonOut - Path for tokens.json
* @param {string} options.cssOut - Path for tokens.css
*/
export async function emitAll({ tokens, jsonOut, cssOut }) {
const [jsonPath, cssPath] = await Promise.all([
emitJSON({ tokens, out: jsonOut }),
emitCSS({ tokens, out: cssOut })
]);

return { jsonPath, cssPath };
}
Loading
Loading