feat(release): prepare 1.1.0 candidate

This commit is contained in:
Yun Chan 2026-08-29 18:33:45 +09:00
parent 5a34f66981
commit 5205dcdfa9
736 changed files with 115667 additions and 12203 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@d3ro/ui-native",
"version": "1.0.0",
"version": "1.1.0",
"private": true,
"description": "D3RO Voice React Native DS — MetalCard/PhosphorText/Led/WaveBars etc.",
"license": "MIT",

View file

@ -2,7 +2,8 @@
// RN FilterChip — pill-shaped toggle chip for filter bars
import { Pressable, type ViewStyle, type StyleProp } from 'react-native'
import { d3roNativePalette, d3roNativeRadius } from '../theme'
import { d3roNativeRadius } from '../theme'
import { useNativePalette } from '../theme-context'
import { PhosphorText } from './PhosphorText'
export interface FilterChipProps {
@ -18,14 +19,21 @@ export function FilterChip({
onPress,
style
}: FilterChipProps): React.ReactElement {
const palette = useNativePalette()
return (
<Pressable
accessibilityRole="button"
accessibilityState={{ selected: active }}
accessibilityLabel={label}
onPress={onPress}
style={[
{
backgroundColor: active ? d3roNativePalette.accent.main : d3roNativePalette.bg.card,
minHeight: 48,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: active ? palette.accent.main : palette.bg.card,
borderWidth: 1,
borderColor: active ? d3roNativePalette.accent.main : d3roNativePalette.border.default,
borderColor: active ? palette.accent.main : palette.border.default,
borderRadius: d3roNativeRadius.pill,
paddingHorizontal: 16,
paddingVertical: 6
@ -36,7 +44,7 @@ export function FilterChip({
<PhosphorText
variant="small"
color={active ? 'primary' : 'muted'}
style={active ? { color: d3roNativePalette.bg.app } : undefined}
style={active ? { color: palette.text.onAccent ?? palette.bg.app } : undefined}
>
{label}
</PhosphorText>

View file

@ -5,7 +5,7 @@
import { View, type ViewStyle, type StyleProp } from 'react-native'
import { Led } from './Led'
import { PhosphorText } from './PhosphorText'
import { d3roNativePalette } from '../theme'
import { useNativePalette } from '../theme-context'
export interface HeaderProps {
title: string
@ -22,6 +22,7 @@ export function Header({
paddingTop = 0,
style
}: HeaderProps): React.ReactElement {
const palette = useNativePalette()
return (
<View
style={[
@ -33,14 +34,18 @@ export function Header({
paddingTop: paddingTop + 8,
paddingBottom: 12,
borderBottomWidth: showBorder ? 1 : 0,
borderBottomColor: d3roNativePalette.border.subtle
borderBottomColor: palette.border.subtle
},
style
]}
>
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<Led color="amber" size={8} />
<PhosphorText variant="label" color="muted" style={{ letterSpacing: 3 }}>
<PhosphorText
accessibilityRole="header"
variant="label"
color="muted"
>
{title}
</PhosphorText>
</View>

View file

@ -2,7 +2,7 @@
// RN용 Led — 작은 발광 원
import { View, type ViewStyle } from 'react-native'
import { d3roNativePalette } from '../theme'
import { useNativePalette } from '../theme-context'
export interface LedProps {
color?: 'amber' | 'green' | 'red' | 'orange' | 'blue'
@ -10,21 +10,21 @@ export interface LedProps {
size?: number
}
const COLOR_MAP: Record<string, string> = {
amber: d3roNativePalette.accent.main,
green: d3roNativePalette.tag.green,
red: d3roNativePalette.tag.red,
orange: d3roNativePalette.tag.orange,
blue: d3roNativePalette.tag.blue
}
export function Led({ color = 'amber', on = true, size = 8 }: LedProps): React.ReactElement {
const c = COLOR_MAP[color] ?? d3roNativePalette.accent.main
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
}
const c = colorMap[color]
const style: ViewStyle = {
width: size,
height: size,
borderRadius: size / 2,
backgroundColor: on ? c : d3roNativePalette.led.off,
backgroundColor: on ? c : palette.led.off,
shadowColor: c,
shadowOffset: { width: 0, height: 0 },
shadowOpacity: on ? 0.8 : 0,

View file

@ -2,7 +2,8 @@
// RN용 MetalCard — View + 섀시 스타일
import { View, type ViewProps, type ViewStyle, type StyleProp } from 'react-native'
import { d3roNativePalette, d3roNativeRadius } from '../theme'
import { d3roNativeRadius } from '../theme'
import { useNativePalette } from '../theme-context'
export interface MetalCardProps extends ViewProps {
inset?: boolean
@ -10,15 +11,16 @@ export interface MetalCardProps extends ViewProps {
}
export function MetalCard({ children, inset = false, style, ...rest }: MetalCardProps): React.ReactElement {
const palette = useNativePalette()
const baseStyle: ViewStyle = {
backgroundColor: inset ? d3roNativePalette.bg.inset : d3roNativePalette.bg.card,
backgroundColor: inset ? palette.bg.inset : palette.bg.card,
borderRadius: inset ? d3roNativeRadius.inner : d3roNativeRadius.card,
padding: inset ? 6 : 16,
borderWidth: inset ? 0 : 1,
borderColor: d3roNativePalette.border.subtle,
borderColor: palette.border.subtle,
overflow: 'hidden',
// RN 그림자
shadowColor: '#000',
shadowColor: palette.shadow ?? '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: inset ? 0 : 0.25,
shadowRadius: inset ? 0 : 8,

View file

@ -1,8 +1,10 @@
// packages/ui-native/src/components/PhosphorText.tsx
// RN용 PhosphorText — Text + 앰버 glow (textShadowColor)
// RN용 PhosphorText — v3: 시스템 산세리프 기반 (한글은 Apple SD Gothic Neo /
// Noto Sans KR 자동 폴백). 텍스트 글로우는 제거하고 명도로 위계를 만든다.
import { Text, Platform, type TextProps, type TextStyle, type StyleProp } from 'react-native'
import { d3roNativePalette, d3roNativeTypo, type D3roNativeTypoKey } from '../theme'
import { Text, type TextProps, type TextStyle, type StyleProp } from 'react-native'
import { d3roNativeFonts, d3roNativeTypo, type D3roNativeTypoKey } from '../theme'
import { useNativePalette } from '../theme-context'
export type PhosphorVariant = D3roNativeTypoKey
@ -19,29 +21,23 @@ export function PhosphorText({
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 d3roNativePalette.accent.main
if (color === 'primary') return d3roNativePalette.text.primary
if (color === 'secondary') return d3roNativePalette.text.secondary
if (color === 'label') return d3roNativePalette.text.label
if (color === 'muted') return d3roNativePalette.text.muted
return isAmberVariant ? d3roNativePalette.accent.main : d3roNativePalette.text.primary
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,
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
...(isAmberVariant
? {
textShadowColor: d3roNativePalette.accent.glow,
textShadowOffset: { width: 0, height: 0 },
textShadowRadius: 6
}
: {})
fontFamily: d3roNativeFonts.sans
}
return (

View file

@ -10,7 +10,8 @@ import {
type TextStyle,
type StyleProp
} from 'react-native'
import { d3roNativePalette, d3roNativeRadius, d3roNativeTypo } from '../theme'
import { d3roNativeRadius, d3roNativeTypo } from '../theme'
import { useNativePalette } from '../theme-context'
export interface PhysicalButtonProps extends Omit<PressableProps, 'children' | 'style'> {
label: string
@ -27,28 +28,30 @@ export function PhysicalButton({
onPress,
...rest
}: PhysicalButtonProps): React.ReactElement {
const palette = useNativePalette()
const [pressed, setPressed] = useState(false)
const bgColor =
variant === 'primary'
? d3roNativePalette.accent.main
? palette.accent.main
: variant === 'danger'
? d3roNativePalette.tag.red
? palette.tag.red
: 'transparent'
const borderColor =
variant === 'secondary' ? d3roNativePalette.border.strong : 'transparent'
variant === 'secondary' ? palette.border.strong : 'transparent'
const textColor =
variant === 'primary' || variant === 'danger'
? d3roNativePalette.text.primary
: d3roNativePalette.text.secondary
? 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',
@ -59,19 +62,22 @@ export function PhysicalButton({
const textStyle: TextStyle = {
...d3roNativeTypo.heading,
color: textColor,
textTransform: 'uppercase',
letterSpacing: 1.5
color: textColor
}
return (
<Pressable
{...rest}
accessibilityRole={rest.accessibilityRole ?? 'button'}
accessibilityState={{
...rest.accessibilityState,
disabled,
}}
onPress={onPress}
onPressIn={() => setPressed(true)}
onPressOut={() => setPressed(false)}
disabled={disabled}
style={[containerStyle, style]}
{...rest}
>
<Text style={textStyle}>{label}</Text>
</Pressable>

View file

@ -2,15 +2,17 @@
// RN ScreenPanel — inset bg + dot grid background
import { View, type ViewProps, type ViewStyle, type StyleProp } from 'react-native'
import { d3roNativePalette, d3roNativeRadius } from '../theme'
import { d3roNativeRadius } from '../theme'
import { useNativePalette } from '../theme-context'
export interface ScreenPanelProps extends ViewProps {
style?: StyleProp<ViewStyle>
}
export function ScreenPanel({ children, style, ...rest }: ScreenPanelProps): React.ReactElement {
const palette = useNativePalette()
const baseStyle: ViewStyle = {
backgroundColor: d3roNativePalette.bg.inset,
backgroundColor: palette.bg.inset,
borderRadius: d3roNativeRadius.inner,
padding: 20,
overflow: 'hidden'

View file

@ -2,7 +2,8 @@
// RN StatusBar — bottom status strip with LED + model label
import { View, type ViewStyle, type StyleProp } from 'react-native'
import { d3roNativePalette, d3roNativeFonts } from '../theme'
import { d3roNativeFonts } from '../theme'
import { useNativePalette } from '../theme-context'
import { Led } from './Led'
import { PhosphorText } from './PhosphorText'
@ -17,6 +18,7 @@ export function AppStatusBar({
label = 'PRECISION DATA LINK',
style
}: AppStatusBarProps): React.ReactElement {
const palette = useNativePalette()
const containerStyle: ViewStyle = {
flexDirection: 'row',
alignItems: 'center',
@ -33,15 +35,15 @@ export function AppStatusBar({
<PhosphorText
variant="label"
color="muted"
style={{ fontFamily: d3roNativeFonts.mono, fontSize: 8, letterSpacing: 1.5 }}
style={{ fontFamily: d3roNativeFonts.mono, fontSize: 10, letterSpacing: 0.6 }}
>
{model}
</PhosphorText>
<View style={{ width: 1, height: 8, backgroundColor: d3roNativePalette.border.default }} />
<View style={{ width: 1, height: 8, backgroundColor: palette.border.default }} />
<PhosphorText
variant="label"
color="muted"
style={{ fontFamily: d3roNativeFonts.mono, fontSize: 8, letterSpacing: 1.5 }}
style={{ fontFamily: d3roNativeFonts.mono, fontSize: 10, letterSpacing: 0.6 }}
>
{label}
</PhosphorText>

View file

@ -1,13 +1,21 @@
// 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'
import { useEffect, useRef, useState } from 'react'
import {
AccessibilityInfo,
View,
Animated,
Easing,
type ViewStyle,
type StyleProp,
} from 'react-native'
import { useNativePalette } from '../theme-context'
export interface WaveBarsProps {
active?: boolean
barCount?: number
reduceMotion?: boolean
style?: StyleProp<ViewStyle>
}
@ -15,7 +23,15 @@ 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 {
function WaveBar({
index,
active,
color,
}: {
index: number
active: boolean
color: string
}): React.ReactElement {
const height = useRef(new Animated.Value(BAR_MIN)).current
useEffect(() => {
@ -41,11 +57,8 @@ function WaveBar({ index, active }: { index: number; active: boolean }): React.R
animation.start()
return () => animation.stop()
} else {
Animated.timing(height, {
toValue: BAR_MIN,
duration: 300,
useNativeDriver: false
}).start()
height.stopAnimation()
height.setValue(BAR_MIN)
}
}, [active, height, index])
@ -54,7 +67,7 @@ function WaveBar({ index, active }: { index: number; active: boolean }): React.R
style={{
width: 6,
height,
backgroundColor: d3roNativePalette.accent.main,
backgroundColor: color,
borderRadius: 3
}}
/>
@ -64,23 +77,54 @@ function WaveBar({ index, active }: { index: number; active: boolean }): React.R
export function WaveBars({
active = false,
barCount = 11,
reduceMotion,
style
}: WaveBarsProps): React.ReactElement {
const palette = useNativePalette()
// Fail closed: do not animate until the asynchronous OS preference is known.
const [systemReduceMotion, setSystemReduceMotion] = useState(
reduceMotion === undefined,
)
useEffect(() => {
if (reduceMotion !== undefined) return undefined
let mounted = true
void AccessibilityInfo.isReduceMotionEnabled()
.then((enabled) => {
if (mounted) setSystemReduceMotion(enabled)
})
.catch(() => undefined)
const subscription = AccessibilityInfo.addEventListener(
'reduceMotionChanged',
setSystemReduceMotion,
)
return () => {
mounted = false
subscription.remove()
}
}, [reduceMotion])
const motionDisabled = reduceMotion ?? systemReduceMotion
const containerStyle: ViewStyle = {
height: 128,
backgroundColor: d3roNativePalette.bg.inset,
backgroundColor: palette.bg.inset,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 6,
borderBottomWidth: 1,
borderBottomColor: d3roNativePalette.border.subtle
borderBottomColor: palette.border.subtle
}
return (
<View style={[containerStyle, style]}>
{Array.from({ length: barCount }).map((_, i) => (
<WaveBar key={i} index={i} active={active} />
<WaveBar
key={i}
index={i}
active={active && !motionDisabled}
color={palette.accent.main}
/>
))}
</View>
)

View file

@ -6,9 +6,15 @@ export {
d3roNativeTypo,
d3roNativeRadius,
d3roNativeFonts,
type D3roNativePalette,
type D3roNativePaletteKey,
type D3roNativeTypoKey
} from './theme'
export {
NativeThemeProvider,
useNativePalette,
type NativeThemeProviderProps
} from './theme-context'
export { MetalCard, type MetalCardProps } from './components/MetalCard'
export {
PhosphorText,

View file

@ -0,0 +1,31 @@
import { createContext, useContext, type ReactNode } from 'react'
import {
d3roNativePalette,
type D3roNativePalette,
} from './theme'
const NativeThemeContext = createContext<D3roNativePalette | null>(null)
export interface NativeThemeProviderProps {
palette: D3roNativePalette
children: ReactNode
}
/**
* Optional runtime palette boundary for React Native consumers.
* Components outside a provider retain the legacy palette for compatibility.
*/
export function NativeThemeProvider({
palette,
children,
}: NativeThemeProviderProps): React.ReactElement {
return (
<NativeThemeContext.Provider value={palette}>
{children}
</NativeThemeContext.Provider>
)
}
export function useNativePalette(): D3roNativePalette {
return useContext(NativeThemeContext) ?? d3roNativePalette
}

View file

@ -4,7 +4,51 @@
import { Platform } from 'react-native'
export const d3roNativePalette = {
export interface D3roNativePalette {
bg: {
app: string
card: string
cardHover: string
elevated: string
sidebar: string
inset: string
chassis: string
}
text: {
primary: string
white: string
onAccent?: string
secondary: string
label: string
disabled: string
inactive: string
muted: string
}
accent: {
main: string
pressed?: string
dim: string
glow: string
green: string
greenGlow: string
}
border: {
subtle: string
default: string
strong: string
}
tag: {
purple: string
orange: string
red: string
green: string
blue: string
}
led: { off: string }
shadow?: string
}
export const d3roNativePalette: D3roNativePalette = {
bg: {
app: '#19191b',
card: '#242427',
@ -24,9 +68,10 @@ export const d3roNativePalette = {
muted: '#71717a'
},
accent: {
main: '#ff5c35',
dim: 'rgba(255, 92, 53, 0.15)',
glow: 'rgba(255, 92, 53, 0.6)',
// 브랜드 통일: 앱(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)'
},
@ -47,15 +92,19 @@ export const d3roNativePalette = {
}
} as const
// v3 "타이포그래피 퍼스트" — packages/ui d3roTypo v3와 동일 철학.
// 볼드(700) 폐기, 크기·행간·명도로 위계. 한글 행간 1.5~1.6,
// 자간 0 근처 (양수 트래킹은 라틴 대문자 라벨에만 소량).
// RN letterSpacing 단위는 px.
export const d3roNativeTypo = {
hero: { fontSize: 42, fontWeight: '300' as const, letterSpacing: -2, lineHeight: 42 },
title: { fontSize: 28, fontWeight: '300' as const, letterSpacing: -1, lineHeight: 34 },
value: { fontSize: 20, fontWeight: '400' as const, letterSpacing: 0.4, lineHeight: 20 },
heading: { fontSize: 16, fontWeight: '600' as const, letterSpacing: 0.32, lineHeight: 22 },
body: { fontSize: 14, fontWeight: '400' as const, letterSpacing: 0.14, lineHeight: 21 },
small: { fontSize: 12, fontWeight: '600' as const, letterSpacing: 0.36, lineHeight: 17 },
meta: { fontSize: 11, fontWeight: '600' as const, letterSpacing: 0.55, lineHeight: 14 },
label: { fontSize: 10, fontWeight: '700' as const, letterSpacing: 2, lineHeight: 12 }
hero: { fontSize: 42, fontWeight: '400' as const, letterSpacing: -0.5, lineHeight: 50 },
title: { fontSize: 28, fontWeight: '400' as const, letterSpacing: -0.3, lineHeight: 36 },
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 }
} as const
export const d3roNativeRadius = {
@ -66,9 +115,14 @@ export const d3roNativeRadius = {
pill: 999
} as const
// 폰트 전략 (bare RN, 커스텀 폰트 번들링 없음):
// - sans: 플랫폼 시스템 산세리프. iOS=SF Pro(한글은 Apple SD Gothic Neo로 자동
// 폴백), Android=Roboto(한글은 Noto Sans KR로 자동 폴백). Apple HIG/Material
// 권장을 따르는 표준 방식이며 한글 렌더링 품질이 가장 안정적.
// - mono: 텔레메트리·코드 등 등폭이 필요한 곳에만 사용 (한글엔 사용 금지).
export const d3roNativeFonts = {
mono: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
sans: Platform.OS === 'ios' ? 'System' : 'Roboto'
sans: Platform.OS === 'ios' ? 'System' : 'Roboto',
mono: Platform.OS === 'ios' ? 'Menlo' : 'monospace'
} as const
export type D3roNativePaletteKey = keyof typeof d3roNativePalette