d3ro-voice/apps/mobile-rn/src/theme/themed-components.tsx
Yun Chan 5ca3709b32
Some checks failed
ci / 정본·보안·린트·타입·테스트 (push) Failing after 50s
ci / 워크스페이스 빌드 검증 (push) Has been skipped
ci / Supabase Edge Functions + Cloudflare Worker (push) Successful in 20s
ci / .NET API 서버 테스트 (push) Successful in 12s
deploy-site / deploy (push) Successful in 39s
ci / 모바일 린트·타입·Jest (push) Failing after 3h10m31s
feat: add mobile shell and unified search
2026-09-28 23:23:36 +09:00

203 lines
4.8 KiB
TypeScript

import {
Pressable,
Text,
View,
type PressableProps,
type StyleProp,
type TextProps,
type TextStyle,
type ViewProps,
type ViewStyle,
} from 'react-native'
import {
d3roNativeControl,
d3roNativeFonts,
d3roNativeRadius,
d3roNativeTypo,
type D3roNativePalette,
type D3roNativeTypoKey,
} from '@d3ro/ui-native'
import { useMobilePreferences } from '../lib/preferences-context'
type ThemeTextColor =
| 'primary'
| 'secondary'
| 'tertiary'
| 'muted'
| 'accent'
| 'success'
| 'danger'
| 'onAccent'
interface ThemeTextProps extends TextProps {
variant?: D3roNativeTypoKey
color?: ThemeTextColor
style?: StyleProp<TextStyle>
}
export function ThemeText({
variant = 'body',
color = 'primary',
style,
children,
...textProps
}: ThemeTextProps): React.ReactElement {
const { palette } = useMobilePreferences()
const resolvedColor: Record<ThemeTextColor, string> = {
primary: palette.text.primary,
secondary: palette.text.secondary,
tertiary: palette.text.tertiary,
muted: palette.text.tertiary,
accent: palette.accent.light,
success: palette.status?.success ?? palette.tag.green,
danger: palette.status?.danger ?? palette.tag.red,
onAccent: palette.accent.ink,
}
return (
<Text
{...textProps}
style={[
d3roNativeTypo[variant],
{
color: resolvedColor[color],
// v3: 시스템 산세리프 — 한글은 iOS Apple SD Gothic Neo /
// Android Noto Sans KR로 자동 폴백 (Menlo/monospace 폐기).
fontFamily: d3roNativeFonts.sans,
},
style,
]}
>
{children}
</Text>
)
}
interface ThemeCardProps extends ViewProps {
inset?: boolean
style?: StyleProp<ViewStyle>
}
export function ThemeCard({
inset = false,
style,
children,
...viewProps
}: ThemeCardProps): React.ReactElement {
const { palette } = useMobilePreferences()
return (
<View
{...viewProps}
style={[
{
backgroundColor: inset ? palette.bg.inset : palette.bg.card,
borderColor: inset ? palette.border.subtle : palette.border.default,
borderWidth: 1,
borderRadius: inset ? d3roNativeRadius.inner : d3roNativeRadius.card,
padding: inset ? 12 : 16,
},
style,
]}
>
{children}
</View>
)
}
interface ThemeButtonProps extends Omit<PressableProps, 'children' | 'style'> {
label: string
variant?: 'primary' | 'secondary' | 'danger' | 'quiet'
style?: StyleProp<ViewStyle>
textStyle?: StyleProp<TextStyle>
}
interface ThemeButtonTone {
background: string
pressed: string
border: string
text: ThemeTextColor
}
export function resolveThemeButtonTone(
palette: D3roNativePalette,
variant: NonNullable<ThemeButtonProps['variant']>,
): ThemeButtonTone {
switch (variant) {
case 'primary':
return {
background: palette.accent.solid,
pressed: palette.accent.solidHover,
border: palette.accent.solid,
text: 'onAccent',
}
case 'danger':
return {
background: 'transparent',
pressed: palette.status.dangerBg,
border: palette.border.strong,
text: 'danger',
}
case 'quiet':
return {
background: 'transparent',
pressed: palette.overlay.soft,
border: 'transparent',
text: 'secondary',
}
case 'secondary':
return {
background: 'transparent',
pressed: palette.overlay.soft,
border: palette.border.strong,
text: 'primary',
}
}
}
export function ThemeButton({
label,
variant = 'primary',
disabled = false,
style,
textStyle,
...pressableProps
}: ThemeButtonProps): React.ReactElement {
const { palette } = useMobilePreferences()
const tone = resolveThemeButtonTone(palette, variant)
const isDisabled = disabled === true
return (
<Pressable
{...pressableProps}
accessibilityRole={pressableProps.accessibilityRole ?? 'button'}
accessibilityState={{
...pressableProps.accessibilityState,
disabled: isDisabled,
}}
disabled={isDisabled}
style={({ pressed }) => [
{
minHeight: d3roNativeControl.h,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: d3roNativeControl.padX,
paddingVertical: 12,
borderRadius: d3roNativeRadius.button,
borderWidth: 1,
borderColor: tone.border,
backgroundColor: pressed && !isDisabled ? tone.pressed : tone.background,
opacity: isDisabled ? 0.5 : 1,
},
style,
]}
>
<ThemeText
variant="small"
color={tone.text}
style={[{ textAlign: 'center', fontWeight: '500' }, textStyle]}
>
{label}
</ThemeText>
</Pressable>
)
}