@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
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// Item — shadcn/ui new-york-v4 item(193줄)을 UniKit 유니버설로 이식.
|
|
2
|
+
//
|
|
3
|
+
// 포팅 판단:
|
|
4
|
+
// - asChild(radix Slot) 제거 — View 렌더 고정.
|
|
5
|
+
// - data-variant/data-size 속성·group/item·group-has-...·[&_svg]·[&>a] 셀렉터
|
|
6
|
+
// 제거(네이티브에 data 속성 없음). 토큰은 cva 변형으로 그대로 보존.
|
|
7
|
+
// - ItemMedia는 순수 컨테이너 — 소비자가 글리프 Text 또는 Image를 넣는다
|
|
8
|
+
// (이중 지정: 글리프 색·크기는 소비자 Text에 명시). image 변형의
|
|
9
|
+
// [&_img]:size-full은 내부 Image에 직접 지정하도록 안내로 대체.
|
|
10
|
+
// - line-clamp-2·text-balance는 웹 전용 → web: 접두(네이티브은 전문 전체 표시).
|
|
11
|
+
// - ItemSeparator는 인라인 View(h-px bg-border) — 원본 Separator 대리
|
|
12
|
+
// (신규 파일 6종 제한).
|
|
13
|
+
// - focus-visible 링·transition은 웹 전용 → web: 접두.
|
|
14
|
+
import { forwardRef } from "react";
|
|
15
|
+
import {
|
|
16
|
+
Text,
|
|
17
|
+
View,
|
|
18
|
+
type BridgeTextRef,
|
|
19
|
+
type BridgeViewRef,
|
|
20
|
+
type TextProps,
|
|
21
|
+
type ViewProps,
|
|
22
|
+
} from "@unikit-bridge/ui/primitives";
|
|
23
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
24
|
+
|
|
25
|
+
import { cn } from "../lib/utils";
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// ItemGroup — 리스트 묶음(role="list").
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
export type ItemGroupProps = ViewProps;
|
|
31
|
+
|
|
32
|
+
const ItemGroup = forwardRef<BridgeViewRef, ItemGroupProps>(
|
|
33
|
+
({ className, ...props }, ref) => (
|
|
34
|
+
<View
|
|
35
|
+
ref={ref}
|
|
36
|
+
accessibilityRole="list"
|
|
37
|
+
className={cn("flex flex-col", className)}
|
|
38
|
+
{...props}
|
|
39
|
+
/>
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
ItemGroup.displayName = "ItemGroup";
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// ItemSeparator — 항목 사이 가로 구분선(원본 Separator 대리).
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
export type ItemSeparatorProps = ViewProps;
|
|
48
|
+
|
|
49
|
+
const ItemSeparator = forwardRef<BridgeViewRef, ItemSeparatorProps>(
|
|
50
|
+
({ className, ...props }, ref) => (
|
|
51
|
+
<View
|
|
52
|
+
ref={ref}
|
|
53
|
+
className={cn("h-px w-full bg-border", className)}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
ItemSeparator.displayName = "ItemSeparator";
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Item — 리스트 아이템 카드(원본 변형·토큰 그대로).
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
const itemVariants = cva(
|
|
64
|
+
"flex flex-wrap items-center rounded-md border border-transparent text-sm web:transition-colors web:duration-100 web:outline-none web:focus-visible:border-ring web:focus-visible:ring-2 web:focus-visible:ring-ring/50",
|
|
65
|
+
{
|
|
66
|
+
variants: {
|
|
67
|
+
variant: {
|
|
68
|
+
default: "bg-transparent",
|
|
69
|
+
outline: "border-border",
|
|
70
|
+
muted: "bg-muted/50",
|
|
71
|
+
},
|
|
72
|
+
size: {
|
|
73
|
+
default: "gap-4 p-4",
|
|
74
|
+
sm: "gap-2.5 px-4 py-3",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
defaultVariants: {
|
|
78
|
+
variant: "default",
|
|
79
|
+
size: "default",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
export type ItemProps = ViewProps & VariantProps<typeof itemVariants>;
|
|
85
|
+
|
|
86
|
+
const Item = forwardRef<BridgeViewRef, ItemProps>(
|
|
87
|
+
({ className, variant, size, ...props }, ref) => (
|
|
88
|
+
<View
|
|
89
|
+
ref={ref}
|
|
90
|
+
className={cn(itemVariants({ variant, size }), className)}
|
|
91
|
+
{...props}
|
|
92
|
+
/>
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
Item.displayName = "Item";
|
|
96
|
+
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// ItemMedia — 미디어 영역(글리프 Text 또는 Image를 소비자가 선택).
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
const itemMediaVariants = cva(
|
|
101
|
+
"flex shrink-0 items-center justify-center gap-2",
|
|
102
|
+
{
|
|
103
|
+
variants: {
|
|
104
|
+
variant: {
|
|
105
|
+
default: "bg-transparent",
|
|
106
|
+
icon: "size-8 rounded-sm border border-border bg-muted",
|
|
107
|
+
image: "size-10 overflow-hidden rounded-sm",
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
defaultVariants: {
|
|
111
|
+
variant: "default",
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
export type ItemMediaProps = ViewProps &
|
|
117
|
+
VariantProps<typeof itemMediaVariants>;
|
|
118
|
+
|
|
119
|
+
const ItemMedia = forwardRef<BridgeViewRef, ItemMediaProps>(
|
|
120
|
+
({ className, variant, ...props }, ref) => (
|
|
121
|
+
<View
|
|
122
|
+
ref={ref}
|
|
123
|
+
className={cn(itemMediaVariants({ variant }), className)}
|
|
124
|
+
{...props}
|
|
125
|
+
/>
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
ItemMedia.displayName = "ItemMedia";
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// ItemContent — 제목·설명 본문 영역.
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
export type ItemContentProps = ViewProps;
|
|
134
|
+
|
|
135
|
+
const ItemContent = forwardRef<BridgeViewRef, ItemContentProps>(
|
|
136
|
+
({ className, ...props }, ref) => (
|
|
137
|
+
<View
|
|
138
|
+
ref={ref}
|
|
139
|
+
className={cn("flex flex-1 flex-col gap-1", className)}
|
|
140
|
+
{...props}
|
|
141
|
+
/>
|
|
142
|
+
),
|
|
143
|
+
);
|
|
144
|
+
ItemContent.displayName = "ItemContent";
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// ItemTitle — 항목 제목(원본 flex/gap는 Text 레이아웃이라 제거, 토큰만 유지).
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
export type ItemTitleProps = TextProps;
|
|
150
|
+
|
|
151
|
+
const ItemTitle = forwardRef<BridgeTextRef, ItemTitleProps>(
|
|
152
|
+
({ className, ...props }, ref) => (
|
|
153
|
+
<Text
|
|
154
|
+
ref={ref}
|
|
155
|
+
className={cn(
|
|
156
|
+
"w-fit font-sans text-sm font-medium leading-snug text-foreground",
|
|
157
|
+
className,
|
|
158
|
+
)}
|
|
159
|
+
{...props}
|
|
160
|
+
/>
|
|
161
|
+
),
|
|
162
|
+
);
|
|
163
|
+
ItemTitle.displayName = "ItemTitle";
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// ItemDescription — 항목 보조 설명.
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
export type ItemDescriptionProps = TextProps;
|
|
169
|
+
|
|
170
|
+
const ItemDescription = forwardRef<BridgeTextRef, ItemDescriptionProps>(
|
|
171
|
+
({ className, ...props }, ref) => (
|
|
172
|
+
<Text
|
|
173
|
+
ref={ref}
|
|
174
|
+
className={cn(
|
|
175
|
+
"font-sans text-sm font-normal leading-normal text-muted-foreground web:line-clamp-2 web:text-balance",
|
|
176
|
+
className,
|
|
177
|
+
)}
|
|
178
|
+
{...props}
|
|
179
|
+
/>
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
ItemDescription.displayName = "ItemDescription";
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// ItemActions — 우측 액션 영역.
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
export type ItemActionsProps = ViewProps;
|
|
188
|
+
|
|
189
|
+
const ItemActions = forwardRef<BridgeViewRef, ItemActionsProps>(
|
|
190
|
+
({ className, ...props }, ref) => (
|
|
191
|
+
<View
|
|
192
|
+
ref={ref}
|
|
193
|
+
className={cn("flex items-center gap-2", className)}
|
|
194
|
+
{...props}
|
|
195
|
+
/>
|
|
196
|
+
),
|
|
197
|
+
);
|
|
198
|
+
ItemActions.displayName = "ItemActions";
|
|
199
|
+
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// ItemHeader — 항목 헤더(basis-full로 첫 줄을 차지).
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
export type ItemHeaderProps = ViewProps;
|
|
204
|
+
|
|
205
|
+
const ItemHeader = forwardRef<BridgeViewRef, ItemHeaderProps>(
|
|
206
|
+
({ className, ...props }, ref) => (
|
|
207
|
+
<View
|
|
208
|
+
ref={ref}
|
|
209
|
+
className={cn("flex basis-full items-center justify-between gap-2", className)}
|
|
210
|
+
{...props}
|
|
211
|
+
/>
|
|
212
|
+
),
|
|
213
|
+
);
|
|
214
|
+
ItemHeader.displayName = "ItemHeader";
|
|
215
|
+
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// ItemFooter — 항목 푸터(헤더와 동일 구조).
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
export type ItemFooterProps = ViewProps;
|
|
220
|
+
|
|
221
|
+
const ItemFooter = forwardRef<BridgeViewRef, ItemFooterProps>(
|
|
222
|
+
({ className, ...props }, ref) => (
|
|
223
|
+
<View
|
|
224
|
+
ref={ref}
|
|
225
|
+
className={cn("flex basis-full items-center justify-between gap-2", className)}
|
|
226
|
+
{...props}
|
|
227
|
+
/>
|
|
228
|
+
),
|
|
229
|
+
);
|
|
230
|
+
ItemFooter.displayName = "ItemFooter";
|
|
231
|
+
|
|
232
|
+
export {
|
|
233
|
+
Item,
|
|
234
|
+
ItemMedia,
|
|
235
|
+
ItemContent,
|
|
236
|
+
ItemActions,
|
|
237
|
+
ItemGroup,
|
|
238
|
+
ItemSeparator,
|
|
239
|
+
ItemTitle,
|
|
240
|
+
ItemDescription,
|
|
241
|
+
ItemHeader,
|
|
242
|
+
ItemFooter,
|
|
243
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Kbd — shadcn/ui new-york-v4 kbd 포팅(/tmp/shadcn-ref/src/kbd). Kbd·KbdGroup 2종.
|
|
2
|
+
// 포팅 판단:
|
|
3
|
+
// - <kbd> 요소는 브리지 계약에 없어 View(배경·크기) + 내부 Text(글자)의
|
|
4
|
+
// 이중 지정으로 재현한다. children은 문자열("⌘", "Ctrl", "K" 등)을 권장한다.
|
|
5
|
+
// - 원본의 svg·tooltip 연동 셀렉터([&_svg...]·[[data-slot=tooltip-content]_&])는
|
|
6
|
+
// 아이콘·툴팁 미지원 포팅이라 소실.
|
|
7
|
+
// - pointer-events-none·select-none은 웹 전용 접두. KbdGroup의 원본이 <div>가
|
|
8
|
+
// 아니라 <kbd>를 다시 쓰는 업스트림 특성은 View로 정리했다.
|
|
9
|
+
import { forwardRef } from "react";
|
|
10
|
+
import {
|
|
11
|
+
Text,
|
|
12
|
+
View,
|
|
13
|
+
type BridgeViewRef,
|
|
14
|
+
type ViewProps,
|
|
15
|
+
} from "@unikit-bridge/ui/primitives";
|
|
16
|
+
import { cva } from "class-variance-authority";
|
|
17
|
+
|
|
18
|
+
import { cn } from "../lib/utils";
|
|
19
|
+
|
|
20
|
+
// View(컨테이너) — 원본 토큰 그대로(h-5 min-w-5 px-1 rounded-sm bg-muted).
|
|
21
|
+
const kbdVariants = cva(
|
|
22
|
+
"flex-row h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm bg-muted px-1 web:pointer-events-none web:select-none",
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// 내부 Text
|
|
26
|
+
const kbdTextVariants = cva(
|
|
27
|
+
"font-sans text-xs font-medium text-muted-foreground",
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// 키 조합 그룹 — Kbd 여러 개를 묶는 인라인 컨테이너(원본 inline-flex gap-1).
|
|
31
|
+
// w-fit은 uniwind 미지원 — tabs.tsx와 동일하게 self-start(교차축 시작 정렬로
|
|
32
|
+
// 내용 폭 수축)로 치환해 양쪽 플랫폼 폭을 맞춘다(M-2).
|
|
33
|
+
const kbdGroupVariants = cva("flex-row self-start items-center gap-1");
|
|
34
|
+
|
|
35
|
+
export type KbdProps = ViewProps;
|
|
36
|
+
export type KbdGroupProps = ViewProps;
|
|
37
|
+
|
|
38
|
+
const Kbd = forwardRef<BridgeViewRef, KbdProps>(
|
|
39
|
+
({ className, children, ...props }, ref) => (
|
|
40
|
+
<View
|
|
41
|
+
ref={ref}
|
|
42
|
+
className={cn(kbdVariants(), className)}
|
|
43
|
+
{...props}
|
|
44
|
+
>
|
|
45
|
+
<Text className={cn(kbdTextVariants())}>{children}</Text>
|
|
46
|
+
</View>
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
Kbd.displayName = "Kbd";
|
|
50
|
+
|
|
51
|
+
const KbdGroup = forwardRef<BridgeViewRef, KbdGroupProps>(
|
|
52
|
+
({ className, ...props }, ref) => (
|
|
53
|
+
<View
|
|
54
|
+
ref={ref}
|
|
55
|
+
className={cn(kbdGroupVariants(), className)}
|
|
56
|
+
{...props}
|
|
57
|
+
/>
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
KbdGroup.displayName = "KbdGroup";
|
|
61
|
+
|
|
62
|
+
export { Kbd, KbdGroup, kbdVariants, kbdTextVariants, kbdGroupVariants };
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// Pagination — shadcn/ui new-york-v4 pagination 포팅(/tmp/shadcn-ref/src/pagination).
|
|
2
|
+
// 7종 export. lucide 아이콘 제거.
|
|
3
|
+
// 포팅 판단:
|
|
4
|
+
// - 원본은 <a>에 buttonVariants를 얹는다 — 포팅은 Pressable role="link"에
|
|
5
|
+
// ./button의 buttonVariants(컨테이너) + buttonTextVariants(내부 Text 전경색,
|
|
6
|
+
// 이중 지정)를 함께 얹는다. import 경로 "./button"은 양 트리(src·registry)에서
|
|
7
|
+
// 동일하다.
|
|
8
|
+
// - 원본의 data-active·aria-current="page" → isActive?: boolean prop 조건부
|
|
9
|
+
// 클래스(ghost↔outline 변형 전환)로 대체. aria-current는 브리지 계약에 없어
|
|
10
|
+
// 미지원.
|
|
11
|
+
// - ChevronLeft/Right → "‹"/"›" 글리프 Text. Previous/Next 라벨은 원본의
|
|
12
|
+
// sm 미만 숨김(hidden sm:block)을 web:hidden web:sm:inline으로 재현 —
|
|
13
|
+
// 네이티브에서는 항상 표시된다(터치 가독성).
|
|
14
|
+
// - PaginationLink의 children이 문자열(숫자 포함)이면 이중 지정 원칙대로 내부
|
|
15
|
+
// Text로 감싸고, ReactNode면 그대로 렌더한다(Previous/Next는 글리프+라벨
|
|
16
|
+
// 두 Text를 flex 간격으로 배치하기 위해 노드를 직접 넘긴다).
|
|
17
|
+
// - 원본 <nav>의 role="navigation"은 BridgeRole에 없어 미지원 — 라벨만 유지.
|
|
18
|
+
// sr-only 보조 텍스트는 웹 전용 클래스라 생략(Ellipsis는 어차피 aria-hidden).
|
|
19
|
+
// - 터치 타깃 44px 보정은 button.tsx와 같은 hitSlop 기준(§3.1).
|
|
20
|
+
import { forwardRef, type ReactNode } from "react";
|
|
21
|
+
import {
|
|
22
|
+
Pressable,
|
|
23
|
+
Text,
|
|
24
|
+
View,
|
|
25
|
+
type BridgePressableRef,
|
|
26
|
+
type BridgeViewRef,
|
|
27
|
+
type PressableProps,
|
|
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
|
+
import { buttonVariants, buttonTextVariants } from "./button";
|
|
34
|
+
|
|
35
|
+
const paginationVariants = cva("mx-auto w-full flex-row justify-center");
|
|
36
|
+
const paginationContentVariants = cva("flex-row items-center gap-1");
|
|
37
|
+
|
|
38
|
+
// 방향 글리프(‹ ›) — 원본 ChevronIcon 대체.
|
|
39
|
+
const paginationGlyphVariants = cva("text-base leading-none text-foreground");
|
|
40
|
+
// Previous/Next 라벨 — 웹에서는 sm 미만 숨김(원본 hidden sm:block 재현).
|
|
41
|
+
const paginationLabelVariants = cva(
|
|
42
|
+
"font-sans text-sm font-medium text-foreground web:whitespace-nowrap web:hidden web:sm:inline",
|
|
43
|
+
);
|
|
44
|
+
const paginationEllipsisVariants = cva(
|
|
45
|
+
"h-9 w-9 flex-row items-center justify-center",
|
|
46
|
+
);
|
|
47
|
+
const paginationEllipsisTextVariants = cva(
|
|
48
|
+
"text-base leading-none text-foreground",
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// 터치 타깃 보정 — button.tsx의 HIT_SLOP과 같은 기준(44px − 시각높이)/2.
|
|
52
|
+
const HIT_SLOP = {
|
|
53
|
+
default: 2,
|
|
54
|
+
sm: 4,
|
|
55
|
+
lg: 0,
|
|
56
|
+
icon: 2,
|
|
57
|
+
} as const;
|
|
58
|
+
|
|
59
|
+
export type PaginationProps = ViewProps;
|
|
60
|
+
export type PaginationContentProps = ViewProps;
|
|
61
|
+
export type PaginationItemProps = ViewProps;
|
|
62
|
+
export type PaginationEllipsisProps = ViewProps;
|
|
63
|
+
|
|
64
|
+
export type PaginationLinkProps = PressableProps &
|
|
65
|
+
Pick<VariantProps<typeof buttonVariants>, "size"> & {
|
|
66
|
+
/** 현재 페이지 — 원본 data-active 대체(outline 변형으로 강조) */
|
|
67
|
+
isActive?: boolean;
|
|
68
|
+
children?: ReactNode;
|
|
69
|
+
};
|
|
70
|
+
export type PaginationPreviousProps = PaginationLinkProps & {
|
|
71
|
+
/** 이전 라벨 텍스트(기본 "Previous" — 현지화 시 지정, G4) */
|
|
72
|
+
label?: string;
|
|
73
|
+
};
|
|
74
|
+
export type PaginationNextProps = PaginationLinkProps & {
|
|
75
|
+
/** 다음 라벨 텍스트(기본 "Next" — 현지화 시 지정, G4) */
|
|
76
|
+
label?: string;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const Pagination = forwardRef<BridgeViewRef, PaginationProps>(
|
|
80
|
+
({ className, ...props }, ref) => (
|
|
81
|
+
<View
|
|
82
|
+
ref={ref}
|
|
83
|
+
accessibilityLabel="pagination"
|
|
84
|
+
className={cn(paginationVariants(), className)}
|
|
85
|
+
{...props}
|
|
86
|
+
/>
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
Pagination.displayName = "Pagination";
|
|
90
|
+
|
|
91
|
+
const PaginationContent = forwardRef<BridgeViewRef, PaginationContentProps>(
|
|
92
|
+
({ className, ...props }, ref) => (
|
|
93
|
+
<View
|
|
94
|
+
ref={ref}
|
|
95
|
+
className={cn(paginationContentVariants(), className)}
|
|
96
|
+
{...props}
|
|
97
|
+
/>
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
PaginationContent.displayName = "PaginationContent";
|
|
101
|
+
|
|
102
|
+
// 원본 <li>처럼 기본 클래스 없이 props를 그대로 통과시킨다.
|
|
103
|
+
const PaginationItem = forwardRef<BridgeViewRef, PaginationItemProps>(
|
|
104
|
+
({ ...props }, ref) => <View ref={ref} {...props} />,
|
|
105
|
+
);
|
|
106
|
+
PaginationItem.displayName = "PaginationItem";
|
|
107
|
+
|
|
108
|
+
const PaginationLink = forwardRef<BridgePressableRef, PaginationLinkProps>(
|
|
109
|
+
(
|
|
110
|
+
{ className, isActive, size = "icon", children, hitSlop, ...props },
|
|
111
|
+
ref,
|
|
112
|
+
) => {
|
|
113
|
+
const variant = isActive ? "outline" : "ghost";
|
|
114
|
+
const resolvedSize = size ?? "icon";
|
|
115
|
+
return (
|
|
116
|
+
<Pressable
|
|
117
|
+
ref={ref}
|
|
118
|
+
accessibilityRole="link"
|
|
119
|
+
hitSlop={hitSlop ?? HIT_SLOP[resolvedSize]}
|
|
120
|
+
className={cn(
|
|
121
|
+
buttonVariants({ variant, size }),
|
|
122
|
+
// 활성 페이지 강조 테두리 — outline 변형의 border-input은 1.27:1로
|
|
123
|
+
// 현재 페이지 표시가 배경에 묻힌다. foreground/50(≥3:1, G4)로 조정.
|
|
124
|
+
isActive && "border-foreground/50",
|
|
125
|
+
className,
|
|
126
|
+
)}
|
|
127
|
+
{...props}
|
|
128
|
+
>
|
|
129
|
+
{typeof children === "string" || typeof children === "number" ? (
|
|
130
|
+
<Text className={cn(buttonTextVariants({ variant }))}>
|
|
131
|
+
{children}
|
|
132
|
+
</Text>
|
|
133
|
+
) : (
|
|
134
|
+
children
|
|
135
|
+
)}
|
|
136
|
+
</Pressable>
|
|
137
|
+
);
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
PaginationLink.displayName = "PaginationLink";
|
|
141
|
+
|
|
142
|
+
const PaginationPrevious = forwardRef<
|
|
143
|
+
BridgePressableRef,
|
|
144
|
+
PaginationPreviousProps
|
|
145
|
+
>(({ className, label = "Previous", ...props }, ref) => (
|
|
146
|
+
<PaginationLink
|
|
147
|
+
ref={ref}
|
|
148
|
+
accessibilityLabel="Go to previous page"
|
|
149
|
+
size="default"
|
|
150
|
+
className={cn("gap-1 px-2.5 web:sm:pl-2.5", className)}
|
|
151
|
+
{...props}
|
|
152
|
+
>
|
|
153
|
+
<Text className={cn(paginationGlyphVariants())}>‹</Text>
|
|
154
|
+
<Text className={cn(paginationLabelVariants())}>{label}</Text>
|
|
155
|
+
</PaginationLink>
|
|
156
|
+
));
|
|
157
|
+
PaginationPrevious.displayName = "PaginationPrevious";
|
|
158
|
+
|
|
159
|
+
const PaginationNext = forwardRef<BridgePressableRef, PaginationNextProps>(
|
|
160
|
+
({ className, label = "Next", ...props }, ref) => (
|
|
161
|
+
<PaginationLink
|
|
162
|
+
ref={ref}
|
|
163
|
+
accessibilityLabel="Go to next page"
|
|
164
|
+
size="default"
|
|
165
|
+
className={cn("gap-1 px-2.5 web:sm:pr-2.5", className)}
|
|
166
|
+
{...props}
|
|
167
|
+
>
|
|
168
|
+
<Text className={cn(paginationLabelVariants())}>{label}</Text>
|
|
169
|
+
<Text className={cn(paginationGlyphVariants())}>›</Text>
|
|
170
|
+
</PaginationLink>
|
|
171
|
+
),
|
|
172
|
+
);
|
|
173
|
+
PaginationNext.displayName = "PaginationNext";
|
|
174
|
+
|
|
175
|
+
// 페이지 생략 글리프 — "⋯"(원본 MoreHorizontalIcon 대체).
|
|
176
|
+
const PaginationEllipsis = forwardRef<BridgeViewRef, PaginationEllipsisProps>(
|
|
177
|
+
({ className, ...props }, ref) => (
|
|
178
|
+
<View
|
|
179
|
+
ref={ref}
|
|
180
|
+
aria-hidden
|
|
181
|
+
className={cn(paginationEllipsisVariants(), className)}
|
|
182
|
+
{...props}
|
|
183
|
+
>
|
|
184
|
+
<Text className={cn(paginationEllipsisTextVariants())}>⋯</Text>
|
|
185
|
+
</View>
|
|
186
|
+
),
|
|
187
|
+
);
|
|
188
|
+
PaginationEllipsis.displayName = "PaginationEllipsis";
|
|
189
|
+
|
|
190
|
+
export {
|
|
191
|
+
Pagination,
|
|
192
|
+
PaginationContent,
|
|
193
|
+
PaginationLink,
|
|
194
|
+
PaginationItem,
|
|
195
|
+
PaginationPrevious,
|
|
196
|
+
PaginationNext,
|
|
197
|
+
PaginationEllipsis,
|
|
198
|
+
paginationVariants,
|
|
199
|
+
paginationContentVariants,
|
|
200
|
+
paginationGlyphVariants,
|
|
201
|
+
paginationLabelVariants,
|
|
202
|
+
paginationEllipsisVariants,
|
|
203
|
+
paginationEllipsisTextVariants,
|
|
204
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Progress — shadcn/ui(new-york-v4) progress.tsx 유니버설 포팅 (사이클 30, T-3b).
|
|
2
|
+
// 원본: shadcn-ref/src/progress/progress.tsx — radix Progress 엔진을 제거하고
|
|
3
|
+
// 진행률은 BridgeStyle 동적 인라인 스타일 계약으로 표현한다.
|
|
4
|
+
//
|
|
5
|
+
// 포팅 판단(원본 대비 차이):
|
|
6
|
+
// - 원본 인디케이터는 w-full + translateX 변환으로 채움을 표현했다. 네이티브
|
|
7
|
+
// 변환 조합 대신 브리지 동적 값 계약(style의 백분율 width)을 쓴다 — className
|
|
8
|
+
// 동적 생성 금지 원칙의 공식 대체 수단. w-full·flex-1 토큰은 이 방식과 충돌해
|
|
9
|
+
// 제거했다.
|
|
10
|
+
// - 원본 value(0~100 고정)에 max 스케일(기본 100)을 추가해 백분율로 정규화하고
|
|
11
|
+
// 0~100 범위로 클램프한다.
|
|
12
|
+
// - aria-valuenow·aria-valuemax는 브리지 ViewProps 계약에 없어 미지원 —
|
|
13
|
+
// 로드맵 항목(브리지 확장 시 지정). 진행률 맥락은 사용처의 accessibilityLabel
|
|
14
|
+
// 전달로 보왈다.
|
|
15
|
+
import { forwardRef } from "react";
|
|
16
|
+
import {
|
|
17
|
+
View,
|
|
18
|
+
type BridgeViewRef,
|
|
19
|
+
type ViewProps,
|
|
20
|
+
} from "@unikit-bridge/ui/primitives";
|
|
21
|
+
|
|
22
|
+
import { cn } from "../lib/utils";
|
|
23
|
+
|
|
24
|
+
export type ProgressProps = ViewProps & {
|
|
25
|
+
/** 현재 진행값 — max 대비 백분율로 렌더(미지정 시 0%) */
|
|
26
|
+
value?: number;
|
|
27
|
+
/** 진행 스케일 상한(기본 100) */
|
|
28
|
+
max?: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const Progress = forwardRef<BridgeViewRef, ProgressProps>(
|
|
32
|
+
({ className, value, max = 100, ...props }, ref) => {
|
|
33
|
+
const scale = max > 0 ? max : 100;
|
|
34
|
+
const percent = Math.min(100, Math.max(0, ((value ?? 0) / scale) * 100));
|
|
35
|
+
return (
|
|
36
|
+
// 트랙 — role 지정 권장 라벨은 사용처 accessibilityLabel로 전달된다.
|
|
37
|
+
<View
|
|
38
|
+
ref={ref}
|
|
39
|
+
accessibilityRole="progressbar"
|
|
40
|
+
className={cn(
|
|
41
|
+
// 트랙 bg-primary/20은 다크 1.75:1·라이트 1.4:1로 채움과의 경계가
|
|
42
|
+
// 흐려 value 신호가 묻힌다 — /50(양 모드 ≥3:1, G4)으로 조정.
|
|
43
|
+
"relative h-2 w-full overflow-hidden rounded-full bg-primary/50",
|
|
44
|
+
className,
|
|
45
|
+
)}
|
|
46
|
+
{...props}
|
|
47
|
+
>
|
|
48
|
+
{/* 인디케이터 — 동적 백분율 폭은 BridgeStyle 전용 영역 */}
|
|
49
|
+
<View
|
|
50
|
+
aria-hidden
|
|
51
|
+
className="h-full bg-primary web:transition-all"
|
|
52
|
+
style={{ width: `${percent}%` }}
|
|
53
|
+
/>
|
|
54
|
+
</View>
|
|
55
|
+
);
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
Progress.displayName = "Progress";
|
|
59
|
+
|
|
60
|
+
export { Progress };
|