feat: add mobile shell and unified search
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
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
This commit is contained in:
parent
f505a03d34
commit
5ca3709b32
98 changed files with 5168 additions and 2093 deletions
|
|
@ -31,9 +31,9 @@ export function FilterChip({
|
|||
minHeight: 48,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: active ? palette.accent.main : palette.bg.card,
|
||||
backgroundColor: active ? palette.accent.solid : palette.bg.card,
|
||||
borderWidth: 1,
|
||||
borderColor: active ? palette.accent.main : palette.border.default,
|
||||
borderColor: active ? palette.accent.solid : palette.border.default,
|
||||
borderRadius: d3roNativeRadius.pill,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 6
|
||||
|
|
@ -43,8 +43,8 @@ export function FilterChip({
|
|||
>
|
||||
<PhosphorText
|
||||
variant="small"
|
||||
color={active ? 'primary' : 'muted'}
|
||||
style={active ? { color: palette.text.onAccent ?? palette.bg.app } : undefined}
|
||||
color={active ? 'primary' : 'tertiary'}
|
||||
style={active ? { color: palette.accent.ink } : undefined}
|
||||
>
|
||||
{label}
|
||||
</PhosphorText>
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
// 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 { useNativePalette } from '../theme-context'
|
||||
|
||||
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 {
|
||||
const palette = useNativePalette()
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: paddingTop + 8,
|
||||
paddingBottom: 12,
|
||||
borderBottomWidth: showBorder ? 1 : 0,
|
||||
borderBottomColor: palette.border.subtle
|
||||
},
|
||||
style
|
||||
]}
|
||||
>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
||||
<Led color="amber" size={8} />
|
||||
<PhosphorText
|
||||
accessibilityRole="header"
|
||||
variant="label"
|
||||
color="muted"
|
||||
>
|
||||
{title}
|
||||
</PhosphorText>
|
||||
</View>
|
||||
{rightContent ?? (
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
||||
<Led color="green" size={6} />
|
||||
<Led color="amber" size={6} />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,35 +1,37 @@
|
|||
// packages/ui-native/src/components/Led.tsx
|
||||
// RN용 Led — 작은 발광 원
|
||||
// v4: 상태 점. 번짐(글로우) 없음. 색만으로 상태를 말하지 않는다 — 옆에 글자를 둔다.
|
||||
|
||||
import { View, type ViewStyle } from 'react-native'
|
||||
import { useNativePalette } from '../theme-context'
|
||||
|
||||
export interface LedProps {
|
||||
color?: 'amber' | 'green' | 'red' | 'orange' | 'blue'
|
||||
/** 'amber' 는 v1 호환 별칭 — 현재 테마의 액센트 */
|
||||
color?: 'amber' | 'green' | 'red' | 'orange' | 'blue' | 'off'
|
||||
on?: boolean
|
||||
size?: number
|
||||
}
|
||||
|
||||
export function Led({ color = 'amber', on = true, size = 8 }: LedProps): React.ReactElement {
|
||||
export function Led({ color = 'off', on = true, size = 8 }: LedProps): React.ReactElement {
|
||||
const palette = useNativePalette()
|
||||
const colorMap: Record<NonNullable<LedProps['color']>, string> = {
|
||||
amber: palette.accent.main,
|
||||
green: palette.tag.green,
|
||||
red: palette.tag.red,
|
||||
orange: palette.tag.orange,
|
||||
blue: palette.tag.blue
|
||||
green: palette.status.success,
|
||||
red: palette.status.danger,
|
||||
orange: palette.status.warning,
|
||||
blue: palette.status.info,
|
||||
off: palette.led.off
|
||||
}
|
||||
const c = colorMap[color]
|
||||
const style: ViewStyle = {
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: size / 2,
|
||||
backgroundColor: on ? c : palette.led.off,
|
||||
shadowColor: c,
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: on ? 0.8 : 0,
|
||||
shadowRadius: on ? 4 : 0,
|
||||
elevation: on ? 2 : 0
|
||||
backgroundColor: on ? colorMap[color] : palette.led.off
|
||||
}
|
||||
return <View style={style} />
|
||||
return (
|
||||
<View
|
||||
style={style}
|
||||
accessibilityElementsHidden
|
||||
importantForAccessibility="no-hide-descendants"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// packages/ui-native/src/components/MetalCard.tsx
|
||||
// RN용 MetalCard — View + 섀시 스타일
|
||||
// v4: 평평한 카드 — 표면 한 단 + 1px 선. 그림자·광택 없음. (데스크톱 MetalCard 와 같은 방식)
|
||||
// inset: 카드 안의 한 단 들어간 영역 (카드 안의 카드가 아니라 우묵한 판).
|
||||
|
||||
import { View, type ViewProps, type ViewStyle, type StyleProp } from 'react-native'
|
||||
import { d3roNativeRadius } from '../theme'
|
||||
|
|
@ -15,16 +16,10 @@ export function MetalCard({ children, inset = false, style, ...rest }: MetalCard
|
|||
const baseStyle: ViewStyle = {
|
||||
backgroundColor: inset ? palette.bg.inset : palette.bg.card,
|
||||
borderRadius: inset ? d3roNativeRadius.inner : d3roNativeRadius.card,
|
||||
padding: inset ? 6 : 16,
|
||||
borderWidth: inset ? 0 : 1,
|
||||
borderColor: palette.border.subtle,
|
||||
overflow: 'hidden',
|
||||
// RN 그림자
|
||||
shadowColor: palette.shadow,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: inset ? 0 : 0.25,
|
||||
shadowRadius: inset ? 0 : 8,
|
||||
elevation: inset ? 0 : 4
|
||||
borderWidth: 1,
|
||||
borderColor: inset ? palette.border.subtle : palette.border.default,
|
||||
padding: inset ? 12 : 16,
|
||||
overflow: 'hidden'
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,42 +1,59 @@
|
|||
// packages/ui-native/src/components/PhosphorText.tsx
|
||||
// RN용 PhosphorText — v3: 시스템 산세리프 기반 (한글은 Apple SD Gothic Neo /
|
||||
// Noto Sans KR 자동 폴백). 텍스트 글로우는 제거하고 명도로 위계를 만든다.
|
||||
// RN 텍스트 — v4: 시스템 산세리프(한글은 Apple SD Gothic Neo / Noto Sans KR 자동 폴백).
|
||||
// 글로우 없음, 대문자 변환 없음. 위계는 크기(variant)와 명도(color 3단)로 만든다.
|
||||
// 기본 색은 모든 변형에서 text.primary — 큰 숫자·제목을 액센트로 칠하지 않는다.
|
||||
|
||||
import { Text, type TextProps, type TextStyle, type StyleProp } from 'react-native'
|
||||
import { d3roNativeFonts, d3roNativeTypo, type D3roNativeTypoKey } from '../theme'
|
||||
import { d3roNativeFonts, d3roNativeTypo, type D3roNativePalette, type D3roNativeTypoKey } from '../theme'
|
||||
import { useNativePalette } from '../theme-context'
|
||||
|
||||
export type PhosphorVariant = D3roNativeTypoKey
|
||||
|
||||
type PhosphorColor =
|
||||
| 'primary'
|
||||
| 'secondary'
|
||||
| 'tertiary'
|
||||
| 'accent'
|
||||
/** @deprecated v4: 'accent' 별칭 */
|
||||
| 'amber'
|
||||
/** @deprecated v4: 'tertiary' 별칭 */
|
||||
| 'label'
|
||||
/** @deprecated v4: 'tertiary' 별칭 */
|
||||
| 'muted'
|
||||
|
||||
export interface PhosphorTextProps extends TextProps {
|
||||
variant?: PhosphorVariant
|
||||
color?: 'amber' | 'primary' | 'secondary' | 'label' | 'muted'
|
||||
color?: PhosphorColor
|
||||
style?: StyleProp<TextStyle>
|
||||
}
|
||||
|
||||
function resolveColor(palette: D3roNativePalette, color: PhosphorColor): string {
|
||||
switch (color) {
|
||||
case 'accent':
|
||||
case 'amber':
|
||||
return palette.accent.light
|
||||
case 'secondary':
|
||||
return palette.text.secondary
|
||||
case 'tertiary':
|
||||
case 'label':
|
||||
case 'muted':
|
||||
return palette.text.tertiary
|
||||
case 'primary':
|
||||
return palette.text.primary
|
||||
}
|
||||
}
|
||||
|
||||
export function PhosphorText({
|
||||
children,
|
||||
variant = 'body',
|
||||
color,
|
||||
color = 'primary',
|
||||
style,
|
||||
...rest
|
||||
}: PhosphorTextProps): React.ReactElement {
|
||||
const palette = useNativePalette()
|
||||
const typo = d3roNativeTypo[variant]
|
||||
const isAmberVariant = variant === 'hero' || variant === 'title' || variant === 'value'
|
||||
|
||||
const resolvedColor: string = (() => {
|
||||
if (color === 'amber') return palette.accent.main
|
||||
if (color === 'primary') return palette.text.primary
|
||||
if (color === 'secondary') return palette.text.secondary
|
||||
if (color === 'label') return palette.text.label
|
||||
if (color === 'muted') return palette.text.muted
|
||||
return isAmberVariant ? palette.accent.main : palette.text.primary
|
||||
})()
|
||||
|
||||
const baseStyle: TextStyle = {
|
||||
...typo,
|
||||
color: resolvedColor,
|
||||
...d3roNativeTypo[variant],
|
||||
color: resolveColor(palette, color),
|
||||
fontFamily: d3roNativeFonts.sans
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
// packages/ui-native/src/components/PhysicalButton.tsx
|
||||
// RN용 PhysicalButton — Pressable 기반 누르는 느낌
|
||||
// v4 "조용한 계기": 버튼 한 종류, 톤 넷 — 데스크톱 PhysicalButton 과 같은 방식.
|
||||
// accent = 채움(accent.solid + accent.ink) / glass = 1px 선 / danger = 선 + 위험색 글자 / ghost = 글자만.
|
||||
// 채움은 accent 하나뿐. 눌림은 색 변화뿐 — 떠오르거나 가라앉지 않는다. 굵기 500.
|
||||
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Pressable,
|
||||
Text,
|
||||
|
|
@ -10,60 +11,60 @@ import {
|
|||
type TextStyle,
|
||||
type StyleProp
|
||||
} from 'react-native'
|
||||
import { d3roNativeRadius, d3roNativeTypo } from '../theme'
|
||||
import { d3roNativeControl, d3roNativeFonts, d3roNativeRadius, d3roNativeTypo, type D3roNativePalette } from '../theme'
|
||||
import { useNativePalette } from '../theme-context'
|
||||
|
||||
export type PhysicalButtonTone = 'accent' | 'glass' | 'danger' | 'ghost'
|
||||
|
||||
export interface PhysicalButtonProps extends Omit<PressableProps, 'children' | 'style'> {
|
||||
label: string
|
||||
/** 시각 톤. 지정하면 variant 보다 우선한다. */
|
||||
tone?: PhysicalButtonTone
|
||||
/** @deprecated v4: tone 을 쓴다. primary=accent, secondary=glass, danger=danger */
|
||||
variant?: 'primary' | 'secondary' | 'danger'
|
||||
disabled?: boolean
|
||||
style?: StyleProp<ViewStyle>
|
||||
textStyle?: StyleProp<TextStyle>
|
||||
}
|
||||
|
||||
interface ToneStyle {
|
||||
bg: string
|
||||
bgPressed: string
|
||||
color: string
|
||||
border: string
|
||||
}
|
||||
|
||||
export function physicalButtonTone(palette: D3roNativePalette, tone: PhysicalButtonTone): ToneStyle {
|
||||
switch (tone) {
|
||||
case 'accent':
|
||||
return { bg: palette.accent.solid, bgPressed: palette.accent.solidHover, color: palette.accent.ink, border: palette.accent.solid }
|
||||
case 'danger':
|
||||
return { bg: 'transparent', bgPressed: palette.status.dangerBg, color: palette.status.danger, border: palette.border.strong }
|
||||
case 'ghost':
|
||||
return { bg: 'transparent', bgPressed: palette.overlay.soft, color: palette.text.secondary, border: 'transparent' }
|
||||
case 'glass':
|
||||
return { bg: 'transparent', bgPressed: palette.overlay.soft, color: palette.text.primary, border: palette.border.strong }
|
||||
}
|
||||
}
|
||||
|
||||
const VARIANT_TONE: Record<NonNullable<PhysicalButtonProps['variant']>, PhysicalButtonTone> = {
|
||||
primary: 'accent',
|
||||
secondary: 'glass',
|
||||
danger: 'danger'
|
||||
}
|
||||
|
||||
export function PhysicalButton({
|
||||
label,
|
||||
tone,
|
||||
variant = 'primary',
|
||||
disabled = false,
|
||||
style,
|
||||
textStyle,
|
||||
onPress,
|
||||
...rest
|
||||
}: PhysicalButtonProps): React.ReactElement {
|
||||
const palette = useNativePalette()
|
||||
const [pressed, setPressed] = useState(false)
|
||||
|
||||
const bgColor =
|
||||
variant === 'primary'
|
||||
? palette.accent.main
|
||||
: variant === 'danger'
|
||||
? palette.tag.red
|
||||
: 'transparent'
|
||||
|
||||
const borderColor =
|
||||
variant === 'secondary' ? palette.border.strong : 'transparent'
|
||||
|
||||
const textColor =
|
||||
variant === 'primary' || variant === 'danger'
|
||||
? palette.text.onAccent ?? palette.text.primary
|
||||
: palette.text.secondary
|
||||
|
||||
const containerStyle: ViewStyle = {
|
||||
backgroundColor: bgColor,
|
||||
borderColor,
|
||||
borderWidth: variant === 'secondary' ? 1 : 0,
|
||||
borderRadius: d3roNativeRadius.button,
|
||||
minHeight: 48,
|
||||
paddingVertical: 14,
|
||||
paddingHorizontal: 24,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
opacity: disabled ? 0.4 : pressed ? 0.75 : 1,
|
||||
transform: [{ translateY: pressed && !disabled ? 1 : 0 }]
|
||||
}
|
||||
|
||||
const textStyle: TextStyle = {
|
||||
...d3roNativeTypo.heading,
|
||||
color: textColor
|
||||
}
|
||||
const t = physicalButtonTone(palette, tone ?? VARIANT_TONE[variant])
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
|
|
@ -74,12 +75,37 @@ export function PhysicalButton({
|
|||
disabled,
|
||||
}}
|
||||
onPress={onPress}
|
||||
onPressIn={() => setPressed(true)}
|
||||
onPressOut={() => setPressed(false)}
|
||||
disabled={disabled}
|
||||
style={[containerStyle, style]}
|
||||
style={({ pressed }) => [
|
||||
{
|
||||
backgroundColor: pressed && !disabled ? t.bgPressed : t.bg,
|
||||
borderColor: t.border,
|
||||
borderWidth: 1,
|
||||
borderRadius: d3roNativeRadius.button,
|
||||
minHeight: d3roNativeControl.h,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: d3roNativeControl.padX,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
opacity: disabled ? 0.5 : 1
|
||||
},
|
||||
style
|
||||
]}
|
||||
>
|
||||
<Text style={textStyle}>{label}</Text>
|
||||
<Text
|
||||
style={[
|
||||
{
|
||||
...d3roNativeTypo.body,
|
||||
fontWeight: '500',
|
||||
fontFamily: d3roNativeFonts.sans,
|
||||
color: t.color,
|
||||
textAlign: 'center'
|
||||
},
|
||||
textStyle
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// packages/ui-native/src/components/ScreenPanel.tsx
|
||||
// RN ScreenPanel — inset bg + dot grid background
|
||||
// v4: 우묵한 판 — inset 표면 + 1px 선. (점 격자·CRT 장식 없음)
|
||||
|
||||
import { View, type ViewProps, type ViewStyle, type StyleProp } from 'react-native'
|
||||
import { d3roNativeRadius } from '../theme'
|
||||
|
|
@ -14,7 +14,9 @@ export function ScreenPanel({ children, style, ...rest }: ScreenPanelProps): Rea
|
|||
const baseStyle: ViewStyle = {
|
||||
backgroundColor: palette.bg.inset,
|
||||
borderRadius: d3roNativeRadius.inner,
|
||||
padding: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: palette.border.subtle,
|
||||
padding: 16,
|
||||
overflow: 'hidden'
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
// 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 { d3roNativeFonts } from '../theme'
|
||||
import { useNativePalette } from '../theme-context'
|
||||
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 palette = useNativePalette()
|
||||
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: 10, letterSpacing: 0.6 }}
|
||||
>
|
||||
{model}
|
||||
</PhosphorText>
|
||||
<View style={{ width: 1, height: 8, backgroundColor: palette.border.default }} />
|
||||
<PhosphorText
|
||||
variant="label"
|
||||
color="muted"
|
||||
style={{ fontFamily: d3roNativeFonts.mono, fontSize: 10, letterSpacing: 0.6 }}
|
||||
>
|
||||
{label}
|
||||
</PhosphorText>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
|
@ -3,11 +3,24 @@
|
|||
|
||||
export {
|
||||
d3roNativePalette,
|
||||
d3roNativeDarkPalette,
|
||||
d3roNativeLightPalette,
|
||||
getD3roNativePalette,
|
||||
D3RO_NATIVE_RAW,
|
||||
D3RO_NATIVE_STATUS,
|
||||
d3roNativeTypo,
|
||||
d3roNativeRadius,
|
||||
d3roNativeFonts,
|
||||
d3roNativeSpace,
|
||||
d3roNativeControl,
|
||||
d3roNativeWeight,
|
||||
d3roNativeMotion,
|
||||
d3roNativeNumerals,
|
||||
d3roNativeOverlayShadow,
|
||||
type D3roNativePalette,
|
||||
type D3roNativePaletteKey,
|
||||
type D3roNativeStatus,
|
||||
type D3roNativeThemeKey,
|
||||
type D3roNativeTypoKey
|
||||
} from './theme'
|
||||
export {
|
||||
|
|
@ -25,6 +38,4 @@ 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'
|
||||
|
|
|
|||
|
|
@ -1,8 +1,31 @@
|
|||
// packages/ui-native/src/theme.ts
|
||||
// RN용 테마 토큰 — MUI 없이 순수 색상/타이포/그림자 값
|
||||
// packages/ui의 d3roPalette와 의도적으로 동기화 (RN은 CSS var 불가, 정적 값)
|
||||
// RN 디자인 토큰 v4 "조용한 계기" — 모바일 팔레트의 단일 출처.
|
||||
//
|
||||
// 색 값은 데스크톱 SSOT `packages/ui/src/theme-raw.ts` 의 BASE.dark / BASE.light 와 STATUS 에서 옮겨 왔다.
|
||||
// RN 은 CSS 변수를 못 쓰고, Metro 가 packages/ui 를 watchFolders 에 두지 않아 직접 import 하지 않는다.
|
||||
// ⚠ theme-raw.ts 와 같은 값 — 바뀌면 함께 바꾼다. apps/mobile-rn/__tests__/theme-parity.test.ts 가 값이 같은지 검사한다.
|
||||
//
|
||||
// 원칙 (루트 design.md "2026-09-28 전면 개편"):
|
||||
// - 표면은 평평하다. 위계는 크기·명도(텍스트 3단)·1px 선·여백으로 만든다. 글로우·그라디언트·장식 LED 없음.
|
||||
// - 텍스트 3단(primary/secondary/tertiary)은 app/card/elevated/sidebar 표면에서 4.5:1 이상.
|
||||
// 옛 label/inactive/muted 는 tertiary 의 호환 별칭이다.
|
||||
// - accent.solid 는 채운 버튼 배경, accent.ink 는 그 위 글자 (4.5:1 이상). accent.main 은 채움에 쓰지 않는다.
|
||||
// - 굵기는 400/500 (강조 600). 대문자 변환·자간 없음. 모노 폰트 없음(한글 글리프가 없다).
|
||||
|
||||
import { Platform } from 'react-native'
|
||||
import { Platform, type TextStyle, type ViewStyle } from 'react-native'
|
||||
|
||||
export type D3roNativeThemeKey = 'dark' | 'light'
|
||||
|
||||
export interface D3roNativeStatus {
|
||||
success: string
|
||||
successBg: string
|
||||
warning: string
|
||||
warningBg: string
|
||||
danger: string
|
||||
dangerBg: string
|
||||
info: string
|
||||
infoBg: string
|
||||
}
|
||||
|
||||
export interface D3roNativePalette {
|
||||
bg: {
|
||||
|
|
@ -10,26 +33,50 @@ export interface D3roNativePalette {
|
|||
card: string
|
||||
cardHover: string
|
||||
elevated: string
|
||||
input: string
|
||||
sidebar: string
|
||||
inset: string
|
||||
/** @deprecated v4: bg.card 와 같다 */
|
||||
chassis: string
|
||||
}
|
||||
text: {
|
||||
primary: string
|
||||
white: string
|
||||
onAccent?: string
|
||||
secondary: string
|
||||
label: string
|
||||
/** 가장 옅은 읽기용 글자 — 그래도 4.5:1 이상 */
|
||||
tertiary: string
|
||||
/** 대비 예외: 비활성 표시 전용 */
|
||||
disabled: string
|
||||
/** @deprecated v4: accent.ink 와 같다 (채운 버튼 위 글자) */
|
||||
onAccent: string
|
||||
/** 스위치 손잡이·체크 표시처럼 테마와 무관하게 흰 것 */
|
||||
white: string
|
||||
/** @deprecated v4: text.tertiary 별칭 */
|
||||
label: string
|
||||
/** @deprecated v4: text.tertiary 별칭 */
|
||||
inactive: string
|
||||
/** @deprecated v4: text.tertiary 별칭 */
|
||||
muted: string
|
||||
}
|
||||
accent: {
|
||||
/** 선·아이콘·선택 표시 */
|
||||
main: string
|
||||
pressed?: string
|
||||
/** 표면 위 강조 글자 (라이트 테마에서는 더 진한 값) */
|
||||
light: string
|
||||
dark: string
|
||||
/** 옅은 선택 배경 */
|
||||
dim: string
|
||||
/** 채운 버튼 배경 — ink 와 4.5:1 이상 */
|
||||
solid: string
|
||||
solidHover: string
|
||||
/** solid 위 글자 */
|
||||
ink: string
|
||||
/** @deprecated v4: accent.solidHover 별칭 (눌림) */
|
||||
pressed: string
|
||||
/** @deprecated v4: 글로우 없음 — accent.dim 과 같다 */
|
||||
glow: string
|
||||
/** @deprecated v4: status.success 별칭 */
|
||||
green: string
|
||||
/** @deprecated v4: status.successBg 별칭 */
|
||||
greenGlow: string
|
||||
}
|
||||
border: {
|
||||
|
|
@ -37,6 +84,7 @@ export interface D3roNativePalette {
|
|||
default: string
|
||||
strong: string
|
||||
}
|
||||
/** 카테고리·역할 구분색. 글자로도 쓰이므로 테마마다 대비를 맞춘 값 (red/orange/green/blue = STATUS). */
|
||||
tag: {
|
||||
purple: string
|
||||
orange: string
|
||||
|
|
@ -44,125 +92,187 @@ export interface D3roNativePalette {
|
|||
green: string
|
||||
blue: string
|
||||
}
|
||||
/** v3: 시맨틱 상태 역할 (액센트와 분리) */
|
||||
status: {
|
||||
success: string
|
||||
successBg: string
|
||||
warning: string
|
||||
warningBg: string
|
||||
danger: string
|
||||
dangerBg: string
|
||||
info: string
|
||||
infoBg: string
|
||||
}
|
||||
/** 시맨틱 상태 역할 (액센트와 분리) — theme-raw STATUS */
|
||||
status: D3roNativeStatus
|
||||
/** 모달 뒤 스크림 */
|
||||
scrim: string
|
||||
/** 표면 위 옅은 오버레이 (눌림·선택) */
|
||||
overlay: { soft: string; strong: string }
|
||||
led: { off: string }
|
||||
shadow?: string
|
||||
/** 떠 있는 층(시트·메뉴·플로팅 버튼) 그림자 색 — 표면에는 그림자를 쓰지 않는다 */
|
||||
shadow: string
|
||||
}
|
||||
|
||||
export const d3roNativePalette: D3roNativePalette = {
|
||||
bg: {
|
||||
app: '#19191b',
|
||||
card: '#242427',
|
||||
cardHover: '#2a2a2d',
|
||||
elevated: '#2e2e32',
|
||||
sidebar: '#1e1f21',
|
||||
inset: '#0f0f11',
|
||||
chassis: '#242528'
|
||||
// ── theme-raw.ts 원시값 사본 ─────────────────────────────────────────
|
||||
// theme-raw.ts 와 같은 값 — 바뀌면 함께. (BASE.dark / BASE.light 의 bg·text·border·accent, STATUS.dark / STATUS.light)
|
||||
export const D3RO_NATIVE_RAW = {
|
||||
dark: {
|
||||
bg: { app: '#0a0e1c', card: '#111a30', cardHover: '#152039', elevated: '#1a2540', input: '#0d1526', sidebar: '#080b17', inset: '#0a111f' },
|
||||
text: { primary: '#eef2fb', secondary: '#93a4c8', tertiary: '#7e8eb0', disabled: '#3c4763', hover: '#c9d5f2' },
|
||||
border: { subtle: 'rgba(148,180,255,0.07)', default: 'rgba(148,180,255,0.12)', strong: 'rgba(148,180,255,0.20)' },
|
||||
accent: { main: '#3b82f6', light: '#60a5fa', dark: '#2563eb', dim: 'rgba(59,130,246,0.14)', solid: '#2563eb', solidHover: '#1d4ed8', ink: '#ffffff' },
|
||||
},
|
||||
text: {
|
||||
primary: '#d4d4d8',
|
||||
white: '#ffffff',
|
||||
secondary: '#8e8e93',
|
||||
label: '#7c7c82',
|
||||
disabled: '#4a4a4e',
|
||||
inactive: '#77797c',
|
||||
muted: '#71717a'
|
||||
light: {
|
||||
bg: { app: '#f2f5fb', card: '#ffffff', cardHover: '#f7f9fd', elevated: '#eef2f9', input: '#ffffff', sidebar: '#e9eef7', inset: '#e4e9f2' },
|
||||
text: { primary: '#111827', secondary: '#4b5a75', tertiary: '#56647c', disabled: '#b5bfd1', hover: '#2b3a55' },
|
||||
border: { subtle: 'rgba(30,58,138,0.07)', default: 'rgba(30,58,138,0.12)', strong: 'rgba(30,58,138,0.22)' },
|
||||
accent: { main: '#2563eb', light: '#1d4ed8', dark: '#1e40af', dim: 'rgba(37,99,235,0.10)', solid: '#2563eb', solidHover: '#1d4ed8', ink: '#ffffff' },
|
||||
},
|
||||
accent: {
|
||||
// 브랜드 통일: 앱(Midnight Glass) blue — 레거시 amber 폐기
|
||||
main: '#3b82f6',
|
||||
dim: 'rgba(59, 130, 246, 0.15)',
|
||||
glow: 'rgba(59, 130, 246, 0.5)',
|
||||
green: '#4ade80',
|
||||
greenGlow: 'rgba(74, 222, 128, 0.6)'
|
||||
},
|
||||
border: {
|
||||
subtle: 'rgba(255,255,255,0.04)',
|
||||
default: '#2e2e32',
|
||||
strong: 'rgba(255,255,255,0.12)'
|
||||
},
|
||||
tag: {
|
||||
purple: '#b854f5',
|
||||
orange: '#f59e0b',
|
||||
red: '#ef4444',
|
||||
green: '#22c55e',
|
||||
blue: '#3b82f6'
|
||||
},
|
||||
status: {
|
||||
success: '#4ade80',
|
||||
successBg: 'rgba(74, 222, 128, 0.14)',
|
||||
warning: '#fbbf24',
|
||||
warningBg: 'rgba(251, 191, 36, 0.14)',
|
||||
danger: '#f87171',
|
||||
dangerBg: 'rgba(248, 113, 113, 0.14)',
|
||||
info: '#60a5fa',
|
||||
infoBg: 'rgba(96, 165, 250, 0.14)'
|
||||
},
|
||||
led: {
|
||||
off: '#111111'
|
||||
},
|
||||
shadow: '#000000'
|
||||
} as const
|
||||
|
||||
// ── v3: 간격·컨트롤·웨이트·모션 (RN은 px 숫자) ──
|
||||
export const D3RO_NATIVE_STATUS: Record<D3roNativeThemeKey, D3roNativeStatus> = {
|
||||
dark: {
|
||||
success: '#34d399', successBg: 'rgba(52,211,153,0.12)',
|
||||
warning: '#fbbf24', warningBg: 'rgba(251,191,36,0.12)',
|
||||
danger: '#f87171', dangerBg: 'rgba(248,113,113,0.12)',
|
||||
info: '#60a5fa', infoBg: 'rgba(96,165,250,0.12)',
|
||||
},
|
||||
light: {
|
||||
success: '#047857', successBg: 'rgba(4,120,87,0.10)',
|
||||
warning: '#b45309', warningBg: 'rgba(180,83,9,0.10)',
|
||||
danger: '#b91c1c', dangerBg: 'rgba(185,28,28,0.10)',
|
||||
info: '#1d4ed8', infoBg: 'rgba(29,78,216,0.10)',
|
||||
},
|
||||
}
|
||||
|
||||
/** theme-raw 에 없는 모바일 전용 값. 보라만 글자 대비를 맞춘 역할색(violet 400/700)이다. */
|
||||
const NATIVE_EXTRA: Record<D3roNativeThemeKey, {
|
||||
purple: string
|
||||
scrim: string
|
||||
overlaySoft: string
|
||||
overlayStrong: string
|
||||
shadow: string
|
||||
}> = {
|
||||
dark: {
|
||||
purple: '#a78bfa',
|
||||
scrim: 'rgba(0,0,0,0.6)', // theme-raw --d3-scrim (dark)
|
||||
overlaySoft: 'rgba(238,242,251,0.06)', // text.primary 6%
|
||||
overlayStrong: 'rgba(238,242,251,0.12)',
|
||||
shadow: '#030712',
|
||||
},
|
||||
light: {
|
||||
purple: '#6d28d9',
|
||||
scrim: 'rgba(17,24,39,0.32)', // theme-raw --d3-scrim (light)
|
||||
overlaySoft: 'rgba(17,24,39,0.06)',
|
||||
overlayStrong: 'rgba(17,24,39,0.12)',
|
||||
shadow: '#1e3a8a',
|
||||
},
|
||||
}
|
||||
|
||||
function buildPalette(key: D3roNativeThemeKey): D3roNativePalette {
|
||||
const raw = D3RO_NATIVE_RAW[key]
|
||||
const status = D3RO_NATIVE_STATUS[key]
|
||||
const extra = NATIVE_EXTRA[key]
|
||||
return {
|
||||
bg: { ...raw.bg, chassis: raw.bg.card },
|
||||
text: {
|
||||
primary: raw.text.primary,
|
||||
secondary: raw.text.secondary,
|
||||
tertiary: raw.text.tertiary,
|
||||
disabled: raw.text.disabled,
|
||||
onAccent: raw.accent.ink,
|
||||
white: '#ffffff',
|
||||
label: raw.text.tertiary,
|
||||
inactive: raw.text.tertiary,
|
||||
muted: raw.text.tertiary,
|
||||
},
|
||||
accent: {
|
||||
...raw.accent,
|
||||
pressed: raw.accent.solidHover,
|
||||
glow: raw.accent.dim,
|
||||
green: status.success,
|
||||
greenGlow: status.successBg,
|
||||
},
|
||||
border: { ...raw.border },
|
||||
tag: {
|
||||
purple: extra.purple,
|
||||
orange: status.warning,
|
||||
red: status.danger,
|
||||
green: status.success,
|
||||
blue: status.info,
|
||||
},
|
||||
status: { ...status },
|
||||
scrim: extra.scrim,
|
||||
overlay: { soft: extra.overlaySoft, strong: extra.overlayStrong },
|
||||
led: { off: raw.text.disabled },
|
||||
shadow: extra.shadow,
|
||||
}
|
||||
}
|
||||
|
||||
export const d3roNativeDarkPalette: D3roNativePalette = buildPalette('dark')
|
||||
export const d3roNativeLightPalette: D3roNativePalette = buildPalette('light')
|
||||
|
||||
/** 프로바이더 밖 기본값 (다크) */
|
||||
export const d3roNativePalette: D3roNativePalette = d3roNativeDarkPalette
|
||||
|
||||
export function getD3roNativePalette(key: D3roNativeThemeKey): D3roNativePalette {
|
||||
return key === 'light' ? d3roNativeLightPalette : d3roNativeDarkPalette
|
||||
}
|
||||
|
||||
// ── 간격·컨트롤·웨이트·모션 (RN 은 숫자) ──
|
||||
export const d3roNativeSpace = {
|
||||
s1: 4, s2: 8, s3: 12, s4: 16, s5: 24, s6: 40, s7: 64, s8: 96
|
||||
} as const
|
||||
|
||||
/** 터치 타깃: iOS 44pt / Android 48dp — 두 플랫폼 모두 48 로 맞춘다. 버튼처럼 생긴 것은 이 높이 이상. */
|
||||
export const d3roNativeControl = {
|
||||
h: 44, hSm: 36, padX: 16, padXSm: 10
|
||||
h: 48,
|
||||
padX: 16,
|
||||
padXSm: 12
|
||||
} as const
|
||||
|
||||
export const d3roNativeWeight = {
|
||||
body: '400' as const, medium: '500' as const, strong: '600' as const
|
||||
} as const
|
||||
|
||||
/** theme-raw MOTION 과 같은 길이(ms). 상태 변화 피드백만 — 등장 연출은 쓰지 않는다. */
|
||||
export const d3roNativeMotion = {
|
||||
instant: 100, quick: 200, normal: 350, slow: 600
|
||||
instant: 100, quick: 160, normal: 240, slow: 400
|
||||
} as const
|
||||
|
||||
// v3 "타이포그래피 퍼스트" — packages/ui d3roTypo v3와 동일 철학.
|
||||
// 볼드(700) 폐기, 크기·행간·명도로 위계. 한글 행간 1.5~1.6,
|
||||
// 자간 0 근처 (양수 트래킹은 라틴 대문자 라벨에만 소량).
|
||||
// RN letterSpacing 단위는 px.
|
||||
// v4 타입 — 데스크톱 d3roTypo v4 와 같은 철학: 크기·명도로 위계, 굵기 400/500, 자간 0(큰 제목만 살짝 음수).
|
||||
// RN letterSpacing 단위는 px, lineHeight 는 px. 한글 본문 행간 1.6.
|
||||
export const d3roNativeTypo = {
|
||||
hero: { fontSize: 42, fontWeight: '400' as const, letterSpacing: -0.5, lineHeight: 50 },
|
||||
title: { fontSize: 28, fontWeight: '400' as const, letterSpacing: -0.3, lineHeight: 36 },
|
||||
hero: { fontSize: 28, fontWeight: '500' as const, letterSpacing: -0.4, lineHeight: 36 },
|
||||
title: { fontSize: 22, fontWeight: '500' as const, letterSpacing: -0.2, lineHeight: 30 },
|
||||
value: { fontSize: 20, fontWeight: '500' as const, letterSpacing: 0, lineHeight: 26 },
|
||||
heading: { fontSize: 16, fontWeight: '500' as const, letterSpacing: 0, lineHeight: 24 },
|
||||
body: { fontSize: 14, fontWeight: '400' as const, letterSpacing: 0, lineHeight: 22 },
|
||||
small: { fontSize: 12, fontWeight: '400' as const, letterSpacing: 0, lineHeight: 18 },
|
||||
meta: { fontSize: 11, fontWeight: '500' as const, letterSpacing: 0.3, lineHeight: 16 },
|
||||
label: { fontSize: 11, fontWeight: '500' as const, letterSpacing: 0.4, lineHeight: 15 }
|
||||
meta: { fontSize: 12, fontWeight: '400' as const, letterSpacing: 0, lineHeight: 18 },
|
||||
label: { fontSize: 11, fontWeight: '500' as const, letterSpacing: 0, lineHeight: 16 }
|
||||
} as const
|
||||
|
||||
/** theme-raw RADIUS 와 같은 값 — 정밀 도구: 작고 일정하게. */
|
||||
export const d3roNativeRadius = {
|
||||
card: 22,
|
||||
inner: 12,
|
||||
card: 10,
|
||||
inner: 8,
|
||||
button: 8,
|
||||
small: 6,
|
||||
xs: 4,
|
||||
pill: 999
|
||||
} as const
|
||||
|
||||
// 폰트 전략 (bare RN, 커스텀 폰트 번들링 없음):
|
||||
// - sans: 플랫폼 시스템 산세리프. iOS=SF Pro(한글은 Apple SD Gothic Neo로 자동
|
||||
// 폴백), Android=Roboto(한글은 Noto Sans KR로 자동 폴백). Apple HIG/Material
|
||||
// 권장을 따르는 표준 방식이며 한글 렌더링 품질이 가장 안정적.
|
||||
// - mono: 텔레메트리·코드 등 등폭이 필요한 곳에만 사용 (한글엔 사용 금지).
|
||||
// 폰트: 플랫폼 시스템 산세리프 하나. iOS=SF Pro(한글 Apple SD Gothic Neo 자동 폴백),
|
||||
// Android=Roboto(한글 Noto Sans KR 자동 폴백). 모노 폰트 토큰은 폐기(design.md v4) —
|
||||
// 수치 정렬이 필요하면 fontVariant: ['tabular-nums'] 를 쓴다.
|
||||
export const d3roNativeFonts = {
|
||||
sans: Platform.OS === 'ios' ? 'System' : 'Roboto',
|
||||
mono: Platform.OS === 'ios' ? 'Menlo' : 'monospace'
|
||||
sans: Platform.OS === 'ios' ? 'System' : 'Roboto'
|
||||
} as const
|
||||
|
||||
export type D3roNativePaletteKey = keyof typeof d3roNativePalette
|
||||
/** 수치 정렬 — 흔들리는 숫자 방지 */
|
||||
export const d3roNativeNumerals: TextStyle = { fontVariant: ['tabular-nums'] }
|
||||
|
||||
/** 떠 있는 층(시트·메뉴·플로팅 버튼) 전용 그림자. 카드·버튼 표면에는 쓰지 않는다. */
|
||||
export function d3roNativeOverlayShadow(palette: D3roNativePalette): ViewStyle {
|
||||
return {
|
||||
shadowColor: palette.shadow,
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowOpacity: 0.28,
|
||||
shadowRadius: 16,
|
||||
elevation: 8
|
||||
}
|
||||
}
|
||||
|
||||
export type D3roNativePaletteKey = keyof D3roNativePalette
|
||||
export type D3roNativeTypoKey = keyof typeof d3roNativeTypo
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue