Add Tailwind v3 integration aligned with production config

- Create tailwind.config.js merging production tokens with design
  system palettes from Style Dictionary
- Extend Style Dictionary to output tailwind-tokens.js module
- Add PostCSS config and Tailwind directive CSS
- Import Tailwind CSS in Storybook preview
- Production breakpoints, colours, typography, spacing preserved
- Design system brand/sage/neutral/red/amber/green/blue palettes
  added alongside production's generic color-1..6 naming

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 13:27:33 +10:00
parent fcc69446f3
commit f18e2ed2e4
6 changed files with 470 additions and 6 deletions

View File

@@ -3,6 +3,7 @@ import type { Preview } from '@storybook/react';
import { ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import { theme } from '../src/theme';
import '../src/tailwind.css';
import '../src/theme/generated/tokens.css';
const preview: Preview = {

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

3
src/tailwind.css Normal file
View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,160 @@
/**
* Auto-generated Tailwind tokens from Style Dictionary.
* Do not edit directly — edit tokens/*.json and run npm run build:tokens.
*/
module.exports = {
"colors": {
"brand": {
"50": "#fef9f5",
"100": "#f7ecdf",
"200": "#ebdac8",
"300": "#d8c3b5",
"400": "#d0a070",
"500": "#ba834e",
"600": "#b0610f",
"700": "#8b4e0d",
"800": "#6b3c13",
"900": "#51301b",
"950": "#251913"
},
"sage": {
"50": "#f2f5f6",
"100": "#e3eaeb",
"200": "#d7e1e2",
"300": "#c8d4d6",
"400": "#b9c7c9",
"500": "#8ea2a7",
"600": "#687d84",
"700": "#4c5b6b",
"800": "#4c5459",
"900": "#343c40",
"950": "#1e2528"
},
"neutral": {
"50": "#fafafa",
"100": "#f5f5f5",
"200": "#e8e8e8",
"300": "#d4d4d4",
"400": "#a3a3a3",
"500": "#737373",
"600": "#525252",
"700": "#404040",
"800": "#2c2e35",
"900": "#1a1a1c",
"950": "#0a0a0b"
},
"red": {
"50": "#fef2f2",
"100": "#fde8e8",
"200": "#f9bfbf",
"300": "#f09898",
"400": "#e56b6b",
"500": "#d64545",
"600": "#bc2f2f",
"700": "#9b2424",
"800": "#7a1d1d",
"900": "#5c1616",
"950": "#3d0e0e"
},
"amber": {
"50": "#fff9eb",
"100": "#fff0cc",
"200": "#ffe099",
"300": "#ffcc66",
"400": "#ffb833",
"500": "#f5a000",
"600": "#cc8500",
"700": "#a36b00",
"800": "#7a5000",
"900": "#523600",
"950": "#331f00"
},
"green": {
"50": "#f0f7f0",
"100": "#d8ecd8",
"200": "#b8d8b8",
"300": "#8dc08d",
"400": "#66a866",
"500": "#4a8f4a",
"600": "#3b7a3b",
"700": "#2e6b2e",
"800": "#235523",
"900": "#1a3f1a",
"950": "#0f2a0f"
},
"blue": {
"50": "#eff6ff",
"100": "#dbeafe",
"200": "#bfdbfe",
"300": "#93c5fd",
"400": "#60a5fa",
"500": "#3b82f6",
"600": "#2563eb",
"700": "#1d4ed8",
"800": "#1e40af",
"900": "#1e3a8a",
"950": "#172554"
},
"white": "#ffffff",
"black": "#000000"
},
"spacing": {
"1": "4px",
"2": "8px",
"3": "12px",
"4": "16px",
"5": "20px",
"6": "24px",
"8": "32px",
"10": "40px",
"12": "48px",
"16": "64px",
"20": "80px",
"05": "2px"
},
"borderRadius": {
"none": "0px",
"sm": "4px",
"md": "8px",
"lg": "12px",
"xl": "16px",
"full": "9999px"
},
"fontFamily": {
"body": "'Montserrat', 'Helvetica Neue', Arial, sans-serif",
"display": "'Noto Serif SC', Georgia, 'Times New Roman', serif",
"mono": "'JetBrains Mono', 'Fira Code', Consolas, monospace"
},
"fontSize": {
"2xs": "0.6875rem",
"xs": "0.75rem",
"sm": "0.875rem",
"base": "1rem",
"md": "1.125rem",
"lg": "1.25rem",
"xl": "1.5rem",
"2xl": "1.875rem",
"3xl": "2.25rem",
"4xl": "3rem"
},
"fontWeight": {
"regular": 400,
"medium": 500,
"semibold": 600,
"bold": 700
},
"lineHeight": {
"tight": 1.25,
"snug": 1.375,
"normal": 1.5,
"relaxed": 1.75
},
"letterSpacing": {
"tighter": "-0.02em",
"tight": "-0.01em",
"normal": "0em",
"wide": "0.02em",
"wider": "0.05em",
"widest": "0.08em"
}
};

View File

@@ -4,17 +4,18 @@
* Transforms W3C DTCG token JSON into:
* - CSS custom properties (for runtime theming)
* - JavaScript ES6 module (for MUI theme consumption)
* - Tailwind token module (for tailwind.config.js consumption)
* - JSON (for Penpot import)
*/
import StyleDictionary from 'style-dictionary';
import { readFileSync, writeFileSync } from 'fs';
const sd = new StyleDictionary({
source: ['tokens/**/*.json'],
usesDtcg: true,
platforms: {
// CSS custom properties
css: {
transformGroup: 'css',
prefix: 'fa',
@@ -28,7 +29,6 @@ const sd = new StyleDictionary({
}],
},
// JavaScript ES6 module for MUI theme
js: {
transformGroup: 'js',
buildPath: 'src/theme/generated/',
@@ -38,7 +38,6 @@ const sd = new StyleDictionary({
}],
},
// Flat JSON for Penpot import and other tools
json: {
transformGroup: 'js',
buildPath: 'tokens/export/',
@@ -53,13 +52,11 @@ const sd = new StyleDictionary({
await sd.buildAllPlatforms();
// Generate TypeScript declarations for the JS token output
import { readFileSync, writeFileSync } from 'fs';
const jsPath = 'src/theme/generated/tokens.js';
const dtsPath = 'src/theme/generated/tokens.d.ts';
const jsContent = readFileSync(jsPath, 'utf-8')
.replace(/=\n\s+/g, '= '); // join continuation lines
.replace(/=\n\s+/g, '= ');
const declarations = ['/**', ' * Do not edit directly, this file was auto-generated.', ' */'];
@@ -73,3 +70,102 @@ for (const line of jsContent.split('\n')) {
writeFileSync(dtsPath, declarations.join('\n') + '\n');
console.log(`✓ Generated ${declarations.length - 3} TypeScript declarations`);
// Generate Tailwind token module from the flat JSON export
// Keys are PascalCase: ColorBrand500, Spacing4, FontFamilyBody, etc.
const flatTokens = JSON.parse(readFileSync('tokens/export/tokens-flat.json', 'utf-8'));
const twColors = {};
const twSpacing = {};
const twBorderRadius = {};
const twFontFamily = {};
const twFontSize = {};
const twFontWeight = {};
const twLineHeight = {};
const twLetterSpacing = {};
// Helper: split PascalCase into parts, e.g. "ColorBrand500" → ["Color", "Brand", "500"]
function splitPascal(str) {
return str.match(/[A-Z][a-z]*|\d+/g) || [];
}
for (const [key, value] of Object.entries(flatTokens)) {
// Primitive colour tokens: ColorBrand500 → brand.500
const colorMatch = key.match(/^Color(Brand|Sage|Neutral|Red|Amber|Green|Blue)(\d+)$/);
if (colorMatch) {
const group = colorMatch[1].toLowerCase();
const shade = colorMatch[2];
if (!twColors[group]) twColors[group] = {};
twColors[group][shade] = value;
continue;
}
// Single-value colors: ColorWhite, ColorBlack
if (key === 'ColorWhite') { twColors['white'] = value; continue; }
if (key === 'ColorBlack') { twColors['black'] = value; continue; }
// Primitive spacing: Spacing1, Spacing0-5, Spacing16, etc.
const spacingMatch = key.match(/^Spacing(\d+)$/);
if (spacingMatch) {
twSpacing[spacingMatch[1]] = value;
continue;
}
if (key === 'Spacing05') { twSpacing['0.5'] = value; continue; }
// Primitive border radius: BorderRadiusSm, BorderRadiusMd, etc.
const radiusMatch = key.match(/^BorderRadius(\w+)$/);
if (radiusMatch) {
twBorderRadius[radiusMatch[1].toLowerCase()] = value;
continue;
}
// Font family: FontFamilyBody, FontFamilyDisplay, FontFamilyMono
const fontFamilyMatch = key.match(/^FontFamily(\w+)$/);
if (fontFamilyMatch) {
twFontFamily[fontFamilyMatch[1].toLowerCase()] = value;
continue;
}
// Primitive font size: FontSize2xs, FontSizeBase, etc. (skip mobile/display)
const fontSizeMatch = key.match(/^FontSize(2xs|Xs|Sm|Base|Md|Lg|Xl|2xl|3xl|4xl)$/i);
if (fontSizeMatch) {
twFontSize[fontSizeMatch[1].toLowerCase()] = value;
continue;
}
// Font weight: FontWeightRegular, FontWeightMedium, etc.
const fontWeightMatch = key.match(/^FontWeight(\w+)$/);
if (fontWeightMatch) {
twFontWeight[fontWeightMatch[1].toLowerCase()] = value;
continue;
}
// Line height: LineHeightTight, LineHeightNormal, etc.
const lineHeightMatch = key.match(/^LineHeight(\w+)$/);
if (lineHeightMatch) {
twLineHeight[lineHeightMatch[1].toLowerCase()] = value;
continue;
}
// Letter spacing: LetterSpacingTight, LetterSpacingNormal, etc.
const letterSpacingMatch = key.match(/^LetterSpacing(\w+)$/);
if (letterSpacingMatch) {
twLetterSpacing[letterSpacingMatch[1].toLowerCase()] = value;
continue;
}
}
const tailwindTokens = {
colors: twColors,
spacing: twSpacing,
borderRadius: twBorderRadius,
fontFamily: twFontFamily,
fontSize: twFontSize,
fontWeight: twFontWeight,
lineHeight: twLineHeight,
letterSpacing: twLetterSpacing,
};
const twOutput = `/**\n * Auto-generated Tailwind tokens from Style Dictionary.\n * Do not edit directly — edit tokens/*.json and run npm run build:tokens.\n */\nmodule.exports = ${JSON.stringify(tailwindTokens, null, 2)};\n`;
writeFileSync('src/theme/generated/tailwind-tokens.js', twOutput);
console.log(`✓ Generated Tailwind token module`);

198
tailwind.config.js Normal file
View File

@@ -0,0 +1,198 @@
/** @type {import('tailwindcss').Config} */
const defaultTheme = require('tailwindcss/defaultTheme');
const plugin = require('tailwindcss/plugin');
// Design system tokens generated from Style Dictionary
const dsTokens = require('./src/theme/generated/tailwind-tokens.js');
// ─── Split-background plugin (from production) ─────────────────────────────
function createSplitBgPlugin() {
const result = {};
result['.split-bg'] = {
background:
'linear-gradient(90deg, var(--left-color, #000) 50%, var(--right-color, #fff) 50%)',
'--left-color': '#000',
'--right-color': '#fff',
};
// Use design system palettes for split-bg utilities
const palettes = dsTokens.colors;
Object.entries(palettes).forEach(([colorName, shades]) => {
if (typeof shades !== 'object') return;
Object.entries(shades).forEach(([shade, value]) => {
result[`.split-left-${colorName}-${shade}`] = { '--left-color': value };
result[`.split-right-${colorName}-${shade}`] = { '--right-color': value };
});
});
return result;
}
// ─── Config ─────────────────────────────────────────────────────────────────
module.exports = {
content: [
'./src/**/*.{js,ts,jsx,tsx,mdx}',
'./.storybook/**/*.{js,ts,jsx,tsx}',
],
safelist: [{ pattern: /^line-clamp-.*/ }],
theme: {
// ── Breakpoints (match production + MUI v5/v7 defaults) ──
screens: {
xs: '0px',
sm: '600px',
md: '960px',
lg: '1280px',
xl: '1536px',
},
// ── List style types (from production) ──
listStyleType: {
none: 'none',
disc: 'disc',
circle: 'circle',
square: 'square',
decimal: 'decimal',
roman: 'lower-roman',
alpha: 'lower-alpha',
},
extend: {
// ── Colours ──
// Production's colour tokens (preserved for backward compatibility)
// plus design system semantic palettes
colors: {
// --- Production tokens (unchanged) ---
'grey-1': '#f4f3ef',
'grey-2': '#D7E1E2',
'grey-3': '#b9c7c9',
'grey-4': '#4c5459',
'grey-5': '#4c5b6b',
'grey-6': '#D9D9D9',
'grey-7': '#EEEEEE',
'warning-red': '#A41623',
'color-1': '#B0610F',
'color-1H': '#9e6f42',
'color-2': '#DCC1A6',
'color-2H': '#d1b79e',
'color-3': '#BA834E',
'color-4': '#D0A070',
'color-5': '#F4F3EF',
'color-6': '#51301B',
'action-orange': '#F89E53',
'success-green': '#76B041',
// --- Design system palettes (from tokens) ---
...dsTokens.colors,
},
// ── Font families ──
fontFamily: {
// Production names
sans: ['Montserrat', ...defaultTheme.fontFamily.sans],
arial: ['Arial', 'sans-serif'],
notoSerifSC: ['"Noto Serif SC"', 'serif'],
// Design system aliases
display: dsTokens.fontFamily.display?.split(',').map((f) => f.trim()) || ['"Noto Serif SC"', 'serif'],
mono: dsTokens.fontFamily.mono?.split(',').map((f) => f.trim()) || defaultTheme.fontFamily.mono,
},
// ── Spacing (production custom values + design system scale) ──
space: {
8.5: '30px',
},
padding: {
1.25: '5px',
3.1: '12px',
3.15: '12.5px',
3.5: '14px',
3.75: '15px',
7.5: '30px',
},
margin: {
1.25: '5px',
3.1: '12px',
3.15: '12.5px',
3.75: '15px',
7.5: '30px',
},
// ── Border width (from production) ──
borderWidth: {
3: '3px',
1.5: '1.5px',
10: '10px',
},
// ── Aspect ratio (from production) ──
aspectRatio: {
'16/9': '16 / 9',
'4/3': '3 / 2',
},
// ── Heights (from production) ──
height: {
18: '4.5rem',
22: '5.5rem',
},
minHeight: {
input: 'calc(30px + 1.5rem)',
},
// ── Box shadows (from production) ──
boxShadow: {
'br-md':
'8px 0 15px -4px rgba(0, 0, 0, 0.15), 0 8px 15px -4px rgba(0, 0, 0, 0.15)',
badge: '0 0 5px 4px #f4f3ef',
'br-sm': '0 4px 6px rgba(0,0,0,0.07)',
},
// ── Font sizes ──
// Production custom sizes
fontSize: {
xxs: '10px',
'1.5xl': '22px',
'2.15xl': '25px',
'2.3xl': '26px',
'3.3xl': '32px',
// Design system sizes (where they add values not in Tailwind defaults)
'2xs': dsTokens.fontSize['2xs'],
},
// ── Gap (from production) ──
gap: {
3.75: '15px',
7.5: '30px',
},
// ── Border radius ──
borderRadius: {
// Production
'1.5xl': '15px',
// Design system
...dsTokens.borderRadius,
},
// ── Animation (from production) ──
animation: {
logoScroll: 'logoScroll 25s linear infinite',
},
keyframes: {
logoScroll: {
'0%': { transform: 'translateX(0)' },
'100%': { transform: 'translateX(-50%)' },
},
},
// ── Line height (design system) ──
lineHeight: dsTokens.lineHeight,
// ── Letter spacing (design system) ──
letterSpacing: dsTokens.letterSpacing,
},
},
plugins: [
plugin(function ({ addUtilities }) {
addUtilities(createSplitBgPlugin(), ['responsive']);
}),
],
};