@mzc-fe/design-system 0.0.5 → 0.0.7-rc.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/components/accordion/accordion.tsx +114 -0
- package/components/accordion/index.ts +1 -0
- package/components/alert/alert.tsx +97 -0
- package/components/alert/index.ts +1 -0
- package/components/alert-dialog/alert-dialog.tsx +190 -0
- package/components/alert-dialog/index.ts +1 -0
- package/components/aspect-ratio/aspect-ratio.tsx +23 -0
- package/components/aspect-ratio/index.ts +1 -0
- package/components/avatar/avatar.tsx +62 -0
- package/components/avatar/index.ts +1 -0
- package/components/badge/badge.tsx +58 -0
- package/components/badge/index.ts +1 -0
- package/components/breadcrumb/breadcrumb.tsx +132 -0
- package/components/breadcrumb/index.ts +1 -0
- package/components/button/button.tsx +77 -0
- package/components/button/index.ts +1 -0
- package/components/button-group/button-group.tsx +99 -0
- package/components/button-group/index.ts +1 -0
- package/components/calendar/calendar.tsx +235 -0
- package/components/calendar/index.ts +1 -0
- package/components/card/card.tsx +107 -0
- package/components/card/index.ts +1 -0
- package/components/carousel/carousel.tsx +263 -0
- package/components/carousel/index.ts +1 -0
- package/components/chart/chart.tsx +377 -0
- package/components/chart/index.ts +1 -0
- package/components/checkbox/checkbox.tsx +41 -0
- package/components/checkbox/index.ts +1 -0
- package/components/collapsible/collapsible.tsx +44 -0
- package/components/collapsible/index.ts +1 -0
- package/components/command/command.tsx +201 -0
- package/components/command/index.ts +1 -0
- package/components/context-menu/context-menu.tsx +270 -0
- package/components/context-menu/index.ts +1 -0
- package/components/dialog/dialog.tsx +166 -0
- package/components/dialog/index.ts +1 -0
- package/components/drawer/drawer.tsx +154 -0
- package/components/drawer/index.ts +1 -0
- package/components/dropdown-menu/dropdown-menu.tsx +276 -0
- package/components/dropdown-menu/index.ts +1 -0
- package/components/empty/empty.tsx +129 -0
- package/components/empty/index.ts +1 -0
- package/components/field/field.tsx +272 -0
- package/components/field/index.ts +1 -0
- package/components/form/form.tsx +197 -0
- package/components/form/index.ts +1 -0
- package/components/hover-card/hover-card.tsx +57 -0
- package/components/hover-card/index.ts +1 -0
- package/components/input/index.ts +1 -0
- package/components/input/input.tsx +31 -0
- package/components/input-group/index.ts +1 -0
- package/components/input-group/input-group.tsx +189 -0
- package/components/input-otp/index.ts +1 -0
- package/components/input-otp/input-otp.tsx +99 -0
- package/components/item/index.ts +1 -0
- package/components/item/item.tsx +225 -0
- package/components/kbd/index.ts +1 -0
- package/components/kbd/kbd.tsx +38 -0
- package/components/label/index.ts +1 -0
- package/components/label/label.tsx +33 -0
- package/components/menubar/index.ts +1 -0
- package/components/menubar/menubar.tsx +299 -0
- package/components/navigation-menu/index.ts +1 -0
- package/components/navigation-menu/navigation-menu.tsx +194 -0
- package/components/pagination/index.ts +1 -0
- package/components/pagination/pagination.tsx +153 -0
- package/components/popover/index.ts +1 -0
- package/components/popover/popover.tsx +106 -0
- package/components/progress/index.ts +1 -0
- package/components/progress/progress.tsx +39 -0
- package/components/radio-group/index.ts +1 -0
- package/components/radio-group/radio-group.tsx +57 -0
- package/components/resizable/index.ts +1 -0
- package/components/resizable/resizable.tsx +73 -0
- package/components/scroll-area/index.ts +1 -0
- package/components/scroll-area/scroll-area.tsx +72 -0
- package/components/select/index.ts +1 -0
- package/components/select/select.tsx +213 -0
- package/components/separator/index.ts +1 -0
- package/components/separator/separator.tsx +39 -0
- package/components/sheet/index.ts +1 -0
- package/components/sheet/sheet.tsx +160 -0
- package/components/sidebar/index.ts +1 -0
- package/components/sidebar/sidebar.tsx +776 -0
- package/components/skeleton/index.ts +1 -0
- package/components/skeleton/skeleton.tsx +21 -0
- package/components/slider/index.ts +1 -0
- package/components/slider/slider.tsx +75 -0
- package/components/sonner/index.ts +2 -0
- package/components/sonner/sonner.tsx +52 -0
- package/components/spinner/index.ts +1 -0
- package/components/spinner/spinner.tsx +26 -0
- package/components/switch/index.ts +1 -0
- package/components/switch/switch.tsx +39 -0
- package/components/table/index.ts +1 -0
- package/components/table/table.tsx +140 -0
- package/components/tabs/index.ts +1 -0
- package/components/tabs/tabs.tsx +94 -0
- package/components/textarea/index.ts +1 -0
- package/components/textarea/textarea.tsx +26 -0
- package/components/toggle/index.ts +1 -0
- package/components/toggle/toggle.tsx +58 -0
- package/components/toggle-group/index.ts +1 -0
- package/components/toggle-group/toggle-group.tsx +97 -0
- package/components/tooltip/index.ts +1 -0
- package/components/tooltip/tooltip.tsx +82 -0
- package/dist/components/accordion/accordion.d.ts +50 -0
- package/dist/components/alert/alert.d.ts +31 -0
- package/dist/components/alert-dialog/alert-dialog.d.ts +35 -0
- package/dist/components/aspect-ratio/aspect-ratio.d.ts +12 -0
- package/dist/components/avatar/avatar.d.ts +11 -0
- package/dist/components/badge/badge.d.ts +12 -0
- package/dist/components/breadcrumb/breadcrumb.d.ts +23 -0
- package/dist/components/button/button.d.ts +15 -0
- package/dist/components/button-group/button-group.d.ts +16 -0
- package/dist/components/calendar/calendar.d.ts +15 -0
- package/dist/components/card/card.d.ts +15 -0
- package/dist/components/carousel/carousel.d.ts +24 -0
- package/dist/components/chart/chart.d.ts +20 -0
- package/dist/components/checkbox/checkbox.d.ts +9 -0
- package/dist/components/collapsible/collapsible.d.ts +13 -0
- package/dist/components/command/command.d.ts +18 -0
- package/dist/components/context-menu/context-menu.d.ts +18 -0
- package/dist/components/dialog/dialog.d.ts +25 -0
- package/dist/components/drawer/drawer.d.ts +18 -0
- package/dist/components/dropdown-menu/dropdown-menu.d.ts +21 -0
- package/dist/components/empty/empty.d.ts +25 -0
- package/dist/components/field/field.d.ts +26 -0
- package/dist/components/form/form.d.ts +30 -1
- package/dist/components/hover-card/hover-card.d.ts +13 -0
- package/dist/components/input/input.d.ts +10 -0
- package/dist/components/input-group/input-group.d.ts +19 -0
- package/dist/components/input-otp/input-otp.d.ts +23 -0
- package/dist/components/item/item.d.ts +33 -1
- package/dist/components/kbd/kbd.d.ts +10 -0
- package/dist/components/label/label.d.ts +9 -0
- package/dist/components/menubar/menubar.d.ts +25 -0
- package/dist/components/navigation-menu/navigation-menu.d.ts +26 -0
- package/dist/components/pagination/pagination.d.ts +26 -0
- package/dist/components/popover/popover.d.ts +17 -0
- package/dist/components/progress/progress.d.ts +10 -0
- package/dist/components/radio-group/radio-group.d.ts +12 -0
- package/dist/components/resizable/resizable.d.ts +19 -0
- package/dist/components/scroll-area/scroll-area.d.ts +14 -0
- package/dist/components/select/select.d.ts +25 -0
- package/dist/components/separator/separator.d.ts +11 -0
- package/dist/components/sheet/sheet.d.ts +23 -0
- package/dist/components/sidebar/sidebar.d.ts +50 -0
- package/dist/components/skeleton/skeleton.d.ts +8 -0
- package/dist/components/slider/slider.d.ts +12 -0
- package/dist/components/sonner/sonner.d.ts +14 -0
- package/dist/components/spinner/spinner.d.ts +9 -0
- package/dist/components/switch/switch.d.ts +8 -0
- package/dist/components/table/table.d.ts +26 -0
- package/dist/components/tabs/tabs.d.ts +16 -6
- package/dist/components/textarea/textarea.d.ts +8 -0
- package/dist/components/toggle/toggle.d.ts +13 -0
- package/dist/components/toggle-group/toggle-group.d.ts +1 -0
- package/dist/components/tooltip/tooltip.d.ts +21 -0
- package/dist/design-system.css +1 -1
- package/dist/design-system.es.js +3493 -28470
- package/dist/design-system.umd.js +4 -257
- package/dist/index.d.ts +1 -1
- package/foundations/ThemeProvider.tsx +77 -0
- package/foundations/color.css +232 -0
- package/foundations/palette.css +249 -0
- package/foundations/spacing.css +8 -0
- package/foundations/typography.css +143 -0
- package/hooks/use-mobile.ts +19 -0
- package/index.css +173 -0
- package/index.ts +339 -0
- package/lib/utils.ts +6 -0
- package/package.json +40 -19
- package/README.md +0 -184
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
|
|
4
|
+
import { cn } from "@/lib/utils";
|
|
5
|
+
import { Label } from "@/components/label";
|
|
6
|
+
import { Separator } from "@/components/separator";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 폼 필드들을 그룹화하는 fieldset 컴포넌트입니다.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* <FieldSet>
|
|
14
|
+
* <FieldLegend>개인 정보</FieldLegend>
|
|
15
|
+
* <Field>
|
|
16
|
+
* <FieldLabel>이름</FieldLabel>
|
|
17
|
+
* <Input />
|
|
18
|
+
* </Field>
|
|
19
|
+
* </FieldSet>
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
|
|
23
|
+
return (
|
|
24
|
+
<fieldset
|
|
25
|
+
data-slot="field-set"
|
|
26
|
+
className={cn(
|
|
27
|
+
"flex flex-col gap-6",
|
|
28
|
+
"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
|
|
29
|
+
className
|
|
30
|
+
)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** FieldSet의 제목 컴포넌트입니다. */
|
|
37
|
+
function FieldLegend({
|
|
38
|
+
className,
|
|
39
|
+
variant = "legend",
|
|
40
|
+
...props
|
|
41
|
+
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
|
|
42
|
+
return (
|
|
43
|
+
<legend
|
|
44
|
+
data-slot="field-legend"
|
|
45
|
+
data-variant={variant}
|
|
46
|
+
className={cn(
|
|
47
|
+
"mb-3 font-medium",
|
|
48
|
+
"data-[variant=legend]:text-base",
|
|
49
|
+
"data-[variant=label]:text-sm",
|
|
50
|
+
className
|
|
51
|
+
)}
|
|
52
|
+
{...props}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 여러 Field를 그룹화하는 컴포넌트입니다. */
|
|
58
|
+
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
data-slot="field-group"
|
|
62
|
+
className={cn(
|
|
63
|
+
"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4",
|
|
64
|
+
className
|
|
65
|
+
)}
|
|
66
|
+
{...props}
|
|
67
|
+
/>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const fieldVariants = cva(
|
|
72
|
+
"group/field flex w-full gap-3 data-[invalid=true]:text-destructive",
|
|
73
|
+
{
|
|
74
|
+
variants: {
|
|
75
|
+
orientation: {
|
|
76
|
+
vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"],
|
|
77
|
+
horizontal: [
|
|
78
|
+
"flex-row items-center",
|
|
79
|
+
"[&>[data-slot=field-label]]:flex-auto",
|
|
80
|
+
"has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
|
81
|
+
],
|
|
82
|
+
responsive: [
|
|
83
|
+
"flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto",
|
|
84
|
+
"@md/field-group:[&>[data-slot=field-label]]:flex-auto",
|
|
85
|
+
"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
defaultVariants: {
|
|
90
|
+
orientation: "vertical",
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 개별 폼 필드 컴포넌트입니다.
|
|
97
|
+
* @param props.orientation - 레이아웃 방향 ('vertical' | 'horizontal' | 'responsive')
|
|
98
|
+
*/
|
|
99
|
+
function Field({
|
|
100
|
+
className,
|
|
101
|
+
orientation = "vertical",
|
|
102
|
+
...props
|
|
103
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
|
|
104
|
+
return (
|
|
105
|
+
<div
|
|
106
|
+
role="group"
|
|
107
|
+
data-slot="field"
|
|
108
|
+
data-orientation={orientation}
|
|
109
|
+
className={cn(fieldVariants({ orientation }), className)}
|
|
110
|
+
{...props}
|
|
111
|
+
/>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Field의 콘텐츠 영역입니다. */
|
|
116
|
+
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
117
|
+
return (
|
|
118
|
+
<div
|
|
119
|
+
data-slot="field-content"
|
|
120
|
+
className={cn(
|
|
121
|
+
"group/field-content flex flex-1 flex-col gap-1.5 leading-snug",
|
|
122
|
+
className
|
|
123
|
+
)}
|
|
124
|
+
{...props}
|
|
125
|
+
/>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Field의 레이블 컴포넌트입니다. */
|
|
130
|
+
function FieldLabel({
|
|
131
|
+
className,
|
|
132
|
+
...props
|
|
133
|
+
}: React.ComponentProps<typeof Label>) {
|
|
134
|
+
return (
|
|
135
|
+
<Label
|
|
136
|
+
data-slot="field-label"
|
|
137
|
+
className={cn(
|
|
138
|
+
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50",
|
|
139
|
+
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>*]:data-[slot=field]:p-4",
|
|
140
|
+
"has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10",
|
|
141
|
+
className
|
|
142
|
+
)}
|
|
143
|
+
{...props}
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Field의 제목 컴포넌트입니다. */
|
|
149
|
+
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
150
|
+
return (
|
|
151
|
+
<div
|
|
152
|
+
data-slot="field-label"
|
|
153
|
+
className={cn(
|
|
154
|
+
"flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",
|
|
155
|
+
className
|
|
156
|
+
)}
|
|
157
|
+
{...props}
|
|
158
|
+
/>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Field의 설명 텍스트 컴포넌트입니다. */
|
|
163
|
+
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
164
|
+
return (
|
|
165
|
+
<p
|
|
166
|
+
data-slot="field-description"
|
|
167
|
+
className={cn(
|
|
168
|
+
"text-muted-foreground text-sm leading-normal font-normal group-has-[[data-orientation=horizontal]]/field:text-balance",
|
|
169
|
+
"last:mt-0 nth-last-2:-mt-1 [[data-variant=legend]+&]:-mt-1.5",
|
|
170
|
+
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
|
171
|
+
className
|
|
172
|
+
)}
|
|
173
|
+
{...props}
|
|
174
|
+
/>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Field 사이의 구분선 컴포넌트입니다. */
|
|
179
|
+
function FieldSeparator({
|
|
180
|
+
children,
|
|
181
|
+
className,
|
|
182
|
+
...props
|
|
183
|
+
}: React.ComponentProps<"div"> & {
|
|
184
|
+
children?: React.ReactNode;
|
|
185
|
+
}) {
|
|
186
|
+
return (
|
|
187
|
+
<div
|
|
188
|
+
data-slot="field-separator"
|
|
189
|
+
data-content={!!children}
|
|
190
|
+
className={cn(
|
|
191
|
+
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
|
|
192
|
+
className
|
|
193
|
+
)}
|
|
194
|
+
{...props}
|
|
195
|
+
>
|
|
196
|
+
<Separator className="absolute inset-0 top-1/2" />
|
|
197
|
+
{children && (
|
|
198
|
+
<span
|
|
199
|
+
className="bg-background text-muted-foreground relative mx-auto block w-fit px-2"
|
|
200
|
+
data-slot="field-separator-content"
|
|
201
|
+
>
|
|
202
|
+
{children}
|
|
203
|
+
</span>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Field의 오류 메시지 컴포넌트입니다. */
|
|
210
|
+
function FieldError({
|
|
211
|
+
className,
|
|
212
|
+
children,
|
|
213
|
+
errors,
|
|
214
|
+
...props
|
|
215
|
+
}: React.ComponentProps<"div"> & {
|
|
216
|
+
errors?: Array<{ message?: string } | undefined>;
|
|
217
|
+
}) {
|
|
218
|
+
const content = useMemo(() => {
|
|
219
|
+
if (children) {
|
|
220
|
+
return children;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!errors?.length) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const uniqueErrors = [
|
|
228
|
+
...new Map(errors.map((error) => [error?.message, error])).values(),
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
if (uniqueErrors?.length == 1) {
|
|
232
|
+
return uniqueErrors[0]?.message;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<ul className="ml-4 flex list-disc flex-col gap-1">
|
|
237
|
+
{uniqueErrors.map(
|
|
238
|
+
(error, index) =>
|
|
239
|
+
error?.message && <li key={index}>{error.message}</li>
|
|
240
|
+
)}
|
|
241
|
+
</ul>
|
|
242
|
+
);
|
|
243
|
+
}, [children, errors]);
|
|
244
|
+
|
|
245
|
+
if (!content) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<div
|
|
251
|
+
role="alert"
|
|
252
|
+
data-slot="field-error"
|
|
253
|
+
className={cn("text-destructive text-sm font-normal", className)}
|
|
254
|
+
{...props}
|
|
255
|
+
>
|
|
256
|
+
{content}
|
|
257
|
+
</div>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export {
|
|
262
|
+
Field,
|
|
263
|
+
FieldLabel,
|
|
264
|
+
FieldDescription,
|
|
265
|
+
FieldError,
|
|
266
|
+
FieldGroup,
|
|
267
|
+
FieldLegend,
|
|
268
|
+
FieldSeparator,
|
|
269
|
+
FieldSet,
|
|
270
|
+
FieldContent,
|
|
271
|
+
FieldTitle,
|
|
272
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./field";
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import type * as LabelPrimitive from "@radix-ui/react-label";
|
|
5
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
6
|
+
import {
|
|
7
|
+
Controller,
|
|
8
|
+
FormProvider,
|
|
9
|
+
useFormContext,
|
|
10
|
+
useFormState,
|
|
11
|
+
type ControllerProps,
|
|
12
|
+
type FieldPath,
|
|
13
|
+
type FieldValues,
|
|
14
|
+
} from "react-hook-form";
|
|
15
|
+
|
|
16
|
+
import { cn } from "@/lib/utils";
|
|
17
|
+
import { Label } from "@/components/label";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* react-hook-form 기반 폼 컴포넌트입니다.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* const form = useForm();
|
|
25
|
+
*
|
|
26
|
+
* <Form {...form}>
|
|
27
|
+
* <FormField
|
|
28
|
+
* control={form.control}
|
|
29
|
+
* name="email"
|
|
30
|
+
* render={({ field }) => (
|
|
31
|
+
* <FormItem>
|
|
32
|
+
* <FormLabel>이메일</FormLabel>
|
|
33
|
+
* <FormControl>
|
|
34
|
+
* <Input {...field} />
|
|
35
|
+
* </FormControl>
|
|
36
|
+
* <FormMessage />
|
|
37
|
+
* </FormItem>
|
|
38
|
+
* )}
|
|
39
|
+
* />
|
|
40
|
+
* </Form>
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
const Form = FormProvider;
|
|
44
|
+
|
|
45
|
+
type FormFieldContextValue<
|
|
46
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
47
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
|
48
|
+
> = {
|
|
49
|
+
name: TName;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
|
53
|
+
{} as FormFieldContextValue
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const FormField = <
|
|
57
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
58
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
|
59
|
+
>({
|
|
60
|
+
...props
|
|
61
|
+
}: ControllerProps<TFieldValues, TName>) => {
|
|
62
|
+
return (
|
|
63
|
+
<FormFieldContext.Provider value={{ name: props.name }}>
|
|
64
|
+
<Controller {...props} />
|
|
65
|
+
</FormFieldContext.Provider>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const useFormField = () => {
|
|
70
|
+
const fieldContext = React.useContext(FormFieldContext);
|
|
71
|
+
const itemContext = React.useContext(FormItemContext);
|
|
72
|
+
const { getFieldState } = useFormContext();
|
|
73
|
+
const formState = useFormState({ name: fieldContext.name });
|
|
74
|
+
const fieldState = getFieldState(fieldContext.name, formState);
|
|
75
|
+
|
|
76
|
+
if (!fieldContext) {
|
|
77
|
+
throw new Error("useFormField should be used within <FormField>");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const { id } = itemContext;
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
id,
|
|
84
|
+
name: fieldContext.name,
|
|
85
|
+
formItemId: `${id}-form-item`,
|
|
86
|
+
formDescriptionId: `${id}-form-item-description`,
|
|
87
|
+
formMessageId: `${id}-form-item-message`,
|
|
88
|
+
...fieldState,
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
type FormItemContextValue = {
|
|
93
|
+
id: string;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const FormItemContext = React.createContext<FormItemContextValue>(
|
|
97
|
+
{} as FormItemContextValue
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
/** 폼 필드를 감싸는 컨테이너 컴포넌트입니다. */
|
|
101
|
+
function FormItem({ className, ...props }: React.ComponentProps<"div">) {
|
|
102
|
+
const id = React.useId();
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<FormItemContext.Provider value={{ id }}>
|
|
106
|
+
<div
|
|
107
|
+
data-slot="form-item"
|
|
108
|
+
className={cn("grid gap-2", className)}
|
|
109
|
+
{...props}
|
|
110
|
+
/>
|
|
111
|
+
</FormItemContext.Provider>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 폼 필드의 레이블 컴포넌트입니다. */
|
|
116
|
+
function FormLabel({
|
|
117
|
+
className,
|
|
118
|
+
...props
|
|
119
|
+
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
|
120
|
+
const { error, formItemId } = useFormField();
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<Label
|
|
124
|
+
data-slot="form-label"
|
|
125
|
+
data-error={!!error}
|
|
126
|
+
className={cn("data-[error=true]:text-destructive", className)}
|
|
127
|
+
htmlFor={formItemId}
|
|
128
|
+
{...props}
|
|
129
|
+
/>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** 폼 입력 컨트롤을 감싸는 컴포넌트입니다. */
|
|
134
|
+
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
|
135
|
+
const { error, formItemId, formDescriptionId, formMessageId } =
|
|
136
|
+
useFormField();
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<Slot
|
|
140
|
+
data-slot="form-control"
|
|
141
|
+
id={formItemId}
|
|
142
|
+
aria-describedby={
|
|
143
|
+
!error
|
|
144
|
+
? `${formDescriptionId}`
|
|
145
|
+
: `${formDescriptionId} ${formMessageId}`
|
|
146
|
+
}
|
|
147
|
+
aria-invalid={!!error}
|
|
148
|
+
{...props}
|
|
149
|
+
/>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** 폼 필드의 설명 텍스트 컴포넌트입니다. */
|
|
154
|
+
function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
155
|
+
const { formDescriptionId } = useFormField();
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<p
|
|
159
|
+
data-slot="form-description"
|
|
160
|
+
id={formDescriptionId}
|
|
161
|
+
className={cn("text-muted-foreground text-sm", className)}
|
|
162
|
+
{...props}
|
|
163
|
+
/>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** 폼 필드의 유효성 검증 메시지 컴포넌트입니다. */
|
|
168
|
+
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
|
|
169
|
+
const { error, formMessageId } = useFormField();
|
|
170
|
+
const body = error ? String(error?.message ?? "") : props.children;
|
|
171
|
+
|
|
172
|
+
if (!body) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<p
|
|
178
|
+
data-slot="form-message"
|
|
179
|
+
id={formMessageId}
|
|
180
|
+
className={cn("text-destructive text-sm", className)}
|
|
181
|
+
{...props}
|
|
182
|
+
>
|
|
183
|
+
{body}
|
|
184
|
+
</p>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export {
|
|
189
|
+
useFormField,
|
|
190
|
+
Form,
|
|
191
|
+
FormItem,
|
|
192
|
+
FormLabel,
|
|
193
|
+
FormControl,
|
|
194
|
+
FormDescription,
|
|
195
|
+
FormMessage,
|
|
196
|
+
FormField,
|
|
197
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./form";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 호버 시 미리보기 카드를 표시하는 컴포넌트입니다.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* <HoverCard>
|
|
14
|
+
* <HoverCardTrigger>@사용자</HoverCardTrigger>
|
|
15
|
+
* <HoverCardContent>프로필 정보</HoverCardContent>
|
|
16
|
+
* </HoverCard>
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
function HoverCard({
|
|
20
|
+
...props
|
|
21
|
+
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
|
|
22
|
+
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 호버 카드를 표시할 트리거 요소입니다. */
|
|
26
|
+
function HoverCardTrigger({
|
|
27
|
+
...props
|
|
28
|
+
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
|
|
29
|
+
return (
|
|
30
|
+
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 호버 카드 콘텐츠 영역입니다. */
|
|
35
|
+
function HoverCardContent({
|
|
36
|
+
className,
|
|
37
|
+
align = "center",
|
|
38
|
+
sideOffset = 4,
|
|
39
|
+
...props
|
|
40
|
+
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
|
|
41
|
+
return (
|
|
42
|
+
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
|
|
43
|
+
<HoverCardPrimitive.Content
|
|
44
|
+
data-slot="hover-card-content"
|
|
45
|
+
align={align}
|
|
46
|
+
sideOffset={sideOffset}
|
|
47
|
+
className={cn(
|
|
48
|
+
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
|
49
|
+
className
|
|
50
|
+
)}
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
</HoverCardPrimitive.Portal>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./hover-card";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./input";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 텍스트 입력을 받는 인풋 컴포넌트입니다.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```tsx
|
|
10
|
+
* <Input type="text" placeholder="이름을 입력하세요" />
|
|
11
|
+
* <Input type="email" placeholder="이메일" />
|
|
12
|
+
* <Input type="password" placeholder="비밀번호" />
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|
16
|
+
return (
|
|
17
|
+
<input
|
|
18
|
+
type={type}
|
|
19
|
+
data-slot="input"
|
|
20
|
+
className={cn(
|
|
21
|
+
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
22
|
+
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
|
23
|
+
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
|
24
|
+
className
|
|
25
|
+
)}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export { Input }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./input-group";
|