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:
parent
541f04d02b
commit
55eb62189a
19 changed files with 10435 additions and 1073 deletions
|
|
@ -1,11 +1,18 @@
|
|||
// apps/mobile/app/login.tsx
|
||||
// 로그인 화면 — 디자인: docs/v3/designs/login.html
|
||||
// Login screen — D3RO styled
|
||||
// Design ref: docs/v3/designs/login.html
|
||||
|
||||
import { useState } from 'react'
|
||||
import { View, Text, TextInput, StyleSheet, Alert, ScrollView, Pressable, Platform } from 'react-native'
|
||||
import { View, TextInput, StyleSheet, Alert, ScrollView, Pressable, Platform } from 'react-native'
|
||||
import * as WebBrowser from 'expo-web-browser'
|
||||
import * as Linking from 'expo-linking'
|
||||
import { d3roNativePalette, d3roNativeFonts, Led } from '@d3ro/ui-native'
|
||||
import {
|
||||
d3roNativePalette,
|
||||
d3roNativeFonts,
|
||||
Led,
|
||||
PhosphorText
|
||||
} from '@d3ro/ui-native'
|
||||
import { useI18n } from '@d3ro/i18n'
|
||||
import { supabase, isSupabaseConfigured } from '../lib/supabase'
|
||||
import { useAuth } from '../lib/auth-context'
|
||||
import { useRouter } from 'expo-router'
|
||||
|
|
@ -13,6 +20,7 @@ import { useRouter } from 'expo-router'
|
|||
WebBrowser.maybeCompleteAuthSession()
|
||||
|
||||
export default function LoginScreen(): React.ReactElement {
|
||||
const { t } = useI18n()
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
|
|
@ -27,7 +35,7 @@ export default function LoginScreen(): React.ReactElement {
|
|||
|
||||
async function signInWithProvider(provider: 'google' | 'github' | 'apple'): Promise<void> {
|
||||
if (!configured) {
|
||||
Alert.alert('Not Configured', 'Supabase environment variables are not set.')
|
||||
Alert.alert('Not Configured', t('mobile.login.notConfigured'))
|
||||
return
|
||||
}
|
||||
setBusy(true)
|
||||
|
|
@ -61,7 +69,7 @@ export default function LoginScreen(): React.ReactElement {
|
|||
|
||||
async function signInWithEmail(): Promise<void> {
|
||||
if (!configured) {
|
||||
Alert.alert('Not Configured', 'Supabase environment variables are not set.')
|
||||
Alert.alert('Not Configured', t('mobile.login.notConfigured'))
|
||||
return
|
||||
}
|
||||
if (!email.trim() || !password.trim()) return
|
||||
|
|
@ -76,8 +84,6 @@ export default function LoginScreen(): React.ReactElement {
|
|||
}
|
||||
}
|
||||
|
||||
const monoFont = d3roNativeFonts.mono
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
style={styles.container}
|
||||
|
|
@ -90,10 +96,12 @@ export default function LoginScreen(): React.ReactElement {
|
|||
<Led color="amber" size={10} />
|
||||
<Led color="green" size={10} />
|
||||
</View>
|
||||
<Text style={[styles.title, { fontFamily: monoFont }]}>D3RO-VOICE</Text>
|
||||
<Text style={[styles.subtitle, { fontFamily: monoFont }]}>
|
||||
PRECISION VOICE INTELLIGENCE
|
||||
</Text>
|
||||
<PhosphorText variant="title" color="primary" style={styles.titleText}>
|
||||
{t('mobile.login.title')}
|
||||
</PhosphorText>
|
||||
<PhosphorText variant="label" color="muted" style={styles.subtitleText}>
|
||||
{t('mobile.login.subtitle')}
|
||||
</PhosphorText>
|
||||
</View>
|
||||
|
||||
{/* OAuth Buttons */}
|
||||
|
|
@ -103,8 +111,10 @@ export default function LoginScreen(): React.ReactElement {
|
|||
onPress={() => void signInWithProvider('google')}
|
||||
disabled={busy}
|
||||
>
|
||||
<Text style={styles.oauthIcon}>G</Text>
|
||||
<Text style={styles.oauthLabel}>Google로 로그인</Text>
|
||||
<PhosphorText variant="body" color="primary">G</PhosphorText>
|
||||
<PhosphorText variant="body" color="primary" style={styles.oauthLabel}>
|
||||
{t('mobile.login.google')}
|
||||
</PhosphorText>
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
|
|
@ -112,8 +122,12 @@ export default function LoginScreen(): React.ReactElement {
|
|||
onPress={() => void signInWithProvider('apple')}
|
||||
disabled={busy}
|
||||
>
|
||||
<Text style={[styles.oauthIcon, { fontSize: 18 }]}></Text>
|
||||
<Text style={styles.oauthLabel}>Apple로 로그인</Text>
|
||||
<PhosphorText variant="body" color="primary" style={styles.appleIcon}>
|
||||
{'\uF8FF'}
|
||||
</PhosphorText>
|
||||
<PhosphorText variant="body" color="primary" style={styles.oauthLabel}>
|
||||
{t('mobile.login.apple')}
|
||||
</PhosphorText>
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
|
|
@ -121,22 +135,28 @@ export default function LoginScreen(): React.ReactElement {
|
|||
onPress={() => void signInWithProvider('github')}
|
||||
disabled={busy}
|
||||
>
|
||||
<Text style={styles.oauthIcon}>⌘</Text>
|
||||
<Text style={styles.oauthLabel}>GitHub로 로그인</Text>
|
||||
<PhosphorText variant="body" color="primary">{'\u2318'}</PhosphorText>
|
||||
<PhosphorText variant="body" color="primary" style={styles.oauthLabel}>
|
||||
{t('mobile.login.github')}
|
||||
</PhosphorText>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{/* Divider */}
|
||||
<View style={styles.divider}>
|
||||
<View style={styles.dividerLine} />
|
||||
<Text style={[styles.dividerText, { fontFamily: monoFont }]}>또는</Text>
|
||||
<PhosphorText variant="label" color="muted" style={styles.dividerText}>
|
||||
{t('mobile.login.or')}
|
||||
</PhosphorText>
|
||||
<View style={styles.dividerLine} />
|
||||
</View>
|
||||
|
||||
{/* Email/Password Fields */}
|
||||
<View style={styles.formSection}>
|
||||
<View style={styles.fieldGroup}>
|
||||
<Text style={[styles.fieldLabel, { fontFamily: monoFont }]}>EMAIL</Text>
|
||||
<View>
|
||||
<PhosphorText variant="label" color="muted" style={styles.fieldLabel}>
|
||||
{t('mobile.login.email')}
|
||||
</PhosphorText>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="user@studio.com"
|
||||
|
|
@ -148,11 +168,13 @@ export default function LoginScreen(): React.ReactElement {
|
|||
autoCorrect={false}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.fieldGroup}>
|
||||
<Text style={[styles.fieldLabel, { fontFamily: monoFont }]}>PASSWORD</Text>
|
||||
<View>
|
||||
<PhosphorText variant="label" color="muted" style={styles.fieldLabel}>
|
||||
{t('mobile.login.password')}
|
||||
</PhosphorText>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="••••••••"
|
||||
placeholder={'\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022'}
|
||||
placeholderTextColor="rgba(113, 113, 122, 0.3)"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
|
|
@ -167,32 +189,38 @@ export default function LoginScreen(): React.ReactElement {
|
|||
onPress={() => void signInWithEmail()}
|
||||
disabled={busy}
|
||||
>
|
||||
<Text style={styles.loginBtnText}>로그인</Text>
|
||||
<PhosphorText variant="heading" style={styles.loginBtnText}>
|
||||
{t('mobile.login.signIn')}
|
||||
</PhosphorText>
|
||||
</Pressable>
|
||||
|
||||
{/* Sign Up Link */}
|
||||
<View style={styles.signupRow}>
|
||||
<Text style={styles.signupText}>계정이 없으신가요?</Text>
|
||||
<PhosphorText variant="small" color="muted">
|
||||
{t('mobile.login.noAccount')}
|
||||
</PhosphorText>
|
||||
<Pressable>
|
||||
<Text style={styles.signupLink}>회원가입</Text>
|
||||
<PhosphorText variant="small" color="amber">
|
||||
{t('mobile.login.signUp')}
|
||||
</PhosphorText>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{/* DEV Skip (개발 전용) */}
|
||||
{/* DEV Skip */}
|
||||
{__DEV__ && (
|
||||
<Pressable style={styles.devSkipBtn} onPress={handleDevSkip}>
|
||||
<Text style={[styles.devSkipText, { fontFamily: monoFont }]}>
|
||||
⚡ DEV SKIP LOGIN
|
||||
</Text>
|
||||
<PhosphorText variant="label" style={styles.devSkipText}>
|
||||
{'\u26A1'} DEV SKIP LOGIN
|
||||
</PhosphorText>
|
||||
</Pressable>
|
||||
)}
|
||||
|
||||
{/* Supabase Warning */}
|
||||
{!configured && (
|
||||
<View style={styles.warningBox}>
|
||||
<Text style={[styles.warningText, { fontFamily: monoFont }]}>
|
||||
⚠ Supabase not configured. Set EXPO_PUBLIC_SUPABASE_URL in .env
|
||||
</Text>
|
||||
<PhosphorText variant="label" color="amber" style={styles.warningText}>
|
||||
{'\u26A0'} {t('mobile.login.notConfigured')}
|
||||
</PhosphorText>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
|
@ -202,10 +230,7 @@ export default function LoginScreen(): React.ReactElement {
|
|||
const P = d3roNativePalette
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: P.bg.app
|
||||
},
|
||||
container: { flex: 1, backgroundColor: P.bg.app },
|
||||
content: {
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
|
|
@ -213,35 +238,11 @@ const styles = StyleSheet.create({
|
|||
paddingBottom: 40,
|
||||
paddingTop: 60
|
||||
},
|
||||
|
||||
// Branding
|
||||
branding: {
|
||||
alignItems: 'center',
|
||||
marginBottom: 40
|
||||
},
|
||||
leds: {
|
||||
flexDirection: 'row',
|
||||
gap: 10,
|
||||
marginBottom: 20
|
||||
},
|
||||
title: {
|
||||
fontSize: 22,
|
||||
letterSpacing: 5,
|
||||
color: P.text.primary
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 9,
|
||||
letterSpacing: 2,
|
||||
color: P.text.muted,
|
||||
marginTop: 8,
|
||||
textTransform: 'uppercase'
|
||||
},
|
||||
|
||||
// OAuth
|
||||
oauthSection: {
|
||||
gap: 12,
|
||||
marginBottom: 24
|
||||
},
|
||||
branding: { alignItems: 'center', marginBottom: 40 },
|
||||
leds: { flexDirection: 'row', gap: 10, marginBottom: 20 },
|
||||
titleText: { letterSpacing: 5 },
|
||||
subtitleText: { marginTop: 8, letterSpacing: 2 },
|
||||
oauthSection: { gap: 12, marginBottom: 24 },
|
||||
oauthBtn: {
|
||||
backgroundColor: P.bg.card,
|
||||
borderWidth: 1,
|
||||
|
|
@ -253,51 +254,14 @@ const styles = StyleSheet.create({
|
|||
justifyContent: 'center',
|
||||
gap: 12
|
||||
},
|
||||
oauthBtnPressed: {
|
||||
backgroundColor: P.bg.cardHover,
|
||||
borderColor: 'rgba(113, 113, 122, 0.4)'
|
||||
},
|
||||
oauthIcon: {
|
||||
fontSize: 16,
|
||||
color: P.text.primary
|
||||
},
|
||||
oauthLabel: {
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
color: P.text.primary
|
||||
},
|
||||
|
||||
// Divider
|
||||
divider: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 16,
|
||||
marginBottom: 24
|
||||
},
|
||||
dividerLine: {
|
||||
flex: 1,
|
||||
height: 1,
|
||||
backgroundColor: P.border.default
|
||||
},
|
||||
dividerText: {
|
||||
fontSize: 10,
|
||||
color: P.text.muted,
|
||||
letterSpacing: 2
|
||||
},
|
||||
|
||||
// Form
|
||||
formSection: {
|
||||
gap: 16,
|
||||
marginBottom: 24
|
||||
},
|
||||
fieldGroup: {},
|
||||
fieldLabel: {
|
||||
fontSize: 10,
|
||||
color: P.text.muted,
|
||||
letterSpacing: 4,
|
||||
marginBottom: 6,
|
||||
marginLeft: 4
|
||||
},
|
||||
oauthBtnPressed: { backgroundColor: P.bg.cardHover, borderColor: 'rgba(113,113,122,0.4)' },
|
||||
oauthLabel: { fontFamily: d3roNativeFonts.sans },
|
||||
appleIcon: { fontSize: 18 },
|
||||
divider: { flexDirection: 'row', alignItems: 'center', gap: 16, marginBottom: 24 },
|
||||
dividerLine: { flex: 1, height: 1, backgroundColor: P.border.default },
|
||||
dividerText: { letterSpacing: 2 },
|
||||
formSection: { gap: 16, marginBottom: 24 },
|
||||
fieldLabel: { letterSpacing: 4, marginBottom: 6, marginLeft: 4 },
|
||||
input: {
|
||||
backgroundColor: P.bg.inset,
|
||||
borderWidth: 1,
|
||||
|
|
@ -309,8 +273,6 @@ const styles = StyleSheet.create({
|
|||
color: P.text.primary,
|
||||
fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto'
|
||||
},
|
||||
|
||||
// Login Button
|
||||
loginBtn: {
|
||||
backgroundColor: P.accent.amber,
|
||||
borderRadius: 12,
|
||||
|
|
@ -323,14 +285,7 @@ const styles = StyleSheet.create({
|
|||
elevation: 6,
|
||||
marginBottom: 24
|
||||
},
|
||||
loginBtnText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: P.bg.app,
|
||||
letterSpacing: 1
|
||||
},
|
||||
|
||||
// Sign Up
|
||||
loginBtnText: { color: P.bg.app, letterSpacing: 1 },
|
||||
signupRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
|
|
@ -338,32 +293,15 @@ const styles = StyleSheet.create({
|
|||
gap: 6,
|
||||
marginBottom: 16
|
||||
},
|
||||
signupText: {
|
||||
fontSize: 11,
|
||||
color: P.text.muted
|
||||
},
|
||||
signupLink: {
|
||||
fontSize: 11,
|
||||
color: P.accent.amber,
|
||||
fontWeight: '500'
|
||||
},
|
||||
|
||||
// Warning
|
||||
warningBox: {
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255, 92, 53, 0.3)',
|
||||
borderColor: 'rgba(255,92,53,0.3)',
|
||||
backgroundColor: P.accent.amberDim,
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
marginTop: 8
|
||||
},
|
||||
warningText: {
|
||||
fontSize: 10,
|
||||
color: P.accent.amber,
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
// DEV Skip
|
||||
warningText: { textAlign: 'center' },
|
||||
devSkipBtn: {
|
||||
borderWidth: 1,
|
||||
borderColor: P.accent.green,
|
||||
|
|
@ -373,9 +311,5 @@ const styles = StyleSheet.create({
|
|||
marginBottom: 12,
|
||||
backgroundColor: 'rgba(74, 222, 128, 0.08)'
|
||||
},
|
||||
devSkipText: {
|
||||
fontSize: 10,
|
||||
color: P.accent.green,
|
||||
letterSpacing: 2
|
||||
}
|
||||
devSkipText: { color: P.accent.green, letterSpacing: 2 }
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue