@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,189 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
import { Button } from "@/components/button";
|
|
8
|
+
import { Input } from "@/components/input";
|
|
9
|
+
import { Textarea } from "@/components/textarea";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 입력 필드와 관련 요소들을 그룹화하는 컴포넌트입니다.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```tsx
|
|
16
|
+
* <InputGroup>
|
|
17
|
+
* <InputGroupAddon align="inline-start">$</InputGroupAddon>
|
|
18
|
+
* <InputGroupInput placeholder="0.00" />
|
|
19
|
+
* </InputGroup>
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
data-slot="input-group"
|
|
26
|
+
role="group"
|
|
27
|
+
className={cn(
|
|
28
|
+
"group/input-group border-input dark:bg-input/30 relative flex w-full items-center rounded-md border shadow-xs transition-[color,box-shadow] outline-none",
|
|
29
|
+
"h-9 min-w-0 has-[>textarea]:h-auto",
|
|
30
|
+
|
|
31
|
+
// Variants based on alignment.
|
|
32
|
+
"has-[>[data-align=inline-start]]:[&>input]:pl-2",
|
|
33
|
+
"has-[>[data-align=inline-end]]:[&>input]:pr-2",
|
|
34
|
+
"has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-start]]:[&>input]:pb-3",
|
|
35
|
+
"has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3",
|
|
36
|
+
|
|
37
|
+
// Focus state.
|
|
38
|
+
"has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot=input-group-control]:focus-visible]:ring-[3px]",
|
|
39
|
+
|
|
40
|
+
// Error state.
|
|
41
|
+
"has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40",
|
|
42
|
+
|
|
43
|
+
className
|
|
44
|
+
)}
|
|
45
|
+
{...props}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const inputGroupAddonVariants = cva(
|
|
51
|
+
"text-muted-foreground flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-[calc(var(--radius)-5px)] group-data-[disabled=true]/input-group:opacity-50",
|
|
52
|
+
{
|
|
53
|
+
variants: {
|
|
54
|
+
align: {
|
|
55
|
+
"inline-start":
|
|
56
|
+
"order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
|
|
57
|
+
"inline-end":
|
|
58
|
+
"order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
|
|
59
|
+
"block-start":
|
|
60
|
+
"order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
|
|
61
|
+
"block-end":
|
|
62
|
+
"order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
defaultVariants: {
|
|
66
|
+
align: "inline-start",
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 입력 그룹에 추가 요소(아이콘, 텍스트, 버튼 등)를 배치하는 컴포넌트입니다.
|
|
73
|
+
* @param props.align - 배치 위치 ('inline-start' | 'inline-end' | 'block-start' | 'block-end')
|
|
74
|
+
*/
|
|
75
|
+
function InputGroupAddon({
|
|
76
|
+
className,
|
|
77
|
+
align = "inline-start",
|
|
78
|
+
...props
|
|
79
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
|
80
|
+
return (
|
|
81
|
+
<div
|
|
82
|
+
role="group"
|
|
83
|
+
data-slot="input-group-addon"
|
|
84
|
+
data-align={align}
|
|
85
|
+
className={cn(inputGroupAddonVariants({ align }), className)}
|
|
86
|
+
onClick={(e) => {
|
|
87
|
+
if ((e.target as HTMLElement).closest("button")) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
e.currentTarget.parentElement?.querySelector("input")?.focus();
|
|
91
|
+
}}
|
|
92
|
+
{...props}
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const inputGroupButtonVariants = cva(
|
|
98
|
+
"text-sm shadow-none flex gap-2 items-center",
|
|
99
|
+
{
|
|
100
|
+
variants: {
|
|
101
|
+
size: {
|
|
102
|
+
xs: "h-6 gap-1 px-2 rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
|
|
103
|
+
sm: "h-8 px-2.5 gap-1.5 rounded-md has-[>svg]:px-2.5",
|
|
104
|
+
"icon-xs":
|
|
105
|
+
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
|
|
106
|
+
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
defaultVariants: {
|
|
110
|
+
size: "xs",
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
/** 입력 그룹 내에서 사용하는 버튼 컴포넌트입니다. */
|
|
116
|
+
function InputGroupButton({
|
|
117
|
+
className,
|
|
118
|
+
type = "button",
|
|
119
|
+
variant = "ghost",
|
|
120
|
+
size = "xs",
|
|
121
|
+
...props
|
|
122
|
+
}: Omit<React.ComponentProps<typeof Button>, "size"> &
|
|
123
|
+
VariantProps<typeof inputGroupButtonVariants>) {
|
|
124
|
+
return (
|
|
125
|
+
<Button
|
|
126
|
+
type={type}
|
|
127
|
+
data-size={size}
|
|
128
|
+
variant={variant}
|
|
129
|
+
className={cn(inputGroupButtonVariants({ size }), className)}
|
|
130
|
+
{...props}
|
|
131
|
+
/>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** 입력 그룹 내 텍스트 레이블 컴포넌트입니다. */
|
|
136
|
+
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
|
137
|
+
return (
|
|
138
|
+
<span
|
|
139
|
+
className={cn(
|
|
140
|
+
"text-muted-foreground flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
|
141
|
+
className
|
|
142
|
+
)}
|
|
143
|
+
{...props}
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 입력 그룹용 입력 필드 컴포넌트입니다. */
|
|
149
|
+
function InputGroupInput({
|
|
150
|
+
className,
|
|
151
|
+
...props
|
|
152
|
+
}: React.ComponentProps<"input">) {
|
|
153
|
+
return (
|
|
154
|
+
<Input
|
|
155
|
+
data-slot="input-group-control"
|
|
156
|
+
className={cn(
|
|
157
|
+
"flex-1 rounded-none border-0 bg-transparent shadow-none focus-visible:ring-0 dark:bg-transparent",
|
|
158
|
+
className
|
|
159
|
+
)}
|
|
160
|
+
{...props}
|
|
161
|
+
/>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** 입력 그룹용 텍스트 영역 컴포넌트입니다. */
|
|
166
|
+
function InputGroupTextarea({
|
|
167
|
+
className,
|
|
168
|
+
...props
|
|
169
|
+
}: React.ComponentProps<"textarea">) {
|
|
170
|
+
return (
|
|
171
|
+
<Textarea
|
|
172
|
+
data-slot="input-group-control"
|
|
173
|
+
className={cn(
|
|
174
|
+
"flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus-visible:ring-0 dark:bg-transparent",
|
|
175
|
+
className
|
|
176
|
+
)}
|
|
177
|
+
{...props}
|
|
178
|
+
/>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export {
|
|
183
|
+
InputGroup,
|
|
184
|
+
InputGroupAddon,
|
|
185
|
+
InputGroupButton,
|
|
186
|
+
InputGroupText,
|
|
187
|
+
InputGroupInput,
|
|
188
|
+
InputGroupTextarea,
|
|
189
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./input-otp";
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { OTPInput, OTPInputContext } from "input-otp";
|
|
3
|
+
import { MinusIcon } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* OTP(일회용 비밀번호) 입력을 위한 컴포넌트입니다.
|
|
9
|
+
*
|
|
10
|
+
* @param props.maxLength - 최대 입력 자릿수
|
|
11
|
+
* @param props.containerClassName - 컨테이너 클래스명
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* <InputOTP maxLength={6}>
|
|
16
|
+
* <InputOTPGroup>
|
|
17
|
+
* <InputOTPSlot index={0} />
|
|
18
|
+
* <InputOTPSlot index={1} />
|
|
19
|
+
* <InputOTPSlot index={2} />
|
|
20
|
+
* </InputOTPGroup>
|
|
21
|
+
* </InputOTP>
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
function InputOTP({
|
|
25
|
+
className,
|
|
26
|
+
containerClassName,
|
|
27
|
+
...props
|
|
28
|
+
}: React.ComponentProps<typeof OTPInput> & {
|
|
29
|
+
containerClassName?: string;
|
|
30
|
+
}) {
|
|
31
|
+
return (
|
|
32
|
+
<OTPInput
|
|
33
|
+
data-slot="input-otp"
|
|
34
|
+
containerClassName={cn(
|
|
35
|
+
"flex items-center gap-2 has-disabled:opacity-50",
|
|
36
|
+
containerClassName
|
|
37
|
+
)}
|
|
38
|
+
className={cn("disabled:cursor-not-allowed", className)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** OTP 슬롯들을 그룹화하는 컴포넌트입니다. */
|
|
45
|
+
function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
data-slot="input-otp-group"
|
|
49
|
+
className={cn("flex items-center", className)}
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 개별 OTP 입력 슬롯입니다.
|
|
57
|
+
* @param props.index - 슬롯 인덱스 (0부터 시작)
|
|
58
|
+
*/
|
|
59
|
+
function InputOTPSlot({
|
|
60
|
+
index,
|
|
61
|
+
className,
|
|
62
|
+
...props
|
|
63
|
+
}: React.ComponentProps<"div"> & {
|
|
64
|
+
index: number;
|
|
65
|
+
}) {
|
|
66
|
+
// @ts-expect-error - React 18/19 호환성: Context 타입 불일치
|
|
67
|
+
const inputOTPContext = React.useContext(OTPInputContext);
|
|
68
|
+
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div
|
|
72
|
+
data-slot="input-otp-slot"
|
|
73
|
+
data-active={isActive}
|
|
74
|
+
className={cn(
|
|
75
|
+
"data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
|
|
76
|
+
className
|
|
77
|
+
)}
|
|
78
|
+
{...props}
|
|
79
|
+
>
|
|
80
|
+
{char}
|
|
81
|
+
{hasFakeCaret && (
|
|
82
|
+
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
|
83
|
+
<div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** OTP 그룹 사이의 구분선 컴포넌트입니다. */
|
|
91
|
+
function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
|
|
92
|
+
return (
|
|
93
|
+
<div data-slot="input-otp-separator" role="separator" {...props}>
|
|
94
|
+
<MinusIcon />
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./item";
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
import { Separator } from "@/components/separator";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 리스트 아이템들을 그룹화하는 컴포넌트입니다.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* <ItemGroup>
|
|
14
|
+
* <Item>
|
|
15
|
+
* <ItemMedia variant="icon"><UserIcon /></ItemMedia>
|
|
16
|
+
* <ItemContent>
|
|
17
|
+
* <ItemTitle>제목</ItemTitle>
|
|
18
|
+
* <ItemDescription>설명</ItemDescription>
|
|
19
|
+
* </ItemContent>
|
|
20
|
+
* </Item>
|
|
21
|
+
* </ItemGroup>
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
role="list"
|
|
28
|
+
data-slot="item-group"
|
|
29
|
+
className={cn("group/item-group flex flex-col", className)}
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 아이템 사이의 구분선 컴포넌트입니다. */
|
|
36
|
+
function ItemSeparator({
|
|
37
|
+
className,
|
|
38
|
+
...props
|
|
39
|
+
}: React.ComponentProps<typeof Separator>) {
|
|
40
|
+
return (
|
|
41
|
+
<Separator
|
|
42
|
+
data-slot="item-separator"
|
|
43
|
+
orientation="horizontal"
|
|
44
|
+
className={cn("my-0", className)}
|
|
45
|
+
{...props}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const itemVariants = cva(
|
|
51
|
+
"group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
|
52
|
+
{
|
|
53
|
+
variants: {
|
|
54
|
+
variant: {
|
|
55
|
+
default: "bg-transparent",
|
|
56
|
+
outline: "border-border",
|
|
57
|
+
muted: "bg-muted/50",
|
|
58
|
+
},
|
|
59
|
+
size: {
|
|
60
|
+
default: "p-4 gap-4 ",
|
|
61
|
+
sm: "py-3 px-4 gap-2.5",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
defaultVariants: {
|
|
65
|
+
variant: "default",
|
|
66
|
+
size: "default",
|
|
67
|
+
},
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 개별 리스트 아이템 컴포넌트입니다.
|
|
73
|
+
* @param props.variant - 스타일 변형 ('default' | 'outline' | 'muted')
|
|
74
|
+
* @param props.size - 크기 ('default' | 'sm')
|
|
75
|
+
*/
|
|
76
|
+
function Item({
|
|
77
|
+
className,
|
|
78
|
+
variant = "default",
|
|
79
|
+
size = "default",
|
|
80
|
+
asChild = false,
|
|
81
|
+
...props
|
|
82
|
+
}: React.ComponentProps<"div"> &
|
|
83
|
+
VariantProps<typeof itemVariants> & { asChild?: boolean }) {
|
|
84
|
+
const Comp = asChild ? Slot : "div";
|
|
85
|
+
return (
|
|
86
|
+
<Comp
|
|
87
|
+
data-slot="item"
|
|
88
|
+
data-variant={variant}
|
|
89
|
+
data-size={size}
|
|
90
|
+
className={cn(itemVariants({ variant, size, className }))}
|
|
91
|
+
{...props}
|
|
92
|
+
/>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const itemMediaVariants = cva(
|
|
97
|
+
"flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5",
|
|
98
|
+
{
|
|
99
|
+
variants: {
|
|
100
|
+
variant: {
|
|
101
|
+
default: "bg-transparent",
|
|
102
|
+
icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4",
|
|
103
|
+
image:
|
|
104
|
+
"size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover",
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
defaultVariants: {
|
|
108
|
+
variant: "default",
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 아이템의 미디어(아이콘/이미지) 영역입니다.
|
|
115
|
+
* @param props.variant - 스타일 변형 ('default' | 'icon' | 'image')
|
|
116
|
+
*/
|
|
117
|
+
function ItemMedia({
|
|
118
|
+
className,
|
|
119
|
+
variant = "default",
|
|
120
|
+
...props
|
|
121
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof itemMediaVariants>) {
|
|
122
|
+
return (
|
|
123
|
+
<div
|
|
124
|
+
data-slot="item-media"
|
|
125
|
+
data-variant={variant}
|
|
126
|
+
className={cn(itemMediaVariants({ variant, className }))}
|
|
127
|
+
{...props}
|
|
128
|
+
/>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 아이템의 콘텐츠 영역입니다. */
|
|
133
|
+
function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
134
|
+
return (
|
|
135
|
+
<div
|
|
136
|
+
data-slot="item-content"
|
|
137
|
+
className={cn(
|
|
138
|
+
"flex flex-1 flex-col gap-1 [&+[data-slot=item-content]]:flex-none",
|
|
139
|
+
className
|
|
140
|
+
)}
|
|
141
|
+
{...props}
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** 아이템의 제목 컴포넌트입니다. */
|
|
147
|
+
function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
148
|
+
return (
|
|
149
|
+
<div
|
|
150
|
+
data-slot="item-title"
|
|
151
|
+
className={cn(
|
|
152
|
+
"flex w-fit items-center gap-2 text-sm leading-snug font-medium",
|
|
153
|
+
className
|
|
154
|
+
)}
|
|
155
|
+
{...props}
|
|
156
|
+
/>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** 아이템의 설명 텍스트입니다. */
|
|
161
|
+
function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
162
|
+
return (
|
|
163
|
+
<p
|
|
164
|
+
data-slot="item-description"
|
|
165
|
+
className={cn(
|
|
166
|
+
"text-muted-foreground line-clamp-2 text-sm leading-normal font-normal text-balance",
|
|
167
|
+
"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
|
|
168
|
+
className
|
|
169
|
+
)}
|
|
170
|
+
{...props}
|
|
171
|
+
/>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** 아이템의 액션 버튼 영역입니다. */
|
|
176
|
+
function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
|
|
177
|
+
return (
|
|
178
|
+
<div
|
|
179
|
+
data-slot="item-actions"
|
|
180
|
+
className={cn("flex items-center gap-2", className)}
|
|
181
|
+
{...props}
|
|
182
|
+
/>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** 아이템의 헤더 영역입니다. */
|
|
187
|
+
function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
188
|
+
return (
|
|
189
|
+
<div
|
|
190
|
+
data-slot="item-header"
|
|
191
|
+
className={cn(
|
|
192
|
+
"flex basis-full items-center justify-between gap-2",
|
|
193
|
+
className
|
|
194
|
+
)}
|
|
195
|
+
{...props}
|
|
196
|
+
/>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** 아이템의 푸터 영역입니다. */
|
|
201
|
+
function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
202
|
+
return (
|
|
203
|
+
<div
|
|
204
|
+
data-slot="item-footer"
|
|
205
|
+
className={cn(
|
|
206
|
+
"flex basis-full items-center justify-between gap-2",
|
|
207
|
+
className
|
|
208
|
+
)}
|
|
209
|
+
{...props}
|
|
210
|
+
/>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export {
|
|
215
|
+
Item,
|
|
216
|
+
ItemMedia,
|
|
217
|
+
ItemContent,
|
|
218
|
+
ItemActions,
|
|
219
|
+
ItemGroup,
|
|
220
|
+
ItemSeparator,
|
|
221
|
+
ItemTitle,
|
|
222
|
+
ItemDescription,
|
|
223
|
+
ItemHeader,
|
|
224
|
+
ItemFooter,
|
|
225
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./kbd";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { cn } from "@/lib/utils"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 키보드 단축키를 표시하는 컴포넌트입니다.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* <Kbd>⌘</Kbd>
|
|
9
|
+
* <Kbd>K</Kbd>
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
|
|
13
|
+
return (
|
|
14
|
+
<kbd
|
|
15
|
+
data-slot="kbd"
|
|
16
|
+
className={cn(
|
|
17
|
+
"bg-muted text-muted-foreground pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-sm px-1 font-sans text-xs font-medium select-none",
|
|
18
|
+
"[&_svg:not([class*='size-'])]:size-3",
|
|
19
|
+
"[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10",
|
|
20
|
+
className
|
|
21
|
+
)}
|
|
22
|
+
{...props}
|
|
23
|
+
/>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 여러 키보드 단축키를 그룹화하는 컴포넌트입니다. */
|
|
28
|
+
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
29
|
+
return (
|
|
30
|
+
<kbd
|
|
31
|
+
data-slot="kbd-group"
|
|
32
|
+
className={cn("inline-flex items-center gap-1", className)}
|
|
33
|
+
{...props}
|
|
34
|
+
/>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { Kbd, KbdGroup }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./label";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as LabelPrimitive from "@radix-ui/react-label"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 폼 요소에 대한 레이블을 표시하는 컴포넌트입니다.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* <Label htmlFor="email">이메일</Label>
|
|
14
|
+
* <Input id="email" type="email" />
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
function Label({
|
|
18
|
+
className,
|
|
19
|
+
...props
|
|
20
|
+
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
|
21
|
+
return (
|
|
22
|
+
<LabelPrimitive.Root
|
|
23
|
+
data-slot="label"
|
|
24
|
+
className={cn(
|
|
25
|
+
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
|
26
|
+
className
|
|
27
|
+
)}
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export { Label }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./menubar";
|