import React from 'react'; import { View, Text, StyleSheet, StyleProp, ViewStyle, TextStyle } from 'react-native'; import Svg, { Circle, Defs, LinearGradient, Stop } from 'react-native-svg'; import { useUx4gTheme } from '../../theme/Ux4gThemeContext'; import { UX4GColors } from '../../foundation/colors'; export type Ux4gCircularProgressSize = 'xs' | 's' | 'm' | 'l' | 'xl' | 'xxl' | 'xxxl'; export type Ux4gStrokeCap = 'butt' | 'round'; export interface Ux4gCircularProgressProps { /** * Progress value between 0.0 and 1.0 */ value: number; size?: Ux4gCircularProgressSize; diameter?: number; strokeWidth?: number; progressColor?: string; trackColor?: string; gradientColors?: string[]; strokeCap?: Ux4gStrokeCap; /** * Start angle in degrees. Default is -90 (top) */ startAngle?: number; centerValueText?: string; centerDescription?: string; center?: React.ReactNode; label?: string; description?: string; footer?: React.ReactNode; gap?: number; backgroundColor?: string; labelStyle?: StyleProp; descriptionStyle?: StyleProp; containerStyle?: StyleProp; testID?: string; } const sizeMap: Record = { xs: 20, s: 28, m: 36, l: 48, xl: 88, xxl: 116, xxxl: 144, }; function scale(ringSize: number, factor: number, min: number, max: number) { return Math.max(min, Math.min(max, ringSize * factor)); } export const Ux4gCircularProgress: React.FC = ({ value, size = 'l', diameter, strokeWidth, progressColor, trackColor, gradientColors, strokeCap = 'butt', startAngle = -90, centerValueText, centerDescription, center, label, description, footer, gap = 8, backgroundColor, labelStyle, descriptionStyle, containerStyle, testID, }) => { const theme = useUx4gTheme(); const { colors, typography, isDark } = theme; const gradientId = React.useId(); const clampedValue = Math.min(Math.max(value, 0.0), 1.0); const ringSize = diameter ?? sizeMap[size]; const resolvedStrokeWidth = strokeWidth ?? scale(ringSize, 0.1, 3.0, 12.0); const radius = (ringSize - resolvedStrokeWidth) / 2; const circumference = 2 * Math.PI * radius; const strokeDashoffset = circumference - clampedValue * circumference; const resolvedProgressColor = progressColor ?? colors.primary ?? UX4GColors.primary600; const resolvedTrackColor = trackColor ?? (isDark ? UX4GColors.neutral600 : UX4GColors.neutral300); const resolvedGradient = gradientColors ?? (progressColor == null ? (isDark ? [UX4GColors.primary700, UX4GColors.primary300] : [UX4GColors.primary200, UX4GColors.primary600]) : undefined); const resolvedGap = Math.max(4, Math.min(16, gap)); const inlineCenter = ringSize >= 72; const hasSmallCenterText = !inlineCenter && (centerValueText != null || centerDescription != null) && label == null; const hasMeta = label != null || description != null || footer != null; // Typography scaling const centerValueFontSize = scale(ringSize, 0.18, 7, 22); const centerDescFontSize = scale(ringSize, 0.1, 6, 12); const labelFontSize = scale(ringSize, 0.16, 10, 16); const descFontSize = scale(ringSize, 0.14, 9, 14); const onSurfaceColor = isDark ? UX4GColors.white : UX4GColors.neutral900; // Approximation for content color based on background const contentColor = backgroundColor ? onSurfaceColor : onSurfaceColor; const renderCenterContent = (isSmall: boolean) => { if (!centerValueText && !centerDescription) return null; return ( {centerValueText && ( {centerValueText} )} {centerDescription && ( {centerDescription} )} ); }; const centerWidget = center ?? (inlineCenter ? renderCenterContent(false) : null); return ( {resolvedGradient && resolvedGradient.length >= 2 && ( {resolvedGradient.map((c, i) => ( ))} )} {backgroundColor && ( )} {/* Track */} {/* Progress */} {clampedValue > 0 && ( )} {centerWidget && ( {centerWidget} )} {hasSmallCenterText && ( {renderCenterContent(true)} )} {hasMeta && } {label && ( {label} )} {description && ( {description} )} {footer && ( {footer} )} ); }; const styles = StyleSheet.create({ container: { flexDirection: 'column', alignItems: 'center', }, centerWrapper: { flex: 1, justifyContent: 'center', alignItems: 'center', }, centerContent: { alignItems: 'center', justifyContent: 'center', }, });