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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,10 +9,10 @@
* - Quantity display between buttons * - 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 {Text} from 'react-native-paper';
import Svg, {Path} from 'react-native-svg'; 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 {type DTVariant, getVariantColor} from '../utils/variantColors';
import {buildButtonBevelPath} from '../utils/bevelPaths'; import {buildButtonBevelPath} from '../utils/bevelPaths';
@@ -124,7 +124,9 @@ export function DTQuantityStepper({
color, color,
style, style,
}: DTQuantityStepperProps) { }: 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 config = sizeConfigs[size];
const atMin = value <= min; const atMin = value <= min;
const atMax = value >= max; const atMax = value >= max;
@@ -168,6 +170,7 @@ export function DTQuantityStepper({
style={({pressed}) => ({ style={({pressed}) => ({
opacity: disabled || isDisabled ? 0.3 : pressed ? 0.7 : 1, opacity: disabled || isDisabled ? 0.3 : pressed ? 0.7 : 1,
})}> })}>
{useBevels ? (
<Svg width={s} height={s} viewBox={`0 0 ${s} ${s}`}> <Svg width={s} height={s} viewBox={`0 0 ${s} ${s}`}>
<Path <Path
d={buildButtonBevelPath(s, s, config.bevelSize, borderWidth)} d={buildButtonBevelPath(s, s, config.bevelSize, borderWidth)}
@@ -183,6 +186,21 @@ export function DTQuantityStepper({
strokeLinecap="round" strokeLinecap="round"
/> />
</Svg> </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> </Pressable>
); );
}; };
@@ -190,7 +208,7 @@ export function DTQuantityStepper({
return ( return (
<View style={[styles.wrapper, style]}> <View style={[styles.wrapper, style]}>
{label && ( {label && (
<Text style={[styles.label, {color: DTColors.light}]}>{label}</Text> <Text style={[styles.label, {color: theme.colors.onSurface}]}>{label}</Text>
)} )}
<View style={styles.container}> <View style={styles.container}>
{renderStepButton('minus', handleDecrement, atMin)} {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 {StyleSheet, View, ViewStyle, StyleProp, Pressable} from 'react-native';
import {Text} from 'react-native-paper'; import {Text} from 'react-native-paper';
import Svg, {Polygon} from 'react-native-svg'; 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'; import {type DTVariant, getVariantColor} from '../utils/variantColors';
// --- Context --- // --- Context ---
@@ -115,6 +115,15 @@ function hexPoints(cx: number, cy: number, r: number): string {
return pts.join(' '); 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 * DT-styled RadioOption with hexagonal indicator
* *
@@ -130,6 +139,7 @@ export function DTRadioOption({
disabled = false, disabled = false,
style, style,
}: DTRadioOptionProps) { }: DTRadioOptionProps) {
const theme = useDTTheme();
const context = useContext(RadioContext); const context = useContext(RadioContext);
if (!context) { if (!context) {
throw new Error('DTRadioOption must be used inside DTRadioGroup'); throw new Error('DTRadioOption must be used inside DTRadioGroup');
@@ -137,14 +147,9 @@ export function DTRadioOption({
const {value: selectedValue, onValueChange, variant} = context; const {value: selectedValue, onValueChange, variant} = context;
const isSelected = selectedValue === value; const isSelected = selectedValue === value;
const accentColor = getVariantColor(variant); const accentColor = getVariantColor(theme, variant);
const selectedBgColor = getColor( const selectedBgColor = hexToRgba(accentColor, 0.7);
variant === 'normal' const unselectedBorderColor = hexToRgba(theme.custom.modeNormal, 0.3);
? 'modeNormalSelected'
: variant === 'emphasis'
? 'modeEmphasisSelected'
: 'modeNormal',
);
const opacity = disabled ? 0.5 : 1; const opacity = disabled ? 0.5 : 1;
return ( return (
@@ -153,7 +158,7 @@ export function DTRadioOption({
style={({pressed}) => [ style={({pressed}) => [
styles.option, styles.option,
{ {
borderColor: isSelected ? accentColor : getColor('modeNormal', 0.3), borderColor: isSelected ? accentColor : unselectedBorderColor,
backgroundColor: isSelected ? selectedBgColor : 'transparent', backgroundColor: isSelected ? selectedBgColor : 'transparent',
opacity: pressed ? 0.7 : opacity, opacity: pressed ? 0.7 : opacity,
}, },
@@ -183,7 +188,7 @@ export function DTRadioOption({
<Text <Text
style={[ style={[
styles.optionLabel, styles.optionLabel,
{color: isSelected ? DTColors.dark : DTColors.light}, {color: isSelected ? theme.colors.onPrimary : theme.colors.onSurface},
]}> ]}>
{label} {label}
</Text> </Text>
@@ -192,7 +197,7 @@ export function DTRadioOption({
variant="bodySmall" variant="bodySmall"
style={[ style={[
styles.optionDescription, styles.optionDescription,
{color: isSelected ? DTColors.dark : DTColors.disabled}, {color: isSelected ? theme.colors.onPrimary : theme.colors.onSurfaceDisabled},
]}> ]}>
{description} {description}
</Text> </Text>

View File

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

View File

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

View File

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

View File

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

View File

@@ -124,6 +124,14 @@ export interface DTExtendedTheme extends MD3Theme {
modeOther: string; modeOther: string;
border: string; border: string;
borderEmphasis: 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, modeOther: DTColors.modeOther,
border: DTColors.border, border: DTColors.border,
borderEmphasis: DTColors.borderEmphasis, 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 * Centralized variant type and color mapping
* *
* Used by all DT components for consistent mode/variant color resolution. * 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 type DTVariant = 'normal' | 'emphasis' | 'warning' | 'success' | 'other';
export const variantColorMap: Record<DTVariant, string> = { /** String-typed keys in DTExtendedTheme['custom'] that map to mode colors */
normal: DTColors.modeNormal, type ModeColorKey = 'modeNormal' | 'modeEmphasis' | 'modeWarning' | 'modeSuccess' | 'modeOther';
emphasis: DTColors.modeEmphasis,
warning: DTColors.modeWarning, const variantToThemeKey: Record<DTVariant, ModeColorKey> = {
success: DTColors.modeSuccess, normal: 'modeNormal',
other: DTColors.modeOther, 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( export function getVariantColor(
theme: DTExtendedTheme,
variant: DTVariant, variant: DTVariant,
customColor?: string, customColor?: string,
): 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'), 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 // Block sibling packages' node_modules to prevent duplicate native modules
const packagesDir = path.join(monorepoRoot, 'packages'); const packagesDir = path.join(monorepoRoot, 'packages');
const escapedPath = packagesDir.replace(/[/\\]/g, '[/\\\\]'); 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 { BrandTokens } from '@dangerousthings/tokens';
import type { DTExtendedTheme } from '@dangerousthings/react-native'; 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 { export function buildThemeFromBrand(brand: BrandTokens): DTExtendedTheme {
const colors = brand.dark; const colors = brand.dark;
@@ -9,7 +37,7 @@ export function buildThemeFromBrand(brand: BrandTokens): DTExtendedTheme {
...MD3DarkTheme, ...MD3DarkTheme,
dark: true, dark: true,
roundness: brand.shape.radiusSm === '0' || brand.shape.radiusSm === '0px' ? 0 : 4, roundness: brand.shape.radiusSm === '0' || brand.shape.radiusSm === '0px' ? 0 : 4,
fonts: MD3DarkTheme.fonts, fonts: buildFonts(brand),
colors: { colors: {
...MD3DarkTheme.colors, ...MD3DarkTheme.colors,
primary: colors.primary, primary: colors.primary,
@@ -63,6 +91,12 @@ export function buildThemeFromBrand(brand: BrandTokens): DTExtendedTheme {
modeOther: colors.other, modeOther: colors.other,
border: colors.border, border: colors.border,
borderEmphasis: colors.secondary, 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),
}, },
}; };
} }