From fbd52adf9685896145a18ad113b16d5e9c2a2b4e Mon Sep 17 00:00:00 2001 From: sterdsterd Date: Fri, 10 Apr 2026 14:57:50 +0900 Subject: [PATCH 1/3] feat(native): add reusable Header component with badge and button support Introduced a flexible Header component for the native app, supporting title, subtitle, badges, back button, and customizable right-side content. --- apps/native/src/components/common/Header.tsx | 131 +++++++++++++++++++ apps/native/src/components/common/index.ts | 2 + 2 files changed, 133 insertions(+) create mode 100644 apps/native/src/components/common/Header.tsx diff --git a/apps/native/src/components/common/Header.tsx b/apps/native/src/components/common/Header.tsx new file mode 100644 index 000000000..8ab4f758c --- /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, From 88edacbe6c8de9dd474266be8d156fcc102b61cb Mon Sep 17 00:00:00 2001 From: sterdsterd Date: Fri, 10 Apr 2026 14:58:50 +0900 Subject: [PATCH 2/3] fix(native): update AlertBellButtonIcon to exclude padding - Adjusted icon size and viewBox to 24x24 - Removed unused Circle import - Updated stroke and fill colors for consistency --- .../system/icons/AlertBellButtonIcon.tsx | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) 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; From 6f4b394b410134f76b74dc2ba5fad5f78fac8da4 Mon Sep 17 00:00:00 2001 From: sterdsterd Date: Fri, 10 Apr 2026 15:10:06 +0900 Subject: [PATCH 3/3] fix(native): update Tailwind class names for text color --- apps/native/src/components/common/Header.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/native/src/components/common/Header.tsx b/apps/native/src/components/common/Header.tsx index 8ab4f758c..3a4935ba5 100644 --- a/apps/native/src/components/common/Header.tsx +++ b/apps/native/src/components/common/Header.tsx @@ -103,8 +103,8 @@ const HeaderRoot = ({ {showBackButton && } {title && ( - {title} - {subtitle && {subtitle}} + {title} + {subtitle && {subtitle}} {badge && } )}