@unikit-bridge/ui 0.0.0-stage → 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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -2
  3. package/docs/tokens.md +63 -0
  4. package/package.json +75 -4
  5. package/src/bridge/native.tsx +301 -0
  6. package/src/bridge/types.ts +174 -0
  7. package/src/bridge/uniwind-env.d.ts +1 -0
  8. package/src/bridge/web.tsx +247 -0
  9. package/src/components/accordion.tsx +231 -0
  10. package/src/components/alert.tsx +157 -0
  11. package/src/components/aspect-ratio.tsx +40 -0
  12. package/src/components/avatar.tsx +275 -0
  13. package/src/components/badge.tsx +65 -0
  14. package/src/components/breadcrumb.tsx +173 -0
  15. package/src/components/button-group.tsx +166 -0
  16. package/src/components/button.tsx +106 -0
  17. package/src/components/card.tsx +112 -0
  18. package/src/components/checkbox.tsx +94 -0
  19. package/src/components/collapsible.tsx +120 -0
  20. package/src/components/empty.tsx +160 -0
  21. package/src/components/field.tsx +349 -0
  22. package/src/components/input.tsx +39 -0
  23. package/src/components/item.tsx +243 -0
  24. package/src/components/kbd.tsx +62 -0
  25. package/src/components/label.tsx +33 -0
  26. package/src/components/pagination.tsx +204 -0
  27. package/src/components/progress.tsx +60 -0
  28. package/src/components/radio-group.tsx +148 -0
  29. package/src/components/separator.tsx +50 -0
  30. package/src/components/skeleton.tsx +36 -0
  31. package/src/components/switch.tsx +128 -0
  32. package/src/components/table.tsx +193 -0
  33. package/src/components/tabs.tsx +213 -0
  34. package/src/components/textarea.tsx +64 -0
  35. package/src/components/toggle-group.tsx +187 -0
  36. package/src/components/toggle.tsx +132 -0
  37. package/src/index.ts +321 -0
  38. package/src/lib/utils.ts +7 -0
  39. package/src/theme/index.ts +4 -0
  40. package/src/theme/presets.ts +187 -0
  41. package/src/theme/provider.tsx +82 -0
  42. package/src/theme/tokens.ts +75 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 loopy-lim
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,100 @@
1
- # Temporary Holding Version
1
+ # @unikit-bridge/ui
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+
5
+ - **Native (iOS/Android)** — components render React Native primitives styled by [uniwind](https://uniwind.dev) (Tailwind CSS v4 bindings).
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
+ - **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
+ - **shadcn-style variants** — built with `class-variance-authority`; `cn()` (clsx + tailwind-merge) is exported for custom styling.
9
+
10
+ ## How the bridge works
11
+
12
+ `@unikit-bridge/ui/primitives` resolves through [package export conditions](https://nodejs.org/api/packages.html#conditional-exports):
13
+
14
+ | Platform | Condition | Implementation |
15
+ |---|---|---|
16
+ | Metro (React Native) | `react-native` | RN `View`/`Text`/`Pressable`/`TextInput` + uniwind |
17
+ | webpack / Turbopack / Vite (web) | `default` | `div`/`span`/`button`/`input` + standard Tailwind CSS |
18
+
19
+ Components import from that one entry point; each bundler picks its side. RN-specific props (`hitSlop`, …) are ignored on web, and web-idiom props (`disabled`, `onChangeText`, …) are translated on native.
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ bun add @unikit-bridge/ui class-variance-authority clsx tailwind-merge
25
+ ```
26
+
27
+ Then add the engine side you need:
28
+
29
+ - **Native**: `bun add uniwind react-native` (Tailwind v4 comes with uniwind's toolchain) — see the [uniwind setup](https://docs.uniwind.dev) (Metro `withUniwindConfig` + `global.css`).
30
+ - **Web**: `bun add tailwindcss` (v4) and make sure your global CSS scans this package, e.g. `@source '../node_modules/@unikit-bridge/ui/src';`.
31
+
32
+ ### Required CSS (both platforms)
33
+
34
+ Add the token mapping + values to your global CSS (or install them via the shadcn registry: `bunx shadcn add <registry>/r/theme.json`):
35
+
36
+ ```css
37
+ @import "tailwindcss";
38
+
39
+ /* web only — uniwind registers these itself */
40
+ @custom-variant dark (&:where(.dark, .dark *));
41
+ @custom-variant web (&:where(:root, :root *));
42
+
43
+ @theme inline {
44
+ --color-background: hsl(var(--background));
45
+ --color-foreground: hsl(var(--foreground));
46
+ /* … full 20-token mapping — see docs/tokens.md */
47
+ }
48
+
49
+ :root { --background: 0 0% 100%; /* … */ }
50
+ .dark { --background: 240 10% 3.9%; /* … */ }
51
+ ```
52
+
53
+ On web, put this in the app that compiles your Tailwind CSS (e.g. `app/globals.css` in Next.js, added to `transpilePackages: ["@unikit-bridge/ui"]`). On native, put it in the `global.css` referenced by uniwind's `cssEntryFile`.
54
+
55
+ ## Usage
56
+
57
+ Wrap your app in the theme provider, then use the components — identical JSX on both platforms:
58
+
59
+ ```tsx
60
+ import { UnikitThemeProvider, Button, Card, CardContent } from "@unikit-bridge/ui";
61
+
62
+ export default function App() {
63
+ return (
64
+ <UnikitThemeProvider defaultColorScheme="light">
65
+ <Card>
66
+ <CardContent className="gap-2">
67
+ <Button onPress={() => {}}>Press me</Button>
68
+ </CardContent>
69
+ </Card>
70
+ </UnikitThemeProvider>
71
+ );
72
+ }
73
+ ```
74
+
75
+ ### Theming
76
+
77
+ ```tsx
78
+ import { useUnikitTheme } from "@unikit-bridge/ui";
79
+
80
+ function ThemeToggle() {
81
+ const { colorScheme, toggleColorScheme } = useUnikitTheme();
82
+ return (
83
+ <Button variant="outline" size="sm" onPress={toggleColorScheme}>
84
+ {colorScheme === "light" ? "Dark" : "Light"} mode
85
+ </Button>
86
+ );
87
+ }
88
+ ```
89
+
90
+ The provider holds the scheme and binds it to each engine: `Uniwind.setTheme()` on native, a `.dark` class on `<html>` on web. Prebuilt token presets (`themePresets`) and raw token access (`getThemeTokens`, `lightTokens`, `darkTokens`) are exported for advanced theming.
91
+
92
+ ## Components
93
+
94
+ `Button` · `Card` (+ Header / Title / Description / Content / Footer) · `Input` · `Label` · `Badge`
95
+
96
+ All components accept a `className` and forward variant helpers (`buttonVariants`, `cardVariants`, …) for composition.
97
+
98
+ ## License
99
+
100
+ MIT
package/docs/tokens.md ADDED
@@ -0,0 +1,63 @@
1
+ # UniKit 디자인 토큰 카탈로그
2
+
3
+ > ⛔ 이 파일은 `bun run generate:design`(packages/unikit)이 생성합니다 — 수동 수정 금지.
4
+ > 값의 출처는 [`src/theme/tokens.ts`](../src/theme/tokens.ts)(기본)와 [`src/theme/presets.ts`](../src/theme/presets.ts)(프리셋 3종)입니다. 값 형식은 CSS `hsl()` 함수 인자로 바로 쓰는 HSL 채널 문자열("H S% L%")입니다.
5
+
6
+ ## 토큰 값 표 (20종 = 색 19 + radius)
7
+
8
+ | 토큰 | 라이트 | 다크 | 역할 | 대표 사용 |
9
+ |---|---|---|---|---|
10
+ | `--background` | `0 0% 100%` | `240 10% 3.9%` | 화면 전체 기본 배경 | `bg-background (provider 래퍼)` |
11
+ | `--foreground` | `240 10% 3.9%` | `0 0% 98%` | 기본 텍스트·아이콘 전경색 | `text-foreground` |
12
+ | `--card` | `0 0% 100%` | `240 10% 3.9%` | 카드 표면 배경 | `bg-card` |
13
+ | `--card-foreground` | `240 10% 3.9%` | `0 0% 98%` | 카드 위 텍스트 전경색 | `text-card-foreground` |
14
+ | `--popover` | `0 0% 100%` | `240 10% 3.9%` | 팝오버·드롭다운 표면 — v0.1 컴포넌트 미사용, 확장용 | `bg-popover (예약)` |
15
+ | `--popover-foreground` | `240 10% 3.9%` | `0 0% 98%` | 팝오버 위 텍스트 — v0.1 미사용, 확장용 | `text-popover-foreground (예약)` |
16
+ | `--primary` | `240 5.9% 10%` | `0 0% 98%` | 주요 행동 색(기본 버튼 배경) | `bg-primary` |
17
+ | `--primary-foreground` | `0 0% 98%` | `240 5.9% 10%` | 주요 행동 위 텍스트 | `text-primary-foreground` |
18
+ | `--secondary` | `240 4.8% 95.9%` | `240 3.7% 15.9%` | 보조 행동 색(secondary 버튼·배지) | `bg-secondary` |
19
+ | `--secondary-foreground` | `240 5.9% 10%` | `0 0% 98%` | 보조 행동 위 텍스트 | `text-secondary-foreground` |
20
+ | `--muted` | `240 4.8% 95.9%` | `240 3.7% 15.9%` | 강조를 낮춘 영역 배경 | `bg-muted` |
21
+ | `--muted-foreground` | `240 3.8% 46.1%` | `240 5% 64.9%` | 강조를 낮춘 텍스트(설명문) | `text-muted-foreground` |
22
+ | `--accent` | `240 4.8% 95.9%` | `240 3.7% 15.9%` | 호버·눌림 강조 배경(outline/ghost active) | `active:bg-accent` |
23
+ | `--accent-foreground` | `240 5.9% 10%` | `0 0% 98%` | 강조 배경 위 텍스트 | `active:text-accent-foreground` |
24
+ | `--destructive` | `0 72.2% 50.6%` | `0 72% 65%` | 위험·삭제 행동 색 | `bg-destructive` |
25
+ | `--destructive-foreground` | `0 0% 98%` | `0 0% 98%` | 위험 행동 위 텍스트 | `text-destructive-foreground` |
26
+ | `--border` | `240 5.9% 90%` | `240 3.7% 15.9%` | 일반 테두리(card outline 등) | `border-border` |
27
+ | `--input` | `240 5.9% 90%` | `240 3.7% 15.9%` | 입력창 테두리 | `border-input` |
28
+ | `--ring` | `240 5.9% 10%` | `240 4.9% 83.9%` | 키보드 포커스 링 | `focus-visible:ring-ring` |
29
+ | `--radius` | `0.5rem` | `0.5rem` | 모서리 반경 기준값(스킴 무관 공통) | `rounded-md (scale 파생)` |
30
+
31
+ ## 프리셋 테마 (기본 + 3종)
32
+
33
+ 값 출처는 Tailwind 공식 팔레트 계산 + WCAG 대비 검증(22-design-plan §2.2 규칙). 재적용: 다른 프리셋을 같은 방식으로 add하면 기존 값이 그 자리에서 교체됩니다(같은 프리셋 재add는 값 불변).
34
+
35
+ | 항목 | 중립 계열 | 라이트 primary | 핵심 대비비¹ | 설치(한 줄) |
36
+ |---|---|---|---|---|
37
+ | `theme` (Zinc (기본)) | zinc | zinc-900 | 16.96:1 | `bunx shadcn add <레지스트리>/r/theme.json -y` |
38
+ | `theme-blue` (Blue) | slate | blue-600 | 5.17:1 | `bunx shadcn add <레지스트리>/r/theme-blue.json -y` |
39
+ | `theme-emerald` (Emerald) | zinc | emerald-700 | 5.49:1 | `bunx shadcn add <레지스트리>/r/theme-emerald.json -y` |
40
+ | `theme-amber` (Amber) | stone | amber-700 | 5.01:1 | `bunx shadcn add <레지스트리>/r/theme-amber.json -y` |
41
+
42
+ ¹ 라이트 primary↔primary-foreground(흰 전경)의 WCAG 대비비 — audit:design가 매 실행 재계산(기준 ≥4.5:1).
43
+ 프리셋 전체 값(20종×라이트/다크)은 [`src/theme/presets.ts`](../src/theme/presets.ts)와 각 `/r/theme-<이름>.json`에서 관찰할 수 있습니다.
44
+
45
+ ## 일관성 규칙 (audit:design가 기계 검사)
46
+
47
+ 1. **원색 리터럴 금지** — 컴포넌트 소스에 `#fff`, `rgb(...)`, `oklch(...)` 등 직접 색 값을 쓰지 않는다. 반드시 위 토큰을 참조하는 유틸리티 클래스(`bg-primary` 등)를 쓴다.
48
+ 2. **Tailwind 기본 팔레트 금지** — `bg-zinc-500`, `text-white` 류는 테마 전환(라이트↔다크)이 되지 않으므로 금지.
49
+ 3. **임의값 클래스 금지** — `h-[37px]`, `bg-[#fff]` 류 대신 스케일 클래스(`h-10`)와 토큰을 쓴다.
50
+ 4. **재테마** — 소비자는 아래 한 줄로 토큰을 받고, 값 커스터마이즈는 해당 CSS 변수값만 바꾼다(컴포넌트 수정 불필요). 런타임 스킴 전환은 `UnikitThemeProvider`가 담당한다.
51
+ ```bash
52
+ bunx shadcn add <레지스트리>/r/theme.json
53
+ ```
54
+
55
+ ## 사본이 생성되는 곳
56
+
57
+ | 사본 | 생성 위치 | 비고 |
58
+ |---|---|---|
59
+ | 웹 전역 CSS | `apps/web/app/globals.css` :root/.dark 블록 | 표준 Tailwind v4 — 기본 테마만 |
60
+ | 네이티브 전역 CSS | `apps/expo/global.css` @variant light/dark 블록 | uniwind(Tailwind v4) — 기본 테마만 |
61
+ | 토큰→유틸리티 매핑 | 두 앱 전역 CSS `@theme inline` 블록 | 토큰 키에서 자동 생성 — 두 플랫폼 동일 바이트 |
62
+ | 레지스트리 theme 항목 4종 | `registry.json` cssVars light/dark | 기본 + 프리셋 3 — 소비자 `shadcn add` 전달 |
63
+ | 이 카탈로그 | `packages/unikit/docs/tokens.md` | 값 드리프트는 audit (f)가 차단 |
package/package.json CHANGED
@@ -1,6 +1,77 @@
1
1
  {
2
2
  "name": "@unikit-bridge/ui",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
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
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/loopy-lim/UniKit.git",
9
+ "directory": "packages/unikit"
10
+ },
11
+ "homepage": "https://github.com/loopy-lim/UniKit#readme",
12
+ "bugs": "https://github.com/loopy-lim/UniKit/issues",
13
+ "keywords": [
14
+ "react-native",
15
+ "expo",
16
+ "uniwind",
17
+ "react",
18
+ "tailwind",
19
+ "tailwindcss",
20
+ "design-system",
21
+ "ui-components",
22
+ "universal",
23
+ "cross-platform",
24
+ "shadcn"
25
+ ],
26
+ "exports": {
27
+ ".": "./src/index.ts",
28
+ "./primitives": {
29
+ "react-native": "./src/bridge/native.tsx",
30
+ "default": "./src/bridge/web.tsx"
31
+ }
32
+ },
33
+ "main": "./src/index.ts",
34
+ "types": "./src/index.ts",
35
+ "files": [
36
+ "src",
37
+ "docs",
38
+ "README.md",
39
+ "LICENSE"
40
+ ],
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "scripts": {
45
+ "typecheck": "tsc --noEmit",
46
+ "prepublishOnly": "tsc --noEmit",
47
+ "generate:design": "bun scripts/generate-design.ts",
48
+ "audit:design": "bun scripts/audit-design.ts",
49
+ "check:consumer": "bun scripts/check-consumer-theme.ts"
50
+ },
51
+ "peerDependencies": {
52
+ "react": "^18.0.0 || ^19.0.0",
53
+ "class-variance-authority": "^0.7.0",
54
+ "clsx": "^2.1.0",
55
+ "tailwind-merge": "^3.0.0",
56
+ "react-native": ">=0.81.0",
57
+ "uniwind": ">=1.13.0"
58
+ },
59
+ "peerDependenciesMeta": {
60
+ "react-native": {
61
+ "optional": true
62
+ },
63
+ "uniwind": {
64
+ "optional": true
65
+ }
66
+ },
67
+ "devDependencies": {
68
+ "typescript": "~5.9.0",
69
+ "@types/react": "~19.3.0",
70
+ "react": "19.3.0",
71
+ "react-native": "0.88.1",
72
+ "uniwind": "~1.13.2",
73
+ "class-variance-authority": "~0.7.1",
74
+ "clsx": "~2.1.1",
75
+ "tailwind-merge": "~3.7.0"
76
+ }
77
+ }
@@ -0,0 +1,301 @@
1
+ /**
2
+ * 네이티브 브리지 — React Native(iOS/Android) + uniwind 소비자용.
3
+ *
4
+ * RN 프리미티브를 그대로 재노출한다. className은 uniwind가 빌드 타임에
5
+ * RN 스타일로 컴파일한다(metro.config.js의 withUniwindConfig — 앱 책임).
6
+ * RN 관용 프로퍼티(hitSlop 등)는 네이티브 그대로 전달된다.
7
+ *
8
+ * 웹 관용 프로퍼티는 RN 관용으로 치환한다:
9
+ * disabled(버튼)→ RN 표준 disabled · onChangeText는 RN 표준 그대로
10
+ * aria-* 프로퍼티는 RN 0.7x+가 네이티브 관용으로 지원한다.
11
+ */
12
+ import { forwardRef, type Ref } from "react";
13
+ import {
14
+ Image as RNImage,
15
+ Pressable as RNPressable,
16
+ Text as RNText,
17
+ TextInput as RNTextInput,
18
+ View as RNView,
19
+ type ImageInstance,
20
+ type ImageStyle,
21
+ type PressableInstance,
22
+ type TextInstance,
23
+ type TextStyle,
24
+ type TextInputInstance,
25
+ type ViewInstance,
26
+ type ViewStyle,
27
+ } from "react-native";
28
+ import { Uniwind } from "uniwind";
29
+
30
+ import type {
31
+ ApplyColorScheme,
32
+ BridgeImageRef,
33
+ BridgePressableRef,
34
+ BridgeTextRef,
35
+ BridgeTextInputRef,
36
+ BridgeViewRef,
37
+ ImageProps,
38
+ PressableProps,
39
+ TextProps,
40
+ TextInputProps,
41
+ ViewProps,
42
+ } from "./types";
43
+
44
+ // 공유 타입 전량 재수출 — 소비자는 @unikit-bridge/ui/primitives에서 타입까지 한 번에 import
45
+ export type * from "./types";
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // View — RN View 재노출(타입만 브리지 계약으로 좁힌다)
49
+ // ---------------------------------------------------------------------------
50
+ const View = forwardRef<BridgeViewRef, ViewProps>(
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
+ ) => (
65
+ <RNView
66
+ ref={ref as Ref<ViewInstance>}
67
+ nativeID={nativeID}
68
+ testID={testID}
69
+ accessibilityRole={accessibilityRole}
70
+ accessibilityLabel={accessibilityLabel}
71
+ aria-hidden={ariaHidden}
72
+ accessibilityElementsHidden={ariaHidden}
73
+ className={className}
74
+ style={style as ViewStyle}
75
+ {...rest}
76
+ >
77
+ {children}
78
+ </RNView>
79
+ ),
80
+ );
81
+ View.displayName = "View";
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Text — RN Text 재노출
85
+ // ---------------------------------------------------------------------------
86
+ const Text = forwardRef<BridgeTextRef, TextProps>(
87
+ (
88
+ {
89
+ className,
90
+ style,
91
+ children,
92
+ nativeID,
93
+ testID,
94
+ onPress,
95
+ accessibilityRole,
96
+ accessibilityLabel,
97
+ ...rest
98
+ },
99
+ ref,
100
+ ) => (
101
+ <RNText
102
+ ref={ref as Ref<TextInstance>}
103
+ nativeID={nativeID}
104
+ testID={testID}
105
+ onPress={onPress}
106
+ accessibilityRole={accessibilityRole}
107
+ accessibilityLabel={accessibilityLabel}
108
+ className={className}
109
+ style={style as TextStyle}
110
+ {...rest}
111
+ >
112
+ {children}
113
+ </RNText>
114
+ ),
115
+ );
116
+ Text.displayName = "Text";
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Pressable — RN Pressable 재노출. onPress는 RN 시그니처(이벤트 인자)를
120
+ // 무인자로 좁혀 브리지 계약과 맞춘다(컴포넌트·데모는 무인자 콜백만 쓴다).
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
+
146
+ const Pressable = forwardRef<BridgePressableRef, PressableProps>(
147
+ (
148
+ {
149
+ className,
150
+ style,
151
+ children,
152
+ onPress,
153
+ disabled,
154
+ accessibilityLabel,
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,
163
+ hitSlop,
164
+ nativeID,
165
+ testID,
166
+ ...rest
167
+ },
168
+ ref,
169
+ ) => (
170
+ <RNPressable
171
+ // BridgePressableRef(구조적 최소 계약) ↔ RN PressableInstance 불일치를
172
+ // unknown 경유 이중 단언으로만 해소 — any 사용 금지(M-3).
173
+ ref={ref as unknown as Ref<PressableInstance>}
174
+ nativeID={nativeID}
175
+ testID={testID}
176
+ onPress={onPress ? () => onPress() : undefined}
177
+ disabled={disabled}
178
+ accessibilityLabel={accessibilityLabel}
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>)}
197
+ hitSlop={hitSlop}
198
+ className={className}
199
+ style={style as ViewStyle}
200
+ {...rest}
201
+ >
202
+ {children}
203
+ </RNPressable>
204
+ ),
205
+ );
206
+ Pressable.displayName = "Pressable";
207
+
208
+ // ---------------------------------------------------------------------------
209
+ // TextInput — RN TextInput 재노출. 웹 관용 disabled는 네이티브 표준 속성이
210
+ // 아니므로 여기서는 버린다(입력 차단 치환 editable={false}는 Input이 수행).
211
+ // ---------------------------------------------------------------------------
212
+ const TextInput = forwardRef<BridgeTextInputRef, TextInputProps>(
213
+ (
214
+ {
215
+ className,
216
+ style,
217
+ value,
218
+ defaultValue,
219
+ placeholder,
220
+ editable,
221
+ disabled: _disabled, // 웹 관용 — 네이티브는 위 editable 치환으로 이미 반영됨
222
+ onChangeText,
223
+ nativeID,
224
+ testID,
225
+ accessibilityLabelledBy,
226
+ autoFocus,
227
+ secureTextEntry,
228
+ inputMode,
229
+ multiline,
230
+ ...rest
231
+ },
232
+ ref,
233
+ ) => (
234
+ <RNTextInput
235
+ ref={ref as Ref<TextInputInstance>}
236
+ nativeID={nativeID}
237
+ testID={testID}
238
+ accessibilityLabelledBy={accessibilityLabelledBy}
239
+ className={className}
240
+ style={style as TextStyle}
241
+ value={value}
242
+ defaultValue={defaultValue}
243
+ placeholder={placeholder}
244
+ editable={editable}
245
+ onChangeText={onChangeText}
246
+ autoFocus={autoFocus}
247
+ secureTextEntry={secureTextEntry}
248
+ inputMode={inputMode}
249
+ multiline={multiline}
250
+ {...rest}
251
+ />
252
+ ),
253
+ );
254
+ TextInput.displayName = "TextInput";
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
+
290
+ // ---------------------------------------------------------------------------
291
+ // 테마 바인딩
292
+ // ---------------------------------------------------------------------------
293
+ /** 색 스킴 → uniwind 활성 테마. setTheme은 Appearance.setColorScheme도 호출해
294
+ * Alert·Modal 등 시스템 UI가 앱 테마를 따르게 한다.
295
+ * 정적 import가 안전한 이유: 웹 번들은 exports "default" 조건으로 web.tsx만
296
+ * 로드하므로 이 파일(와 uniwind 의존)은 네이티브 그래프에만 존재한다. */
297
+ const applyColorScheme: ApplyColorScheme = (colorScheme) => {
298
+ Uniwind.setTheme(colorScheme);
299
+ };
300
+
301
+ export { View, Text, Pressable, TextInput, Image, applyColorScheme };