2026-03-19 01:12:17 +08:00
|
|
|
const { hairlineWidth, platformSelect } = require('nativewind/theme');
|
|
|
|
|
const tokens = require('./design-tokens.json');
|
|
|
|
|
|
|
|
|
|
const toKebabCase = (value) =>
|
|
|
|
|
value.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
|
|
|
|
|
const px = (value) => `${value}px`;
|
|
|
|
|
const fontConfig = (fontKey) => {
|
|
|
|
|
const font = tokens.fonts[fontKey];
|
|
|
|
|
|
|
|
|
|
return platformSelect(font);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const typographyVarName = (key) => {
|
|
|
|
|
const kebab = toKebabCase(key);
|
|
|
|
|
return key.startsWith('lineHeight')
|
|
|
|
|
? `--line-height-${kebab.replace('line-height-', '')}`
|
|
|
|
|
: `--font-size-${kebab}`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const typographyVars = (mode) =>
|
|
|
|
|
Object.fromEntries(
|
|
|
|
|
Object.entries(tokens.typography[mode]).map(([key, value]) => [
|
|
|
|
|
typographyVarName(key),
|
|
|
|
|
px(value),
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
|
2026-04-10 20:35:57 +08:00
|
|
|
// Use default theme for Tailwind base (runtime theme switch via ThemeProvider).
|
|
|
|
|
// Typography vars use the same comfortable scale as TypographyProvider (large),
|
|
|
|
|
// not the compact `normal` tier — see typography-context.tsx
|
2026-03-19 01:12:17 +08:00
|
|
|
const defaultLight = tokens.colors.default.light;
|
|
|
|
|
const defaultDark = tokens.colors.default.dark;
|
|
|
|
|
|
|
|
|
|
const rootVariables = Object.fromEntries([
|
|
|
|
|
...Object.entries(defaultLight).map(([key, value]) => [
|
|
|
|
|
`--${toKebabCase(key)}`,
|
|
|
|
|
value,
|
|
|
|
|
]),
|
|
|
|
|
['--radius', px(tokens.radius.default)],
|
2026-04-10 20:35:57 +08:00
|
|
|
...Object.entries(typographyVars('large')),
|
2026-03-19 01:12:17 +08:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const darkVariables = Object.fromEntries(
|
|
|
|
|
Object.entries(defaultDark).map(([key, value]) => [
|
|
|
|
|
`--${toKebabCase(key)}`,
|
|
|
|
|
value,
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Mobile-first breakpoints for native (phone/tablet) and web
|
|
|
|
|
const breakpoints = Object.fromEntries(
|
|
|
|
|
Object.entries(tokens.layout.breakpoints).map(([key, value]) => [
|
|
|
|
|
key,
|
|
|
|
|
`${value}px`,
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/** @type {import('tailwindcss').Config} */
|
|
|
|
|
module.exports = {
|
|
|
|
|
darkMode: 'class',
|
|
|
|
|
content: [
|
|
|
|
|
'./src/app/**/*.{ts,tsx}',
|
|
|
|
|
'./src/components/**/*.{ts,tsx}',
|
|
|
|
|
'./src/core/**/*.{ts,tsx}',
|
|
|
|
|
'./src/hooks/**/*.{ts,tsx}',
|
|
|
|
|
'./src/lib/**/*.{ts,tsx}',
|
|
|
|
|
],
|
|
|
|
|
presets: [require('nativewind/preset')],
|
|
|
|
|
theme: {
|
|
|
|
|
screens: breakpoints,
|
|
|
|
|
extend: {
|
|
|
|
|
colors: {
|
|
|
|
|
border: 'hsl(var(--border))',
|
|
|
|
|
input: 'hsl(var(--input))',
|
|
|
|
|
ring: 'hsl(var(--ring))',
|
|
|
|
|
background: 'hsl(var(--background))',
|
|
|
|
|
foreground: 'hsl(var(--foreground))',
|
|
|
|
|
primary: {
|
|
|
|
|
DEFAULT: 'hsl(var(--primary))',
|
|
|
|
|
foreground: 'hsl(var(--primary-foreground))',
|
|
|
|
|
},
|
|
|
|
|
secondary: {
|
|
|
|
|
DEFAULT: 'hsl(var(--secondary))',
|
|
|
|
|
foreground: 'hsl(var(--secondary-foreground))',
|
|
|
|
|
},
|
|
|
|
|
destructive: {
|
|
|
|
|
DEFAULT: 'hsl(var(--destructive))',
|
|
|
|
|
foreground: 'hsl(var(--destructive-foreground))',
|
|
|
|
|
},
|
|
|
|
|
muted: {
|
|
|
|
|
DEFAULT: 'hsl(var(--muted))',
|
|
|
|
|
foreground: 'hsl(var(--muted-foreground))',
|
|
|
|
|
},
|
|
|
|
|
accent: {
|
|
|
|
|
DEFAULT: 'hsl(var(--accent))',
|
|
|
|
|
foreground: 'hsl(var(--accent-foreground))',
|
|
|
|
|
},
|
|
|
|
|
popover: {
|
|
|
|
|
DEFAULT: 'hsl(var(--popover))',
|
|
|
|
|
foreground: 'hsl(var(--popover-foreground))',
|
|
|
|
|
},
|
|
|
|
|
card: {
|
|
|
|
|
DEFAULT: 'hsl(var(--card))',
|
|
|
|
|
foreground: 'hsl(var(--card-foreground))',
|
|
|
|
|
},
|
|
|
|
|
success: {
|
|
|
|
|
DEFAULT: 'hsl(var(--success))',
|
|
|
|
|
foreground: 'hsl(var(--success-foreground))',
|
|
|
|
|
},
|
|
|
|
|
warning: {
|
|
|
|
|
DEFAULT: 'hsl(var(--warning))',
|
|
|
|
|
foreground: 'hsl(var(--warning-foreground))',
|
|
|
|
|
},
|
|
|
|
|
info: {
|
|
|
|
|
DEFAULT: 'hsl(var(--info))',
|
|
|
|
|
foreground: 'hsl(var(--info-foreground))',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
fontFamily: {
|
|
|
|
|
sans: fontConfig('sans'),
|
|
|
|
|
display: fontConfig('display'),
|
|
|
|
|
rounded: fontConfig('rounded'),
|
|
|
|
|
serif: fontConfig('serif'),
|
|
|
|
|
mono: fontConfig('mono'),
|
|
|
|
|
},
|
|
|
|
|
fontSize: {
|
|
|
|
|
headingLarge: 'var(--font-size-heading-large)',
|
|
|
|
|
headingMedium: 'var(--font-size-heading-medium)',
|
|
|
|
|
headingSmall: 'var(--font-size-heading-small)',
|
|
|
|
|
titleLarge: 'var(--font-size-title-large)',
|
|
|
|
|
titleMedium: 'var(--font-size-title-medium)',
|
|
|
|
|
titleSmall: 'var(--font-size-title-small)',
|
|
|
|
|
bodyLarge: 'var(--font-size-body-large)',
|
|
|
|
|
bodyMedium: 'var(--font-size-body-medium)',
|
|
|
|
|
bodySmall: 'var(--font-size-body-small)',
|
|
|
|
|
captionLarge: 'var(--font-size-caption-large)',
|
|
|
|
|
captionMedium: 'var(--font-size-caption-medium)',
|
|
|
|
|
captionSmall: 'var(--font-size-caption-small)',
|
|
|
|
|
sectionTitle: 'var(--font-size-section-title)',
|
|
|
|
|
badge: 'var(--font-size-badge)',
|
|
|
|
|
},
|
|
|
|
|
lineHeight: {
|
|
|
|
|
normal: 'var(--line-height-normal)',
|
|
|
|
|
tight: 'var(--line-height-tight)',
|
|
|
|
|
loose: 'var(--line-height-loose)',
|
|
|
|
|
xLoose: 'var(--line-height-x-loose)',
|
|
|
|
|
},
|
|
|
|
|
borderRadius: {
|
|
|
|
|
lg: 'var(--radius)',
|
|
|
|
|
md: 'calc(var(--radius) - 2px)',
|
|
|
|
|
sm: 'calc(var(--radius) - 4px)',
|
|
|
|
|
xl: px(tokens.radius.xl),
|
|
|
|
|
},
|
|
|
|
|
spacing: {
|
|
|
|
|
'screen-gutter': px(tokens.layout.screenGutter),
|
|
|
|
|
},
|
|
|
|
|
borderWidth: {
|
|
|
|
|
hairline: hairlineWidth(),
|
|
|
|
|
},
|
|
|
|
|
maxWidth: {
|
|
|
|
|
content: px(tokens.layout.contentMaxWidth),
|
|
|
|
|
},
|
|
|
|
|
keyframes: {
|
|
|
|
|
'accordion-down': {
|
|
|
|
|
from: { height: '0' },
|
|
|
|
|
to: { height: 'var(--radix-accordion-content-height)' },
|
|
|
|
|
},
|
|
|
|
|
'accordion-up': {
|
|
|
|
|
from: { height: 'var(--radix-accordion-content-height)' },
|
|
|
|
|
to: { height: '0' },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
animation: {
|
|
|
|
|
'accordion-down': 'accordion-down 0.2s ease-out',
|
|
|
|
|
'accordion-up': 'accordion-up 0.2s ease-out',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
future: {
|
|
|
|
|
hoverOnlyWhenSupported: true,
|
|
|
|
|
},
|
|
|
|
|
plugins: [
|
|
|
|
|
require('tailwindcss-animate'),
|
|
|
|
|
({ addBase }) => {
|
|
|
|
|
addBase({
|
|
|
|
|
':root': rootVariables,
|
|
|
|
|
'.dark:root': darkVariables,
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
};
|