- packages/ui-native theme.ts accent: amber->main, amberDim->dim, amberGlow->glow (값 보존)
- apps/mobile 20건 + apps/mobile-rn 18건 + ui-native 컴포넌트 9건 = 47 참조 치환
SKIP: Led/PhosphorText/Header color prop 'amber'(의미론적 컴포넌트 API, 별도 마이그레이션)
green/greenGlow 키(amber계 범위外)
정책: docs/REFACTOR_POLICY.md DP1, 이식 인사이트 P7
203 lines
5.1 KiB
TypeScript
203 lines
5.1 KiB
TypeScript
// apps/mobile/app/(tabs)/_layout.tsx
|
|
// 5탭 네비게이션: DASH / HIST / REC(FAB) / TALK / SET
|
|
|
|
import { useEffect } from 'react'
|
|
import { View, StyleSheet, Pressable, Platform } from 'react-native'
|
|
import { Tabs, useRouter } from 'expo-router'
|
|
import { useAuth } from '../../lib/auth-context'
|
|
import { d3roNativePalette } from '@d3ro/ui-native'
|
|
|
|
const MONO_FONT = Platform.OS === 'ios' ? 'Menlo' : 'monospace'
|
|
|
|
function RecordFAB({ onPress }: { onPress: () => void }): React.ReactElement {
|
|
return (
|
|
<Pressable onPress={onPress} style={styles.fab}>
|
|
<View style={styles.fabInner}>
|
|
<View style={styles.micIcon}>
|
|
<View style={styles.micBody} />
|
|
<View style={styles.micBase} />
|
|
</View>
|
|
</View>
|
|
</Pressable>
|
|
)
|
|
}
|
|
|
|
export default function TabsLayout(): React.ReactElement {
|
|
const router = useRouter()
|
|
const { user, loading } = useAuth()
|
|
|
|
useEffect(() => {
|
|
if (!loading && !user) {
|
|
router.replace('/login')
|
|
}
|
|
}, [user, loading, router])
|
|
|
|
return (
|
|
<Tabs
|
|
screenOptions={{
|
|
tabBarStyle: {
|
|
backgroundColor: 'rgba(25, 25, 27, 0.95)',
|
|
borderTopColor: d3roNativePalette.border.default,
|
|
borderTopWidth: 1,
|
|
height: 80,
|
|
paddingBottom: 20,
|
|
paddingTop: 8
|
|
},
|
|
tabBarActiveTintColor: d3roNativePalette.accent.main,
|
|
tabBarInactiveTintColor: d3roNativePalette.text.muted,
|
|
tabBarLabelStyle: {
|
|
fontFamily: MONO_FONT,
|
|
fontSize: 9,
|
|
fontWeight: '500',
|
|
letterSpacing: 0.5
|
|
},
|
|
headerStyle: { backgroundColor: d3roNativePalette.bg.app },
|
|
headerTintColor: d3roNativePalette.accent.main,
|
|
headerTitleStyle: { fontWeight: '300', letterSpacing: 1, fontFamily: MONO_FONT },
|
|
headerShown: false
|
|
}}
|
|
>
|
|
<Tabs.Screen
|
|
name="dash"
|
|
options={{
|
|
title: 'DASH',
|
|
tabBarIcon: ({ color }: { color: string }) => <TabIcon type="dash" color={color} />
|
|
}}
|
|
/>
|
|
<Tabs.Screen
|
|
name="history"
|
|
options={{
|
|
title: 'HIST',
|
|
tabBarIcon: ({ color }: { color: string }) => <TabIcon type="hist" color={color} />
|
|
}}
|
|
/>
|
|
<Tabs.Screen
|
|
name="record"
|
|
options={{
|
|
title: '',
|
|
tabBarIcon: () => <RecordFAB onPress={() => router.push('/(tabs)/record')} />,
|
|
tabBarLabel: () => null
|
|
}}
|
|
/>
|
|
<Tabs.Screen
|
|
name="talk"
|
|
options={{
|
|
title: 'TALK',
|
|
tabBarIcon: ({ color }: { color: string }) => <TabIcon type="talk" color={color} />
|
|
}}
|
|
/>
|
|
<Tabs.Screen
|
|
name="settings"
|
|
options={{
|
|
title: 'SET',
|
|
tabBarIcon: ({ color }: { color: string }) => <TabIcon type="set" color={color} />
|
|
}}
|
|
/>
|
|
</Tabs>
|
|
)
|
|
}
|
|
|
|
// 단순 아이콘 대용 (Phase M-2에서 SVG 아이콘으로 교체)
|
|
function TabIcon({ type, color }: { type: string; color: string }): React.ReactElement {
|
|
const iconStyles: Record<string, React.ReactElement> = {
|
|
dash: (
|
|
<View style={[styles.iconGrid, { borderColor: color }]}>
|
|
{[0, 1, 2, 3].map((i) => (
|
|
<View key={i} style={[styles.iconGridCell, { borderColor: color }]} />
|
|
))}
|
|
</View>
|
|
),
|
|
hist: (
|
|
<View style={[styles.iconCircle, { borderColor: color }]}>
|
|
<View style={[styles.iconClockHand, { backgroundColor: color }]} />
|
|
</View>
|
|
),
|
|
talk: (
|
|
<View style={[styles.iconChat, { borderColor: color }]} />
|
|
),
|
|
set: (
|
|
<View style={[styles.iconGear, { borderColor: color }]} />
|
|
)
|
|
}
|
|
return iconStyles[type] ?? <View />
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
fab: {
|
|
position: 'relative',
|
|
top: -20,
|
|
width: 56,
|
|
height: 56,
|
|
borderRadius: 28,
|
|
backgroundColor: d3roNativePalette.accent.main,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
shadowColor: d3roNativePalette.accent.main,
|
|
shadowOffset: { width: 0, height: 0 },
|
|
shadowOpacity: 0.4,
|
|
shadowRadius: 10,
|
|
elevation: 8,
|
|
borderWidth: 4,
|
|
borderColor: d3roNativePalette.bg.app
|
|
},
|
|
fabInner: {
|
|
justifyContent: 'center',
|
|
alignItems: 'center'
|
|
},
|
|
micIcon: {
|
|
alignItems: 'center'
|
|
},
|
|
micBody: {
|
|
width: 8,
|
|
height: 14,
|
|
borderRadius: 4,
|
|
backgroundColor: d3roNativePalette.bg.app,
|
|
marginBottom: 2
|
|
},
|
|
micBase: {
|
|
width: 14,
|
|
height: 2,
|
|
borderRadius: 1,
|
|
backgroundColor: d3roNativePalette.bg.app
|
|
},
|
|
// Tab icons (placeholder — Phase M-2에서 SVG로 교체)
|
|
iconGrid: {
|
|
width: 22,
|
|
height: 22,
|
|
flexDirection: 'row',
|
|
flexWrap: 'wrap',
|
|
gap: 2
|
|
},
|
|
iconGridCell: {
|
|
width: 9,
|
|
height: 9,
|
|
borderWidth: 1.5,
|
|
borderRadius: 2
|
|
},
|
|
iconCircle: {
|
|
width: 22,
|
|
height: 22,
|
|
borderRadius: 11,
|
|
borderWidth: 1.5,
|
|
justifyContent: 'center',
|
|
alignItems: 'center'
|
|
},
|
|
iconClockHand: {
|
|
width: 1.5,
|
|
height: 7,
|
|
position: 'absolute',
|
|
top: 3
|
|
},
|
|
iconChat: {
|
|
width: 22,
|
|
height: 18,
|
|
borderWidth: 1.5,
|
|
borderRadius: 4
|
|
},
|
|
iconGear: {
|
|
width: 22,
|
|
height: 22,
|
|
borderWidth: 1.5,
|
|
borderRadius: 11
|
|
}
|
|
})
|