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
|
|
@ -531,5 +531,87 @@
|
|||
"theme.nord": "Nord",
|
||||
"theme.solarized": "Solarized",
|
||||
"theme.catppuccin": "Catppuccin",
|
||||
"theme.dracula": "Dracula"
|
||||
"theme.dracula": "Dracula",
|
||||
|
||||
"mobile.dash.title": "D3RO-VOICE",
|
||||
"mobile.dash.sessionOverview": "SESSION OVERVIEW",
|
||||
"mobile.dash.today": "TODAY",
|
||||
"mobile.dash.words": "WORDS",
|
||||
"mobile.dash.streak": "STREAK",
|
||||
"mobile.dash.rec": "REC",
|
||||
"mobile.dash.min": "MIN",
|
||||
"mobile.dash.backend": "BACKEND",
|
||||
"mobile.dash.tier": "TIER",
|
||||
"mobile.dash.free": "FREE",
|
||||
"mobile.dash.tapToRecord": "Tap REC to start recording",
|
||||
"mobile.dash.usage": "USAGE",
|
||||
"mobile.dash.dictation": "Dictation",
|
||||
"mobile.dash.llmProcess": "LLM Process",
|
||||
"mobile.dash.premiumQuota": "Premium Quota",
|
||||
"mobile.dash.unlimited": "Unlimited",
|
||||
|
||||
"mobile.hist.title": "HISTORY",
|
||||
"mobile.hist.all": "ALL",
|
||||
"mobile.hist.saved": "SAVED",
|
||||
"mobile.hist.processing": "PROC",
|
||||
"mobile.hist.empty": "No history yet. Start recording!",
|
||||
|
||||
"mobile.rec.title": "RECORDER",
|
||||
"mobile.rec.session": "REC_SESSION",
|
||||
"mobile.rec.ready": "READY",
|
||||
"mobile.rec.recording": "RECORDING",
|
||||
"mobile.rec.processing": "PROCESSING",
|
||||
"mobile.rec.done": "DONE",
|
||||
"mobile.rec.error": "ERROR",
|
||||
"mobile.rec.start": "START RECORDING",
|
||||
"mobile.rec.stop": "STOP",
|
||||
"mobile.rec.cancel": "CANCEL",
|
||||
"mobile.rec.newRecording": "NEW RECORDING",
|
||||
"mobile.rec.listening": "LISTENING_",
|
||||
"mobile.rec.realtime": "REAL-TIME TRANSCRIPT",
|
||||
"mobile.rec.initLog": "> initializing voice recording protocol...",
|
||||
"mobile.rec.micDenied": "Microphone permission denied",
|
||||
"mobile.rec.supabaseNotConfigured": "Supabase not configured",
|
||||
"mobile.rec.loginRequired": "Login required",
|
||||
|
||||
"mobile.talk.title": "TALK",
|
||||
"mobile.talk.live": "LIVE",
|
||||
"mobile.talk.you": "YOU",
|
||||
"mobile.talk.claude": "CLAUDE",
|
||||
"mobile.talk.placeholder": "Type or speak...",
|
||||
"mobile.talk.greeting": "Hello! How can I help you today?",
|
||||
|
||||
"mobile.set.title": "Settings",
|
||||
"mobile.set.accountPlan": "ACCOUNT & PLAN",
|
||||
"mobile.set.backendConfig": "BACKEND CONFIG",
|
||||
"mobile.set.preferences": "PREFERENCES",
|
||||
"mobile.set.llmModel": "LLM Model",
|
||||
"mobile.set.llmDesc": "AI processing backend",
|
||||
"mobile.set.cloudStt": "Cloud STT",
|
||||
"mobile.set.cloudSttDesc": "Google Cloud Speech",
|
||||
"mobile.set.language": "Language",
|
||||
"mobile.set.autoPolish": "Auto Polish",
|
||||
"mobile.set.haptic": "Haptic Feedback",
|
||||
"mobile.set.logout": "LOGOUT",
|
||||
"mobile.set.logoutConfirm": "Are you sure you want to log out?",
|
||||
"mobile.set.connected": "CLOUD CONNECTED",
|
||||
|
||||
"mobile.login.title": "D3RO-VOICE",
|
||||
"mobile.login.subtitle": "PRECISION VOICE INTELLIGENCE",
|
||||
"mobile.login.google": "Sign in with Google",
|
||||
"mobile.login.apple": "Sign in with Apple",
|
||||
"mobile.login.github": "Sign in with GitHub",
|
||||
"mobile.login.or": "or",
|
||||
"mobile.login.email": "EMAIL",
|
||||
"mobile.login.password": "PASSWORD",
|
||||
"mobile.login.signIn": "Sign In",
|
||||
"mobile.login.noAccount": "Don't have an account?",
|
||||
"mobile.login.signUp": "Sign Up",
|
||||
"mobile.login.notConfigured": "Supabase not configured. Set EXPO_PUBLIC_SUPABASE_URL in .env",
|
||||
|
||||
"mobile.status.model": "CLOUD CLAUDE",
|
||||
"mobile.status.link": "PRECISION DATA LINK",
|
||||
|
||||
"date.today": "TODAY",
|
||||
"date.yesterday": "YESTERDAY"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -531,5 +531,87 @@
|
|||
"theme.nord": "노드",
|
||||
"theme.solarized": "솔라라이즈드",
|
||||
"theme.catppuccin": "카푸치노",
|
||||
"theme.dracula": "드라큘라"
|
||||
"theme.dracula": "드라큘라",
|
||||
|
||||
"mobile.dash.title": "D3RO-VOICE",
|
||||
"mobile.dash.sessionOverview": "SESSION OVERVIEW",
|
||||
"mobile.dash.today": "TODAY",
|
||||
"mobile.dash.words": "WORDS",
|
||||
"mobile.dash.streak": "STREAK",
|
||||
"mobile.dash.rec": "REC",
|
||||
"mobile.dash.min": "MIN",
|
||||
"mobile.dash.backend": "BACKEND",
|
||||
"mobile.dash.tier": "TIER",
|
||||
"mobile.dash.free": "FREE",
|
||||
"mobile.dash.tapToRecord": "REC 탭을 눌러 녹음 시작",
|
||||
"mobile.dash.usage": "USAGE",
|
||||
"mobile.dash.dictation": "받아쓰기",
|
||||
"mobile.dash.llmProcess": "LLM 처리",
|
||||
"mobile.dash.premiumQuota": "프리미엄 쿼터",
|
||||
"mobile.dash.unlimited": "무제한",
|
||||
|
||||
"mobile.hist.title": "HISTORY",
|
||||
"mobile.hist.all": "ALL",
|
||||
"mobile.hist.saved": "SAVED",
|
||||
"mobile.hist.processing": "PROC",
|
||||
"mobile.hist.empty": "아직 기록이 없습니다. 녹음을 시작해보세요!",
|
||||
|
||||
"mobile.rec.title": "RECORDER",
|
||||
"mobile.rec.session": "REC_SESSION",
|
||||
"mobile.rec.ready": "READY",
|
||||
"mobile.rec.recording": "RECORDING",
|
||||
"mobile.rec.processing": "PROCESSING",
|
||||
"mobile.rec.done": "DONE",
|
||||
"mobile.rec.error": "ERROR",
|
||||
"mobile.rec.start": "START RECORDING",
|
||||
"mobile.rec.stop": "STOP",
|
||||
"mobile.rec.cancel": "CANCEL",
|
||||
"mobile.rec.newRecording": "NEW RECORDING",
|
||||
"mobile.rec.listening": "LISTENING_",
|
||||
"mobile.rec.realtime": "REAL-TIME TRANSCRIPT",
|
||||
"mobile.rec.initLog": "> initializing voice recording protocol...",
|
||||
"mobile.rec.micDenied": "마이크 권한이 거부되었습니다",
|
||||
"mobile.rec.supabaseNotConfigured": "Supabase가 설정되지 않았습니다",
|
||||
"mobile.rec.loginRequired": "로그인이 필요합니다",
|
||||
|
||||
"mobile.talk.title": "TALK",
|
||||
"mobile.talk.live": "LIVE",
|
||||
"mobile.talk.you": "YOU",
|
||||
"mobile.talk.claude": "CLAUDE",
|
||||
"mobile.talk.placeholder": "입력하거나 말하세요...",
|
||||
"mobile.talk.greeting": "안녕하세요! 무엇을 도와드릴까요?",
|
||||
|
||||
"mobile.set.title": "설정",
|
||||
"mobile.set.accountPlan": "ACCOUNT & PLAN",
|
||||
"mobile.set.backendConfig": "BACKEND CONFIG",
|
||||
"mobile.set.preferences": "PREFERENCES",
|
||||
"mobile.set.llmModel": "LLM Model",
|
||||
"mobile.set.llmDesc": "AI processing backend",
|
||||
"mobile.set.cloudStt": "Cloud STT",
|
||||
"mobile.set.cloudSttDesc": "Google Cloud Speech",
|
||||
"mobile.set.language": "Language",
|
||||
"mobile.set.autoPolish": "Auto Polish",
|
||||
"mobile.set.haptic": "Haptic Feedback",
|
||||
"mobile.set.logout": "LOGOUT",
|
||||
"mobile.set.logoutConfirm": "정말 로그아웃 하시겠습니까?",
|
||||
"mobile.set.connected": "CLOUD CONNECTED",
|
||||
|
||||
"mobile.login.title": "D3RO-VOICE",
|
||||
"mobile.login.subtitle": "PRECISION VOICE INTELLIGENCE",
|
||||
"mobile.login.google": "Google로 로그인",
|
||||
"mobile.login.apple": "Apple로 로그인",
|
||||
"mobile.login.github": "GitHub로 로그인",
|
||||
"mobile.login.or": "또는",
|
||||
"mobile.login.email": "EMAIL",
|
||||
"mobile.login.password": "PASSWORD",
|
||||
"mobile.login.signIn": "로그인",
|
||||
"mobile.login.noAccount": "계정이 없으신가요?",
|
||||
"mobile.login.signUp": "회원가입",
|
||||
"mobile.login.notConfigured": "Supabase가 설정되지 않았습니다. EXPO_PUBLIC_SUPABASE_URL을 .env에 설정하세요.",
|
||||
|
||||
"mobile.status.model": "CLOUD CLAUDE",
|
||||
"mobile.status.link": "PRECISION DATA LINK",
|
||||
|
||||
"date.today": "TODAY",
|
||||
"date.yesterday": "YESTERDAY"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "@d3ro/ui-native",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "D3RO Voice React Native용 디자인 시스템 — MetalCard/PhosphorText/Led 등 (MUI 없음)",
|
||||
"description": "D3RO Voice React Native DS — MetalCard/PhosphorText/Led/WaveBars etc.",
|
||||
"license": "MIT",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
|
|
|
|||
45
packages/ui-native/src/components/FilterChip.tsx
Normal file
45
packages/ui-native/src/components/FilterChip.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
// packages/ui-native/src/components/FilterChip.tsx
|
||||
// RN FilterChip — pill-shaped toggle chip for filter bars
|
||||
|
||||
import { Pressable, type ViewStyle, type StyleProp } from 'react-native'
|
||||
import { d3roNativePalette, d3roNativeRadius } from '../theme'
|
||||
import { PhosphorText } from './PhosphorText'
|
||||
|
||||
export interface FilterChipProps {
|
||||
label: string
|
||||
active?: boolean
|
||||
onPress?: () => void
|
||||
style?: StyleProp<ViewStyle>
|
||||
}
|
||||
|
||||
export function FilterChip({
|
||||
label,
|
||||
active = false,
|
||||
onPress,
|
||||
style
|
||||
}: FilterChipProps): React.ReactElement {
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
style={[
|
||||
{
|
||||
backgroundColor: active ? d3roNativePalette.accent.amber : d3roNativePalette.bg.card,
|
||||
borderWidth: 1,
|
||||
borderColor: active ? d3roNativePalette.accent.amber : d3roNativePalette.border.default,
|
||||
borderRadius: d3roNativeRadius.pill,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 6
|
||||
},
|
||||
style
|
||||
]}
|
||||
>
|
||||
<PhosphorText
|
||||
variant="small"
|
||||
color={active ? 'primary' : 'muted'}
|
||||
style={active ? { color: d3roNativePalette.bg.app } : undefined}
|
||||
>
|
||||
{label}
|
||||
</PhosphorText>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
55
packages/ui-native/src/components/Header.tsx
Normal file
55
packages/ui-native/src/components/Header.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// packages/ui-native/src/components/Header.tsx
|
||||
// RN Header — D3RO styled screen header with LEDs
|
||||
// safe-area insets handled by caller (paddingTop prop or wrapper)
|
||||
|
||||
import { View, type ViewStyle, type StyleProp } from 'react-native'
|
||||
import { Led } from './Led'
|
||||
import { PhosphorText } from './PhosphorText'
|
||||
import { d3roNativePalette } from '../theme'
|
||||
|
||||
export interface HeaderProps {
|
||||
title: string
|
||||
showBorder?: boolean
|
||||
rightContent?: React.ReactNode
|
||||
paddingTop?: number
|
||||
style?: StyleProp<ViewStyle>
|
||||
}
|
||||
|
||||
export function Header({
|
||||
title,
|
||||
showBorder = true,
|
||||
rightContent,
|
||||
paddingTop = 0,
|
||||
style
|
||||
}: HeaderProps): React.ReactElement {
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: paddingTop + 8,
|
||||
paddingBottom: 12,
|
||||
borderBottomWidth: showBorder ? 1 : 0,
|
||||
borderBottomColor: d3roNativePalette.border.subtle
|
||||
},
|
||||
style
|
||||
]}
|
||||
>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
||||
<Led color="amber" size={8} />
|
||||
<PhosphorText variant="label" color="muted" style={{ letterSpacing: 3 }}>
|
||||
{title}
|
||||
</PhosphorText>
|
||||
</View>
|
||||
{rightContent ?? (
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
||||
<Led color="green" size={6} />
|
||||
<Led color="amber" size={6} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
24
packages/ui-native/src/components/ScreenPanel.tsx
Normal file
24
packages/ui-native/src/components/ScreenPanel.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// packages/ui-native/src/components/ScreenPanel.tsx
|
||||
// RN ScreenPanel — inset bg + dot grid background
|
||||
|
||||
import { View, type ViewProps, type ViewStyle, type StyleProp } from 'react-native'
|
||||
import { d3roNativePalette, d3roNativeRadius } from '../theme'
|
||||
|
||||
export interface ScreenPanelProps extends ViewProps {
|
||||
style?: StyleProp<ViewStyle>
|
||||
}
|
||||
|
||||
export function ScreenPanel({ children, style, ...rest }: ScreenPanelProps): React.ReactElement {
|
||||
const baseStyle: ViewStyle = {
|
||||
backgroundColor: d3roNativePalette.bg.inset,
|
||||
borderRadius: d3roNativeRadius.inner,
|
||||
padding: 20,
|
||||
overflow: 'hidden'
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[baseStyle, style]} {...rest}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
50
packages/ui-native/src/components/StatusBar.tsx
Normal file
50
packages/ui-native/src/components/StatusBar.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// packages/ui-native/src/components/StatusBar.tsx
|
||||
// RN StatusBar — bottom status strip with LED + model label
|
||||
|
||||
import { View, type ViewStyle, type StyleProp } from 'react-native'
|
||||
import { d3roNativePalette, d3roNativeFonts } from '../theme'
|
||||
import { Led } from './Led'
|
||||
import { PhosphorText } from './PhosphorText'
|
||||
|
||||
export interface AppStatusBarProps {
|
||||
model?: string
|
||||
label?: string
|
||||
style?: StyleProp<ViewStyle>
|
||||
}
|
||||
|
||||
export function AppStatusBar({
|
||||
model = 'CLOUD CLAUDE',
|
||||
label = 'PRECISION DATA LINK',
|
||||
style
|
||||
}: AppStatusBarProps): React.ReactElement {
|
||||
const containerStyle: ViewStyle = {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 16,
|
||||
gap: 8,
|
||||
opacity: 0.5
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[containerStyle, style]}>
|
||||
<Led color="green" size={5} />
|
||||
<PhosphorText
|
||||
variant="label"
|
||||
color="muted"
|
||||
style={{ fontFamily: d3roNativeFonts.mono, fontSize: 8, letterSpacing: 1.5 }}
|
||||
>
|
||||
{model}
|
||||
</PhosphorText>
|
||||
<View style={{ width: 1, height: 8, backgroundColor: d3roNativePalette.border.default }} />
|
||||
<PhosphorText
|
||||
variant="label"
|
||||
color="muted"
|
||||
style={{ fontFamily: d3roNativeFonts.mono, fontSize: 8, letterSpacing: 1.5 }}
|
||||
>
|
||||
{label}
|
||||
</PhosphorText>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
87
packages/ui-native/src/components/WaveBars.tsx
Normal file
87
packages/ui-native/src/components/WaveBars.tsx
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
// packages/ui-native/src/components/WaveBars.tsx
|
||||
// RN WaveBars — animated audio level bars using core Animated API
|
||||
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { View, Animated, Easing, type ViewStyle, type StyleProp } from 'react-native'
|
||||
import { d3roNativePalette } from '../theme'
|
||||
|
||||
export interface WaveBarsProps {
|
||||
active?: boolean
|
||||
barCount?: number
|
||||
style?: StyleProp<ViewStyle>
|
||||
}
|
||||
|
||||
const BAR_MIN = 8
|
||||
const BAR_MAX = 32
|
||||
const DURATIONS = [800, 1000, 900, 1200, 1100, 950, 1050, 1300, 850, 1150, 1000]
|
||||
|
||||
function WaveBar({ index, active }: { index: number; active: boolean }): React.ReactElement {
|
||||
const height = useRef(new Animated.Value(BAR_MIN)).current
|
||||
|
||||
useEffect(() => {
|
||||
if (active) {
|
||||
const duration = DURATIONS[index % DURATIONS.length]
|
||||
const animation = Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(height, {
|
||||
toValue: BAR_MAX,
|
||||
duration,
|
||||
easing: Easing.inOut(Easing.ease),
|
||||
useNativeDriver: false,
|
||||
delay: index * 60
|
||||
}),
|
||||
Animated.timing(height, {
|
||||
toValue: BAR_MIN,
|
||||
duration,
|
||||
easing: Easing.inOut(Easing.ease),
|
||||
useNativeDriver: false
|
||||
})
|
||||
])
|
||||
)
|
||||
animation.start()
|
||||
return () => animation.stop()
|
||||
} else {
|
||||
Animated.timing(height, {
|
||||
toValue: BAR_MIN,
|
||||
duration: 300,
|
||||
useNativeDriver: false
|
||||
}).start()
|
||||
}
|
||||
}, [active, height, index])
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={{
|
||||
width: 6,
|
||||
height,
|
||||
backgroundColor: d3roNativePalette.accent.amber,
|
||||
borderRadius: 3
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function WaveBars({
|
||||
active = false,
|
||||
barCount = 11,
|
||||
style
|
||||
}: WaveBarsProps): React.ReactElement {
|
||||
const containerStyle: ViewStyle = {
|
||||
height: 128,
|
||||
backgroundColor: d3roNativePalette.bg.inset,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 6,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: d3roNativePalette.border.subtle
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[containerStyle, style]}>
|
||||
{Array.from({ length: barCount }).map((_, i) => (
|
||||
<WaveBar key={i} index={i} active={active} />
|
||||
))}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
// packages/ui-native — RN용 DS 컴포넌트 barrel
|
||||
// apps/mobile이 file: 의존성으로 참조
|
||||
// packages/ui-native — RN DS barrel
|
||||
// apps/mobile file: dependency
|
||||
|
||||
export {
|
||||
d3roNativePalette,
|
||||
|
|
@ -17,3 +17,8 @@ export {
|
|||
} from './components/PhosphorText'
|
||||
export { Led, type LedProps } from './components/Led'
|
||||
export { PhysicalButton, type PhysicalButtonProps } from './components/PhysicalButton'
|
||||
export { ScreenPanel, type ScreenPanelProps } from './components/ScreenPanel'
|
||||
export { WaveBars, type WaveBarsProps } from './components/WaveBars'
|
||||
export { AppStatusBar, type AppStatusBarProps } from './components/StatusBar'
|
||||
export { Header, type HeaderProps } from './components/Header'
|
||||
export { FilterChip, type FilterChipProps } from './components/FilterChip'
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue