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

@@ -20,7 +20,7 @@ import {
Animated,
} from 'react-native';
import {Text} from 'react-native-paper';
import {DTColors} from '../theme/colors';
import {useDTTheme} from '../theme/DTThemeProvider';
import {type DTVariant, getVariantColor} from '../utils/variantColors';
export interface DTAccordionSection {
@@ -93,6 +93,7 @@ export function DTAccordion({
onSectionToggle,
style,
}: DTAccordionProps) {
const theme = useDTTheme();
const [openKeys, setOpenKeys] = useState<Set<string>>(
new Set(initialOpenKeys),
);
@@ -114,8 +115,8 @@ export function DTAccordion({
[allowMultiple, onSectionToggle],
);
const inactiveColor = getVariantColor(variant);
const activeColor = getVariantColor(activeVariant);
const inactiveColor = getVariantColor(theme, variant);
const activeColor = getVariantColor(theme, activeVariant);
return (
<View style={[styles.container, style]}>
@@ -146,6 +147,7 @@ function AccordionSection({
inactiveColor: string;
activeColor: string;
}) {
const theme = useDTTheme();
const heightAnim = useRef(new Animated.Value(isOpen ? 1 : 0)).current;
const chevronAnim = useRef(new Animated.Value(isOpen ? 1 : 0)).current;
const [contentHeight, setContentHeight] = useState(0);
@@ -187,6 +189,7 @@ function AccordionSection({
{
borderColor: sectionColor,
borderTopColor: sectionColor,
backgroundColor: theme.colors.background,
opacity: pressed ? 0.7 : 1,
},
]}>
@@ -214,7 +217,7 @@ function AccordionSection({
},
]}>
<View
style={styles.content}
style={[styles.content, {backgroundColor: theme.colors.background}]}
onLayout={e => {
const h = e.nativeEvent.layout.height;
if (h > 0 && !measured) {
@@ -240,7 +243,6 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: DTColors.dark,
borderWidth: 1,
borderTopWidth: 5,
paddingVertical: 12,
@@ -256,7 +258,6 @@ const styles = StyleSheet.create({
content: {
paddingHorizontal: 16,
paddingVertical: 12,
backgroundColor: DTColors.dark,
},
chevron: {
justifyContent: 'center',

View File

@@ -8,7 +8,7 @@
import {StyleSheet, View, ViewStyle, StyleProp, Pressable} from 'react-native';
import {Text} from 'react-native-paper';
import Svg, {Path} from 'react-native-svg';
import {DTColors} from '../theme/colors';
import {useDTTheme} from '../theme/DTThemeProvider';
import {type DTVariant, getVariantColor} from '../utils/variantColors';
import {buildButtonBevelPath} from '../utils/bevelPaths';
import {useComponentLayout} from '../utils/useComponentLayout';
@@ -83,16 +83,18 @@ export function DTButton({
borderWidth = 2,
bevelSize = 8,
}: DTButtonProps) {
const theme = useDTTheme();
const {dimensions, onLayout, hasDimensions} = useComponentLayout();
const [pressed, setPressed] = useState(false);
const accentColor = getVariantColor(variant, color);
const accentColor = getVariantColor(theme, variant, color);
const isContained = mode === 'contained';
const bgColor = isContained ? accentColor : 'transparent';
const textColor = isContained ? DTColors.dark : accentColor;
const textColor = isContained ? theme.colors.onPrimary : accentColor;
const {width, height} = dimensions;
const opacity = disabled ? 0.5 : pressed ? 0.7 : 1;
const useBevels = theme.custom.bevelMd > 0;
return (
<Pressable
@@ -101,8 +103,18 @@ export function DTButton({
onPressIn={() => setPressed(true)}
onPressOut={() => setPressed(false)}
style={[{opacity}, style]}>
<View style={styles.container} onLayout={onLayout}>
{hasDimensions && (
<View
style={[
styles.container,
!useBevels && {
borderWidth,
borderColor: accentColor,
borderRadius: theme.custom.radiusSm,
backgroundColor: bgColor,
},
]}
onLayout={onLayout}>
{useBevels && hasDimensions && (
<Svg
style={StyleSheet.absoluteFill}
width={width}

View File

@@ -14,7 +14,7 @@ import {ReactNode} from 'react';
import {StyleSheet, View, ViewStyle, StyleProp, Pressable} from 'react-native';
import {Text} from 'react-native-paper';
import Svg, {Path} from 'react-native-svg';
import {DTColors} from '../theme/colors';
import {useDTTheme} from '../theme/DTThemeProvider';
import {type DTVariant, getVariantColor} from '../utils/variantColors';
import {buildCardBevelPath} from '../utils/bevelPaths';
import {useComponentLayout} from '../utils/useComponentLayout';
@@ -68,8 +68,7 @@ interface DTCardProps {
*/
bevelSizeSmall?: number;
/**
* Background color
* @default '#000000'
* Background color (defaults to theme background)
*/
backgroundColor?: string;
/**
@@ -103,34 +102,49 @@ export function DTCard({
borderWidth = 3,
bevelSize = 32,
bevelSizeSmall = 16,
backgroundColor = '#000000',
backgroundColor,
onPress,
}: DTCardProps) {
const theme = useDTTheme();
const {dimensions, onLayout, hasDimensions} = useComponentLayout();
const accentColor = getVariantColor(mode, borderColor);
const accentColor = getVariantColor(theme, mode, borderColor);
const shouldShowHeader = showHeader ?? !!title;
const bgColor = backgroundColor ?? theme.colors.background;
const {width, height} = dimensions;
const useBevels = theme.custom.bevelMd > 0;
// Outer bevel path (full card shape)
const outerPath = hasDimensions
const outerPath = useBevels && hasDimensions
? buildCardBevelPath(width, height, bevelSize, bevelSizeSmall, 0)
: '';
// Inner bevel path at border inset (for background + frame cutout)
const innerPath = hasDimensions
const innerPath = useBevels && hasDimensions
? buildCardBevelPath(width, height, bevelSize - borderWidth, bevelSizeSmall - borderWidth, borderWidth)
: '';
const content = (
<View style={[styles.container, style]} onLayout={onLayout}>
{/* Background fill (behind content) */}
{hasDimensions && (
<View
style={[
styles.container,
!useBevels && {
borderWidth,
borderColor: accentColor,
borderRadius: theme.custom.radius,
backgroundColor: bgColor,
overflow: 'hidden',
},
style,
]}
onLayout={onLayout}>
{/* Background fill (behind content) — beveled mode only */}
{useBevels && hasDimensions && (
<Svg
style={StyleSheet.absoluteFill}
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}>
<Path d={innerPath} fill={backgroundColor} />
<Path d={innerPath} fill={bgColor} />
</Svg>
)}
<View style={styles.innerContainer}>
@@ -139,7 +153,7 @@ export function DTCard({
{title && (
<Text
variant="titleMedium"
style={[styles.headerText, {color: DTColors.dark}]}>
style={[styles.headerText, {color: theme.colors.onPrimary}]}>
{title}
</Text>
)}
@@ -147,8 +161,8 @@ export function DTCard({
)}
<View style={[styles.content, {padding}, contentStyle]}>{children}</View>
</View>
{/* Frame overlay (above content) — clips content at beveled border */}
{hasDimensions && (
{/* Frame overlay (above content) — beveled mode only */}
{useBevels && hasDimensions && (
<Svg
style={[StyleSheet.absoluteFill, styles.frameOverlay]}
width={width}

View File

@@ -8,10 +8,10 @@
* Uses diagonal-symmetry bevels (top-left + bottom-right) on the indicator.
*/
import {StyleSheet, ViewStyle, TextStyle, StyleProp, Pressable} from 'react-native';
import {StyleSheet, View, ViewStyle, TextStyle, StyleProp, Pressable} from 'react-native';
import {Text} from 'react-native-paper';
import Svg, {Path} from 'react-native-svg';
import {DTColors} from '../theme/colors';
import {useDTTheme} from '../theme/DTThemeProvider';
import {type DTVariant, getVariantColor} from '../utils/variantColors';
import {buildBeveledRectPath} from '../utils/bevelPaths';
@@ -76,16 +76,20 @@ export function DTCheckbox({
style,
labelStyle,
}: DTCheckboxProps) {
const accentColor = getVariantColor(variant, color);
const theme = useDTTheme();
const accentColor = getVariantColor(theme, variant, color);
const opacity = disabled ? 0.5 : 1;
const borderWidth = 2;
const useBevels = theme.custom.bevelMd > 0;
const bevelSize = Math.round(size * 0.3);
// Outer beveled path (border)
const outerPath = buildBeveledRectPath(size, size, {
const outerPath = useBevels
? buildBeveledRectPath(size, size, {
corners: {topLeft: bevelSize, bottomRight: bevelSize},
strokeWidth: borderWidth,
});
})
: '';
return (
<Pressable
@@ -96,28 +100,44 @@ export function DTCheckbox({
{opacity: pressed ? 0.7 : opacity},
style,
]}>
{useBevels ? (
<Svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
{/* Beveled border */}
<Path
d={outerPath}
fill={checked ? accentColor : 'transparent'}
stroke={accentColor}
strokeWidth={borderWidth}
/>
{/* Checkmark path (only when checked) */}
{checked && (
<Path
d={`M ${size * 0.2} ${size * 0.5} L ${size * 0.4} ${size * 0.7} L ${size * 0.8} ${size * 0.25}`}
fill="none"
stroke={DTColors.dark}
stroke={theme.colors.onPrimary}
strokeWidth={borderWidth + 0.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
)}
</Svg>
) : (
<View
style={{
width: size,
height: size,
borderWidth,
borderColor: accentColor,
borderRadius: theme.custom.radiusSm,
backgroundColor: checked ? accentColor : 'transparent',
alignItems: 'center',
justifyContent: 'center',
}}>
{checked && (
<Text style={{color: theme.colors.onPrimary, fontSize: size * 0.6, lineHeight: size * 0.7}}></Text>
)}
</View>
)}
{label && (
<Text style={[styles.label, {color: DTColors.light}, labelStyle]}>
<Text style={[styles.label, {color: theme.colors.onSurface}, labelStyle]}>
{label}
</Text>
)}

View File

@@ -8,16 +8,15 @@
// React import not needed with new JSX transform
import { StyleSheet, ViewStyle, StyleProp } from 'react-native';
import { Chip, ChipProps } from 'react-native-paper';
import { DTColors } from '../theme/colors';
type DTChipVariant = 'normal' | 'emphasis' | 'warning' | 'success' | 'other';
import { useDTTheme } from '../theme/DTThemeProvider';
import { type DTVariant, getVariantColor } from '../utils/variantColors';
interface DTChipProps extends Omit<ChipProps, 'mode'> {
/**
* Visual variant of the chip
* @default 'normal'
*/
variant?: DTChipVariant;
variant?: DTVariant;
/**
* Whether the chip is in selected state
* @default false
@@ -29,14 +28,6 @@ interface DTChipProps extends Omit<ChipProps, 'mode'> {
style?: StyleProp<ViewStyle>;
}
const variantColors: Record<DTChipVariant, string> = {
normal: DTColors.modeNormal,
emphasis: DTColors.modeEmphasis,
warning: DTColors.modeWarning,
success: DTColors.modeSuccess,
other: DTColors.modeOther,
};
/**
* DT-styled Chip component
*
@@ -56,7 +47,8 @@ export function DTChip({
children,
...props
}: DTChipProps) {
const color = variantColors[variant];
const theme = useDTTheme();
const color = getVariantColor(theme, variant);
const chipStyle: ViewStyle = {
backgroundColor: selected ? color : 'transparent',
@@ -71,7 +63,7 @@ export function DTChip({
mode="outlined"
textStyle={[
styles.text,
{ color: selected ? DTColors.dark : color },
{ color: selected ? theme.colors.onPrimary : color },
]}
style={[styles.chip, chipStyle, style]}
selectedColor={color}

View File

@@ -31,7 +31,7 @@ import {
import {Portal, Text} from 'react-native-paper';
import Svg, {Path} from 'react-native-svg';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {DTColors} from '../theme/colors';
import {useDTTheme} from '../theme/DTThemeProvider';
import {type DTVariant, getVariantColor} from '../utils/variantColors';
import {buildDrawerBevelPath} from '../utils/bevelPaths';
@@ -108,9 +108,11 @@ export function DTDrawer({
children,
style,
}: DTDrawerProps) {
const theme = useDTTheme();
const useBevels = theme.custom.bevelMd > 0;
const slideAnim = useRef(new Animated.Value(0)).current;
const overlayAnim = useRef(new Animated.Value(0)).current;
const headerColor = getVariantColor(headingVariant);
const headerColor = getVariantColor(theme, headingVariant);
const {width: screenWidth, height: screenHeight} = useWindowDimensions();
const insets = useSafeAreaInsets();
const effectiveWidth = Math.min(drawerWidth, screenWidth);
@@ -172,7 +174,7 @@ export function DTDrawer({
{/* Overlay backdrop */}
<Animated.View style={[StyleSheet.absoluteFill, {opacity: overlayAnim}]}>
<Pressable
style={[StyleSheet.absoluteFill, styles.overlay]}
style={[StyleSheet.absoluteFill, {backgroundColor: theme.colors.backdrop}]}
onPress={onDismiss}
/>
</Animated.View>
@@ -183,12 +185,19 @@ export function DTDrawer({
styles.panel,
position === 'right' ? styles.panelRight : styles.panelLeft,
{width: effectiveWidth, transform: [{translateX}]},
!useBevels && {
borderWidth,
borderColor: headerColor,
borderRadius: theme.custom.radius,
backgroundColor: theme.colors.background,
overflow: 'hidden',
},
style,
]}>
{/* Dual-SVG-path technique: outer border + inner dark fill */}
{/* Dual-SVG-path technique: outer border + inner dark fill — beveled mode only */}
{useBevels && (
<View style={StyleSheet.absoluteFill}>
<Svg width={effectiveWidth} height={screenHeight}>
{/* Outer path: accent border color */}
<Path
d={buildDrawerBevelPath(
effectiveWidth,
@@ -198,7 +207,6 @@ export function DTDrawer({
)}
fill={headerColor}
/>
{/* Inner path: dark background (inset by borderWidth) */}
<Path
d={buildDrawerBevelPath(
effectiveWidth - borderWidth * 2,
@@ -206,19 +214,20 @@ export function DTDrawer({
bevelSize - borderWidth,
position,
)}
fill={DTColors.dark}
fill={theme.colors.background}
transform={`translate(${borderWidth}, ${borderWidth})`}
/>
</Svg>
</View>
)}
{/* Header bar */}
<View style={[styles.header, {backgroundColor: headerColor, paddingTop: insets.top + 16}]}>
<Text style={styles.headerText}>{heading}</Text>
<Text style={[styles.headerText, {color: theme.colors.onPrimary}]}>{heading}</Text>
<Pressable
onPress={onDismiss}
style={({pressed}) => ({opacity: pressed ? 0.7 : 1})}>
<Text style={styles.closeButton}></Text>
<Text style={[styles.closeButton, {color: theme.colors.onPrimary}]}></Text>
</Pressable>
</View>
@@ -235,9 +244,6 @@ export function DTDrawer({
}
const styles = StyleSheet.create({
overlay: {
backgroundColor: DTColors.overlay,
},
panel: {
position: 'absolute',
top: 0,
@@ -258,13 +264,11 @@ const styles = StyleSheet.create({
zIndex: 1,
},
headerText: {
color: DTColors.dark,
fontWeight: '700',
fontSize: 18,
letterSpacing: 0.5,
},
closeButton: {
color: DTColors.dark,
fontSize: 20,
fontWeight: '700',
paddingHorizontal: 8,

View File

@@ -26,7 +26,7 @@ import {
Platform,
} from 'react-native';
import Svg, {Path} from 'react-native-svg';
import {DTColors} from '../theme/colors';
import {useDTTheme} from '../theme/DTThemeProvider';
import {type DTVariant, getVariantColor} from '../utils/variantColors';
import {buildButtonBevelPath, buildMediaFrameBevelPath} from '../utils/bevelPaths';
import {useComponentLayout} from '../utils/useComponentLayout';
@@ -110,9 +110,10 @@ export function DTGallery({
borderWidth = 3,
style,
}: DTGalleryProps) {
const theme = useDTTheme();
const flatListRef = useRef<FlatList>(null);
const {dimensions, onLayout, hasDimensions} = useComponentLayout();
const accentColor = getVariantColor(variant);
const accentColor = getVariantColor(theme, variant);
const atStart = activeIndex <= 0;
const atEnd = activeIndex >= items.length - 1;
@@ -171,6 +172,7 @@ export function DTGallery({
style={({pressed}) => ({
opacity: isDisabled ? 0.3 : pressed ? 0.7 : 1,
})}>
{useBevels ? (
<Svg width={ARROW_SIZE} height={ARROW_SIZE} viewBox={`0 0 ${ARROW_SIZE} ${ARROW_SIZE}`}>
<Path
d={buildButtonBevelPath(ARROW_SIZE, ARROW_SIZE, ARROW_BEVEL, ARROW_BORDER)}
@@ -187,6 +189,28 @@ export function DTGallery({
strokeLinejoin="round"
/>
</Svg>
) : (
<View style={{
width: ARROW_SIZE,
height: ARROW_SIZE,
borderWidth: ARROW_BORDER,
borderColor: accentColor,
borderRadius: theme.custom.radiusSm,
alignItems: 'center',
justifyContent: 'center',
}}>
<Svg width={ARROW_SIZE * 0.6} height={ARROW_SIZE * 0.6} viewBox={`${ARROW_SIZE * 0.2} ${ARROW_SIZE * 0.2} ${ARROW_SIZE * 0.6} ${ARROW_SIZE * 0.6}`}>
<Path
d={iconPath}
fill="none"
stroke={accentColor}
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</Svg>
</View>
)}
</Pressable>
);
};
@@ -204,6 +228,7 @@ export function DTGallery({
width: thumbnailSize,
height: thumbnailSize,
borderColor: isActive ? accentColor : 'transparent',
borderRadius: useBevels ? 0 : theme.custom.radiusSm,
},
isActive && styles.thumbnailActive,
]}>
@@ -235,30 +260,41 @@ export function DTGallery({
if (items.length === 0) return null;
const {width, height} = dimensions;
const useBevels = theme.custom.bevelMd > 0;
// Outer and inner bevel paths for frame overlay
const outerPath = hasDimensions
const outerPath = useBevels && hasDimensions
? buildMediaFrameBevelPath(width, height, bevelSize)
: '';
const innerPath = hasDimensions
const innerPath = useBevels && hasDimensions
? buildMediaFrameBevelPath(width, height, bevelSize - borderWidth, borderWidth)
: '';
return (
<View style={[styles.container, style]}>
{/* Main image with beveled frame */}
<View style={styles.mainImageContainer} onLayout={onLayout}>
{/* Background fill */}
{hasDimensions && (
<View
style={[
styles.mainImageContainer,
!useBevels && {
borderWidth,
borderColor: accentColor,
borderRadius: theme.custom.radius,
overflow: 'hidden',
},
]}
onLayout={onLayout}>
{/* Background fill — beveled mode only */}
{useBevels && hasDimensions && (
<Svg
style={StyleSheet.absoluteFill}
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}>
<Path d={innerPath} fill={DTColors.surfaceVariant} />
<Path d={innerPath} fill={theme.colors.surfaceVariant} />
</Svg>
)}
<View style={[styles.mainImageContent, {padding: borderWidth}]}>
<View style={[styles.mainImageContent, {padding: useBevels ? borderWidth : 0}]}>
<Image
source={{uri: items[activeIndex]?.uri}}
style={styles.mainImage}
@@ -266,21 +302,19 @@ export function DTGallery({
resizeMode="contain"
/>
</View>
{/* Frame overlay (above content) — clips image at beveled border */}
{hasDimensions && (
{/* Frame overlay — beveled mode only */}
{useBevels && hasDimensions && (
<Svg
style={[StyleSheet.absoluteFill, styles.frameOverlay]}
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
pointerEvents="none">
{/* Corner mask: fill area outside outer bevel with dark to hide overflow */}
<Path
d={`M 0 0 L ${width} 0 L ${width} ${height} L 0 ${height} Z ` + outerPath}
fillRule="evenodd"
fill={DTColors.dark}
fill={theme.colors.background}
/>
{/* Colored border between outer and inner bevel paths */}
<Path
d={outerPath + ' ' + innerPath}
fillRule="evenodd"

View File

@@ -13,6 +13,7 @@
import {useRef, useEffect} from 'react';
import {StyleSheet, View, ViewStyle, StyleProp, Animated} from 'react-native';
import Svg, {Polygon} from 'react-native-svg';
import {useDTTheme} from '../theme/DTThemeProvider';
import {type DTVariant, getVariantColor} from '../utils/variantColors';
interface DTHexagonProps {
@@ -112,7 +113,8 @@ export function DTHexagon({
style,
children,
}: DTHexagonProps) {
const accentColor = getVariantColor(variant, color);
const theme = useDTTheme();
const accentColor = getVariantColor(theme, variant, color);
const rotateAnim = useRef(new Animated.Value(0)).current;
const pulseAnim = useRef(new Animated.Value(1)).current;

View File

@@ -14,7 +14,7 @@ import {ReactNode, useEffect, useRef} from 'react';
import {StyleSheet, View, ViewStyle, StyleProp, Animated} from 'react-native';
import {Text} from 'react-native-paper';
import Svg, {Path} from 'react-native-svg';
import {DTColors} from '../theme/colors';
import {useDTTheme} from '../theme/DTThemeProvider';
import {type DTVariant, getVariantColor} from '../utils/variantColors';
import {buildLabelBevelPath} from '../utils/bevelPaths';
import {useComponentLayout} from '../utils/useComponentLayout';
@@ -150,12 +150,14 @@ export function DTLabel({
bevelSize,
style,
}: DTLabelProps) {
const theme = useDTTheme();
const {dimensions, onLayout, hasDimensions} = useComponentLayout();
const scaleAnim = useRef(new Animated.Value(animated ? 0 : 1)).current;
const pingAnim = useRef(new Animated.Value(1)).current;
const backgroundColor = getVariantColor(mode);
const backgroundColor = getVariantColor(theme, mode);
const sizeConfig = sizeConfigs[size];
const effectiveBevelSize = bevelSize ?? sizeConfig.bevel;
const onPrimaryColor = theme.colors.onPrimary;
// Mount animation
useEffect(() => {
@@ -196,6 +198,7 @@ export function DTLabel({
}, [showIndicator, pingAnim]);
const {width, height} = dimensions;
const useBevels = theme.custom.bevelMd > 0;
return (
<Animated.View
@@ -203,10 +206,14 @@ export function DTLabel({
styles.container,
fullWidth && styles.fullWidth,
{transform: [{scale: scaleAnim}]},
!useBevels && {
backgroundColor,
borderRadius: theme.custom.radiusSm,
},
style,
]}
onLayout={onLayout}>
{hasDimensions && (
{useBevels && hasDimensions && (
<Svg
style={StyleSheet.absoluteFill}
width={width}
@@ -228,7 +235,7 @@ export function DTLabel({
<Text
style={[
styles.primaryText,
{fontSize: sizeConfig.fontSize},
{fontSize: sizeConfig.fontSize, color: onPrimaryColor},
]}>
{primaryText}
</Text>
@@ -236,7 +243,7 @@ export function DTLabel({
<Text
style={[
styles.secondaryText,
{fontSize: sizeConfig.fontSizeSecondary},
{fontSize: sizeConfig.fontSizeSecondary, color: onPrimaryColor},
]}>
{secondaryText}
</Text>
@@ -251,7 +258,7 @@ export function DTLabel({
<View
style={[
styles.indicatorLine,
{borderColor: DTColors.dark, width: sizeConfig.indicatorWidth},
{borderColor: onPrimaryColor, width: sizeConfig.indicatorWidth},
]}
/>
</Animated.View>
@@ -299,11 +306,9 @@ const styles = StyleSheet.create({
alignItems: 'center',
},
primaryText: {
color: DTColors.dark,
fontWeight: '500',
},
secondaryText: {
color: DTColors.dark,
fontWeight: '800',
},
indicator: {},

View File

@@ -13,7 +13,7 @@
import {ReactNode, useRef, useEffect} from 'react';
import {StyleSheet, View, ViewStyle, StyleProp, Animated} from 'react-native';
import Svg, {Path, Defs, ClipPath} from 'react-native-svg';
import {DTColors} from '../theme/colors';
import {useDTTheme} from '../theme/DTThemeProvider';
import {type DTVariant, getVariantColor} from '../utils/variantColors';
import {buildMediaFrameBevelPath} from '../utils/bevelPaths';
import {useComponentLayout} from '../utils/useComponentLayout';
@@ -91,9 +91,10 @@ export function DTMediaFrame({
style,
contentStyle,
}: DTMediaFrameProps) {
const theme = useDTTheme();
const {dimensions, onLayout, hasDimensions} = useComponentLayout();
const scaleAnim = useRef(new Animated.Value(animated ? 0 : 1)).current;
const accentColor = getVariantColor(variant, color);
const accentColor = getVariantColor(theme, variant, color);
useEffect(() => {
if (animated) {
@@ -107,13 +108,14 @@ export function DTMediaFrame({
}, [animated, animationDelay, scaleAnim]);
const {width, height} = dimensions;
const useBevels = theme.custom.bevelMd > 0;
// Outer bevel path (full frame shape)
const outerPath = hasDimensions
const outerPath = useBevels && hasDimensions
? buildMediaFrameBevelPath(width, height, bevelSize)
: '';
// Inner bevel path at border inset
const innerPath = hasDimensions
const innerPath = useBevels && hasDimensions
? buildMediaFrameBevelPath(width, height, bevelSize - borderWidth, borderWidth)
: '';
@@ -123,21 +125,27 @@ export function DTMediaFrame({
styles.container,
aspectRatio ? {aspectRatio} : undefined,
{transform: [{scale: scaleAnim}]},
!useBevels && {
borderWidth,
borderColor: accentColor,
borderRadius: theme.custom.radius,
overflow: 'hidden',
},
style,
]}
onLayout={onLayout}>
{/* Background fill (behind content) */}
{hasDimensions && (
{/* Background fill (behind content) — beveled mode only */}
{useBevels && hasDimensions && (
<Svg
style={StyleSheet.absoluteFill}
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}>
<Path d={innerPath} fill={DTColors.dark} />
<Path d={innerPath} fill={theme.colors.background} />
</Svg>
)}
{/* Content clipped to the inner bevel shape */}
{hasDimensions ? (
{/* Content */}
{useBevels && hasDimensions ? (
<View style={[styles.content, contentStyle]}>
<Svg
width={width}
@@ -155,7 +163,6 @@ export function DTMediaFrame({
StyleSheet.absoluteFill,
{
margin: borderWidth,
// Use borderRadius 0 to keep angular, overflow hidden clips rectangular part
overflow: 'hidden',
},
]}>
@@ -163,30 +170,27 @@ export function DTMediaFrame({
</View>
</View>
) : (
<View style={[styles.content, {padding: borderWidth}, contentStyle]}>
<View style={[styles.content, !useBevels ? undefined : {padding: borderWidth}, contentStyle]}>
{children}
</View>
)}
{/* Frame overlay (above content) — masks content at beveled corners */}
{hasDimensions && (
{/* Frame overlay (above content) — beveled mode only */}
{useBevels && hasDimensions && (
<Svg
style={[StyleSheet.absoluteFill, styles.frameOverlay]}
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
pointerEvents="none">
{/* Outer frame border */}
<Path
d={outerPath + ' ' + innerPath}
fillRule="evenodd"
fill={accentColor}
/>
{/* Corner masks: fill the rectangular area outside the bevel shape with black
to hide content that overflows past the beveled corners */}
<Path
d={`M 0 0 L ${width} 0 L ${width} ${height} L 0 ${height} Z ` + outerPath}
fillRule="evenodd"
fill={DTColors.dark}
fill={theme.colors.background}
/>
</Svg>
)}

View File

@@ -20,7 +20,7 @@ import {
Animated,
} from 'react-native';
import {Menu, Text} from 'react-native-paper';
import {DTColors} from '../theme/colors';
import {useDTTheme} from '../theme/DTThemeProvider';
import {type DTVariant, getVariantColor} from '../utils/variantColors';
export interface DTMenuItem {
@@ -163,7 +163,8 @@ export function DTMenuDropdown({
onDismiss,
style,
}: DTMenuDropdownProps) {
const accentColor = getVariantColor(variant);
const theme = useDTTheme();
const accentColor = getVariantColor(theme, variant);
return (
<Menu
@@ -173,12 +174,12 @@ export function DTMenuDropdown({
anchorPosition="bottom"
contentStyle={[
styles.dropdownContent,
{borderColor: accentColor},
{borderColor: accentColor, backgroundColor: theme.colors.background},
style,
]}
theme={{
colors: {
elevation: {level2: DTColors.dark},
elevation: {level2: theme.colors.background},
},
}}>
{items.map(item => (
@@ -189,7 +190,7 @@ export function DTMenuDropdown({
item.onPress?.();
onDismiss();
}}
titleStyle={[styles.dropdownItemTitle, {color: DTColors.light}]}
titleStyle={[styles.dropdownItemTitle, {color: theme.colors.onSurface}]}
style={styles.dropdownItem}
/>
))}
@@ -210,10 +211,11 @@ function DTMenuItemRow({
activeVariant: DTVariant;
level: number;
}) {
const theme = useDTTheme();
const [expanded, setExpanded] = useState(false);
const hasChildren = item.items && item.items.length > 0;
const isActive = item.isActive || expanded;
const itemColor = getVariantColor(isActive ? activeVariant : variant);
const itemColor = getVariantColor(theme, isActive ? activeVariant : variant);
const heightAnim = useRef(new Animated.Value(0)).current;
const chevronAnim = useRef(new Animated.Value(0)).current;
@@ -265,7 +267,7 @@ function DTMenuItemRow({
borderTopColor: itemColor,
backgroundColor: isActive
? `${itemColor}20`
: DTColors.dark,
: theme.colors.background,
paddingLeft: 16 + level * 16,
opacity: pressed ? 0.7 : 1,
},
@@ -360,7 +362,6 @@ const styles = StyleSheet.create({
dropdownContent: {
borderWidth: 2,
borderRadius: 0,
backgroundColor: DTColors.dark,
},
dropdownItem: {
borderRadius: 0,

View File

@@ -11,7 +11,7 @@
import {StyleSheet, ViewStyle, StyleProp, KeyboardAvoidingView, Platform} from 'react-native';
import {Portal, Modal} from 'react-native-paper';
import {DTColors} from '../theme/colors';
import {useDTTheme} from '../theme/DTThemeProvider';
import {type DTVariant} from '../utils/variantColors';
import {DTCard} from './DTCard';
@@ -75,6 +75,8 @@ export function DTModal({
contentStyle,
style,
}: DTModalProps) {
const theme = useDTTheme();
return (
<Portal>
<Modal
@@ -85,7 +87,7 @@ export function DTModal({
style={styles.modal}
theme={{
colors: {
backdrop: DTColors.overlay,
backdrop: theme.colors.backdrop,
},
}}>
<KeyboardAvoidingView

View File

@@ -11,7 +11,7 @@
import {StyleSheet, View, ViewStyle, StyleProp} from 'react-native';
import {ProgressBar, Text} from 'react-native-paper';
import {DTColors} from '../theme/colors';
import {useDTTheme} from '../theme/DTThemeProvider';
import {type DTVariant, getVariantColor} from '../utils/variantColors';
import {useComponentLayout} from '../utils/useComponentLayout';
@@ -77,7 +77,8 @@ export function DTProgressBar({
color,
style,
}: DTProgressBarProps) {
const accentColor = getVariantColor(variant, color);
const theme = useDTTheme();
const accentColor = getVariantColor(theme, variant, color);
const clampedValue = Math.max(0, Math.min(1, value));
if (orientation === 'vertical') {
@@ -85,6 +86,7 @@ export function DTProgressBar({
<VerticalProgressBar
value={clampedValue}
color={accentColor}
trackColor={theme.colors.surfaceDisabled}
width={height}
showLabel={showLabel}
style={style}
@@ -100,7 +102,7 @@ export function DTProgressBar({
style={[styles.bar, {height}]}
theme={{
colors: {
surfaceVariant: DTColors.disabledBackground,
surfaceVariant: theme.colors.surfaceDisabled,
},
}}
/>
@@ -120,12 +122,14 @@ export function DTProgressBar({
function VerticalProgressBar({
value,
color,
trackColor,
width,
showLabel,
style,
}: {
value: number;
color: string;
trackColor: string;
width: number;
showLabel: boolean;
style?: StyleProp<ViewStyle>;
@@ -135,7 +139,7 @@ function VerticalProgressBar({
return (
<View style={[styles.verticalContainer, {width}, style]}>
<View style={styles.verticalTrack} onLayout={onLayout}>
<View style={[styles.verticalTrack, {backgroundColor: trackColor}]} onLayout={onLayout}>
<View
style={[
styles.verticalFill,
@@ -169,7 +173,6 @@ const styles = StyleSheet.create({
verticalTrack: {
flex: 1,
width: '100%',
backgroundColor: DTColors.disabledBackground,
justifyContent: 'flex-end',
},
verticalFill: {

View File

@@ -9,10 +9,10 @@
* - Quantity display between buttons
*/
import {StyleSheet, View, ViewStyle, StyleProp, Pressable} from 'react-native';
import {StyleSheet, View, ViewStyle, StyleProp, Pressable, Text as RNText} from 'react-native';
import {Text} from 'react-native-paper';
import Svg, {Path} from 'react-native-svg';
import {DTColors} from '../theme/colors';
import {useDTTheme} from '../theme/DTThemeProvider';
import {type DTVariant, getVariantColor} from '../utils/variantColors';
import {buildButtonBevelPath} from '../utils/bevelPaths';
@@ -124,7 +124,9 @@ export function DTQuantityStepper({
color,
style,
}: DTQuantityStepperProps) {
const accentColor = getVariantColor(variant, color);
const theme = useDTTheme();
const useBevels = theme.custom.bevelMd > 0;
const accentColor = getVariantColor(theme, variant, color);
const config = sizeConfigs[size];
const atMin = value <= min;
const atMax = value >= max;
@@ -168,6 +170,7 @@ export function DTQuantityStepper({
style={({pressed}) => ({
opacity: disabled || isDisabled ? 0.3 : pressed ? 0.7 : 1,
})}>
{useBevels ? (
<Svg width={s} height={s} viewBox={`0 0 ${s} ${s}`}>
<Path
d={buildButtonBevelPath(s, s, config.bevelSize, borderWidth)}
@@ -183,6 +186,21 @@ export function DTQuantityStepper({
strokeLinecap="round"
/>
</Svg>
) : (
<View style={{
width: s,
height: s,
borderWidth,
borderColor: accentColor,
borderRadius: theme.custom.radiusSm,
alignItems: 'center',
justifyContent: 'center',
}}>
<RNText style={{color: accentColor, fontSize: s * 0.5, lineHeight: s * 0.6}}>
{type === 'minus' ? '' : '+'}
</RNText>
</View>
)}
</Pressable>
);
};
@@ -190,7 +208,7 @@ export function DTQuantityStepper({
return (
<View style={[styles.wrapper, style]}>
{label && (
<Text style={[styles.label, {color: DTColors.light}]}>{label}</Text>
<Text style={[styles.label, {color: theme.colors.onSurface}]}>{label}</Text>
)}
<View style={styles.container}>
{renderStepButton('minus', handleDecrement, atMin)}

View File

@@ -14,7 +14,7 @@ import {createContext, useContext} from 'react';
import {StyleSheet, View, ViewStyle, StyleProp, Pressable} from 'react-native';
import {Text} from 'react-native-paper';
import Svg, {Polygon} from 'react-native-svg';
import {DTColors, getColor} from '../theme/colors';
import {useDTTheme} from '../theme/DTThemeProvider';
import {type DTVariant, getVariantColor} from '../utils/variantColors';
// --- Context ---
@@ -115,6 +115,15 @@ function hexPoints(cx: number, cy: number, r: number): string {
return pts.join(' ');
}
/** Convert hex color to rgba with given alpha */
function hexToRgba(hex: string, alpha: number): string {
const clean = hex.replace('#', '');
const r = parseInt(clean.substring(0, 2), 16);
const g = parseInt(clean.substring(2, 4), 16);
const b = parseInt(clean.substring(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}
/**
* DT-styled RadioOption with hexagonal indicator
*
@@ -130,6 +139,7 @@ export function DTRadioOption({
disabled = false,
style,
}: DTRadioOptionProps) {
const theme = useDTTheme();
const context = useContext(RadioContext);
if (!context) {
throw new Error('DTRadioOption must be used inside DTRadioGroup');
@@ -137,14 +147,9 @@ export function DTRadioOption({
const {value: selectedValue, onValueChange, variant} = context;
const isSelected = selectedValue === value;
const accentColor = getVariantColor(variant);
const selectedBgColor = getColor(
variant === 'normal'
? 'modeNormalSelected'
: variant === 'emphasis'
? 'modeEmphasisSelected'
: 'modeNormal',
);
const accentColor = getVariantColor(theme, variant);
const selectedBgColor = hexToRgba(accentColor, 0.7);
const unselectedBorderColor = hexToRgba(theme.custom.modeNormal, 0.3);
const opacity = disabled ? 0.5 : 1;
return (
@@ -153,7 +158,7 @@ export function DTRadioOption({
style={({pressed}) => [
styles.option,
{
borderColor: isSelected ? accentColor : getColor('modeNormal', 0.3),
borderColor: isSelected ? accentColor : unselectedBorderColor,
backgroundColor: isSelected ? selectedBgColor : 'transparent',
opacity: pressed ? 0.7 : opacity,
},
@@ -183,7 +188,7 @@ export function DTRadioOption({
<Text
style={[
styles.optionLabel,
{color: isSelected ? DTColors.dark : DTColors.light},
{color: isSelected ? theme.colors.onPrimary : theme.colors.onSurface},
]}>
{label}
</Text>
@@ -192,7 +197,7 @@ export function DTRadioOption({
variant="bodySmall"
style={[
styles.optionDescription,
{color: isSelected ? DTColors.dark : DTColors.disabled},
{color: isSelected ? theme.colors.onPrimary : theme.colors.onSurfaceDisabled},
]}>
{description}
</Text>

View File

@@ -11,7 +11,7 @@
import {useState, useRef, useEffect} from 'react';
import {StyleSheet, View, ViewStyle, TextStyle, StyleProp, Animated} from 'react-native';
import {Searchbar, ActivityIndicator} from 'react-native-paper';
import {DTColors} from '../theme/colors';
import {useDTTheme} from '../theme/DTThemeProvider';
import {type DTVariant, getVariantColor} from '../utils/variantColors';
interface DTSearchInputProps {
@@ -86,7 +86,8 @@ export function DTSearchInput({
style,
inputStyle,
}: DTSearchInputProps) {
const accentColor = getVariantColor(variant, color);
const theme = useDTTheme();
const accentColor = getVariantColor(theme, variant, color);
const [focused, setFocused] = useState(false);
const focusAnim = useRef(new Animated.Value(0)).current;
@@ -110,7 +111,7 @@ export function DTSearchInput({
onBlur={() => setFocused(false)}
returnKeyType="search"
placeholder={placeholder}
placeholderTextColor={DTColors.disabled}
placeholderTextColor={theme.colors.onSurfaceDisabled}
editable={!disabled}
autoFocus={autoFocus}
icon={
@@ -119,13 +120,13 @@ export function DTSearchInput({
: 'magnify'
}
iconColor={accentColor}
inputStyle={[styles.input, {color: DTColors.light}, inputStyle]}
style={[styles.searchbar, {borderColor: accentColor}]}
inputStyle={[styles.input, {color: theme.colors.onSurface}, inputStyle]}
style={[styles.searchbar, {borderColor: accentColor, backgroundColor: theme.colors.background}]}
rippleColor={accentColor}
theme={{
colors: {
elevation: {level3: DTColors.dark},
onSurface: DTColors.light,
elevation: {level3: theme.colors.background},
onSurface: theme.colors.onSurface,
onSurfaceVariant: accentColor,
},
roundness: 0,
@@ -149,7 +150,6 @@ const styles = StyleSheet.create({
container: {},
searchbar: {
borderWidth: 2,
backgroundColor: DTColors.dark,
elevation: 0,
},
input: {

View File

@@ -15,7 +15,7 @@ import {
Animated,
} from 'react-native';
import {Text} from 'react-native-paper';
import {DTColors} from '../theme/colors';
import {useDTTheme} from '../theme/DTThemeProvider';
import {type DTVariant, getVariantColor} from '../utils/variantColors';
const TRACK_WIDTH = 48;
@@ -73,7 +73,8 @@ export function DTSwitch({
color,
style,
}: DTSwitchProps) {
const accentColor = getVariantColor(variant, color);
const theme = useDTTheme();
const accentColor = getVariantColor(theme, variant, color);
const thumbAnim = useRef(new Animated.Value(value ? 1 : 0)).current;
useEffect(() => {
@@ -91,10 +92,9 @@ export function DTSwitch({
const trackColor = thumbAnim.interpolate({
inputRange: [0, 1],
outputRange: [DTColors.disabledBackground, accentColor],
outputRange: [theme.colors.surfaceDisabled, accentColor],
});
return (
<Pressable
onPress={() => !disabled && onValueChange(!value)}
@@ -104,7 +104,7 @@ export function DTSwitch({
style,
]}>
{label && (
<Text style={[styles.label, {color: DTColors.light}]}>{label}</Text>
<Text style={[styles.label, {color: theme.colors.onSurface}]}>{label}</Text>
)}
<Animated.View
style={[
@@ -118,7 +118,7 @@ export function DTSwitch({
style={[
styles.thumb,
{
backgroundColor: value ? DTColors.dark : DTColors.light,
backgroundColor: value ? theme.colors.onPrimary : theme.colors.onSurface,
transform: [{translateX: thumbTranslateX}],
},
]}

View File

@@ -20,7 +20,7 @@ import {
Animated,
} from 'react-native';
import {TextInput, TextInputProps} from 'react-native-paper';
import {DTColors} from '../theme/colors';
import {useDTTheme} from '../theme/DTThemeProvider';
import {type DTVariant, getVariantColor} from '../utils/variantColors';
interface DTTextInputProps
@@ -79,12 +79,13 @@ export function DTTextInput({
style,
...props
}: DTTextInputProps) {
const theme = useDTTheme();
const [focused, setFocused] = useState(false);
const focusAnim = useRef(new Animated.Value(0)).current;
const accentColor = error
? DTColors.modeWarning
: getVariantColor(variant, color);
? theme.custom.modeWarning
: getVariantColor(theme, variant, color);
useEffect(() => {
const anim = Animated.timing(focusAnim, {
@@ -101,7 +102,7 @@ export function DTTextInput({
<View
style={[
styles.inputWrapper,
{borderColor: accentColor, borderWidth},
{borderColor: accentColor, borderWidth, backgroundColor: theme.colors.background},
]}>
<TextInput
{...props}
@@ -116,8 +117,8 @@ export function DTTextInput({
onBlur?.(e);
}}
style={[styles.input, inputStyle, style]}
textColor={DTColors.light}
placeholderTextColor={DTColors.disabled}
textColor={theme.colors.onSurface}
placeholderTextColor={theme.colors.onSurfaceDisabled}
activeUnderlineColor="transparent"
underlineColor="transparent"
theme={{
@@ -141,7 +142,7 @@ export function DTTextInput({
/>
{error && errorMessage && (
<View style={styles.errorContainer}>
<Animated.Text style={[styles.errorText, {color: DTColors.modeWarning}]}>
<Animated.Text style={[styles.errorText, {color: theme.custom.modeWarning}]}>
{errorMessage}
</Animated.Text>
</View>
@@ -151,9 +152,7 @@ export function DTTextInput({
}
const styles = StyleSheet.create({
inputWrapper: {
backgroundColor: DTColors.dark,
},
inputWrapper: {},
input: {
backgroundColor: 'transparent',
},

View File

@@ -27,7 +27,7 @@ export { DTThemeProvider, useDTTheme } from './theme/DTThemeProvider';
// Shared utilities
export type { DTVariant } from './utils/variantColors';
export { variantColorMap, getVariantColor } from './utils/variantColors';
export { getVariantColor } from './utils/variantColors';
export { buildBeveledRectPath } from './utils/bevelPaths';
export { useComponentLayout } from './utils/useComponentLayout';

View File

@@ -124,6 +124,14 @@ export interface DTExtendedTheme extends MD3Theme {
modeOther: string;
border: string;
borderEmphasis: string;
/** Bevel sizes in pixels (0 = no bevels, use borderRadius instead) */
bevelSm: number;
bevelMd: number;
bevelLg: number;
/** Border radius in pixels (0 = angular/beveled) */
radiusSm: number;
radius: number;
radiusLg: number;
};
}
@@ -139,6 +147,12 @@ export const DTExtendedDarkTheme: DTExtendedTheme = {
modeOther: DTColors.modeOther,
border: DTColors.border,
borderEmphasis: DTColors.borderEmphasis,
bevelSm: 16,
bevelMd: 32,
bevelLg: 40,
radiusSm: 0,
radius: 0,
radiusLg: 0,
},
};

View File

@@ -2,26 +2,31 @@
* Centralized variant type and color mapping
*
* Used by all DT components for consistent mode/variant color resolution.
* Reads colors from the active DTExtendedTheme rather than static defaults.
*/
import {DTColors} from '../theme/colors';
import type {DTExtendedTheme} from '../theme/paperTheme';
export type DTVariant = 'normal' | 'emphasis' | 'warning' | 'success' | 'other';
export const variantColorMap: Record<DTVariant, string> = {
normal: DTColors.modeNormal,
emphasis: DTColors.modeEmphasis,
warning: DTColors.modeWarning,
success: DTColors.modeSuccess,
other: DTColors.modeOther,
/** String-typed keys in DTExtendedTheme['custom'] that map to mode colors */
type ModeColorKey = 'modeNormal' | 'modeEmphasis' | 'modeWarning' | 'modeSuccess' | 'modeOther';
const variantToThemeKey: Record<DTVariant, ModeColorKey> = {
normal: 'modeNormal',
emphasis: 'modeEmphasis',
warning: 'modeWarning',
success: 'modeSuccess',
other: 'modeOther',
};
/**
* Resolve variant to color, with optional custom color override.
* Resolve variant to color from the active theme, with optional custom color override.
*/
export function getVariantColor(
theme: DTExtendedTheme,
variant: DTVariant,
customColor?: string,
): string {
return customColor ?? variantColorMap[variant];
return customColor ?? theme.custom[variantToThemeKey[variant]];
}

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),
},
};
}