feat(mobile): Phase M-2 D3RO 디자인 시스템 모바일 적용

DS 컴포넌트 9개 (ScreenPanel, WaveBars, AppStatusBar, Header, FilterChip 신규),
5개 탭 + 로그인 화면 D3RO 인스트루먼트 미학 적용,
i18n 연결 (I18nProvider + 모바일 전용 키 80+개),
모노레포 workspaces에 apps/mobile 추가.
This commit is contained in:
yunchan8804 2026-04-13 17:32:31 +09:00
parent 541f04d02b
commit 55eb62189a
19 changed files with 10435 additions and 1073 deletions

View file

@ -1,137 +1,156 @@
// apps/mobile/app/(tabs)/dash.tsx
// 대시보드 탭 — 세션 통계, 사용량, 시스템 상태
// Phase M-2에서 디자인(docs/v3/designs/dashboard.html) 정밀 적용
// 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, d3roNativePalette } from '@d3ro/ui-native'
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, { paddingTop: insets.top + 8, paddingBottom: insets.bottom + 100 }]}
contentContainerStyle={[styles.content, { paddingBottom: insets.bottom + 100 }]}
>
{/* Header */}
<View style={styles.header}>
<View style={styles.headerLeft}>
<Led color="amber" size={8} />
<PhosphorText variant="label" color="muted" style={styles.headerLabel}>
D3RO-VOICE
</PhosphorText>
</View>
<View style={styles.headerRight}>
<PhosphorText variant="label" color="muted">
v1.0.0
</PhosphorText>
<Led color="green" size={6} />
<Led color="amber" size={6} />
</View>
</View>
<Header title={t('mobile.dash.title')} showBorder={false} paddingTop={insets.top} />
{/* Session Overview */}
<MetalCard inset style={styles.overviewCard}>
<View style={styles.overviewHeader}>
<PhosphorText variant="small" color="muted">
SESSION OVERVIEW
</PhosphorText>
</View>
{/* 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={{ marginLeft: 12 }}>
TODAY
<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">WORDS</PhosphorText>
<PhosphorText variant="label" color="muted">{t('mobile.dash.words')}</PhosphorText>
<PhosphorText variant="value" color="amber">0</PhosphorText>
</View>
<View style={{ alignItems: 'flex-end' }}>
<PhosphorText variant="label" color="muted">STREAK</PhosphorText>
<View style={styles.alignEnd}>
<PhosphorText variant="label" color="muted">{t('mobile.dash.streak')}</PhosphorText>
<PhosphorText variant="value" color="amber">0</PhosphorText>
</View>
</View>
</MetalCard>
</ScreenPanel>
{/* 2x2 Stats Grid */}
<View style={styles.statsGrid}>
<MetalCard style={styles.statCard}>
<PhosphorText variant="label" color="muted">REC</PhosphorText>
<PhosphorText variant="label" color="muted">{t('mobile.dash.rec')}</PhosphorText>
<PhosphorText variant="value" color="amber">0</PhosphorText>
<PhosphorText variant="label" color="muted">MIN</PhosphorText>
<PhosphorText variant="label" color="muted">{t('mobile.dash.min')}</PhosphorText>
</MetalCard>
<MetalCard style={styles.statCard}>
<PhosphorText variant="label" color="muted">WORDS</PhosphorText>
<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">TODAY</PhosphorText>
<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">STREAK</PhosphorText>
<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">BACKEND</PhosphorText>
<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={{ marginLeft: 8 }}>
<PhosphorText variant="body" color="amber" style={styles.infoValue}>
CLOUD (CLAUDE)
</PhosphorText>
</View>
</MetalCard>
<MetalCard style={styles.infoCard}>
<PhosphorText variant="small" color="muted">TIER</PhosphorText>
<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={{ marginLeft: 8 }}>
FREE
<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: { padding: 20 },
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 12
},
headerLeft: { flexDirection: 'row', alignItems: 'center', gap: 8 },
headerRight: { flexDirection: 'row', alignItems: 'center', gap: 8 },
headerLabel: { letterSpacing: 3 },
overviewCard: {
padding: 20,
marginBottom: 12
},
overviewHeader: { marginBottom: 16 },
bigNumber: {
flexDirection: 'row',
alignItems: 'baseline',
marginBottom: 24
},
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: 'rgba(255,255,255,0.04)',
borderTopColor: d3roNativePalette.border.subtle,
paddingTop: 12
},
alignEnd: { alignItems: 'flex-end' },
statsGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
@ -139,7 +158,7 @@ const styles = StyleSheet.create({
marginBottom: 12
},
statCard: {
width: '47%',
width: '47%' as unknown as number,
alignItems: 'center',
paddingVertical: 16,
gap: 4
@ -151,5 +170,28 @@ const styles = StyleSheet.create({
marginBottom: 12,
paddingVertical: 14
},
infoRow: { flexDirection: 'row', alignItems: 'center' }
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
}
})