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

@ -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",

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View file

@ -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'