Add storefront component migration: cards, badges, progress bars, animations, and React wrapper package

Major feature migration from dt-shopify-storefront into the design system:

- Card system: per-card color modes (normal/emphasis/warning/success/other),
  progress bars, full-bleed card body (card-body-flush), image bevel clip-paths,
  and visible structural borders at bevel corners
- Badge system: dt-card-badge with position-aware offsets for cards vs media frames
- Media frames: dt-bevel-media with inner surface fill, image clipping, and
  "MISSING MEDIA" placeholder for empty containers
- Interactive bevel buttons: dt-btn with hover/selected states and pulse animation
- Menu items: dt-menu-item with active states and level indentation
- Animations: scale-in, fade-in, slide-up, pulse, ping, spin keyframes
- Feature legend: CSS grid for product feature icons with state colors
- Scrollbar styling for DT brand
- New @dangerousthings/react package wrapping web CSS components
- New @dangerousthings/tailwind-preset package
- New @dangerousthings/hex-background package
- Desktop showcase rewritten in React with Tailwind CSS
- Mobile showcase updated with new screens (animations, filters, advanced cards)
- Tokens: added mode color tokens, RGB variants, and selected-state tokens

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
michael
2026-03-05 13:26:03 -08:00
parent 47e8085581
commit e74c285b70
109 changed files with 7196 additions and 1026 deletions

View File

