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
278 lines
11 KiB
TypeScript
278 lines
11 KiB
TypeScript
// packages/ui-native/src/theme.ts
|
|
// 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, 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: {
|
|
app: string
|
|
card: string
|
|
cardHover: string
|
|
elevated: string
|
|
input: string
|
|
sidebar: string
|
|
inset: string
|
|
/** @deprecated v4: bg.card 와 같다 */
|
|
chassis: string
|
|
}
|
|
text: {
|
|
primary: string
|
|
secondary: 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
|
|
/** 표면 위 강조 글자 (라이트 테마에서는 더 진한 값) */
|
|
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: {
|
|
subtle: string
|
|
default: string
|
|
strong: string
|
|
}
|
|
/** 카테고리·역할 구분색. 글자로도 쓰이므로 테마마다 대비를 맞춘 값 (red/orange/green/blue = STATUS). */
|
|
tag: {
|
|
purple: string
|
|
orange: string
|
|
red: string
|
|
green: string
|
|
blue: string
|
|
}
|
|
/** 시맨틱 상태 역할 (액센트와 분리) — theme-raw STATUS */
|
|
status: D3roNativeStatus
|
|
/** 모달 뒤 스크림 */
|
|
scrim: string
|
|
/** 표면 위 옅은 오버레이 (눌림·선택) */
|
|
overlay: { soft: string; strong: string }
|
|
led: { off: string }
|
|
/** 떠 있는 층(시트·메뉴·플로팅 버튼) 그림자 색 — 표면에는 그림자를 쓰지 않는다 */
|
|
shadow: string
|
|
}
|
|
|
|
// ── 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' },
|
|
},
|
|
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' },
|
|
},
|
|
} as const
|
|
|
|
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: 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: 160, normal: 240, slow: 400
|
|
} as const
|
|
|
|
// v4 타입 — 데스크톱 d3roTypo v4 와 같은 철학: 크기·명도로 위계, 굵기 400/500, 자간 0(큰 제목만 살짝 음수).
|
|
// RN letterSpacing 단위는 px, lineHeight 는 px. 한글 본문 행간 1.6.
|
|
export const d3roNativeTypo = {
|
|
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: 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: 10,
|
|
inner: 8,
|
|
button: 8,
|
|
small: 6,
|
|
xs: 4,
|
|
pill: 999
|
|
} as const
|
|
|
|
// 폰트: 플랫폼 시스템 산세리프 하나. 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'
|
|
} as const
|
|
|
|
/** 수치 정렬 — 흔들리는 숫자 방지 */
|
|
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
|