diff --git a/apps/native/src/components/common/Header.tsx b/apps/native/src/components/common/Header.tsx new file mode 100644 index 000000000..3a4935ba5 --- /dev/null +++ b/apps/native/src/components/common/Header.tsx @@ -0,0 +1,131 @@ +import React, { type FC, type ReactNode } from 'react'; +import { View, Text } from 'react-native'; +import { ChevronLeft, type LucideIcon } from 'lucide-react-native'; +import { useNavigation } from '@react-navigation/native'; + +import { colors } from '@theme/tokens'; + +import ContentInset from './ContentInset'; +import AnimatedPressable from './AnimatedPressable'; + +type HeaderBadge = 'correct' | 'incorrect'; + +type HeaderProps = { + title?: string; + subtitle?: string; + badge?: HeaderBadge; + showBackButton?: boolean; + onPressBack?: () => void; + right?: ReactNode; +}; + +const badgeConfig = { + correct: { bg: 'bg-primary-200', text: 'text-primary-600', label: '정답' }, + incorrect: { bg: 'bg-red-100', text: 'text-red-500', label: '오답' }, +} as const; + +const Badge = ({ variant }: { variant: HeaderBadge }) => { + const config = badgeConfig[variant]; + return ( + + {config.label} + + ); +}; + +const HeaderTextButton = ({ + children, + onPress, + color = colors['gray-700'], +}: { + children: ReactNode; + onPress?: () => void; + color?: string; +}) => ( + + + {children} + + +); + +const HeaderIconButton = ({ + icon: Icon, + onPress, + color, +}: { + icon: LucideIcon; + onPress?: () => void; + color?: string; +}) => ( + + + +); + +const getRightGap = (children: ReactNode): number => { + let hasTextButton = false; + const check = (node: ReactNode) => { + React.Children.forEach(node, (child) => { + if (!React.isValidElement(child)) return; + if (child.type === HeaderTextButton) hasTextButton = true; + if (child.type === React.Fragment) check((child.props as { children?: ReactNode }).children); + }); + }; + check(children); + return hasTextButton ? 8 : 4; +}; + +const HeaderRoot = ({ + title, + subtitle, + badge, + showBackButton, + onPressBack, + right, +}: HeaderProps) => { + const navigation = useNavigation(); + + const handleBack = () => { + if (onPressBack) { + onPressBack(); + return; + } + if (navigation.canGoBack()) { + navigation.goBack(); + } + }; + + return ( + + + + {showBackButton && } + {title && ( + + {title} + {subtitle && {subtitle}} + {badge && } + + )} + + {right && ( + + {right} + + )} + + + ); +}; + +type HeaderComponent = FC & { + TextButton: typeof HeaderTextButton; + IconButton: typeof HeaderIconButton; +}; + +const Header = HeaderRoot as HeaderComponent; +Header.TextButton = HeaderTextButton; +Header.IconButton = HeaderIconButton; + +export default Header; diff --git a/apps/native/src/components/common/index.ts b/apps/native/src/components/common/index.ts index c86f30e39..b62046181 100644 --- a/apps/native/src/components/common/index.ts +++ b/apps/native/src/components/common/index.ts @@ -1,3 +1,4 @@ +import Header from './Header'; import AnimatedPressable from './AnimatedPressable'; import ContentInset from './ContentInset'; import LoadingScreen from './LoadingScreen'; @@ -7,6 +8,7 @@ import SegmentedControl from './SegmentedControl'; import { ImageWithSkeleton } from './ImageWithSkeleton'; export { + Header, AnimatedPressable, ContentInset, LoadingScreen, diff --git a/apps/native/src/components/system/icons/AlertBellButtonIcon.tsx b/apps/native/src/components/system/icons/AlertBellButtonIcon.tsx index 422992289..0c36e12f3 100644 --- a/apps/native/src/components/system/icons/AlertBellButtonIcon.tsx +++ b/apps/native/src/components/system/icons/AlertBellButtonIcon.tsx @@ -1,36 +1,36 @@ import React from 'react'; -import { Path, Circle, Svg } from 'react-native-svg'; +import { Path, Svg } from 'react-native-svg'; import type { LucideIcon, LucideProps } from 'lucide-react-native'; const AlertButtonIcon = React.forwardRef, LucideProps>( - ({ color = 'black', size = 48, strokeWidth = 2, ...rest }, ref) => { - const resolvedStrokeWidth = Number(strokeWidth); - + ({ size = 24, ...rest }, ref) => { return ( - - - - - + <> + + + + + + ); } ) as LucideIcon;