Make RN components fully theme-driven for multi-brand support

Refactor all 18 React Native components from static DTColors imports to
dynamic useDTTheme() context, enabling proper visual differentiation
when switching between DT and Classic brands.

Key changes:
- Replace static DTColors with theme context in all components
- Add shape tokens (bevel/radius) to DTExtendedTheme interface
- Conditionally render SVG bevels (DT) vs borderRadius (Classic)
- Configure fonts per brand (Tektur for DT, system sans-serif for Classic)
- Update buildThemeFromBrand to parse shape and typography tokens
- Fix Metro config for monorepo singleton resolution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
michael
2026-03-04 12:19:25 -08:00
parent 1b0d09313c
commit 47e8085581
23 changed files with 436 additions and 239 deletions

View File

@@ -15,6 +15,34 @@ config.resolver.nodeModulesPaths = [
path.resolve(monorepoRoot, 'node_modules'),
];
// Force singleton packages to always resolve from the mobile project.
// In monorepos, Metro can bundle two copies of react (one from root, one from
// the project) causing "Cannot read property 'use' of null" crashes.
const singletons = ['react', 'react-native', 'react-native-safe-area-context'];
config.resolver.resolveRequest = (context, moduleName, platform) => {
// Check if this is a singleton or a deep import of one (e.g. react/jsx-runtime)
const isSingleton = singletons.some(
(s) => moduleName === s || moduleName.startsWith(s + '/')
);
if (isSingleton) {
// Resolve as if the import originated from the mobile project root
const mobileContext = {
...context,
resolveRequest: undefined,
originModulePath: path.join(projectRoot, 'package.json'),
};
return context.resolveRequest(mobileContext, moduleName, platform);
}
return context.resolveRequest(
{ ...context, resolveRequest: undefined },
moduleName,
platform,
);
};
// Block sibling packages' node_modules to prevent duplicate native modules
const packagesDir = path.join(monorepoRoot, 'packages');
const escapedPath = packagesDir.replace(/[/\\]/g, '[/\\\\]');

View File

@@ -1,7 +1,35 @@
import { MD3DarkTheme } from 'react-native-paper';
import { Platform } from 'react-native';
import { MD3DarkTheme, configureFonts } from 'react-native-paper';
import type { BrandTokens } from '@dangerousthings/tokens';
import type { DTExtendedTheme } from '@dangerousthings/react-native';
/** Parse CSS size token ("1rem", "0.25rem", "0px", "0") to pixels (1rem = 16px) */
function parseSize(value: string): number {
if (value === '0' || value === '0px') return 0;
const num = parseFloat(value);
if (value.endsWith('rem')) return num * 16;
if (value.endsWith('px')) return num;
return num;
}
/** Build RNP font config from brand typography tokens */
function buildFonts(brand: BrandTokens) {
const usesTektur = brand.typography.heading.includes('Tektur');
if (!usesTektur) {
// System sans-serif — use RNP defaults
return MD3DarkTheme.fonts;
}
// Tektur custom font
const fontFamily = Platform.select({
ios: 'Tektur',
android: 'Tektur-Regular',
default: 'Tektur',
})!;
return configureFonts({
config: { fontFamily },
});
}
export function buildThemeFromBrand(brand: BrandTokens): DTExtendedTheme {
const colors = brand.dark;
@@ -9,7 +37,7 @@ export function buildThemeFromBrand(brand: BrandTokens): DTExtendedTheme {
...MD3DarkTheme,
dark: true,
roundness: brand.shape.radiusSm === '0' || brand.shape.radiusSm === '0px' ? 0 : 4,
fonts: MD3DarkTheme.fonts,
fonts: buildFonts(brand),
colors: {
...MD3DarkTheme.colors,
primary: colors.primary,
@@ -63,6 +91,12 @@ export function buildThemeFromBrand(brand: BrandTokens): DTExtendedTheme {
modeOther: colors.other,
border: colors.border,
borderEmphasis: colors.secondary,
bevelSm: parseSize(brand.shape.bevelSm),
bevelMd: parseSize(brand.shape.bevelMd),
bevelLg: parseSize(brand.shape.bevelLg),
radiusSm: parseSize(brand.shape.radiusSm),
radius: parseSize(brand.shape.radius),
radiusLg: parseSize(brand.shape.radiusLg),
},
};
}