d3ro-voice/apps/mobile/app/(tabs)/dash.tsx
yunchan8804 55eb62189a feat(mobile): Phase M-2 D3RO 디자인 시스템 모바일 적용
DS 컴포넌트 9개 (ScreenPanel, WaveBars, AppStatusBar, Header, FilterChip 신규),
5개 탭 + 로그인 화면 D3RO 인스트루먼트 미학 적용,
i18n 연결 (I18nProvider + 모바일 전용 키 80+개),
모노레포 workspaces에 apps/mobile 추가.
2026-04-13 17:32:31 +09:00

197 lines
6.7 KiB
TypeScript

// apps/mobile/app/(tabs)/dash.tsx
// Dashboard tab — session stats, usage, system status
// Design ref: docs/v3/designs/dashboard.html
import { View, ScrollView, StyleSheet } from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import {
MetalCard,
PhosphorText,
Led,
ScreenPanel,
AppStatusBar,
Header,
d3roNativePalette
} from '@d3ro/ui-native'
import { useI18n } from '@d3ro/i18n'
export default function DashScreen(): React.ReactElement {
const insets = useSafeAreaInsets()
const { t } = useI18n()
return (
<ScrollView
style={styles.container}
contentContainerStyle={[styles.content, { paddingBottom: insets.bottom + 100 }]}
>
<Header title={t('mobile.dash.title')} showBorder={false} paddingTop={insets.top} />
{/* Session Overview — InsetPanel */}
<ScreenPanel style={styles.overviewPanel}>
<PhosphorText variant="small" color="muted" style={styles.sectionLabel}>
{t('mobile.dash.sessionOverview')}
</PhosphorText>
<View style={styles.bigNumber}>
<PhosphorText variant="hero" color="amber">0</PhosphorText>
<PhosphorText variant="body" color="muted" style={styles.todayLabel}>
{t('mobile.dash.today')}
</PhosphorText>
</View>
<PhosphorText variant="small" color="muted" style={styles.guideText}>
{t('mobile.dash.tapToRecord')}
</PhosphorText>
<View style={styles.overviewFooter}>
<View>
<PhosphorText variant="label" color="muted">{t('mobile.dash.words')}</PhosphorText>
<PhosphorText variant="value" color="amber">0</PhosphorText>
</View>
<View style={styles.alignEnd}>
<PhosphorText variant="label" color="muted">{t('mobile.dash.streak')}</PhosphorText>
<PhosphorText variant="value" color="amber">0</PhosphorText>
</View>
</View>
</ScreenPanel>
{/* 2x2 Stats Grid */}
<View style={styles.statsGrid}>
<MetalCard style={styles.statCard}>
<PhosphorText variant="label" color="muted">{t('mobile.dash.rec')}</PhosphorText>
<PhosphorText variant="value" color="amber">0</PhosphorText>
<PhosphorText variant="label" color="muted">{t('mobile.dash.min')}</PhosphorText>
</MetalCard>
<MetalCard style={styles.statCard}>
<PhosphorText variant="label" color="muted">{t('mobile.dash.words')}</PhosphorText>
<PhosphorText variant="value" color="amber">0</PhosphorText>
</MetalCard>
<MetalCard style={styles.statCard}>
<PhosphorText variant="label" color="muted">{t('mobile.dash.today')}</PhosphorText>
<PhosphorText variant="value" color="amber">0</PhosphorText>
</MetalCard>
<MetalCard style={styles.statCard}>
<PhosphorText variant="label" color="muted">{t('mobile.dash.streak')}</PhosphorText>
<PhosphorText variant="value" color="amber">0</PhosphorText>
</MetalCard>
</View>
{/* Backend Info */}
<MetalCard style={styles.infoCard}>
<PhosphorText variant="small" color="muted">{t('mobile.dash.backend')}</PhosphorText>
<View style={styles.infoRow}>
<Led color="green" size={6} />
<PhosphorText variant="body" color="amber" style={styles.infoValue}>
CLOUD (CLAUDE)
</PhosphorText>
</View>
</MetalCard>
<MetalCard style={styles.infoCard}>
<PhosphorText variant="small" color="muted">{t('mobile.dash.tier')}</PhosphorText>
<View style={styles.infoRow}>
<Led color="amber" size={6} />
<PhosphorText variant="body" color="amber" style={styles.infoValue}>
{t('mobile.dash.free')}
</PhosphorText>
</View>
</MetalCard>
{/* Usage */}
<PhosphorText variant="label" color="muted" style={styles.usageTitle}>
{t('mobile.dash.usage')}
</PhosphorText>
<MetalCard style={styles.usageCard}>
<UsageRow label={t('mobile.dash.dictation')} value={t('mobile.dash.unlimited')} />
<View style={styles.usageDivider} />
<UsageRow label={t('mobile.dash.llmProcess')} value={t('mobile.dash.unlimited')} />
<View style={styles.usageDivider} />
<UsageRow label={t('mobile.dash.premiumQuota')} value="250 / 500" progress={0.5} />
</MetalCard>
<AppStatusBar />
</ScrollView>
)
}
function UsageRow({
label,
value,
progress
}: {
label: string
value: string
progress?: number
}): React.ReactElement {
return (
<View style={styles.usageRow}>
<PhosphorText variant="body" color="primary">{label}</PhosphorText>
<View style={styles.usageRight}>
<PhosphorText variant="small" color="amber">{value}</PhosphorText>
{progress != null && (
<View style={styles.progressBar}>
<View style={[styles.progressFill, { width: `${progress * 100}%` }]} />
</View>
)}
</View>
</View>
)
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: d3roNativePalette.bg.app },
content: { paddingHorizontal: 20 },
overviewPanel: { marginBottom: 12 },
sectionLabel: { marginBottom: 16 },
bigNumber: { flexDirection: 'row', alignItems: 'baseline', marginBottom: 8 },
todayLabel: { marginLeft: 12 },
guideText: { marginBottom: 20 },
overviewFooter: {
flexDirection: 'row',
justifyContent: 'space-between',
borderTopWidth: 1,
borderTopColor: d3roNativePalette.border.subtle,
paddingTop: 12
},
alignEnd: { alignItems: 'flex-end' },
statsGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 12,
marginBottom: 12
},
statCard: {
width: '47%' as unknown as number,
alignItems: 'center',
paddingVertical: 16,
gap: 4
},
infoCard: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12,
paddingVertical: 14
},
infoRow: { flexDirection: 'row', alignItems: 'center' },
infoValue: { marginLeft: 8 },
usageTitle: { letterSpacing: 3, marginBottom: 8, marginLeft: 4 },
usageCard: { marginBottom: 12, padding: 0, overflow: 'hidden' },
usageRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16
},
usageRight: { alignItems: 'flex-end', gap: 4 },
usageDivider: { height: 1, backgroundColor: d3roNativePalette.border.subtle },
progressBar: {
width: 80,
height: 4,
backgroundColor: d3roNativePalette.bg.inset,
borderRadius: 2,
overflow: 'hidden'
},
progressFill: {
height: '100%' as unknown as number,
backgroundColor: d3roNativePalette.accent.green,
borderRadius: 2
}
})