@@ -1,18 +1,20 @@
/**
* DT Button Component
*
* A themed button following the Dangerous Things design language
* with SVG-based beveled corners.
* Interactive bevel button matching the storefront .menu-item-clipped pattern:
* - Default: outlined rectangle, no bevel, colored border with thick top
* - Pressed/hover: bottom-right bevel appears, fills with mode color, text goes dark
* - Selected: bevel persists, fills with 70% opacity mode color
*/
import {StyleSheet, View, ViewStyle, StyleProp, Pressable} from 'react-native';
import {useRef, useEffect} from 'react';
import {StyleSheet, View, ViewStyle, StyleProp, Pressable, Animated} from 'react-native';
import {Text} from 'react-native-paper';
import Svg, {Path} from 'react-native-svg';
import Svg, {Path, Rect} from 'react-native-svg';
import {useDTTheme} from '../theme/DTThemeProvider';
import {type DTVariant, getVariantColor} from '../utils/variantColors';
import {buildButtonBevelPath} from '../utils/bevelPaths';
import {buildBeveledRectPath} from '../utils/bevelPaths';
import {useComponentLayout} from '../utils/useComponentLayout';
import {useState} from 'react';
interface DTButtonProps {
/**
@@ -26,11 +28,16 @@ interface DTButtonProps {
variant?: DTVariant;
/**
* Button display mode
* - 'outlined': Border with transparent background (default)
* - 'contained': Filled background
* - 'outlined': Border with transparent background, bevel on press (default)
* - 'contained': Filled background with static bevel
* @default 'outlined'
*/
mode?: 'outlined' | 'contained';
/**
* Whether the button is in a persistent selected state
* (bevel visible, filled with 70% opacity mode color)
*/
selected?: boolean;
/**
* Custom color (overrides variant)
*/
@@ -53,91 +60,169 @@ interface DTButtonProps {
*/
borderWidth?: number;
/**
* Bevel size in pixels
* @default 8
* Bevel size in pixels (bottom-right corner)
* @default 16
*/
bevelSize?: number;
}
/**
* DT-styled Button component with SVG beveled corners
*
* @example
* <DTButton variant="normal" onPress={handleScan}>
* Scan NFC Tag
* </DTButton>
*
* @example
* <DTButton variant="emphasis" mode="contained" onPress={handleAction}>
* View Results
* </DTButton>
*/
export function DTButton({
children,
variant = 'normal',
mode = 'outlined',
selected = false,
color,
onPress,
disabled = false,
style,
borderWidth = 2,
bevelSize = 8,
bevelSize = 16,
}: DTButtonProps) {
const theme = useDTTheme();
const {dimensions, onLayout, hasDimensions} = useComponentLayout();
const [pressed, setPressed] = useState(false);
const pulseAnim = useRef(new Animated.Value(1)).current;
const pressed = useRef(false);
const accentColor = getVariantColor(theme, variant, color);
const selectedColor = accentColor + 'B3'; // ~70% opacity
const isContained = mode === 'contained';
const bgColor = isContained ? accentColor : 'transparent';
const textColor = isContained ? theme.colors.onPrimary : accentColor;
const useBevels = theme.custom.bevelMd > 0;
const {width, height} = dimensions;
const opacity = disabled ? 0.5 : pressed ? 0.7 : 1;
const useBevels = theme.custom.bevelMd > 0;
// Build SVG paths: rectangle (default) and beveled (active)
const rectPath = hasDimensions
? buildBeveledRectPath(width, height, {corners: {}, strokeWidth: borderWidth})
: '';
const bevelPath = useBevels && hasDimensions
? buildBeveledRectPath(width, height, {
corners: {bottomRight: bevelSize},
strokeWidth: borderWidth,
})
: '';
// Pulse animation for pressed/selected hover
useEffect(() => {
if (selected) {
// No pulse when just selected (matches storefront: animation: none on .selected)
pulseAnim.setValue(1);
}
return () => {
pulseAnim.stopAnimation();
};
}, [selected, pulseAnim]);
const startPulse = () => {
Animated.loop(
Animated.sequence([
Animated.timing(pulseAnim, {
toValue: 0.5,
duration: 1000,
useNativeDriver: true,
}),
Animated.timing(pulseAnim, {
toValue: 1,
duration: 1000,
useNativeDriver: true,
}),
]),
).start();
};
const stopPulse = () => {
pulseAnim.stopAnimation();
pulseAnim.setValue(1);
};
const handlePressIn = () => {
pressed.current = true;
startPulse();
};
const handlePressOut = () => {
pressed.current = false;
if (!selected) {
stopPulse();
}
};
// Determine visual state
const showBevel = isContained || selected;
const getBgColor = (isPressed: boolean) => {
if (isContained) return accentColor;
if (selected) return selectedColor;
if (isPressed) return accentColor;
return 'transparent';
};
const getTextColor = (isPressed: boolean) => {
if (isContained || selected || isPressed) return theme.colors.onPrimary;
return accentColor;
};
return (
<Pressable
onPress={onPress}
disabled={disabled}
onPressIn={() => setPressed(true)}
onPressOut={() => setPressed(false)}
style={[{opacity}, style]}>
<View
style={[
styles.container,
!useBevels && {
borderWidth,
borderColor: accentColor,
borderRadius: theme.custom.radiusSm,
backgroundColor: bgColor,
},
]}
onLayout={onLayout}>
{useBevels && hasDimensions && (
<Svg
style={StyleSheet.absoluteFill}
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}>
<Path
d={buildButtonBevelPath(width, height, bevelSize, borderWidth)}
fill={bgColor}
stroke={accentColor}
strokeWidth={borderWidth}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
style={[{opacity: disabled ? 0.5 : 1}, style]}>
{({pressed: isPressedState}) => (
<Animated.View
style={[styles.container, {opacity: pulseAnim}]}
onLayout={onLayout}>
{/* Non-beveled mode (classic brand) */}
{!useBevels && (
<View
style={[
StyleSheet.absoluteFill,
{
borderWidth,
borderColor: accentColor,
borderTopWidth: 5,
borderRadius: theme.custom.radiusSm,
backgroundColor: getBgColor(isPressedState),
},
]}
/>
</Svg>
)}
<View style={styles.content}>
{typeof children === 'string' ? (
<Text variant="labelLarge" style={[styles.label, {color: textColor}]}>
{children}
</Text>
) : (
children
)}
</View>
</View>
{/* Beveled mode (DT brand) */}
{useBevels && hasDimensions && (
<Svg
style={StyleSheet.absoluteFill}
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}>
<Path
d={(isPressedState || showBevel) ? bevelPath : rectPath}
fill={getBgColor(isPressedState)}
stroke={accentColor}
strokeWidth={borderWidth}
/>
{/* Thick top accent — rendered inside SVG for exact alignment */}
<Rect
x={borderWidth / 2}
y={0}
width={width - borderWidth}
height={5}
fill={accentColor}
/>
</Svg>
)}
<View style={styles.content}>
{typeof children === 'string' ? (
<Text
variant="labelLarge"
style={[styles.label, {color: getTextColor(isPressedState)}]}>
{children}
</Text>
) : (
children
)}
</View>
</Animated.View>
)}
</Pressable>
);
}