@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
@@ -0,0 +1,173 @@
1
+ // Breadcrumb — shadcn/ui new-york-v4 breadcrumb 포팅(/tmp/shadcn-ref/src/breadcrumb).
2
+ // 7종 export. radix Slot·lucide 아이콘 제거.
3
+ // 포팅 판단:
4
+ // - 아이콘 금지 규칙에 따라 ChevronRight → "›", MoreHorizontal → "⋯" 글리프 Text.
5
+ // - BreadcrumbLink는 Pressable role="link" + onPress(무인자). radix Slot(asChild)은
6
+ // 미지원 — 사용처 라우팅은 onPress에서 호출한다.
7
+ // - BreadcrumbPage의 aria-current는 브리지 계약에 없어 현재 페이지를 시각(색·
8
+ // 굵기)으로만 표현한다. 원본의 role="link"·aria-disabled도 Text 계약에 없어
9
+ // 생략(활성 아닌 링크로 오독 방지).
10
+ // - 원본 <nav>의 내재 role(navigation)도 BridgeRole에 없어 미지원 — 라벨만
11
+ // 유지. 스크린리더 전용(sr-only) 보조 텍스트는 웹 전용 클래스라 생략하되,
12
+ // 원본처럼 장식 요소(Separator·Ellipsis)에는 aria-hidden을 지정한다.
13
+ // - 웹 전용 토큰(transition·hover·break-words·sm: 반응형)은 web: 접두.
14
+ import { forwardRef } from "react";
15
+ import {
16
+ Pressable,
17
+ Text,
18
+ View,
19
+ type BridgePressableRef,
20
+ type BridgeTextRef,
21
+ type BridgeViewRef,
22
+ type PressableProps,
23
+ type TextProps,
24
+ type ViewProps,
25
+ } from "@unikit-bridge/ui/primitives";
26
+ import { cva } from "class-variance-authority";
27
+
28
+ import { cn } from "../lib/utils";
29
+
30
+ const breadcrumbListVariants = cva(
31
+ "flex-row flex-wrap items-center gap-1.5 web:break-words web:sm:gap-2.5",
32
+ );
33
+ const breadcrumbItemVariants = cva("flex-row items-center gap-1.5");
34
+
35
+ // 링크 컨테이너(Pressable) — 투명 배경·무테두리, 전경색은 내부 Text에.
36
+ const breadcrumbLinkVariants = cva("flex-row items-center");
37
+ const breadcrumbLinkTextVariants = cva(
38
+ "font-sans text-sm text-muted-foreground web:transition-colors web:hover:text-foreground active:text-foreground",
39
+ );
40
+
41
+ // 현재 페이지 — 원본 font-normal text-foreground(시각적 구분만).
42
+ const breadcrumbPageVariants = cva(
43
+ "font-sans text-sm font-normal text-foreground",
44
+ );
45
+
46
+ const breadcrumbSeparatorVariants = cva("flex-row items-center");
47
+ const breadcrumbSeparatorTextVariants = cva(
48
+ "font-sans text-sm leading-none text-muted-foreground",
49
+ );
50
+ const breadcrumbEllipsisVariants = cva(
51
+ "h-9 w-9 flex-row items-center justify-center",
52
+ );
53
+ const breadcrumbEllipsisTextVariants = cva(
54
+ "font-sans text-base leading-none text-muted-foreground",
55
+ );
56
+
57
+ export type BreadcrumbProps = ViewProps;
58
+ export type BreadcrumbListProps = ViewProps;
59
+ export type BreadcrumbItemProps = ViewProps;
60
+ export type BreadcrumbLinkProps = PressableProps;
61
+ export type BreadcrumbPageProps = TextProps;
62
+ export type BreadcrumbSeparatorProps = ViewProps;
63
+ export type BreadcrumbEllipsisProps = ViewProps;
64
+
65
+ const Breadcrumb = forwardRef<BridgeViewRef, BreadcrumbProps>(
66
+ ({ className, ...props }, ref) => (
67
+ <View
68
+ ref={ref}
69
+ accessibilityLabel="breadcrumb"
70
+ className={className}
71
+ {...props}
72
+ />
73
+ ),
74
+ );
75
+ Breadcrumb.displayName = "Breadcrumb";
76
+
77
+ const BreadcrumbList = forwardRef<BridgeViewRef, BreadcrumbListProps>(
78
+ ({ className, ...props }, ref) => (
79
+ <View
80
+ ref={ref}
81
+ className={cn(breadcrumbListVariants(), className)}
82
+ {...props}
83
+ />
84
+ ),
85
+ );
86
+ BreadcrumbList.displayName = "BreadcrumbList";
87
+
88
+ const BreadcrumbItem = forwardRef<BridgeViewRef, BreadcrumbItemProps>(
89
+ ({ className, ...props }, ref) => (
90
+ <View
91
+ ref={ref}
92
+ className={cn(breadcrumbItemVariants(), className)}
93
+ {...props}
94
+ />
95
+ ),
96
+ );
97
+ BreadcrumbItem.displayName = "BreadcrumbItem";
98
+
99
+ const BreadcrumbLink = forwardRef<BridgePressableRef, BreadcrumbLinkProps>(
100
+ ({ className, children, ...props }, ref) => (
101
+ <Pressable
102
+ ref={ref}
103
+ accessibilityRole="link"
104
+ className={cn(breadcrumbLinkVariants(), className)}
105
+ {...props}
106
+ >
107
+ <Text className={cn(breadcrumbLinkTextVariants())}>{children}</Text>
108
+ </Pressable>
109
+ ),
110
+ );
111
+ BreadcrumbLink.displayName = "BreadcrumbLink";
112
+
113
+ const BreadcrumbPage = forwardRef<BridgeTextRef, BreadcrumbPageProps>(
114
+ ({ className, ...props }, ref) => (
115
+ <Text
116
+ ref={ref}
117
+ className={cn(breadcrumbPageVariants(), className)}
118
+ {...props}
119
+ />
120
+ ),
121
+ );
122
+ BreadcrumbPage.displayName = "BreadcrumbPage";
123
+
124
+ // 구분 글리프 — children 미지정 시 기본 "›"(원본 ChevronRight 대체).
125
+ const BreadcrumbSeparator = forwardRef<BridgeViewRef, BreadcrumbSeparatorProps>(
126
+ ({ className, children, ...props }, ref) => (
127
+ <View
128
+ ref={ref}
129
+ aria-hidden
130
+ className={cn(breadcrumbSeparatorVariants(), className)}
131
+ {...props}
132
+ >
133
+ <Text className={cn(breadcrumbSeparatorTextVariants())}>
134
+ {children ?? "›"}
135
+ </Text>
136
+ </View>
137
+ ),
138
+ );
139
+ BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
140
+
141
+ // 생략 글리프 — "⋯"(원본 MoreHorizontal 대체).
142
+ const BreadcrumbEllipsis = forwardRef<BridgeViewRef, BreadcrumbEllipsisProps>(
143
+ ({ className, ...props }, ref) => (
144
+ <View
145
+ ref={ref}
146
+ aria-hidden
147
+ className={cn(breadcrumbEllipsisVariants(), className)}
148
+ {...props}
149
+ >
150
+ <Text className={cn(breadcrumbEllipsisTextVariants())}>⋯</Text>
151
+ </View>
152
+ ),
153
+ );
154
+ BreadcrumbEllipsis.displayName = "BreadcrumbEllipsis";
155
+
156
+ export {
157
+ Breadcrumb,
158
+ BreadcrumbList,
159
+ BreadcrumbItem,
160
+ BreadcrumbLink,
161
+ BreadcrumbPage,
162
+ BreadcrumbSeparator,
163
+ BreadcrumbEllipsis,
164
+ breadcrumbListVariants,
165
+ breadcrumbItemVariants,
166
+ breadcrumbLinkVariants,
167
+ breadcrumbLinkTextVariants,
168
+ breadcrumbPageVariants,
169
+ breadcrumbSeparatorVariants,
170
+ breadcrumbSeparatorTextVariants,
171
+ breadcrumbEllipsisVariants,
172
+ breadcrumbEllipsisTextVariants,
173
+ };
@@ -0,0 +1,166 @@
1
+ // ButtonGroup — shadcn/ui new-york-v4 button-group을 UniKit 유니버설로 이식.
2
+ //
3
+ // 포팅 판단(자식 셀렉터 제거):
4
+ // - 원본의 붙임(attach) 처리는 자식 셀렉터([&>*:not(:first-child)]:
5
+ // rounded-l-none 등)로 동작한다. 네이티브에 자식 셀렉터가 없으므로
6
+ // ButtonGroup이 Children.map으로 각 자식 요소에 위치 클래스(첫/끝 모서리
7
+ // 정리·테두리 중복 제거·포커스 z-순서)를 cloneElement로 주입한다 —
8
+ // CSS 셀렉터 없이 양쪽에서 동일하게 동작한다(Button 등 기존 컴포넌트의
9
+ // className 병합 규칙(twMerge)과 맞물려 나중 값이 이긴다).
10
+ // - data-slot 기반 중첩 그룹 간격 토큰(gap-2)과 select-trigger
11
+ // 관련 토큰은 data-slot 없이 표현 불가 → 제거.
12
+ // - ButtonGroupText의 asChild(radix Slot) 제거 — View+Text 구조 고정
13
+ // (이중 지정: 전경색은 내부 Text에).
14
+ // - ButtonGroupSeparator가 감싸던 Separator는 인라인 View(bg-border)로 대리
15
+ // (신규 파일 6종 제한 — separator.tsx 생성 불가).
16
+ import {
17
+ Children,
18
+ cloneElement,
19
+ forwardRef,
20
+ isValidElement,
21
+ type ReactElement,
22
+ type ReactNode,
23
+ } from "react";
24
+ import {
25
+ Text,
26
+ View,
27
+ type BridgeViewRef,
28
+ type ViewProps,
29
+ } from "@unikit-bridge/ui/primitives";
30
+ import { cva, type VariantProps } from "class-variance-authority";
31
+
32
+ import { cn } from "../lib/utils";
33
+
34
+ // 그룹 뼈대 — 원본의 방향 변형. 모서리 붙임 토큰은 아래 주입 로직으로 이동.
35
+ // w-fit은 uniwind 미지원 — self-start 치환으로 양쪽 플랫폼 내용 폭을 맞춘다(M-2).
36
+ const buttonGroupVariants = cva("flex self-start items-stretch", {
37
+ variants: {
38
+ orientation: {
39
+ horizontal: "flex-row",
40
+ vertical: "flex-col",
41
+ },
42
+ },
43
+ defaultVariants: {
44
+ orientation: "horizontal",
45
+ },
46
+ });
47
+
48
+ type ButtonGroupOrientation = "horizontal" | "vertical";
49
+
50
+ // 위치별 붙임 클래스 — 원본 자식 셀렉터의 치환.
51
+ // 첫 자식은 시작 모서리를, 끝 자식은 끝 모서리를 유지하고 나머지 모서리를
52
+ // 없애 붙은 모양을 만든다. 이웃 테두리 중복(border-l-0/border-t-0)도 정리.
53
+ function buttonGroupAttachClasses(
54
+ index: number,
55
+ total: number,
56
+ orientation: ButtonGroupOrientation,
57
+ ): string {
58
+ const isFirst = index === 0;
59
+ const isLast = index === total - 1;
60
+ const horizontal = orientation === "horizontal";
61
+ return cn(
62
+ // 포커스 받은 자식이 위로 — 원본 [&>*]:focus-visible:relative/z-10 치환
63
+ "web:focus-visible:relative web:focus-visible:z-10",
64
+ !isFirst &&
65
+ (horizontal ? "rounded-l-none border-l-0" : "rounded-t-none border-t-0"),
66
+ !isLast && (horizontal ? "rounded-r-none" : "rounded-b-none"),
67
+ );
68
+ }
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // ButtonGroup — 버튼 무리 컨테이너. 자식에 붙임 클래스를 주입한다.
72
+ // ---------------------------------------------------------------------------
73
+ export type ButtonGroupProps = ViewProps &
74
+ VariantProps<typeof buttonGroupVariants> & {
75
+ children?: ReactNode;
76
+ };
77
+
78
+ const ButtonGroup = forwardRef<BridgeViewRef, ButtonGroupProps>(
79
+ ({ className, orientation, children, ...props }, ref) => {
80
+ const resolvedOrientation: ButtonGroupOrientation =
81
+ orientation ?? "horizontal";
82
+ const items = Children.toArray(children);
83
+ const attached = items.map((child, index) => {
84
+ // 문자열 등 비(非)요소 자식은 그대로 둔다(Children.toArray가 null·
85
+ // boolean은 이미 걷어냈다). React 19 타입은 unknown props로 좁히므로
86
+ // className을 받는 요소로 단언해 cloneElement한다.
87
+ if (!isValidElement(child)) {
88
+ return child;
89
+ }
90
+ const typedChild = child as ReactElement<{ className?: string }>;
91
+ return cloneElement(typedChild, {
92
+ className: cn(
93
+ typedChild.props.className,
94
+ buttonGroupAttachClasses(index, items.length, resolvedOrientation),
95
+ ),
96
+ });
97
+ });
98
+ return (
99
+ <View
100
+ ref={ref}
101
+ className={cn(buttonGroupVariants({ orientation }), className)}
102
+ {...props}
103
+ >
104
+ {attached}
105
+ </View>
106
+ );
107
+ },
108
+ );
109
+ ButtonGroup.displayName = "ButtonGroup";
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // ButtonGroupText — 그룹 안 텍스트 조각(예: 승수 "×2"). View+Text 구조.
113
+ // ---------------------------------------------------------------------------
114
+ export type ButtonGroupTextProps = ViewProps & {
115
+ children?: ReactNode;
116
+ };
117
+
118
+ const ButtonGroupText = forwardRef<BridgeViewRef, ButtonGroupTextProps>(
119
+ ({ className, children, ...props }, ref) => (
120
+ <View
121
+ ref={ref}
122
+ className={cn(
123
+ "flex items-center gap-2 rounded-md border border-border bg-muted px-4 web:shadow-xs",
124
+ className,
125
+ )}
126
+ {...props}
127
+ >
128
+ {/* 이중 지정 원칙: 배경·테두리는 View, 전경색은 내부 Text에 */}
129
+ <Text className="font-sans text-sm font-medium text-foreground">
130
+ {children}
131
+ </Text>
132
+ </View>
133
+ ),
134
+ );
135
+ ButtonGroupText.displayName = "ButtonGroupText";
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // ButtonGroupSeparator — 그룹 안 구분선. 기본 vertical(가로 무리의 세로 줄).
139
+ // ---------------------------------------------------------------------------
140
+ export type ButtonGroupSeparatorProps = ViewProps & {
141
+ /** 구분선 방향 — 원본 기본값 vertical 유지(self-stretch로 교차축 채움) */
142
+ orientation?: "horizontal" | "vertical";
143
+ };
144
+
145
+ const ButtonGroupSeparator = forwardRef<
146
+ BridgeViewRef,
147
+ ButtonGroupSeparatorProps
148
+ >(({ className, orientation = "vertical", ...props }, ref) => (
149
+ <View
150
+ ref={ref}
151
+ className={cn(
152
+ "self-stretch bg-border",
153
+ orientation === "vertical" ? "w-px" : "h-px w-full",
154
+ className,
155
+ )}
156
+ {...props}
157
+ />
158
+ ));
159
+ ButtonGroupSeparator.displayName = "ButtonGroupSeparator";
160
+
161
+ export {
162
+ ButtonGroup,
163
+ ButtonGroupSeparator,
164
+ ButtonGroupText,
165
+ buttonGroupVariants,
166
+ };
@@ -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,94 @@
1
+ // Checkbox — shadcn/ui new-york-v4 checkbox를 UniKit 브리지로 이식.
2
+ // 포팅 판단:
3
+ // - radix-ui Checkbox(Root/Indicator) 제거 — 체크 상태는 React state(제어/비제어)로
4
+ // 직접 관리하고, 원본의 data-state=checked 셀렉터는 조건부 클래스 문자열로 전개
5
+ // (네이티브에 data 속성이 없어 상태 셀렉터를 쓸 수 없다).
6
+ // - CheckIcon(lucide) 제거 — ✓ 글리프를 Text children으로 대체(새 의존성 금지).
7
+ // - aria-invalid:* 클래스는 v0.1 API에 invalid 상태가 없어 제외.
8
+ // - 이중 지정 원칙(§0.5): 체크 배경·테두리는 Pressable, 글리프 색은 내부 Text에 명시.
9
+ import { forwardRef, useState } from "react";
10
+ import {
11
+ Pressable,
12
+ Text,
13
+ type BridgePressableRef,
14
+ type PressableProps,
15
+ } from "@unikit-bridge/ui/primitives";
16
+
17
+ import { cn } from "../lib/utils";
18
+
19
+ // 원본 크기 토큰 유지: size-4(16px) 박스·rounded-sm(UniKit 토큰 --radius-sm=4px)·border-input,
20
+ // 체크 시 border/bg-primary.
21
+ // 라벨 없음 — 라벨은 소비자가 Label과 조합한다(checkbox 원본과 동일한 계약).
22
+ export type CheckboxProps = Omit<PressableProps, "onPress" | "aria-checked"> & {
23
+ /** 제어형 체크 상태(지정 시 내부 state를 대체) */
24
+ checked?: boolean;
25
+ /** 비제어형 초기 체크 상태 */
26
+ defaultChecked?: boolean;
27
+ /** 체크 전환 콜백 — indeterminate 상태에서 누르면 false가 전달된다 */
28
+ onCheckedChange?: (checked: boolean) => void;
29
+ /** 중간 상태 — aria-checked="mixed" + − 글리프(shadcn checked="indeterminate" 대응) */
30
+ indeterminate?: boolean;
31
+ };
32
+
33
+ // 터치 타깃 44px 보정: 16px 박스 → hitSlop = (44 − 16) / 2 = 14px.
34
+ const HIT_SLOP = 14;
35
+
36
+ const Checkbox = forwardRef<BridgePressableRef, CheckboxProps>(
37
+ (
38
+ {
39
+ className,
40
+ checked,
41
+ defaultChecked = false,
42
+ onCheckedChange,
43
+ indeterminate = false,
44
+ disabled,
45
+ hitSlop,
46
+ ...props
47
+ },
48
+ ref,
49
+ ) => {
50
+ // 제어(checked 지정)/비제어(내부 state) 통합 — 웹·네이티브 한 벌 로직.
51
+ const [uncontrolled, setUncontrolled] = useState(defaultChecked);
52
+ const isControlled = checked !== undefined;
53
+ const isChecked = isControlled ? checked : uncontrolled;
54
+
55
+ const handlePress = () => {
56
+ const next = !isChecked;
57
+ if (!isControlled) setUncontrolled(next);
58
+ onCheckedChange?.(next);
59
+ };
60
+
61
+ return (
62
+ <Pressable
63
+ ref={ref}
64
+ accessibilityRole="checkbox"
65
+ aria-checked={indeterminate ? "mixed" : isChecked}
66
+ aria-disabled={disabled ? true : undefined}
67
+ disabled={disabled}
68
+ hitSlop={hitSlop ?? HIT_SLOP}
69
+ onPress={handlePress}
70
+ className={cn(
71
+ "flex size-4 shrink-0 flex-row items-center justify-center rounded-sm border web:shadow-xs web:transition-shadow web:outline-none web:focus-visible:border-ring web:focus-visible:ring-2 web:focus-visible:ring-ring/50",
72
+ // 미체크 링은 border-input(라이트 1.27:1)이 배경에 묻혀 경계가 안
73
+ // 보이므로 foreground/50(양 모드 ≥3:1, G4)로 조정.
74
+ indeterminate || isChecked
75
+ ? "border-primary bg-primary"
76
+ : "border-foreground/50 dark:bg-input/30",
77
+ disabled && "web:pointer-events-none web:cursor-not-allowed opacity-50",
78
+ className,
79
+ )}
80
+ {...props}
81
+ >
82
+ {/* 글리프 — 체크 ✓ / 중간 −. 원본 Indicator처럼 상태일 때만 렌더. */}
83
+ {indeterminate || isChecked ? (
84
+ <Text className="font-sans text-sm font-bold leading-none text-primary-foreground">
85
+ {indeterminate ? "−" : "✓"}
86
+ </Text>
87
+ ) : null}
88
+ </Pressable>
89
+ );
90
+ },
91
+ );
92
+ Checkbox.displayName = "Checkbox";
93
+
94
+ export { Checkbox };