/** * DT QuantityStepper Component * * Custom-built quantity increment/decrement control following the * Dangerous Things design language. No RNP equivalent exists. * * Web CSS reference (AddOnOptionItem quantity controls): * - Plus/minus buttons with beveled corners * - Quantity display between buttons */ 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 {type DTVariant, getVariantColor} from '../utils/variantColors'; import {buildButtonBevelPath} from '../utils/bevelPaths'; type DTStepperSize = 'small' | 'medium' | 'large'; interface SizeConfig { buttonSize: number; fontSize: number; bevelSize: number; iconStrokeWidth: number; minWidth: number; } const sizeConfigs: Record = { small: { buttonSize: 28, fontSize: 12, bevelSize: 4, iconStrokeWidth: 1.5, minWidth: 28, }, medium: { buttonSize: 36, fontSize: 16, bevelSize: 6, iconStrokeWidth: 2, minWidth: 36, }, large: { buttonSize: 44, fontSize: 20, bevelSize: 8, iconStrokeWidth: 2.5, minWidth: 44, }, }; interface DTQuantityStepperProps { /** * Current quantity value */ value: number; /** * Called when value changes */ onValueChange: (value: number) => void; /** * Minimum value * @default 0 */ min?: number; /** * Maximum value * @default 99 */ max?: number; /** * Step increment * @default 1 */ step?: number; /** * Visual variant * @default 'normal' */ variant?: DTVariant; /** * Whether the stepper is disabled */ disabled?: boolean; /** * Label text displayed above the stepper */ label?: string; /** * Size preset * @default 'medium' */ size?: DTStepperSize; /** * Custom color (overrides variant) */ color?: string; /** * Additional styles */ style?: StyleProp; } /** * DT-styled QuantityStepper with beveled +/- buttons * * @example * * * @example * */ export function DTQuantityStepper({ value, onValueChange, min = 0, max = 99, step = 1, variant = 'normal', disabled = false, label, size = 'medium', color, style, }: DTQuantityStepperProps) { const accentColor = getVariantColor(variant, color); const config = sizeConfigs[size]; const atMin = value <= min; const atMax = value >= max; const borderWidth = 2; const handleDecrement = () => { const next = Math.max(min, value - step); onValueChange(next); }; const handleIncrement = () => { const next = Math.min(max, value + step); onValueChange(next); }; const renderStepButton = ( type: 'minus' | 'plus', onPress: () => void, isDisabled: boolean, ) => { const s = config.buttonSize; const center = s / 2; const iconSize = s * 0.35; const sw = config.iconStrokeWidth; // Minus: horizontal line; Plus: cross const iconPath = type === 'minus' ? `M ${center - iconSize} ${center} L ${center + iconSize} ${center}` : `M ${center - iconSize} ${center} L ${center + iconSize} ${center} M ${center} ${center - iconSize} L ${center} ${center + iconSize}`; // Expand touch target to at least 44px const hitSlopSize = Math.max(0, (44 - s) / 2); const hitSlop = hitSlopSize > 0 ? hitSlopSize : undefined; return ( ({ opacity: disabled || isDisabled ? 0.3 : pressed ? 0.7 : 1, })}> ); }; return ( {label && ( {label} )} {renderStepButton('minus', handleDecrement, atMin)} {value} {renderStepButton('plus', handleIncrement, atMax)} ); } const styles = StyleSheet.create({ wrapper: { gap: 8, }, container: { flexDirection: 'row', alignItems: 'center', gap: 4, }, label: { fontSize: 14, letterSpacing: 0.3, }, valueContainer: { alignItems: 'center', justifyContent: 'center', paddingHorizontal: 8, }, valueText: { fontWeight: '700', letterSpacing: 0.5, }, });