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:
@@ -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
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
3
src/tailwind.css
Normal file
3
src/tailwind.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
160
src/theme/generated/tailwind-tokens.js
Normal file
160
src/theme/generated/tailwind-tokens.js
Normal 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"
|
||||
}
|
||||
};
|
||||
@@ -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
198
tailwind.config.js
Normal 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']);
|
||||
}),
|
||||
],
|
||||
};
|
||||
Reference in New Issue
Block a user