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
203 lines
4.8 KiB
TypeScript
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>
|
|
)
|
|
}
|