@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.
Files changed (174) hide show
  1. package/components/accordion/accordion.tsx +114 -0
  2. package/components/accordion/index.ts +1 -0
  3. package/components/alert/alert.tsx +97 -0
  4. package/components/alert/index.ts +1 -0
  5. package/components/alert-dialog/alert-dialog.tsx +190 -0
  6. package/components/alert-dialog/index.ts +1 -0
  7. package/components/aspect-ratio/aspect-ratio.tsx +23 -0
  8. package/components/aspect-ratio/index.ts +1 -0
  9. package/components/avatar/avatar.tsx +62 -0
  10. package/components/avatar/index.ts +1 -0
  11. package/components/badge/badge.tsx +58 -0
  12. package/components/badge/index.ts +1 -0
  13. package/components/breadcrumb/breadcrumb.tsx +132 -0
  14. package/components/breadcrumb/index.ts +1 -0
  15. package/components/button/button.tsx +77 -0
  16. package/components/button/index.ts +1 -0
  17. package/components/button-group/button-group.tsx +99 -0
  18. package/components/button-group/index.ts +1 -0
  19. package/components/calendar/calendar.tsx +235 -0
  20. package/components/calendar/index.ts +1 -0
  21. package/components/card/card.tsx +107 -0
  22. package/components/card/index.ts +1 -0
  23. package/components/carousel/carousel.tsx +263 -0
  24. package/components/carousel/index.ts +1 -0
  25. package/components/chart/chart.tsx +377 -0
  26. package/components/chart/index.ts +1 -0
  27. package/components/checkbox/checkbox.tsx +41 -0
  28. package/components/checkbox/index.ts +1 -0
  29. package/components/collapsible/collapsible.tsx +44 -0
  30. package/components/collapsible/index.ts +1 -0
  31. package/components/command/command.tsx +201 -0
  32. package/components/command/index.ts +1 -0
  33. package/components/context-menu/context-menu.tsx +270 -0
  34. package/components/context-menu/index.ts +1 -0
  35. package/components/dialog/dialog.tsx +166 -0
  36. package/components/dialog/index.ts +1 -0
  37. package/components/drawer/drawer.tsx +154 -0
  38. package/components/drawer/index.ts +1 -0
  39. package/components/dropdown-menu/dropdown-menu.tsx +276 -0
  40. package/components/dropdown-menu/index.ts +1 -0
  41. package/components/empty/empty.tsx +129 -0
  42. package/components/empty/index.ts +1 -0
  43. package/components/field/field.tsx +272 -0
  44. package/components/field/index.ts +1 -0
  45. package/components/form/form.tsx +197 -0
  46. package/components/form/index.ts +1 -0
  47. package/components/hover-card/hover-card.tsx +57 -0
  48. package/components/hover-card/index.ts +1 -0
  49. package/components/input/index.ts +1 -0
  50. package/components/input/input.tsx +31 -0
  51. package/components/input-group/index.ts +1 -0
  52. package/components/input-group/input-group.tsx +189 -0
  53. package/components/input-otp/index.ts +1 -0
  54. package/components/input-otp/input-otp.tsx +99 -0
  55. package/components/item/index.ts +1 -0
  56. package/components/item/item.tsx +225 -0
  57. package/components/kbd/index.ts +1 -0
  58. package/components/kbd/kbd.tsx +38 -0
  59. package/components/label/index.ts +1 -0
  60. package/components/label/label.tsx +33 -0
  61. package/components/menubar/index.ts +1 -0
  62. package/components/menubar/menubar.tsx +299 -0
  63. package/components/navigation-menu/index.ts +1 -0
  64. package/components/navigation-menu/navigation-menu.tsx +194 -0
  65. package/components/pagination/index.ts +1 -0
  66. package/components/pagination/pagination.tsx +153 -0
  67. package/components/popover/index.ts +1 -0
  68. package/components/popover/popover.tsx +106 -0
  69. package/components/progress/index.ts +1 -0
  70. package/components/progress/progress.tsx +39 -0
  71. package/components/radio-group/index.ts +1 -0
  72. package/components/radio-group/radio-group.tsx +57 -0
  73. package/components/resizable/index.ts +1 -0
  74. package/components/resizable/resizable.tsx +73 -0
  75. package/components/scroll-area/index.ts +1 -0
  76. package/components/scroll-area/scroll-area.tsx +72 -0
  77. package/components/select/index.ts +1 -0
  78. package/components/select/select.tsx +213 -0
  79. package/components/separator/index.ts +1 -0
  80. package/components/separator/separator.tsx +39 -0
  81. package/components/sheet/index.ts +1 -0
  82. package/components/sheet/sheet.tsx +160 -0
  83. package/components/sidebar/index.ts +1 -0
  84. package/components/sidebar/sidebar.tsx +776 -0
  85. package/components/skeleton/index.ts +1 -0
  86. package/components/skeleton/skeleton.tsx +21 -0
  87. package/components/slider/index.ts +1 -0
  88. package/components/slider/slider.tsx +75 -0
  89. package/components/sonner/index.ts +2 -0
  90. package/components/sonner/sonner.tsx +52 -0
  91. package/components/spinner/index.ts +1 -0
  92. package/components/spinner/spinner.tsx +26 -0
  93. package/components/switch/index.ts +1 -0
  94. package/components/switch/switch.tsx +39 -0
  95. package/components/table/index.ts +1 -0
  96. package/components/table/table.tsx +140 -0
  97. package/components/tabs/index.ts +1 -0
  98. package/components/tabs/tabs.tsx +94 -0
  99. package/components/textarea/index.ts +1 -0
  100. package/components/textarea/textarea.tsx +26 -0
  101. package/components/toggle/index.ts +1 -0
  102. package/components/toggle/toggle.tsx +58 -0
  103. package/components/toggle-group/index.ts +1 -0
  104. package/components/toggle-group/toggle-group.tsx +97 -0
  105. package/components/tooltip/index.ts +1 -0
  106. package/components/tooltip/tooltip.tsx +82 -0
  107. package/dist/components/accordion/accordion.d.ts +50 -0
  108. package/dist/components/alert/alert.d.ts +31 -0
  109. package/dist/components/alert-dialog/alert-dialog.d.ts +35 -0
  110. package/dist/components/aspect-ratio/aspect-ratio.d.ts +12 -0
  111. package/dist/components/avatar/avatar.d.ts +11 -0
  112. package/dist/components/badge/badge.d.ts +12 -0
  113. package/dist/components/breadcrumb/breadcrumb.d.ts +23 -0
  114. package/dist/components/button/button.d.ts +15 -0
  115. package/dist/components/button-group/button-group.d.ts +16 -0
  116. package/dist/components/calendar/calendar.d.ts +15 -0
  117. package/dist/components/card/card.d.ts +15 -0
  118. package/dist/components/carousel/carousel.d.ts +24 -0
  119. package/dist/components/chart/chart.d.ts +20 -0
  120. package/dist/components/checkbox/checkbox.d.ts +9 -0
  121. package/dist/components/collapsible/collapsible.d.ts +13 -0
  122. package/dist/components/command/command.d.ts +18 -0
  123. package/dist/components/context-menu/context-menu.d.ts +18 -0
  124. package/dist/components/dialog/dialog.d.ts +25 -0
  125. package/dist/components/drawer/drawer.d.ts +18 -0
  126. package/dist/components/dropdown-menu/dropdown-menu.d.ts +21 -0
  127. package/dist/components/empty/empty.d.ts +25 -0
  128. package/dist/components/field/field.d.ts +26 -0
  129. package/dist/components/form/form.d.ts +30 -1
  130. package/dist/components/hover-card/hover-card.d.ts +13 -0
  131. package/dist/components/input/input.d.ts +10 -0
  132. package/dist/components/input-group/input-group.d.ts +19 -0
  133. package/dist/components/input-otp/input-otp.d.ts +23 -0
  134. package/dist/components/item/item.d.ts +33 -1
  135. package/dist/components/kbd/kbd.d.ts +10 -0
  136. package/dist/components/label/label.d.ts +9 -0
  137. package/dist/components/menubar/menubar.d.ts +25 -0
  138. package/dist/components/navigation-menu/navigation-menu.d.ts +26 -0
  139. package/dist/components/pagination/pagination.d.ts +26 -0
  140. package/dist/components/popover/popover.d.ts +17 -0
  141. package/dist/components/progress/progress.d.ts +10 -0
  142. package/dist/components/radio-group/radio-group.d.ts +12 -0
  143. package/dist/components/resizable/resizable.d.ts +19 -0
  144. package/dist/components/scroll-area/scroll-area.d.ts +14 -0
  145. package/dist/components/select/select.d.ts +25 -0
  146. package/dist/components/separator/separator.d.ts +11 -0
  147. package/dist/components/sheet/sheet.d.ts +23 -0
  148. package/dist/components/sidebar/sidebar.d.ts +50 -0
  149. package/dist/components/skeleton/skeleton.d.ts +8 -0
  150. package/dist/components/slider/slider.d.ts +12 -0
  151. package/dist/components/sonner/sonner.d.ts +14 -0
  152. package/dist/components/spinner/spinner.d.ts +9 -0
  153. package/dist/components/switch/switch.d.ts +8 -0
  154. package/dist/components/table/table.d.ts +26 -0
  155. package/dist/components/tabs/tabs.d.ts +16 -6
  156. package/dist/components/textarea/textarea.d.ts +8 -0
  157. package/dist/components/toggle/toggle.d.ts +13 -0
  158. package/dist/components/toggle-group/toggle-group.d.ts +1 -0
  159. package/dist/components/tooltip/tooltip.d.ts +21 -0
  160. package/dist/design-system.css +1 -1
  161. package/dist/design-system.es.js +3493 -28470
  162. package/dist/design-system.umd.js +4 -257
  163. package/dist/index.d.ts +1 -1
  164. package/foundations/ThemeProvider.tsx +77 -0
  165. package/foundations/color.css +232 -0
  166. package/foundations/palette.css +249 -0
  167. package/foundations/spacing.css +8 -0
  168. package/foundations/typography.css +143 -0
  169. package/hooks/use-mobile.ts +19 -0
  170. package/index.css +173 -0
  171. package/index.ts +339 -0
  172. package/lib/utils.ts +6 -0
  173. package/package.json +40 -19
  174. 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";