Files
dt-design-system/packages/react-native/src/components/DTHexagon.tsx
michael 47e8085581 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>
2026-03-06 10:18:39 -08:00

213 lines
4.9 KiB
TypeScript

/**
* DT Hexagon Component
*
* Decorative hexagon shape following the Dangerous Things design language.
* No RNP equivalent — purely SVG-based.
*
* Web CSS reference (AnimatedHexagon / HexagonHamburgerMenu):
* - Flat-top hexagon shape
* - Optional rotation and pulse animations
* - Used for loading indicators, backgrounds, menu icons
*/
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 {
/**
* Hexagon diameter in pixels
*/
size: number;
/**
* Visual variant
* @default 'normal'
*/
variant?: DTVariant;
/**
* Whether the hexagon has a solid fill
* @default true
*/
filled?: boolean;
/**
* Whether to animate
* @default false
*/
animated?: boolean;
/**
* Animation type
* @default 'none'
*/
animationType?: 'rotate' | 'pulse' | 'none';
/**
* Animation duration in milliseconds
* @default 2000
*/
animationDuration?: number;
/**
* Stroke/border width when not filled
* @default 2
*/
borderWidth?: number;
/**
* Custom color (overrides variant)
*/
color?: string;
/**
* Opacity
* @default 1
*/
opacity?: number;
/**
* Additional styles
*/
style?: StyleProp<ViewStyle>;
/**
* Content rendered centered inside the hexagon
*/
children?: React.ReactNode;
}
/**
* Build flat-top hexagon SVG polygon points
*/
function buildHexagonPoints(size: number): string {
const cx = size / 2;
const cy = size / 2;
const r = size / 2;
return [0, 1, 2, 3, 4, 5]
.map(i => {
const angle = (Math.PI / 3) * i - Math.PI / 6; // flat-top orientation
const x = cx + r * Math.cos(angle);
const y = cy + r * Math.sin(angle);
return `${x},${y}`;
})
.join(' ');
}
/**
* DT-styled decorative Hexagon
*
* @example
* <DTHexagon size={60} variant="normal" />
*
* @example
* <DTHexagon size={40} variant="emphasis" animated animationType="rotate" />
*
* @example
* <DTHexagon size={80} variant="success" filled={false} animated animationType="pulse" />
*/
export function DTHexagon({
size,
variant = 'normal',
filled = true,
animated = false,
animationType = 'none',
animationDuration = 2000,
borderWidth = 2,
color,
opacity: opacityProp = 1,
style,
children,
}: DTHexagonProps) {
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;
useEffect(() => {
if (!animated) {
rotateAnim.setValue(0);
pulseAnim.setValue(1);
return;
}
if (animationType === 'rotate') {
const animation = Animated.loop(
Animated.timing(rotateAnim, {
toValue: 1,
duration: animationDuration,
useNativeDriver: true,
}),
);
animation.start();
return () => {
animation.stop();
rotateAnim.setValue(0);
};
}
if (animationType === 'pulse') {
const animation = Animated.loop(
Animated.sequence([
Animated.timing(pulseAnim, {
toValue: 0.3,
duration: animationDuration / 2,
useNativeDriver: true,
}),
Animated.timing(pulseAnim, {
toValue: 1,
duration: animationDuration / 2,
useNativeDriver: true,
}),
]),
);
animation.start();
return () => {
animation.stop();
pulseAnim.setValue(1);
};
}
}, [animated, animationType, animationDuration, rotateAnim, pulseAnim]);
const rotation = rotateAnim.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg'],
});
const animatedOpacity =
animated && animationType === 'pulse' ? pulseAnim : opacityProp;
const transform =
animated && animationType === 'rotate'
? [{rotate: rotation}]
: [];
const points = buildHexagonPoints(size);
return (
<Animated.View
style={[
styles.container,
{width: size, height: size, opacity: animatedOpacity},
transform.length > 0 ? {transform} : undefined,
style,
]}>
<Svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
<Polygon
points={points}
fill={filled ? accentColor : 'transparent'}
stroke={accentColor}
strokeWidth={filled ? 0 : borderWidth}
/>
</Svg>
{children && (
<View style={[StyleSheet.absoluteFill, styles.childrenContainer]}>
{children}
</View>
)}
</Animated.View>
);
}
const styles = StyleSheet.create({
container: {},
childrenContainer: {
justifyContent: 'center',
alignItems: 'center',
},
});