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:
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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: {},
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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}],
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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]];
|
||||
}
|
||||
|
||||
@@ -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, '[/\\\\]');
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user