import React from 'react'; import { View, Text, StyleProp, ViewStyle, TextStyle, Pressable, StyleSheet, } from 'react-native'; import { useUx4gTheme } from '../../theme/Ux4gThemeContext'; import { Ux4gIcons } from '../../foundation/icons'; import { UX4GColors } from '../../foundation/colors'; import { Ux4gUnifiedPillTag, Ux4gPillSegment } from '../tag/Tag'; // ─── Enums & Interfaces ────────────────────────────────────────────────────── export type Ux4gJourneyStepState = 'completed' | 'current' | 'upcoming'; export type Ux4gJourneyOrientation = 'vertical' | 'horizontal'; export type Ux4gJourneyBadgePosition = 'bottom' | 'topRight'; export interface Ux4gJourneyStepStatus { /** Status text (e.g. "2 days remaining") */ text: string; /** Color of the status dot */ dotColor?: string; /** Optional badge/tag text (e.g. "Pending") */ badgeText?: string; /** Badge background color */ badgeColor?: string; /** Badge text color */ badgeTextColor?: string; /** Position of the badge in the card (default: 'bottom') */ badgePosition?: Ux4gJourneyBadgePosition; } export interface Ux4gJourneyStep { /** State of this step (completed, current, upcoming) */ state?: Ux4gJourneyStepState; /** Date text shown at the top-left of the card */ date?: string; /** Tag text shown at the top-right of the card */ tag?: string; /** Title text */ title: string; /** Optional helping/description text below the title */ helpingText?: string; /** Optional icon below the title (e.g. link icon) */ icon?: React.ReactNode; /** Optional icon color */ iconColor?: string; /** Optional status indicator with dot + badge */ status?: Ux4gJourneyStepStatus; /** Optional custom content widget below the title area */ customContent?: React.ReactNode; /** Optional step number to show inside the indicator circle */ stepNumber?: string; /** Optional background color override for this specific step card */ cardColor?: string; /** Optional border color override for this specific step card */ cardBorderColor?: string; /** Optional date text color override */ dateColor?: string; /** Optional title text color override */ titleColor?: string; /** Optional date text style override */ dateStyle?: StyleProp; /** Optional title text style override */ titleStyle?: StyleProp; } export interface Ux4gJourneyHeader { /** Title text */ title: string; /** Description text below the title */ description?: string; /** Optional trailing icon */ icon?: React.ReactNode; /** Callback when trailing icon is tapped */ onIconPressed?: () => void; } export interface Ux4gJourneyTimelineProps { /** List of journey steps */ steps: Ux4gJourneyStep[]; /** Optional header above the timeline */ header?: Ux4gJourneyHeader; /** Current step index (0-based). Set to null to use each step's state */ currentStep?: number | null; /** Timeline orientation (default: 'vertical') */ orientation?: Ux4gJourneyOrientation; /** Size of the step indicator circle (default: 20) */ indicatorSize?: number; /** Width of the connecting line (default: 3) */ lineWidth?: number; /** Spacing between the indicator and the card (default: 12) */ indicatorCardSpacing?: number; /** Vertical spacing between step cards (default: 12) */ stepSpacing?: number; /** Color for completed/current indicators and lines */ activeColor?: string; /** Color for upcoming indicators and lines */ inactiveColor?: string; /** Card border radius (default: 8) */ cardBorderRadius?: number; /** Card padding */ cardPadding?: StyleProp; /** Card background color */ cardColor?: string; /** Card border color */ cardBorderColor?: string; /** Optional default date text style override for all steps */ dateStyle?: StyleProp; /** Optional default title text style override for all steps */ titleStyle?: StyleProp; /** Custom container style */ style?: StyleProp; } // ─── Sub-Component: Step Indicator ────────────────────────────────────────── const StepIndicator: React.FC<{ state: Ux4gJourneyStepState; size: number; activeColor: string; inactiveColor: string; stepNumber?: string; }> = ({ state, size, activeColor, inactiveColor, stepNumber }) => { const theme = useUx4gTheme(); const surfaceColor = theme.colors.surface ?? (theme.isDark ? UX4GColors.neutral950 : UX4GColors.neutral0); if (state === 'completed') { return ( {stepNumber ? ( {stepNumber} ) : ( )} ); } if (state === 'current') { return ( {stepNumber ? ( {stepNumber} ) : ( )} ); } // upcoming return ( {stepNumber ? ( {stepNumber} ) : ( )} ); }; // ─── Sub-Component: Step Card ─────────────────────────────────────────────── const JourneyStepCard: React.FC<{ step: Ux4gJourneyStep; state: Ux4gJourneyStepState; borderRadius: number; padding?: StyleProp; cardColor?: string; cardBorderColor?: string; dateStyle?: StyleProp; titleStyle?: StyleProp; }> = ({ step, state, borderRadius, padding, cardColor, cardBorderColor, dateStyle, titleStyle, }) => { const theme = useUx4gTheme(); const colors = theme.colors; const isDark = theme.isDark; const isUpcoming = state === 'upcoming'; const textOpacity = isUpcoming ? 0.5 : 1.0; const onSurface = colors.onSurface ?? UX4GColors.neutral900; const primary = colors.primary ?? UX4GColors.primary600; const secondaryColor = colors.secondary ?? (isDark ? UX4GColors.secondary300 : UX4GColors.secondary600); const defaultBg = step.cardColor ?? cardColor ?? (isDark ? UX4GColors.neutral950 : UX4GColors.neutral0); const defaultBorder = step.cardBorderColor ?? cardBorderColor ?? `${onSurface}1A`; const hasTopRightBadge = step.status != null && step.status.badgeText != null && step.status.badgePosition === 'topRight'; const hasHeaderRow = step.date != null || step.tag != null || hasTopRightBadge; // Build bottom status pill segments const statusPillSegments: Ux4gPillSegment[] = []; if (step.status && step.status.badgePosition !== 'topRight') { const dotColor = step.status.dotColor ?? secondaryColor; statusPillSegments.push({ text: step.status.text, leading: ( ), }); if (step.status.badgeText) { statusPillSegments.push({ text: step.status.badgeText, bold: true, textColor: step.status.badgeTextColor ?? secondaryColor, }); } } return ( {/* Date + Tag / Badge Top Row */} {hasHeaderRow && ( {step.date ? ( {step.date} ) : ( )} {hasTopRightBadge ? ( {step.status!.badgeText} ) : step.tag ? ( {step.tag} ) : null} )} {/* Title */} {step.title} {/* Helping Text */} {step.helpingText != null && ( {step.helpingText} )} {/* Optional Icon */} {step.icon != null && ( {typeof step.icon === 'function' ? step.icon : step.icon} )} {/* Status indicator at bottom (when badgePosition is 'bottom') */} {statusPillSegments.length > 0 && ( )} {/* Custom Content */} {step.customContent != null && ( {step.customContent} )} ); }; // ─── Main Component: Ux4gJourneyTimeline ──────────────────────────────────── export const Ux4gJourneyTimeline: React.FC = ({ steps = [], header, currentStep, orientation = 'vertical', indicatorSize = 20, lineWidth = 3, indicatorCardSpacing = 12, stepSpacing = 12, activeColor, inactiveColor, cardBorderRadius = 8, cardPadding, cardColor, cardBorderColor, dateStyle, titleStyle, style, }) => { const theme = useUx4gTheme(); const colors = theme.colors; const primary = colors.primary ?? UX4GColors.primary600; const onSurface = colors.onSurface ?? UX4GColors.neutral900; const resolvedActiveColor = activeColor ?? primary; const resolvedInactiveColor = inactiveColor ?? `${onSurface}40`; // 0.25 opacity const resolveState = (index: number): Ux4gJourneyStepState => { if (currentStep !== undefined && currentStep !== null) { if (index < currentStep) return 'completed'; if (index === currentStep) return 'current'; return 'upcoming'; } return steps[index].state ?? 'upcoming'; }; // ── Render Header ── const renderHeader = () => { if (!header) return null; return ( {header.title} {header.icon && ( {header.icon} )} {header.description && ( {header.description} )} ); }; // ── Render Vertical Layout ── if (orientation === 'vertical') { return ( {renderHeader()} {steps.map((step, i) => { const state = resolveState(i); const isFirst = i === 0; const isLast = i === steps.length - 1; // Top line color const topLineColor = state === 'completed' || state === 'current' ? resolvedActiveColor : resolvedInactiveColor; // Bottom line color const bottomLineColor = state === 'completed' ? resolvedActiveColor : resolvedInactiveColor; return ( {/* Indicator Column */} {/* Top connecting line */} {!isFirst ? ( ) : ( )} {/* Circle Indicator */} {/* Bottom connecting line */} {!isLast ? ( ) : ( )} {/* Step Card Container */} ); })} ); } // ── Render Horizontal Layout ── const activeIndex = currentStep !== undefined && currentStep !== null ? currentStep : steps.findIndex((s) => s.state === 'current'); const resolvedActiveIndex = activeIndex >= 0 ? activeIndex : 0; return ( {renderHeader()} {/* Horizontal Stepper Line Row */} {steps.map((step, i) => { const state = resolveState(i); const isLast = i === steps.length - 1; return ( {!isLast && ( )} ); })} {/* Active Step Card */} {steps[resolvedActiveIndex] && ( )} ); }; export default Ux4gJourneyTimeline;