@unikit-bridge/ui 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/tokens.md +1 -1
- package/package.json +1 -1
- package/src/bridge/native.tsx +135 -4
- package/src/bridge/types.ts +79 -1
- package/src/bridge/web.tsx +87 -6
- package/src/components/accordion.tsx +231 -0
- package/src/components/alert.tsx +157 -0
- package/src/components/aspect-ratio.tsx +40 -0
- package/src/components/avatar.tsx +275 -0
- package/src/components/breadcrumb.tsx +173 -0
- package/src/components/button-group.tsx +166 -0
- package/src/components/checkbox.tsx +94 -0
- package/src/components/collapsible.tsx +120 -0
- package/src/components/empty.tsx +160 -0
- package/src/components/field.tsx +349 -0
- package/src/components/item.tsx +243 -0
- package/src/components/kbd.tsx +62 -0
- package/src/components/pagination.tsx +204 -0
- package/src/components/progress.tsx +60 -0
- package/src/components/radio-group.tsx +148 -0
- package/src/components/separator.tsx +50 -0
- package/src/components/skeleton.tsx +36 -0
- package/src/components/switch.tsx +128 -0
- package/src/components/table.tsx +193 -0
- package/src/components/tabs.tsx +213 -0
- package/src/components/textarea.tsx +64 -0
- package/src/components/toggle-group.tsx +187 -0
- package/src/components/toggle.tsx +132 -0
- package/src/index.ts +276 -0
- package/src/theme/presets.ts +5 -4
- package/src/theme/tokens.ts +3 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
One typed component set that **renders React Native views on native and plain DOM on web** — the same `className` strings, the same look, no platform branches in your code.
|
|
4
4
|
|
|
5
|
-
- **Native (iOS/Android)** — components render React Native primitives styled by [uniwind](https://uniwind.dev) (Tailwind CSS v4 bindings
|
|
5
|
+
- **Native (iOS/Android)** — components render React Native primitives styled by [uniwind](https://uniwind.dev) (Tailwind CSS v4 bindings).
|
|
6
6
|
- **Web (React DOM)** — the very same components render `div`/`span`/`button`/`input` styled by **standard Tailwind CSS v4**. No react-native-web, no extra styling engine: your Tailwind CSS is the engine.
|
|
7
7
|
- **Design tokens as a single source of truth** — light/dark themes are CSS variables generated from one `tokens.ts`; switching schemes is instant on both platforms (`UnikitThemeProvider`).
|
|
8
8
|
- **shadcn-style variants** — built with `class-variance-authority`; `cn()` (clsx + tailwind-merge) is exported for custom styling.
|
package/docs/tokens.md
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
| `--muted-foreground` | `240 3.8% 46.1%` | `240 5% 64.9%` | 강조를 낮춘 텍스트(설명문) | `text-muted-foreground` |
|
|
22
22
|
| `--accent` | `240 4.8% 95.9%` | `240 3.7% 15.9%` | 호버·눌림 강조 배경(outline/ghost active) | `active:bg-accent` |
|
|
23
23
|
| `--accent-foreground` | `240 5.9% 10%` | `0 0% 98%` | 강조 배경 위 텍스트 | `active:text-accent-foreground` |
|
|
24
|
-
| `--destructive` | `0 72.2% 50.6%` | `0
|
|
24
|
+
| `--destructive` | `0 72.2% 50.6%` | `0 72% 65%` | 위험·삭제 행동 색 | `bg-destructive` |
|
|
25
25
|
| `--destructive-foreground` | `0 0% 98%` | `0 0% 98%` | 위험 행동 위 텍스트 | `text-destructive-foreground` |
|
|
26
26
|
| `--border` | `240 5.9% 90%` | `240 3.7% 15.9%` | 일반 테두리(card outline 등) | `border-border` |
|
|
27
27
|
| `--input` | `240 5.9% 90%` | `240 3.7% 15.9%` | 입력창 테두리 | `border-input` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unikit-bridge/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Bridge UI kit — one component set that renders React Native views on native (uniwind) and plain DOM on web (Tailwind CSS v4). Same className, same look.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
package/src/bridge/native.tsx
CHANGED
|
@@ -11,23 +11,30 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { forwardRef, type Ref } from "react";
|
|
13
13
|
import {
|
|
14
|
+
Image as RNImage,
|
|
14
15
|
Pressable as RNPressable,
|
|
15
16
|
Text as RNText,
|
|
16
17
|
TextInput as RNTextInput,
|
|
17
18
|
View as RNView,
|
|
19
|
+
type ImageInstance,
|
|
20
|
+
type ImageStyle,
|
|
18
21
|
type PressableInstance,
|
|
19
22
|
type TextInstance,
|
|
23
|
+
type TextStyle,
|
|
20
24
|
type TextInputInstance,
|
|
21
25
|
type ViewInstance,
|
|
26
|
+
type ViewStyle,
|
|
22
27
|
} from "react-native";
|
|
23
28
|
import { Uniwind } from "uniwind";
|
|
24
29
|
|
|
25
30
|
import type {
|
|
26
31
|
ApplyColorScheme,
|
|
32
|
+
BridgeImageRef,
|
|
27
33
|
BridgePressableRef,
|
|
28
34
|
BridgeTextRef,
|
|
29
35
|
BridgeTextInputRef,
|
|
30
36
|
BridgeViewRef,
|
|
37
|
+
ImageProps,
|
|
31
38
|
PressableProps,
|
|
32
39
|
TextProps,
|
|
33
40
|
TextInputProps,
|
|
@@ -41,12 +48,30 @@ export type * from "./types";
|
|
|
41
48
|
// View — RN View 재노출(타입만 브리지 계약으로 좁힌다)
|
|
42
49
|
// ---------------------------------------------------------------------------
|
|
43
50
|
const View = forwardRef<BridgeViewRef, ViewProps>(
|
|
44
|
-
(
|
|
51
|
+
(
|
|
52
|
+
{
|
|
53
|
+
className,
|
|
54
|
+
style,
|
|
55
|
+
children,
|
|
56
|
+
nativeID,
|
|
57
|
+
testID,
|
|
58
|
+
accessibilityRole,
|
|
59
|
+
accessibilityLabel,
|
|
60
|
+
"aria-hidden": ariaHidden,
|
|
61
|
+
...rest
|
|
62
|
+
},
|
|
63
|
+
ref,
|
|
64
|
+
) => (
|
|
45
65
|
<RNView
|
|
46
66
|
ref={ref as Ref<ViewInstance>}
|
|
47
67
|
nativeID={nativeID}
|
|
48
68
|
testID={testID}
|
|
69
|
+
accessibilityRole={accessibilityRole}
|
|
70
|
+
accessibilityLabel={accessibilityLabel}
|
|
71
|
+
aria-hidden={ariaHidden}
|
|
72
|
+
accessibilityElementsHidden={ariaHidden}
|
|
49
73
|
className={className}
|
|
74
|
+
style={style as ViewStyle}
|
|
50
75
|
{...rest}
|
|
51
76
|
>
|
|
52
77
|
{children}
|
|
@@ -59,13 +84,29 @@ View.displayName = "View";
|
|
|
59
84
|
// Text — RN Text 재노출
|
|
60
85
|
// ---------------------------------------------------------------------------
|
|
61
86
|
const Text = forwardRef<BridgeTextRef, TextProps>(
|
|
62
|
-
(
|
|
87
|
+
(
|
|
88
|
+
{
|
|
89
|
+
className,
|
|
90
|
+
style,
|
|
91
|
+
children,
|
|
92
|
+
nativeID,
|
|
93
|
+
testID,
|
|
94
|
+
onPress,
|
|
95
|
+
accessibilityRole,
|
|
96
|
+
accessibilityLabel,
|
|
97
|
+
...rest
|
|
98
|
+
},
|
|
99
|
+
ref,
|
|
100
|
+
) => (
|
|
63
101
|
<RNText
|
|
64
102
|
ref={ref as Ref<TextInstance>}
|
|
65
103
|
nativeID={nativeID}
|
|
66
104
|
testID={testID}
|
|
67
105
|
onPress={onPress}
|
|
106
|
+
accessibilityRole={accessibilityRole}
|
|
107
|
+
accessibilityLabel={accessibilityLabel}
|
|
68
108
|
className={className}
|
|
109
|
+
style={style as TextStyle}
|
|
69
110
|
{...rest}
|
|
70
111
|
>
|
|
71
112
|
{children}
|
|
@@ -78,15 +119,47 @@ Text.displayName = "Text";
|
|
|
78
119
|
// Pressable — RN Pressable 재노출. onPress는 RN 시그니처(이벤트 인자)를
|
|
79
120
|
// 무인자로 좁혀 브리지 계약과 맞춘다(컴포넌트·데모는 무인자 콜백만 쓴다).
|
|
80
121
|
// ---------------------------------------------------------------------------
|
|
122
|
+
// aria-* 상태 프로퍼티는 RN 0.77+ 네이티브 관용 별칭으로 그대로 전달한다.
|
|
123
|
+
// react-native-web(Expo web 등)은 accessibilityState를 0.21부터 폐기하고
|
|
124
|
+
// aria-* 직접 전달만 DOM 속성으로 렌더하므로, 양쪽 모두 aria-* 경로가 정답.
|
|
125
|
+
// 네이티브 실기기에서도 동일 값을 accessibilityState로 명시 전달해 구버전
|
|
126
|
+
// 스크린리더 호환을 유지한다(코어는 aria-* 별칭을 같은 상태로 병합).
|
|
127
|
+
function toAccessibilityState(props: {
|
|
128
|
+
disabled?: boolean;
|
|
129
|
+
"aria-checked"?: boolean | "mixed";
|
|
130
|
+
"aria-selected"?: boolean;
|
|
131
|
+
"aria-expanded"?: boolean;
|
|
132
|
+
}) {
|
|
133
|
+
const state: {
|
|
134
|
+
disabled?: boolean;
|
|
135
|
+
checked?: boolean | "mixed";
|
|
136
|
+
selected?: boolean;
|
|
137
|
+
expanded?: boolean;
|
|
138
|
+
} = {};
|
|
139
|
+
if (props.disabled !== undefined) state.disabled = props.disabled;
|
|
140
|
+
if (props["aria-checked"] !== undefined) state.checked = props["aria-checked"];
|
|
141
|
+
if (props["aria-selected"] !== undefined) state.selected = props["aria-selected"];
|
|
142
|
+
if (props["aria-expanded"] !== undefined) state.expanded = props["aria-expanded"];
|
|
143
|
+
return Object.keys(state).length > 0 ? state : undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
81
146
|
const Pressable = forwardRef<BridgePressableRef, PressableProps>(
|
|
82
147
|
(
|
|
83
148
|
{
|
|
84
149
|
className,
|
|
150
|
+
style,
|
|
85
151
|
children,
|
|
86
152
|
onPress,
|
|
87
153
|
disabled,
|
|
88
154
|
accessibilityLabel,
|
|
89
155
|
accessibilityRole,
|
|
156
|
+
"aria-disabled": ariaDisabled,
|
|
157
|
+
"aria-pressed": ariaPressed,
|
|
158
|
+
"aria-checked": ariaChecked,
|
|
159
|
+
"aria-selected": ariaSelected,
|
|
160
|
+
"aria-expanded": ariaExpanded,
|
|
161
|
+
"aria-controls": ariaControls,
|
|
162
|
+
"aria-labelledby": ariaLabelledBy,
|
|
90
163
|
hitSlop,
|
|
91
164
|
nativeID,
|
|
92
165
|
testID,
|
|
@@ -95,15 +168,35 @@ const Pressable = forwardRef<BridgePressableRef, PressableProps>(
|
|
|
95
168
|
ref,
|
|
96
169
|
) => (
|
|
97
170
|
<RNPressable
|
|
98
|
-
|
|
171
|
+
// BridgePressableRef(구조적 최소 계약) ↔ RN PressableInstance 불일치를
|
|
172
|
+
// unknown 경유 이중 단언으로만 해소 — any 사용 금지(M-3).
|
|
173
|
+
ref={ref as unknown as Ref<PressableInstance>}
|
|
99
174
|
nativeID={nativeID}
|
|
100
175
|
testID={testID}
|
|
101
176
|
onPress={onPress ? () => onPress() : undefined}
|
|
102
177
|
disabled={disabled}
|
|
103
178
|
accessibilityLabel={accessibilityLabel}
|
|
104
179
|
accessibilityRole={accessibilityRole}
|
|
180
|
+
aria-disabled={ariaDisabled}
|
|
181
|
+
aria-checked={ariaChecked}
|
|
182
|
+
aria-selected={ariaSelected}
|
|
183
|
+
aria-expanded={ariaExpanded}
|
|
184
|
+
aria-labelledby={ariaLabelledBy}
|
|
185
|
+
accessibilityState={toAccessibilityState({
|
|
186
|
+
disabled,
|
|
187
|
+
"aria-checked": ariaChecked,
|
|
188
|
+
"aria-selected": ariaSelected,
|
|
189
|
+
"aria-expanded": ariaExpanded,
|
|
190
|
+
})}
|
|
191
|
+
// aria-pressed(toggle)·aria-controls(tab↔panel 연결)은 RN 코어 타입에
|
|
192
|
+
// 없는 RNW 전용 속성 — 무형 스프레드로 전달(네이티브에서는 폐기됨).
|
|
193
|
+
{...({
|
|
194
|
+
"aria-pressed": ariaPressed,
|
|
195
|
+
"aria-controls": ariaControls,
|
|
196
|
+
} as Record<string, unknown>)}
|
|
105
197
|
hitSlop={hitSlop}
|
|
106
198
|
className={className}
|
|
199
|
+
style={style as ViewStyle}
|
|
107
200
|
{...rest}
|
|
108
201
|
>
|
|
109
202
|
{children}
|
|
@@ -120,6 +213,7 @@ const TextInput = forwardRef<BridgeTextInputRef, TextInputProps>(
|
|
|
120
213
|
(
|
|
121
214
|
{
|
|
122
215
|
className,
|
|
216
|
+
style,
|
|
123
217
|
value,
|
|
124
218
|
defaultValue,
|
|
125
219
|
placeholder,
|
|
@@ -132,6 +226,7 @@ const TextInput = forwardRef<BridgeTextInputRef, TextInputProps>(
|
|
|
132
226
|
autoFocus,
|
|
133
227
|
secureTextEntry,
|
|
134
228
|
inputMode,
|
|
229
|
+
multiline,
|
|
135
230
|
...rest
|
|
136
231
|
},
|
|
137
232
|
ref,
|
|
@@ -142,6 +237,7 @@ const TextInput = forwardRef<BridgeTextInputRef, TextInputProps>(
|
|
|
142
237
|
testID={testID}
|
|
143
238
|
accessibilityLabelledBy={accessibilityLabelledBy}
|
|
144
239
|
className={className}
|
|
240
|
+
style={style as TextStyle}
|
|
145
241
|
value={value}
|
|
146
242
|
defaultValue={defaultValue}
|
|
147
243
|
placeholder={placeholder}
|
|
@@ -150,12 +246,47 @@ const TextInput = forwardRef<BridgeTextInputRef, TextInputProps>(
|
|
|
150
246
|
autoFocus={autoFocus}
|
|
151
247
|
secureTextEntry={secureTextEntry}
|
|
152
248
|
inputMode={inputMode}
|
|
249
|
+
multiline={multiline}
|
|
153
250
|
{...rest}
|
|
154
251
|
/>
|
|
155
252
|
),
|
|
156
253
|
);
|
|
157
254
|
TextInput.displayName = "TextInput";
|
|
158
255
|
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
// Image — RN Image 재노출. 원격 {uri} 소스만 브리지 공통 계약으로 한다
|
|
258
|
+
// (require() 정적 소스는 네이티브 전용 기능이므로 타입에서 제외).
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
const Image = forwardRef<BridgeImageRef, ImageProps>(
|
|
261
|
+
(
|
|
262
|
+
{
|
|
263
|
+
className,
|
|
264
|
+
style,
|
|
265
|
+
source,
|
|
266
|
+
alt: _alt, // 웹 관용 — 네이티브는 accessibilityLabel이 스크린리더 텍스트
|
|
267
|
+
onError,
|
|
268
|
+
accessibilityLabel,
|
|
269
|
+
nativeID,
|
|
270
|
+
testID,
|
|
271
|
+
...rest
|
|
272
|
+
},
|
|
273
|
+
ref,
|
|
274
|
+
) => (
|
|
275
|
+
<RNImage
|
|
276
|
+
ref={ref as Ref<ImageInstance>}
|
|
277
|
+
nativeID={nativeID}
|
|
278
|
+
testID={testID}
|
|
279
|
+
accessibilityLabel={accessibilityLabel}
|
|
280
|
+
source={source}
|
|
281
|
+
className={className}
|
|
282
|
+
style={style as ImageStyle}
|
|
283
|
+
onError={onError ? () => onError() : undefined}
|
|
284
|
+
{...rest}
|
|
285
|
+
/>
|
|
286
|
+
),
|
|
287
|
+
);
|
|
288
|
+
Image.displayName = "Image";
|
|
289
|
+
|
|
159
290
|
// ---------------------------------------------------------------------------
|
|
160
291
|
// 테마 바인딩
|
|
161
292
|
// ---------------------------------------------------------------------------
|
|
@@ -167,4 +298,4 @@ const applyColorScheme: ApplyColorScheme = (colorScheme) => {
|
|
|
167
298
|
Uniwind.setTheme(colorScheme);
|
|
168
299
|
};
|
|
169
300
|
|
|
170
|
-
export { View, Text, Pressable, TextInput, applyColorScheme };
|
|
301
|
+
export { View, Text, Pressable, TextInput, Image, applyColorScheme };
|
package/src/bridge/types.ts
CHANGED
|
@@ -15,43 +15,118 @@ import type { ReactNode } from "react";
|
|
|
15
15
|
|
|
16
16
|
export type BridgeColorScheme = "light" | "dark";
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* 플랫폼 공통 인라인 스타일 최소 계약 — className 문자열로 표현할 수 없는
|
|
20
|
+
* 동적 값(진행률 폭 %, 가로세로 비율 등)용. RN 스타일 객체와 DOM
|
|
21
|
+
* CSSProperties의 실용 교집합(프로퍼티 이름은 camelCase, 값은 문자열·숫자).
|
|
22
|
+
* 정적 스타일은 여전히 className이 원칙이고, 여기는 동적 값 전용이다.
|
|
23
|
+
*/
|
|
24
|
+
export type BridgeStyle = Record<string, string | number | undefined>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 플랫폼 공통 접근성 역할 — RN accessibilityRole과 ARIA role의 교집합(컴포넌트
|
|
28
|
+
* 카탈로그 확장 시 필요한 역할까지 포함). 웹에서는 그대로 role 속성이 된다.
|
|
29
|
+
*/
|
|
30
|
+
export type BridgeRole =
|
|
31
|
+
| "button"
|
|
32
|
+
| "link"
|
|
33
|
+
| "search"
|
|
34
|
+
| "image"
|
|
35
|
+
| "text"
|
|
36
|
+
| "tab"
|
|
37
|
+
| "tablist"
|
|
38
|
+
| "tabpanel"
|
|
39
|
+
| "switch"
|
|
40
|
+
| "checkbox"
|
|
41
|
+
| "radio"
|
|
42
|
+
| "radiogroup"
|
|
43
|
+
| "option"
|
|
44
|
+
| "menuitem"
|
|
45
|
+
| "separator"
|
|
46
|
+
| "heading"
|
|
47
|
+
| "list"
|
|
48
|
+
| "listitem"
|
|
49
|
+
| "progressbar"
|
|
50
|
+
| "alert"
|
|
51
|
+
| "status"
|
|
52
|
+
| "combobox";
|
|
53
|
+
|
|
18
54
|
// ---------------------------------------------------------------------------
|
|
19
55
|
// Props
|
|
20
56
|
// ---------------------------------------------------------------------------
|
|
21
57
|
|
|
22
58
|
export type ViewProps = {
|
|
23
59
|
className?: string;
|
|
60
|
+
/** 동적 값 전용 인라인 스타일(정적 스타일은 className 원칙) */
|
|
61
|
+
style?: BridgeStyle;
|
|
24
62
|
children?: ReactNode;
|
|
25
63
|
/** 네이티브 nativeID — 웹에서는 id로 전달된다 */
|
|
26
64
|
nativeID?: string;
|
|
27
65
|
testID?: string;
|
|
66
|
+
accessibilityRole?: BridgeRole;
|
|
67
|
+
accessibilityLabel?: string;
|
|
68
|
+
/** 장식용 요소 표시 — 웹 aria-hidden, 네이티브 accessibilityElementsHidden */
|
|
69
|
+
"aria-hidden"?: boolean;
|
|
28
70
|
};
|
|
29
71
|
|
|
30
72
|
export type TextProps = {
|
|
31
73
|
className?: string;
|
|
74
|
+
/** 동적 값 전용 인라인 스타일(정적 스타일은 className 원칙) */
|
|
75
|
+
style?: BridgeStyle;
|
|
32
76
|
children?: ReactNode;
|
|
33
77
|
nativeID?: string;
|
|
34
78
|
testID?: string;
|
|
35
79
|
onPress?: () => void;
|
|
80
|
+
accessibilityRole?: BridgeRole;
|
|
81
|
+
accessibilityLabel?: string;
|
|
36
82
|
};
|
|
37
83
|
|
|
38
84
|
export type PressableProps = {
|
|
39
85
|
className?: string;
|
|
86
|
+
/** 동적 값 전용 인라인 스타일(정적 스타일은 className 원칙) */
|
|
87
|
+
style?: BridgeStyle;
|
|
40
88
|
children?: ReactNode;
|
|
41
89
|
onPress?: () => void;
|
|
42
90
|
disabled?: boolean;
|
|
43
91
|
accessibilityLabel?: string;
|
|
44
|
-
accessibilityRole?:
|
|
92
|
+
accessibilityRole?: BridgeRole;
|
|
45
93
|
/** RN 0.7x+ 관용 — 웹 DOM에서도 동일 속성으로 전달된다 */
|
|
46
94
|
"aria-disabled"?: boolean;
|
|
95
|
+
/** 눌림 유지형 컨트롤(toggle) 상태 — 웹 DOM 그대로, 네이티브는 시각 상태로만 반영 */
|
|
96
|
+
"aria-pressed"?: boolean;
|
|
97
|
+
/** 체크형 컨트롤(checkbox/radio/switch) 상태 — 네이티브 accessibilityState.checked로 */
|
|
98
|
+
"aria-checked"?: boolean | "mixed";
|
|
99
|
+
/** 탭·옵션 등 선택 상태 — 네이티브 accessibilityState.selected로 */
|
|
100
|
+
"aria-selected"?: boolean;
|
|
101
|
+
/** 아코디언·콤보박스 등 전개 상태 — 네이티브 accessibilityState.expanded로 */
|
|
102
|
+
"aria-expanded"?: boolean;
|
|
103
|
+
"aria-controls"?: string;
|
|
104
|
+
"aria-labelledby"?: string;
|
|
47
105
|
/** RN 전용 터치 타깃 확장(px) — 웹에서는 무시 */
|
|
48
106
|
hitSlop?: number | { top: number; bottom: number; left: number; right: number };
|
|
49
107
|
nativeID?: string;
|
|
50
108
|
testID?: string;
|
|
51
109
|
};
|
|
52
110
|
|
|
111
|
+
export type ImageProps = {
|
|
112
|
+
className?: string;
|
|
113
|
+
/** 동적 값 전용 인라인 스타일(정적 스타일은 className 원칙) */
|
|
114
|
+
style?: BridgeStyle;
|
|
115
|
+
/** 원격 이미지 — 네이티브 RN source, 웹 img src. require() 정적 소스는 네이티브 전용 */
|
|
116
|
+
source: { uri: string };
|
|
117
|
+
/** 웹 img alt — 네이티브에서는 accessibilityLabel로 전달된다 */
|
|
118
|
+
alt?: string;
|
|
119
|
+
/** 로드 실패 콜백(무인자) — Avatar 등의 폴백 전환용 */
|
|
120
|
+
onError?: () => void;
|
|
121
|
+
accessibilityLabel?: string;
|
|
122
|
+
nativeID?: string;
|
|
123
|
+
testID?: string;
|
|
124
|
+
};
|
|
125
|
+
|
|
53
126
|
export type TextInputProps = {
|
|
54
127
|
className?: string;
|
|
128
|
+
/** 동적 값 전용 인라인 스타일(정적 스타일은 className 원칙) */
|
|
129
|
+
style?: BridgeStyle;
|
|
55
130
|
value?: string;
|
|
56
131
|
defaultValue?: string;
|
|
57
132
|
placeholder?: string;
|
|
@@ -69,6 +144,8 @@ export type TextInputProps = {
|
|
|
69
144
|
autoFocus?: boolean;
|
|
70
145
|
secureTextEntry?: boolean;
|
|
71
146
|
inputMode?: "text" | "tel" | "url" | "email" | "numeric" | "decimal" | "search";
|
|
147
|
+
/** 여러 줄 입력 — 네이티브 RN multiline, 웹은 <input> 대신 <textarea>로 렌더 */
|
|
148
|
+
multiline?: boolean;
|
|
72
149
|
};
|
|
73
150
|
|
|
74
151
|
// ---------------------------------------------------------------------------
|
|
@@ -79,6 +156,7 @@ export type TextInputProps = {
|
|
|
79
156
|
export interface BridgeViewRef {}
|
|
80
157
|
export interface BridgeTextRef {}
|
|
81
158
|
export interface BridgePressableRef {}
|
|
159
|
+
export interface BridgeImageRef {}
|
|
82
160
|
export interface BridgeTextInputRef {
|
|
83
161
|
focus(): void;
|
|
84
162
|
blur(): void;
|
package/src/bridge/web.tsx
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
* 적용한다(hover/focus-visible/:active 등 웹 전용 변형도 CSS 표준 동작).
|
|
7
7
|
*
|
|
8
8
|
* RN 프리미티브 관용 프로퍼티는 DOM 속성으로 치환된다:
|
|
9
|
-
* onPress→onClick · accessibilityLabel→aria-label ·
|
|
10
|
-
* accessibilityLabelledBy→aria-labelledby · editable→readOnly ·
|
|
9
|
+
* onPress→onClick · accessibilityRole→role · accessibilityLabel→aria-label ·
|
|
10
|
+
* nativeID→id · accessibilityLabelledBy→aria-labelledby · editable→readOnly ·
|
|
11
11
|
* secureTextEntry→type="password" · onChangeText→onChange(value 추출)
|
|
12
|
+
* 치환은 View/Text/Pressable 전 프리미티브에 동일 적용된다(M-1 — React DOM은
|
|
13
|
+
* camelCase 미지 prop을 렌더에서 제거하므로 치환 없이는 role·라벨이 사라진다).
|
|
12
14
|
* hitSlop은 웹에서 무시된다(포인터 정밀도가 높아 불필요).
|
|
13
15
|
*/
|
|
14
16
|
import { forwardRef, type ChangeEvent } from "react";
|
|
@@ -16,10 +18,13 @@ import { forwardRef, type ChangeEvent } from "react";
|
|
|
16
18
|
import { cn } from "../lib/utils";
|
|
17
19
|
import type {
|
|
18
20
|
ApplyColorScheme,
|
|
21
|
+
BridgeImageRef,
|
|
19
22
|
BridgePressableRef,
|
|
23
|
+
BridgeStyle,
|
|
20
24
|
BridgeTextRef,
|
|
21
25
|
BridgeTextInputRef,
|
|
22
26
|
BridgeViewRef,
|
|
27
|
+
ImageProps,
|
|
23
28
|
PressableProps,
|
|
24
29
|
TextProps,
|
|
25
30
|
TextInputProps,
|
|
@@ -34,12 +39,18 @@ export type * from "./types";
|
|
|
34
39
|
// 기본 클래스로 재현한다. 충돌 시 tailwind-merge(cn)이 방향 유틸리티를 정리한다.
|
|
35
40
|
// ---------------------------------------------------------------------------
|
|
36
41
|
const View = forwardRef<BridgeViewRef, ViewProps>(
|
|
37
|
-
(
|
|
42
|
+
(
|
|
43
|
+
{ className, style, children, nativeID, testID, accessibilityLabel, accessibilityRole, ...rest },
|
|
44
|
+
ref,
|
|
45
|
+
) => (
|
|
38
46
|
<div
|
|
39
47
|
ref={ref as React.Ref<HTMLDivElement>}
|
|
40
48
|
id={nativeID}
|
|
41
49
|
data-testid={testID}
|
|
50
|
+
role={accessibilityRole}
|
|
51
|
+
aria-label={accessibilityLabel}
|
|
42
52
|
className={cn("flex flex-col", className)}
|
|
53
|
+
style={style as React.CSSProperties}
|
|
43
54
|
{...rest}
|
|
44
55
|
>
|
|
45
56
|
{children}
|
|
@@ -52,12 +63,18 @@ View.displayName = "View";
|
|
|
52
63
|
// Text → <span>
|
|
53
64
|
// ---------------------------------------------------------------------------
|
|
54
65
|
const Text = forwardRef<BridgeTextRef, TextProps>(
|
|
55
|
-
(
|
|
66
|
+
(
|
|
67
|
+
{ className, style, children, nativeID, testID, onPress, accessibilityLabel, accessibilityRole, ...rest },
|
|
68
|
+
ref,
|
|
69
|
+
) => (
|
|
56
70
|
<span
|
|
57
71
|
ref={ref as React.Ref<HTMLSpanElement>}
|
|
58
72
|
id={nativeID}
|
|
59
73
|
data-testid={testID}
|
|
74
|
+
role={accessibilityRole}
|
|
75
|
+
aria-label={accessibilityLabel}
|
|
60
76
|
className={className}
|
|
77
|
+
style={style as React.CSSProperties}
|
|
61
78
|
onClick={onPress}
|
|
62
79
|
{...rest}
|
|
63
80
|
>
|
|
@@ -76,6 +93,7 @@ const Pressable = forwardRef<BridgePressableRef, PressableProps>(
|
|
|
76
93
|
(
|
|
77
94
|
{
|
|
78
95
|
className,
|
|
96
|
+
style,
|
|
79
97
|
children,
|
|
80
98
|
onPress,
|
|
81
99
|
disabled,
|
|
@@ -94,6 +112,7 @@ const Pressable = forwardRef<BridgePressableRef, PressableProps>(
|
|
|
94
112
|
id={nativeID}
|
|
95
113
|
data-testid={testID}
|
|
96
114
|
className={className}
|
|
115
|
+
style={style as React.CSSProperties}
|
|
97
116
|
onClick={onPress}
|
|
98
117
|
disabled={disabled}
|
|
99
118
|
aria-label={accessibilityLabel}
|
|
@@ -113,6 +132,7 @@ const TextInput = forwardRef<BridgeTextInputRef, TextInputProps>(
|
|
|
113
132
|
(
|
|
114
133
|
{
|
|
115
134
|
className,
|
|
135
|
+
style,
|
|
116
136
|
value,
|
|
117
137
|
defaultValue,
|
|
118
138
|
placeholder,
|
|
@@ -125,13 +145,38 @@ const TextInput = forwardRef<BridgeTextInputRef, TextInputProps>(
|
|
|
125
145
|
autoFocus,
|
|
126
146
|
secureTextEntry,
|
|
127
147
|
inputMode,
|
|
148
|
+
multiline,
|
|
128
149
|
...rest
|
|
129
150
|
},
|
|
130
151
|
ref,
|
|
131
152
|
) => {
|
|
132
|
-
const handleChange = (
|
|
153
|
+
const handleChange = (
|
|
154
|
+
event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
|
155
|
+
) => {
|
|
133
156
|
onChangeText?.(event.target.value);
|
|
134
157
|
};
|
|
158
|
+
// multiline → <textarea>(웹 표준 여러 줄 입력). 나머지 계약은 <input>과 동일.
|
|
159
|
+
if (multiline) {
|
|
160
|
+
return (
|
|
161
|
+
<textarea
|
|
162
|
+
ref={ref as React.Ref<HTMLTextAreaElement>}
|
|
163
|
+
id={nativeID}
|
|
164
|
+
data-testid={testID}
|
|
165
|
+
aria-labelledby={accessibilityLabelledBy}
|
|
166
|
+
className={className}
|
|
167
|
+
style={style as React.CSSProperties}
|
|
168
|
+
value={value}
|
|
169
|
+
defaultValue={defaultValue}
|
|
170
|
+
placeholder={placeholder}
|
|
171
|
+
disabled={disabled}
|
|
172
|
+
readOnly={editable === false}
|
|
173
|
+
autoFocus={autoFocus}
|
|
174
|
+
inputMode={inputMode}
|
|
175
|
+
onChange={handleChange}
|
|
176
|
+
{...rest}
|
|
177
|
+
/>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
135
180
|
return (
|
|
136
181
|
<input
|
|
137
182
|
ref={ref as React.Ref<HTMLInputElement>}
|
|
@@ -139,6 +184,7 @@ const TextInput = forwardRef<BridgeTextInputRef, TextInputProps>(
|
|
|
139
184
|
data-testid={testID}
|
|
140
185
|
aria-labelledby={accessibilityLabelledBy}
|
|
141
186
|
className={className}
|
|
187
|
+
style={style as React.CSSProperties}
|
|
142
188
|
value={value}
|
|
143
189
|
defaultValue={defaultValue}
|
|
144
190
|
placeholder={placeholder}
|
|
@@ -155,6 +201,41 @@ const TextInput = forwardRef<BridgeTextInputRef, TextInputProps>(
|
|
|
155
201
|
);
|
|
156
202
|
TextInput.displayName = "TextInput";
|
|
157
203
|
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// Image → <img> — 원격 {uri} 소스를 src로 푼다. 정적 require() 소스는
|
|
206
|
+
// 네이티브 전용 기능이라 웹 계약에 없다.
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
const Image = forwardRef<BridgeImageRef, ImageProps>(
|
|
209
|
+
(
|
|
210
|
+
{
|
|
211
|
+
className,
|
|
212
|
+
style,
|
|
213
|
+
source,
|
|
214
|
+
alt = "",
|
|
215
|
+
onError,
|
|
216
|
+
accessibilityLabel, // 웹은 alt가 스크린리더 텍스트 표준
|
|
217
|
+
nativeID,
|
|
218
|
+
testID,
|
|
219
|
+
...rest
|
|
220
|
+
},
|
|
221
|
+
ref,
|
|
222
|
+
) => (
|
|
223
|
+
<img
|
|
224
|
+
ref={ref as React.Ref<HTMLImageElement>}
|
|
225
|
+
id={nativeID}
|
|
226
|
+
data-testid={testID}
|
|
227
|
+
src={source.uri}
|
|
228
|
+
alt={alt || accessibilityLabel}
|
|
229
|
+
loading="lazy"
|
|
230
|
+
className={className}
|
|
231
|
+
style={style as React.CSSProperties}
|
|
232
|
+
onError={onError}
|
|
233
|
+
{...rest}
|
|
234
|
+
/>
|
|
235
|
+
),
|
|
236
|
+
);
|
|
237
|
+
Image.displayName = "Image";
|
|
238
|
+
|
|
158
239
|
// ---------------------------------------------------------------------------
|
|
159
240
|
// 테마 바인딩
|
|
160
241
|
// ---------------------------------------------------------------------------
|
|
@@ -163,4 +244,4 @@ const applyColorScheme: ApplyColorScheme = (colorScheme) => {
|
|
|
163
244
|
document.documentElement.classList.toggle("dark", colorScheme === "dark");
|
|
164
245
|
};
|
|
165
246
|
|
|
166
|
-
export { View, Text, Pressable, TextInput, applyColorScheme };
|
|
247
|
+
export { View, Text, Pressable, TextInput, Image, applyColorScheme };
|