@unikit-bridge/ui 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +99 -2
- package/docs/tokens.md +63 -0
- package/package.json +75 -4
- package/src/bridge/native.tsx +170 -0
- package/src/bridge/types.ts +96 -0
- package/src/bridge/uniwind-env.d.ts +1 -0
- package/src/bridge/web.tsx +166 -0
- package/src/components/badge.tsx +65 -0
- package/src/components/button.tsx +106 -0
- package/src/components/card.tsx +112 -0
- package/src/components/input.tsx +39 -0
- package/src/components/label.tsx +33 -0
- package/src/index.ts +45 -0
- package/src/lib/utils.ts +7 -0
- package/src/theme/index.ts +4 -0
- package/src/theme/presets.ts +186 -0
- package/src/theme/provider.tsx +82 -0
- package/src/theme/tokens.ts +73 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 웹 브리지 — 표준 Tailwind CSS v4 소비자용(Next.js/Vite 등).
|
|
3
|
+
*
|
|
4
|
+
* 같은 className 문자열을 DOM 요소에 그대로 얹는다. 스타일링 엔진이 별도로
|
|
5
|
+
* 필요하지 않다 — 소비자 앱의 Tailwind CSS가 클래스를 생성하고 브라우저가
|
|
6
|
+
* 적용한다(hover/focus-visible/:active 등 웹 전용 변형도 CSS 표준 동작).
|
|
7
|
+
*
|
|
8
|
+
* RN 프리미티브 관용 프로퍼티는 DOM 속성으로 치환된다:
|
|
9
|
+
* onPress→onClick · accessibilityLabel→aria-label · nativeID→id ·
|
|
10
|
+
* accessibilityLabelledBy→aria-labelledby · editable→readOnly ·
|
|
11
|
+
* secureTextEntry→type="password" · onChangeText→onChange(value 추출)
|
|
12
|
+
* hitSlop은 웹에서 무시된다(포인터 정밀도가 높아 불필요).
|
|
13
|
+
*/
|
|
14
|
+
import { forwardRef, type ChangeEvent } from "react";
|
|
15
|
+
|
|
16
|
+
import { cn } from "../lib/utils";
|
|
17
|
+
import type {
|
|
18
|
+
ApplyColorScheme,
|
|
19
|
+
BridgePressableRef,
|
|
20
|
+
BridgeTextRef,
|
|
21
|
+
BridgeTextInputRef,
|
|
22
|
+
BridgeViewRef,
|
|
23
|
+
PressableProps,
|
|
24
|
+
TextProps,
|
|
25
|
+
TextInputProps,
|
|
26
|
+
ViewProps,
|
|
27
|
+
} from "./types";
|
|
28
|
+
|
|
29
|
+
// 공유 타입 전량 재수출 — 소비자는 @unikit-bridge/ui/primitives에서 타입까지 한 번에 import
|
|
30
|
+
export type * from "./types";
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// View → <div> — RN View 기본값(display:flex, flexDirection:column)을
|
|
34
|
+
// 기본 클래스로 재현한다. 충돌 시 tailwind-merge(cn)이 방향 유틸리티를 정리한다.
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
const View = forwardRef<BridgeViewRef, ViewProps>(
|
|
37
|
+
({ className, children, nativeID, testID, ...rest }, ref) => (
|
|
38
|
+
<div
|
|
39
|
+
ref={ref as React.Ref<HTMLDivElement>}
|
|
40
|
+
id={nativeID}
|
|
41
|
+
data-testid={testID}
|
|
42
|
+
className={cn("flex flex-col", className)}
|
|
43
|
+
{...rest}
|
|
44
|
+
>
|
|
45
|
+
{children}
|
|
46
|
+
</div>
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
View.displayName = "View";
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Text → <span>
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
const Text = forwardRef<BridgeTextRef, TextProps>(
|
|
55
|
+
({ className, children, nativeID, testID, onPress, ...rest }, ref) => (
|
|
56
|
+
<span
|
|
57
|
+
ref={ref as React.Ref<HTMLSpanElement>}
|
|
58
|
+
id={nativeID}
|
|
59
|
+
data-testid={testID}
|
|
60
|
+
className={className}
|
|
61
|
+
onClick={onPress}
|
|
62
|
+
{...rest}
|
|
63
|
+
>
|
|
64
|
+
{children}
|
|
65
|
+
</span>
|
|
66
|
+
),
|
|
67
|
+
);
|
|
68
|
+
Text.displayName = "Text";
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Pressable → <button type="button">
|
|
72
|
+
// disabled는 네이티브 속성으로 전달돼 CSS :disabled/disabled: 변형과 포커스·클릭
|
|
73
|
+
// 차단이 함께 동작한다. active: 변형은 브라우저 :active와 대응한다.
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
const Pressable = forwardRef<BridgePressableRef, PressableProps>(
|
|
76
|
+
(
|
|
77
|
+
{
|
|
78
|
+
className,
|
|
79
|
+
children,
|
|
80
|
+
onPress,
|
|
81
|
+
disabled,
|
|
82
|
+
accessibilityLabel,
|
|
83
|
+
accessibilityRole,
|
|
84
|
+
nativeID,
|
|
85
|
+
testID,
|
|
86
|
+
hitSlop: _hitSlop,
|
|
87
|
+
...rest
|
|
88
|
+
},
|
|
89
|
+
ref,
|
|
90
|
+
) => (
|
|
91
|
+
<button
|
|
92
|
+
ref={ref as React.Ref<HTMLButtonElement>}
|
|
93
|
+
type="button"
|
|
94
|
+
id={nativeID}
|
|
95
|
+
data-testid={testID}
|
|
96
|
+
className={className}
|
|
97
|
+
onClick={onPress}
|
|
98
|
+
disabled={disabled}
|
|
99
|
+
aria-label={accessibilityLabel}
|
|
100
|
+
role={accessibilityRole}
|
|
101
|
+
{...rest}
|
|
102
|
+
>
|
|
103
|
+
{children}
|
|
104
|
+
</button>
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
Pressable.displayName = "Pressable";
|
|
108
|
+
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// TextInput → <input>
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
const TextInput = forwardRef<BridgeTextInputRef, TextInputProps>(
|
|
113
|
+
(
|
|
114
|
+
{
|
|
115
|
+
className,
|
|
116
|
+
value,
|
|
117
|
+
defaultValue,
|
|
118
|
+
placeholder,
|
|
119
|
+
editable,
|
|
120
|
+
disabled,
|
|
121
|
+
onChangeText,
|
|
122
|
+
nativeID,
|
|
123
|
+
testID,
|
|
124
|
+
accessibilityLabelledBy,
|
|
125
|
+
autoFocus,
|
|
126
|
+
secureTextEntry,
|
|
127
|
+
inputMode,
|
|
128
|
+
...rest
|
|
129
|
+
},
|
|
130
|
+
ref,
|
|
131
|
+
) => {
|
|
132
|
+
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
133
|
+
onChangeText?.(event.target.value);
|
|
134
|
+
};
|
|
135
|
+
return (
|
|
136
|
+
<input
|
|
137
|
+
ref={ref as React.Ref<HTMLInputElement>}
|
|
138
|
+
id={nativeID}
|
|
139
|
+
data-testid={testID}
|
|
140
|
+
aria-labelledby={accessibilityLabelledBy}
|
|
141
|
+
className={className}
|
|
142
|
+
value={value}
|
|
143
|
+
defaultValue={defaultValue}
|
|
144
|
+
placeholder={placeholder}
|
|
145
|
+
disabled={disabled}
|
|
146
|
+
readOnly={editable === false}
|
|
147
|
+
autoFocus={autoFocus}
|
|
148
|
+
type={secureTextEntry ? "password" : undefined}
|
|
149
|
+
inputMode={inputMode}
|
|
150
|
+
onChange={handleChange}
|
|
151
|
+
{...rest}
|
|
152
|
+
/>
|
|
153
|
+
);
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
TextInput.displayName = "TextInput";
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// 테마 바인딩
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
/** 색 스킴 → 문서 루트의 .dark 클래스(표준 Tailwind dark 변형 — @custom-variant dark) */
|
|
162
|
+
const applyColorScheme: ApplyColorScheme = (colorScheme) => {
|
|
163
|
+
document.documentElement.classList.toggle("dark", colorScheme === "dark");
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export { View, Text, Pressable, TextInput, applyColorScheme };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Badge — docs/sdd/02-design.md v2 §3.5 그대로.
|
|
2
|
+
// 이중 지정 원칙(§0.5): 배경·테두리는 View, 전경색은 내부 Text에 명시(Button과 동일 패턴).
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Text,
|
|
6
|
+
View,
|
|
7
|
+
type BridgeTextRef,
|
|
8
|
+
type BridgeViewRef,
|
|
9
|
+
type TextProps,
|
|
10
|
+
type ViewProps,
|
|
11
|
+
} from "@unikit-bridge/ui/primitives";
|
|
12
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
13
|
+
|
|
14
|
+
import { cn } from "../lib/utils";
|
|
15
|
+
|
|
16
|
+
// View(컨테이너)
|
|
17
|
+
const badgeVariants = cva(
|
|
18
|
+
"flex flex-row items-center rounded-md border px-2.5 py-0.5",
|
|
19
|
+
{
|
|
20
|
+
variants: {
|
|
21
|
+
variant: {
|
|
22
|
+
default: "border-transparent bg-primary",
|
|
23
|
+
secondary: "border-transparent bg-secondary",
|
|
24
|
+
destructive: "border-transparent bg-destructive",
|
|
25
|
+
outline: "border-border",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
defaultVariants: {
|
|
29
|
+
variant: "default",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// 내부 Text
|
|
35
|
+
const badgeTextVariants = cva("font-sans text-xs font-semibold", {
|
|
36
|
+
variants: {
|
|
37
|
+
variant: {
|
|
38
|
+
default: "text-primary-foreground",
|
|
39
|
+
secondary: "text-secondary-foreground",
|
|
40
|
+
destructive: "text-destructive-foreground",
|
|
41
|
+
outline: "text-foreground",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
defaultVariants: {
|
|
45
|
+
variant: "default",
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export type BadgeProps = ViewProps &
|
|
50
|
+
VariantProps<typeof badgeVariants>;
|
|
51
|
+
|
|
52
|
+
const Badge = forwardRef<BridgeViewRef, BadgeProps>(
|
|
53
|
+
({ className, variant, children, ...props }, ref) => (
|
|
54
|
+
<View
|
|
55
|
+
ref={ref}
|
|
56
|
+
className={cn(badgeVariants({ variant }), className)}
|
|
57
|
+
{...props}
|
|
58
|
+
>
|
|
59
|
+
<Text className={cn(badgeTextVariants({ variant }))}>{children}</Text>
|
|
60
|
+
</View>
|
|
61
|
+
),
|
|
62
|
+
);
|
|
63
|
+
Badge.displayName = "Badge";
|
|
64
|
+
|
|
65
|
+
export { Badge, badgeVariants, badgeTextVariants };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Button — docs/sdd/02-design.md v2 §3.1 그대로.
|
|
2
|
+
// 이중 지정 원칙(§0.5): 배경·테두리는 Pressable, 전경색·밑줄은 내부 Text에 명시.
|
|
3
|
+
// (DOM 브리지에서도 span은 부모 상속이 막히므로 전경색 명시가 양쪽에 필요)
|
|
4
|
+
import { forwardRef, type ReactNode } from "react";
|
|
5
|
+
import {
|
|
6
|
+
Pressable,
|
|
7
|
+
Text,
|
|
8
|
+
type BridgePressableRef,
|
|
9
|
+
type PressableProps,
|
|
10
|
+
} from "@unikit-bridge/ui/primitives";
|
|
11
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
12
|
+
|
|
13
|
+
import { cn } from "../lib/utils";
|
|
14
|
+
|
|
15
|
+
// Pressable(컨테이너) — base + focus-visible(전역)은 §3.1 표의 문자열을 그대로 사용.
|
|
16
|
+
const buttonVariants = cva(
|
|
17
|
+
"flex flex-row items-center justify-center gap-2 rounded-md web:select-none web:focus-visible:ring-2 web:focus-visible:ring-ring web:focus-visible:ring-offset-2 web:focus-visible:ring-offset-background",
|
|
18
|
+
{
|
|
19
|
+
variants: {
|
|
20
|
+
variant: {
|
|
21
|
+
default: "bg-primary web:hover:bg-primary/90 active:bg-primary/90",
|
|
22
|
+
secondary: "bg-secondary web:hover:bg-secondary/80 active:bg-secondary/80",
|
|
23
|
+
destructive:
|
|
24
|
+
"bg-destructive web:hover:bg-destructive/90 active:bg-destructive/90",
|
|
25
|
+
outline:
|
|
26
|
+
"border border-input bg-background web:hover:bg-accent active:bg-accent",
|
|
27
|
+
ghost: "web:hover:bg-accent active:bg-accent",
|
|
28
|
+
link: "",
|
|
29
|
+
},
|
|
30
|
+
size: {
|
|
31
|
+
default: "h-10 px-4 py-2",
|
|
32
|
+
sm: "h-9 px-3",
|
|
33
|
+
lg: "h-11 px-8",
|
|
34
|
+
icon: "h-10 w-10",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
defaultVariants: {
|
|
38
|
+
variant: "default",
|
|
39
|
+
size: "default",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// 내부 Text — 변형별 전경색. active:* 눌림 전파 — 네이티브는 uniwind(Pressable 상태), 웹은 <button>의 :active.
|
|
45
|
+
const buttonTextVariants = cva(
|
|
46
|
+
"font-sans text-sm font-medium web:whitespace-nowrap",
|
|
47
|
+
{
|
|
48
|
+
variants: {
|
|
49
|
+
variant: {
|
|
50
|
+
default: "text-primary-foreground",
|
|
51
|
+
secondary: "text-secondary-foreground",
|
|
52
|
+
destructive: "text-destructive-foreground",
|
|
53
|
+
outline: "text-foreground active:text-accent-foreground",
|
|
54
|
+
ghost: "text-foreground active:text-accent-foreground",
|
|
55
|
+
link: "text-primary underline underline-offset-4",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
defaultVariants: {
|
|
59
|
+
variant: "default",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// 터치 타깃 44px 보정(§3.1): hitSlop = max(0, (44 − 시각높이px) / 2) — 각 방향 px.
|
|
65
|
+
const HIT_SLOP = {
|
|
66
|
+
sm: 4,
|
|
67
|
+
default: 2,
|
|
68
|
+
lg: 0,
|
|
69
|
+
icon: 2,
|
|
70
|
+
} as const;
|
|
71
|
+
|
|
72
|
+
// Pressable의 함수형 children((state) => ReactNode)은 내부 Text 래핑 구조(§3.1)와
|
|
73
|
+
// 양립하지 않으므로 children은 ReactNode로 한정한다(v0.1은 문자열 children 사용).
|
|
74
|
+
export type ButtonProps = PressableProps &
|
|
75
|
+
VariantProps<typeof buttonVariants> & {
|
|
76
|
+
children?: ReactNode;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const Button = forwardRef<BridgePressableRef, ButtonProps>(
|
|
80
|
+
(
|
|
81
|
+
{ className, variant, size, disabled, hitSlop, children, ...props },
|
|
82
|
+
ref,
|
|
83
|
+
) => {
|
|
84
|
+
const resolvedSize = size ?? "default";
|
|
85
|
+
return (
|
|
86
|
+
<Pressable
|
|
87
|
+
ref={ref}
|
|
88
|
+
accessibilityRole="button"
|
|
89
|
+
disabled={disabled}
|
|
90
|
+
aria-disabled={disabled ? true : undefined}
|
|
91
|
+
hitSlop={hitSlop ?? HIT_SLOP[resolvedSize]}
|
|
92
|
+
className={cn(
|
|
93
|
+
buttonVariants({ variant, size }),
|
|
94
|
+
disabled && "web:pointer-events-none opacity-50",
|
|
95
|
+
className,
|
|
96
|
+
)}
|
|
97
|
+
{...props}
|
|
98
|
+
>
|
|
99
|
+
<Text className={cn(buttonTextVariants({ variant }))}>{children}</Text>
|
|
100
|
+
</Pressable>
|
|
101
|
+
);
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
Button.displayName = "Button";
|
|
105
|
+
|
|
106
|
+
export { Button, buttonVariants, buttonTextVariants };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Card — docs/sdd/02-design.md v2 §3.2 그대로. 하위 컴포넌트 5종 포함 총 6종 export.
|
|
2
|
+
// Card의 text-card-foreground는 §0.5(상속 차단)로 자식 Text에 닿지 않아 클래스에서 제외됨.
|
|
3
|
+
// CardContent 안의 본문 텍스트는 사용처에서 색·폰트를 명시한다(§1.2 표기 규칙).
|
|
4
|
+
import { forwardRef } from "react";
|
|
5
|
+
import {
|
|
6
|
+
Text,
|
|
7
|
+
View,
|
|
8
|
+
type BridgeTextRef,
|
|
9
|
+
type BridgeViewRef,
|
|
10
|
+
type TextProps,
|
|
11
|
+
type ViewProps,
|
|
12
|
+
} from "@unikit-bridge/ui/primitives";
|
|
13
|
+
import { cva } from "class-variance-authority";
|
|
14
|
+
|
|
15
|
+
import { cn } from "../lib/utils";
|
|
16
|
+
|
|
17
|
+
const cardVariants = cva("rounded-xl border border-border bg-card shadow-sm");
|
|
18
|
+
const cardHeaderVariants = cva("flex flex-col gap-1.5 p-6");
|
|
19
|
+
const cardTitleVariants = cva(
|
|
20
|
+
"font-sans text-foreground text-2xl web:tracking-tight font-semibold",
|
|
21
|
+
);
|
|
22
|
+
const cardDescriptionVariants = cva("font-sans text-sm text-muted-foreground");
|
|
23
|
+
const cardContentVariants = cva("p-6 pt-0");
|
|
24
|
+
const cardFooterVariants = cva("flex-row items-center gap-2 p-6 pt-0");
|
|
25
|
+
|
|
26
|
+
export type CardProps = ViewProps;
|
|
27
|
+
export type CardHeaderProps = ViewProps;
|
|
28
|
+
export type CardTitleProps = TextProps;
|
|
29
|
+
export type CardDescriptionProps = TextProps;
|
|
30
|
+
export type CardContentProps = ViewProps;
|
|
31
|
+
export type CardFooterProps = ViewProps;
|
|
32
|
+
|
|
33
|
+
const Card = forwardRef<BridgeViewRef, CardProps>(
|
|
34
|
+
({ className, ...props }, ref) => (
|
|
35
|
+
<View
|
|
36
|
+
ref={ref}
|
|
37
|
+
className={cn(cardVariants(), className)}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
Card.displayName = "Card";
|
|
43
|
+
|
|
44
|
+
const CardHeader = forwardRef<BridgeViewRef, CardHeaderProps>(
|
|
45
|
+
({ className, ...props }, ref) => (
|
|
46
|
+
<View
|
|
47
|
+
ref={ref}
|
|
48
|
+
className={cn(cardHeaderVariants(), className)}
|
|
49
|
+
{...props}
|
|
50
|
+
/>
|
|
51
|
+
),
|
|
52
|
+
);
|
|
53
|
+
CardHeader.displayName = "CardHeader";
|
|
54
|
+
|
|
55
|
+
const CardTitle = forwardRef<BridgeTextRef, CardTitleProps>(
|
|
56
|
+
({ className, ...props }, ref) => (
|
|
57
|
+
<Text
|
|
58
|
+
ref={ref}
|
|
59
|
+
className={cn(cardTitleVariants(), className)}
|
|
60
|
+
{...props}
|
|
61
|
+
/>
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
CardTitle.displayName = "CardTitle";
|
|
65
|
+
|
|
66
|
+
const CardDescription = forwardRef<BridgeTextRef, CardDescriptionProps>(
|
|
67
|
+
({ className, ...props }, ref) => (
|
|
68
|
+
<Text
|
|
69
|
+
ref={ref}
|
|
70
|
+
className={cn(cardDescriptionVariants(), className)}
|
|
71
|
+
{...props}
|
|
72
|
+
/>
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
CardDescription.displayName = "CardDescription";
|
|
76
|
+
|
|
77
|
+
const CardContent = forwardRef<BridgeViewRef, CardContentProps>(
|
|
78
|
+
({ className, ...props }, ref) => (
|
|
79
|
+
<View
|
|
80
|
+
ref={ref}
|
|
81
|
+
className={cn(cardContentVariants(), className)}
|
|
82
|
+
{...props}
|
|
83
|
+
/>
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
CardContent.displayName = "CardContent";
|
|
87
|
+
|
|
88
|
+
const CardFooter = forwardRef<BridgeViewRef, CardFooterProps>(
|
|
89
|
+
({ className, ...props }, ref) => (
|
|
90
|
+
<View
|
|
91
|
+
ref={ref}
|
|
92
|
+
className={cn(cardFooterVariants(), className)}
|
|
93
|
+
{...props}
|
|
94
|
+
/>
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
CardFooter.displayName = "CardFooter";
|
|
98
|
+
|
|
99
|
+
export {
|
|
100
|
+
Card,
|
|
101
|
+
CardHeader,
|
|
102
|
+
CardTitle,
|
|
103
|
+
CardDescription,
|
|
104
|
+
CardContent,
|
|
105
|
+
CardFooter,
|
|
106
|
+
cardVariants,
|
|
107
|
+
cardHeaderVariants,
|
|
108
|
+
cardTitleVariants,
|
|
109
|
+
cardDescriptionVariants,
|
|
110
|
+
cardContentVariants,
|
|
111
|
+
cardFooterVariants,
|
|
112
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Input — docs/sdd/02-design.md v2 §3.3 그대로.
|
|
2
|
+
// forwardRef로 입력 인스턴스(focus()·blur())를 노출 — §3.4 Label의
|
|
3
|
+
// onPress → ref.focus() 라벨-입력창 클릭 연결용.
|
|
4
|
+
import { forwardRef } from "react";
|
|
5
|
+
import {
|
|
6
|
+
TextInput,
|
|
7
|
+
type BridgeTextInputRef,
|
|
8
|
+
type TextInputProps,
|
|
9
|
+
} from "@unikit-bridge/ui/primitives";
|
|
10
|
+
import { cva } from "class-variance-authority";
|
|
11
|
+
|
|
12
|
+
import { cn } from "../lib/utils";
|
|
13
|
+
|
|
14
|
+
const inputVariants = cva(
|
|
15
|
+
"font-sans h-10 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base text-foreground placeholder:text-muted-foreground focus:border-ring focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background web:focus-visible:border-ring disabled:opacity-50",
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
// InputProps — TextInputProps 그대로. 웹 관용 disabled의 치환 계약
|
|
19
|
+
// (native: 아래 editable={false} · web: input[disabled] + aria-disabled)은
|
|
20
|
+
// bridge/types.ts의 disabled 주석이 단일 출처다.
|
|
21
|
+
export type InputProps = TextInputProps;
|
|
22
|
+
|
|
23
|
+
const Input = forwardRef<BridgeTextInputRef, InputProps>(
|
|
24
|
+
({ className, disabled, editable, ...props }, ref) => (
|
|
25
|
+
<TextInput
|
|
26
|
+
ref={ref}
|
|
27
|
+
editable={disabled ? false : editable}
|
|
28
|
+
aria-disabled={disabled ? true : undefined}
|
|
29
|
+
className={cn(inputVariants(), className)}
|
|
30
|
+
{...props}
|
|
31
|
+
// 브리지 계약에는 editable 치환으로 이미 반영됐으므로 disabled는 그대로 전달
|
|
32
|
+
// (웹 input[disabled] · RN은 위 editable={false}가 차단)
|
|
33
|
+
disabled={disabled}
|
|
34
|
+
/>
|
|
35
|
+
),
|
|
36
|
+
);
|
|
37
|
+
Input.displayName = "Input";
|
|
38
|
+
|
|
39
|
+
export { Input, inputVariants };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Label — docs/sdd/02-design.md v2 §3.4 그대로.
|
|
2
|
+
//
|
|
3
|
+
// 입력창 연결 방법(§3.4 — RN 프리미티브만으로 <label for>를 렌더할 수 없는 대안):
|
|
4
|
+
// 1. (필수) Label에 nativeID를 지정하고 대상 Input에 accessibilityLabelledBy로 같은 값을 전달
|
|
5
|
+
// 예: <Label nativeID="unikit-label-name">이름</Label>
|
|
6
|
+
// <Input accessibilityLabelledBy="unikit-label-name" ... />
|
|
7
|
+
// → 웹 DOM에서 id ↔ aria-labelledby로 연결된다.
|
|
8
|
+
// 2. (권장) Label의 onPress에서 입력창 ref의 focus()를 호출하면 클릭→포커스가 이어진다.
|
|
9
|
+
// (Input은 forwardRef로 focus() 노출)
|
|
10
|
+
import { forwardRef } from "react";
|
|
11
|
+
import { Text, type BridgeTextRef, type TextProps } from "@unikit-bridge/ui/primitives";
|
|
12
|
+
import { cva } from "class-variance-authority";
|
|
13
|
+
|
|
14
|
+
import { cn } from "../lib/utils";
|
|
15
|
+
|
|
16
|
+
const labelVariants = cva(
|
|
17
|
+
"font-sans text-sm font-medium leading-none text-foreground",
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export type LabelProps = TextProps;
|
|
21
|
+
|
|
22
|
+
const Label = forwardRef<BridgeTextRef, LabelProps>(
|
|
23
|
+
({ className, ...props }, ref) => (
|
|
24
|
+
<Text
|
|
25
|
+
ref={ref}
|
|
26
|
+
className={cn(labelVariants(), className)}
|
|
27
|
+
{...props}
|
|
28
|
+
/>
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
Label.displayName = "Label";
|
|
32
|
+
|
|
33
|
+
export { Label, labelVariants };
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// @unikit-bridge/ui 공개 API (T-6 완성)
|
|
2
|
+
export { cn } from "./lib/utils";
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
Button,
|
|
6
|
+
buttonVariants,
|
|
7
|
+
buttonTextVariants,
|
|
8
|
+
type ButtonProps,
|
|
9
|
+
} from "./components/button";
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
Card,
|
|
13
|
+
CardHeader,
|
|
14
|
+
CardTitle,
|
|
15
|
+
CardDescription,
|
|
16
|
+
CardContent,
|
|
17
|
+
CardFooter,
|
|
18
|
+
cardVariants,
|
|
19
|
+
cardHeaderVariants,
|
|
20
|
+
cardTitleVariants,
|
|
21
|
+
cardDescriptionVariants,
|
|
22
|
+
cardContentVariants,
|
|
23
|
+
cardFooterVariants,
|
|
24
|
+
type CardProps,
|
|
25
|
+
type CardHeaderProps,
|
|
26
|
+
type CardTitleProps,
|
|
27
|
+
type CardDescriptionProps,
|
|
28
|
+
type CardContentProps,
|
|
29
|
+
type CardFooterProps,
|
|
30
|
+
} from "./components/card";
|
|
31
|
+
|
|
32
|
+
export { Input, inputVariants, type InputProps } from "./components/input";
|
|
33
|
+
|
|
34
|
+
export { Label, labelVariants, type LabelProps } from "./components/label";
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
Badge,
|
|
38
|
+
badgeVariants,
|
|
39
|
+
badgeTextVariants,
|
|
40
|
+
type BadgeProps,
|
|
41
|
+
} from "./components/badge";
|
|
42
|
+
|
|
43
|
+
// 테마(T-5가 packages/unikit/src/theme/ 을 병렬 생성) — 디렉터리 존재 여부와
|
|
44
|
+
// 무관하게 이 라인은 항상 유지한다(T-6 지시).
|
|
45
|
+
export * from "./theme";
|
package/src/lib/utils.ts
ADDED