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,12 +1,27 @@
|
|||
// apps/mobile/app/(tabs)/talk.tsx
|
||||
// AI 대화 탭 — 텍스트+음성 채팅
|
||||
// Phase M-2에서 디자인(docs/v3/designs/talk.html) 정밀 적용
|
||||
// Phase M-5에서 음성 파이프라인 구현
|
||||
// AI Chat tab — text+voice chat
|
||||
// Design ref: docs/v3/designs/talk.html
|
||||
|
||||
import { useState } from 'react'
|
||||
import { View, ScrollView, TextInput, StyleSheet, Pressable, KeyboardAvoidingView, Platform } from 'react-native'
|
||||
import { useState, useRef } from 'react'
|
||||
import {
|
||||
View,
|
||||
ScrollView,
|
||||
TextInput,
|
||||
StyleSheet,
|
||||
Pressable,
|
||||
KeyboardAvoidingView,
|
||||
Platform
|
||||
} from 'react-native'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { MetalCard, PhosphorText, Led, d3roNativePalette } from '@d3ro/ui-native'
|
||||
import {
|
||||
PhosphorText,
|
||||
Led,
|
||||
Header,
|
||||
AppStatusBar,
|
||||
d3roNativePalette,
|
||||
d3roNativeFonts
|
||||
} from '@d3ro/ui-native'
|
||||
import { useI18n } from '@d3ro/i18n'
|
||||
|
||||
interface ChatMessage {
|
||||
id: string
|
||||
|
|
@ -16,11 +31,13 @@ interface ChatMessage {
|
|||
|
||||
export default function TalkScreen(): React.ReactElement {
|
||||
const insets = useSafeAreaInsets()
|
||||
const { t } = useI18n()
|
||||
const scrollRef = useRef<ScrollView>(null)
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([
|
||||
{
|
||||
id: '1',
|
||||
role: 'assistant',
|
||||
content: 'Hello! How can I help you today?'
|
||||
content: t('mobile.talk.greeting')
|
||||
}
|
||||
])
|
||||
const [input, setInput] = useState('')
|
||||
|
|
@ -34,7 +51,10 @@ export default function TalkScreen(): React.ReactElement {
|
|||
}
|
||||
setMessages((prev) => [...prev, userMsg])
|
||||
setInput('')
|
||||
// Phase M-5: 실제 AI 응답 구현
|
||||
// Phase M-5: actual AI response
|
||||
setTimeout(() => {
|
||||
scrollRef.current?.scrollToEnd({ animated: true })
|
||||
}, 100)
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -43,22 +63,33 @@ export default function TalkScreen(): React.ReactElement {
|
|||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
keyboardVerticalOffset={80}
|
||||
>
|
||||
{/* Header */}
|
||||
<View style={[styles.header, { paddingTop: insets.top + 8 }]}>
|
||||
<View style={styles.headerLeft}>
|
||||
<Led color="amber" size={8} />
|
||||
<PhosphorText variant="label" color="muted" style={{ letterSpacing: 3 }}>
|
||||
TALK
|
||||
</PhosphorText>
|
||||
</View>
|
||||
<View style={styles.headerRight}>
|
||||
<PhosphorText variant="label" color="amber">LIVE</PhosphorText>
|
||||
<Led color="green" size={6} />
|
||||
</View>
|
||||
</View>
|
||||
<Header
|
||||
title={t('mobile.talk.title')}
|
||||
paddingTop={insets.top}
|
||||
rightContent={
|
||||
<View style={styles.headerRight}>
|
||||
<PhosphorText variant="label" color="amber">{t('mobile.talk.live')}</PhosphorText>
|
||||
<Led color="green" size={6} />
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Chat Messages */}
|
||||
<ScrollView style={styles.chatArea} contentContainerStyle={styles.chatContent}>
|
||||
<ScrollView
|
||||
ref={scrollRef}
|
||||
style={styles.chatArea}
|
||||
contentContainerStyle={styles.chatContent}
|
||||
>
|
||||
{/* Date Badge */}
|
||||
<View style={styles.dateBadge}>
|
||||
<PhosphorText variant="label" color="muted">
|
||||
{new Date().toLocaleDateString(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
}).toUpperCase()}
|
||||
</PhosphorText>
|
||||
</View>
|
||||
|
||||
{messages.map((msg) => (
|
||||
<View
|
||||
key={msg.id}
|
||||
|
|
@ -70,12 +101,12 @@ export default function TalkScreen(): React.ReactElement {
|
|||
<PhosphorText
|
||||
variant="body"
|
||||
color={msg.role === 'user' ? 'amber' : 'primary'}
|
||||
style={{ fontFamily: undefined }}
|
||||
style={styles.bubbleText}
|
||||
>
|
||||
{msg.content}
|
||||
</PhosphorText>
|
||||
<PhosphorText variant="label" color="muted" style={styles.bubbleLabel}>
|
||||
{msg.role === 'user' ? 'YOU' : 'CLAUDE'}
|
||||
{msg.role === 'user' ? t('mobile.talk.you') : t('mobile.talk.claude')}
|
||||
</PhosphorText>
|
||||
</View>
|
||||
))}
|
||||
|
|
@ -86,7 +117,7 @@ export default function TalkScreen(): React.ReactElement {
|
|||
<View style={styles.inputRow}>
|
||||
<TextInput
|
||||
style={styles.textInput}
|
||||
placeholder="Type or speak..."
|
||||
placeholder={t('mobile.talk.placeholder')}
|
||||
placeholderTextColor={d3roNativePalette.text.muted}
|
||||
value={input}
|
||||
onChangeText={setInput}
|
||||
|
|
@ -97,6 +128,7 @@ export default function TalkScreen(): React.ReactElement {
|
|||
<View style={styles.sendArrow} />
|
||||
</Pressable>
|
||||
</View>
|
||||
<AppStatusBar style={styles.inputStatus} />
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
)
|
||||
|
|
@ -104,25 +136,24 @@ export default function TalkScreen(): React.ReactElement {
|
|||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: d3roNativePalette.bg.app },
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 20,
|
||||
paddingBottom: 12,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: d3roNativePalette.border.default
|
||||
},
|
||||
headerLeft: { flexDirection: 'row', alignItems: 'center', gap: 8 },
|
||||
headerRight: { flexDirection: 'row', alignItems: 'center', gap: 8 },
|
||||
chatArea: { flex: 1 },
|
||||
chatContent: { padding: 20, paddingBottom: 20, gap: 16 },
|
||||
dateBadge: {
|
||||
alignSelf: 'center',
|
||||
backgroundColor: d3roNativePalette.bg.card,
|
||||
borderWidth: 1,
|
||||
borderColor: d3roNativePalette.border.default,
|
||||
borderRadius: 999,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 4,
|
||||
marginBottom: 8
|
||||
},
|
||||
bubble: {
|
||||
maxWidth: '85%',
|
||||
padding: 16,
|
||||
borderRadius: 16,
|
||||
position: 'relative',
|
||||
marginBottom: 12
|
||||
marginBottom: 20
|
||||
},
|
||||
aiBubble: {
|
||||
alignSelf: 'flex-start',
|
||||
|
|
@ -138,17 +169,20 @@ const styles = StyleSheet.create({
|
|||
borderColor: 'rgba(255, 92, 53, 0.3)',
|
||||
borderTopRightRadius: 4
|
||||
},
|
||||
bubbleText: { fontFamily: d3roNativeFonts.sans },
|
||||
bubbleLabel: {
|
||||
position: 'absolute',
|
||||
bottom: -16,
|
||||
fontSize: 9
|
||||
fontSize: 9,
|
||||
letterSpacing: 1
|
||||
},
|
||||
inputArea: {
|
||||
backgroundColor: 'rgba(25, 25, 27, 0.95)',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: d3roNativePalette.border.default,
|
||||
padding: 12,
|
||||
paddingBottom: 24
|
||||
paddingHorizontal: 12,
|
||||
paddingTop: 12,
|
||||
paddingBottom: 8
|
||||
},
|
||||
inputRow: {
|
||||
flexDirection: 'row',
|
||||
|
|
@ -163,7 +197,8 @@ const styles = StyleSheet.create({
|
|||
flex: 1,
|
||||
color: d3roNativePalette.text.primary,
|
||||
fontSize: 14,
|
||||
paddingHorizontal: 8
|
||||
paddingHorizontal: 8,
|
||||
fontFamily: d3roNativeFonts.sans
|
||||
},
|
||||
sendBtn: {
|
||||
width: 32,
|
||||
|
|
@ -184,5 +219,6 @@ const styles = StyleSheet.create({
|
|||
borderLeftColor: 'transparent',
|
||||
borderRightColor: 'transparent',
|
||||
borderBottomColor: d3roNativePalette.accent.amber
|
||||
}
|
||||
},
|
||||
inputStatus: { paddingVertical: 4 }
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue