132 lines
5.6 KiB
TypeScript
132 lines
5.6 KiB
TypeScript
import { type ReactNode } from 'react'
|
|
import {
|
|
View, Text, TouchableOpacity, StyleSheet, ActivityIndicator, TextInput,
|
|
type StyleProp, type ViewStyle, type TextStyle,
|
|
} from 'react-native'
|
|
import { Feather } from '@expo/vector-icons'
|
|
import { useTheme } from '../constants/ThemeContext'
|
|
import { statusColor, type ThemeTokens } from '../constants/ItmsTheme'
|
|
|
|
/** 카드 컨테이너. */
|
|
export function Card({ children, style }: { children: ReactNode; style?: StyleProp<ViewStyle> }) {
|
|
const { t } = useTheme()
|
|
return (
|
|
<View style={[{ backgroundColor: t.card, borderRadius: 14, borderWidth: 1, borderColor: t.border, padding: 16 }, style]}>
|
|
{children}
|
|
</View>
|
|
)
|
|
}
|
|
|
|
/** 진행상태 배지(라벨 부분일치 색). */
|
|
export function StatusPill({ label }: { label?: string | null }) {
|
|
const { t } = useTheme()
|
|
const text = (label ?? '').toString() || '-'
|
|
const color = statusColor(label, t)
|
|
return (
|
|
<View style={{ flexDirection: 'row', alignItems: 'center', alignSelf: 'flex-start', gap: 5, backgroundColor: color + '22', borderRadius: 8, paddingHorizontal: 8, paddingVertical: 3 }}>
|
|
<View style={{ width: 7, height: 7, borderRadius: 4, backgroundColor: color }} />
|
|
<Text style={{ color, fontSize: 11, fontWeight: '800' }}>{text}</Text>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
/** 기본 버튼(primary/ghost/danger/outline). */
|
|
export function Btn({
|
|
label, onPress, kind = 'primary', busy, disabled, icon, style,
|
|
}: {
|
|
label: string; onPress: () => void; kind?: 'primary' | 'ghost' | 'danger' | 'outline'
|
|
busy?: boolean; disabled?: boolean; icon?: keyof typeof Feather.glyphMap; style?: StyleProp<ViewStyle>
|
|
}) {
|
|
const { t } = useTheme()
|
|
const bg = kind === 'primary' ? t.brand : kind === 'danger' ? t.danger : 'transparent'
|
|
const fg = kind === 'ghost' || kind === 'outline' ? t.text : t.onBrand
|
|
const border = kind === 'outline' ? t.border : 'transparent'
|
|
return (
|
|
<TouchableOpacity
|
|
onPress={onPress}
|
|
disabled={busy || disabled}
|
|
style={[{ flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 7, backgroundColor: bg, borderColor: border, borderWidth: kind === 'outline' ? 1 : 0, borderRadius: 12, paddingVertical: 13, paddingHorizontal: 16, opacity: busy || disabled ? 0.5 : 1 }, style]}
|
|
>
|
|
{busy ? <ActivityIndicator color={fg} /> : (
|
|
<>
|
|
{icon && <Feather name={icon} size={16} color={fg} />}
|
|
<Text style={{ color: fg, fontSize: 15, fontWeight: '800' }}>{label}</Text>
|
|
</>
|
|
)}
|
|
</TouchableOpacity>
|
|
)
|
|
}
|
|
|
|
/** 라벨 + 입력 필드. */
|
|
export function Field({
|
|
label, value, onChangeText, placeholder, keyboardType, multiline, secureTextEntry, autoCapitalize,
|
|
}: {
|
|
label?: string; value: string; onChangeText: (s: string) => void; placeholder?: string
|
|
keyboardType?: 'default' | 'number-pad' | 'email-address'; multiline?: boolean; secureTextEntry?: boolean
|
|
autoCapitalize?: 'none' | 'sentences'
|
|
}) {
|
|
const { t } = useTheme()
|
|
return (
|
|
<View style={{ marginTop: 12 }}>
|
|
{!!label && <Text style={{ color: t.muted, fontSize: 12, marginBottom: 6 }}>{label}</Text>}
|
|
<TextInput
|
|
value={value} onChangeText={onChangeText} placeholder={placeholder} placeholderTextColor={t.muted}
|
|
keyboardType={keyboardType} multiline={multiline} secureTextEntry={secureTextEntry} autoCapitalize={autoCapitalize}
|
|
autoCorrect={false}
|
|
style={{ backgroundColor: t.cardAlt, borderRadius: 10, borderWidth: 1, borderColor: t.border, color: t.text, padding: 12, fontSize: 15, minHeight: multiline ? 96 : undefined, textAlignVertical: multiline ? 'top' : 'center' }}
|
|
/>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
/** 빈 상태 안내(서버 미기동/무결과 공통). */
|
|
export function EmptyState({ icon = 'inbox', text }: { icon?: keyof typeof Feather.glyphMap; text: string }) {
|
|
const { t } = useTheme()
|
|
return (
|
|
<View style={{ alignItems: 'center', paddingVertical: 48 }}>
|
|
<Feather name={icon} size={40} color={t.muted} />
|
|
<Text style={{ color: t.muted, fontSize: 14, marginTop: 12, textAlign: 'center', paddingHorizontal: 24 }}>{text}</Text>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
/** 전체 화면 로딩. */
|
|
export function Loading() {
|
|
const { t } = useTheme()
|
|
return (
|
|
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: t.bg }}>
|
|
<ActivityIndicator color={t.accent} size="large" />
|
|
</View>
|
|
)
|
|
}
|
|
|
|
/** 라벨:값 한 줄. */
|
|
export function Row({ label, value }: { label: string; value?: ReactNode }) {
|
|
const { t } = useTheme()
|
|
return (
|
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 8, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: t.border }}>
|
|
<Text style={{ color: t.muted, fontSize: 13 }}>{label}</Text>
|
|
<View style={{ flexShrink: 1, marginLeft: 12 }}>
|
|
{typeof value === 'string' || typeof value === 'number'
|
|
? <Text style={{ color: t.text, fontSize: 14, fontWeight: '600', textAlign: 'right' }}>{value || '-'}</Text>
|
|
: (value ?? <Text style={{ color: t.text }}>-</Text>)}
|
|
</View>
|
|
</View>
|
|
)
|
|
}
|
|
|
|
/** 섹션 제목. */
|
|
export function SectionTitle({ children, style }: { children: ReactNode; style?: StyleProp<TextStyle> }) {
|
|
const { t } = useTheme()
|
|
return <Text style={[{ color: t.text, fontSize: 15, fontWeight: '900', marginBottom: 10 }, style]}>{children}</Text>
|
|
}
|
|
|
|
/** 인라인 에러 텍스트. */
|
|
export function ErrText({ msg }: { msg?: string }) {
|
|
const { t } = useTheme()
|
|
if (!msg) return null
|
|
return <Text style={{ color: t.danger, fontSize: 12, marginTop: 10 }}>{msg}</Text>
|
|
}
|
|
|
|
export type { ThemeTokens }
|