@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,263 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import useEmblaCarousel, {
|
|
3
|
+
type UseEmblaCarouselType,
|
|
4
|
+
} from "embla-carousel-react";
|
|
5
|
+
import { ArrowLeft, ArrowRight } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
import { cn } from "@/lib/utils";
|
|
8
|
+
import { Button } from "@/components/button";
|
|
9
|
+
|
|
10
|
+
type CarouselApi = UseEmblaCarouselType[1];
|
|
11
|
+
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
|
12
|
+
type CarouselOptions = UseCarouselParameters[0];
|
|
13
|
+
type CarouselPlugin = UseCarouselParameters[1];
|
|
14
|
+
|
|
15
|
+
type CarouselProps = {
|
|
16
|
+
opts?: CarouselOptions;
|
|
17
|
+
plugins?: CarouselPlugin;
|
|
18
|
+
orientation?: "horizontal" | "vertical";
|
|
19
|
+
setApi?: (api: CarouselApi) => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type CarouselContextProps = {
|
|
23
|
+
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
|
24
|
+
api: ReturnType<typeof useEmblaCarousel>[1];
|
|
25
|
+
scrollPrev: () => void;
|
|
26
|
+
scrollNext: () => void;
|
|
27
|
+
canScrollPrev: boolean;
|
|
28
|
+
canScrollNext: boolean;
|
|
29
|
+
} & CarouselProps;
|
|
30
|
+
|
|
31
|
+
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
|
32
|
+
|
|
33
|
+
function useCarousel() {
|
|
34
|
+
const context = React.useContext(CarouselContext);
|
|
35
|
+
|
|
36
|
+
if (!context) {
|
|
37
|
+
throw new Error("useCarousel must be used within a <Carousel />");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return context;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 슬라이드 형태로 콘텐츠를 표시하는 캐러셀 컴포넌트입니다.
|
|
45
|
+
*
|
|
46
|
+
* @param props.orientation - 캐러셀 방향 ('horizontal' | 'vertical')
|
|
47
|
+
* @param props.opts - Embla Carousel 옵션
|
|
48
|
+
* @param props.plugins - Embla Carousel 플러그인
|
|
49
|
+
* @param props.setApi - API 인스턴스 설정 콜백
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```tsx
|
|
53
|
+
* <Carousel>
|
|
54
|
+
* <CarouselContent>
|
|
55
|
+
* <CarouselItem>슬라이드 1</CarouselItem>
|
|
56
|
+
* <CarouselItem>슬라이드 2</CarouselItem>
|
|
57
|
+
* </CarouselContent>
|
|
58
|
+
* <CarouselPrevious />
|
|
59
|
+
* <CarouselNext />
|
|
60
|
+
* </Carousel>
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
function Carousel({
|
|
64
|
+
orientation = "horizontal",
|
|
65
|
+
opts,
|
|
66
|
+
setApi,
|
|
67
|
+
plugins,
|
|
68
|
+
className,
|
|
69
|
+
children,
|
|
70
|
+
...props
|
|
71
|
+
}: React.ComponentProps<"div"> & CarouselProps) {
|
|
72
|
+
const [carouselRef, api] = useEmblaCarousel(
|
|
73
|
+
{
|
|
74
|
+
...opts,
|
|
75
|
+
axis: orientation === "horizontal" ? "x" : "y",
|
|
76
|
+
},
|
|
77
|
+
plugins
|
|
78
|
+
);
|
|
79
|
+
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
|
80
|
+
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
|
81
|
+
|
|
82
|
+
const onSelect = React.useCallback((api: CarouselApi) => {
|
|
83
|
+
if (!api) return;
|
|
84
|
+
setCanScrollPrev(api.canScrollPrev());
|
|
85
|
+
setCanScrollNext(api.canScrollNext());
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
const scrollPrev = React.useCallback(() => {
|
|
89
|
+
api?.scrollPrev();
|
|
90
|
+
}, [api]);
|
|
91
|
+
|
|
92
|
+
const scrollNext = React.useCallback(() => {
|
|
93
|
+
api?.scrollNext();
|
|
94
|
+
}, [api]);
|
|
95
|
+
|
|
96
|
+
const handleKeyDown = React.useCallback(
|
|
97
|
+
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
98
|
+
if (event.key === "ArrowLeft") {
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
scrollPrev();
|
|
101
|
+
} else if (event.key === "ArrowRight") {
|
|
102
|
+
event.preventDefault();
|
|
103
|
+
scrollNext();
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
[scrollPrev, scrollNext]
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
React.useEffect(() => {
|
|
110
|
+
if (!api || !setApi) return;
|
|
111
|
+
setApi(api);
|
|
112
|
+
}, [api, setApi]);
|
|
113
|
+
|
|
114
|
+
React.useEffect(() => {
|
|
115
|
+
if (!api) return;
|
|
116
|
+
onSelect(api);
|
|
117
|
+
api.on("reInit", onSelect);
|
|
118
|
+
api.on("select", onSelect);
|
|
119
|
+
|
|
120
|
+
return () => {
|
|
121
|
+
api?.off("select", onSelect);
|
|
122
|
+
};
|
|
123
|
+
}, [api, onSelect]);
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<CarouselContext.Provider
|
|
127
|
+
value={{
|
|
128
|
+
carouselRef,
|
|
129
|
+
api: api,
|
|
130
|
+
opts,
|
|
131
|
+
orientation:
|
|
132
|
+
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
|
133
|
+
scrollPrev,
|
|
134
|
+
scrollNext,
|
|
135
|
+
canScrollPrev,
|
|
136
|
+
canScrollNext,
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
<div
|
|
140
|
+
onKeyDownCapture={handleKeyDown}
|
|
141
|
+
className={cn("relative", className)}
|
|
142
|
+
role="region"
|
|
143
|
+
aria-roledescription="carousel"
|
|
144
|
+
data-slot="carousel"
|
|
145
|
+
{...props}
|
|
146
|
+
>
|
|
147
|
+
{children}
|
|
148
|
+
</div>
|
|
149
|
+
</CarouselContext.Provider>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** 캐러셀 콘텐츠 컨테이너입니다. */
|
|
154
|
+
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
155
|
+
const { carouselRef, orientation } = useCarousel();
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div
|
|
159
|
+
ref={carouselRef}
|
|
160
|
+
className="overflow-hidden"
|
|
161
|
+
data-slot="carousel-content"
|
|
162
|
+
>
|
|
163
|
+
<div
|
|
164
|
+
className={cn(
|
|
165
|
+
"flex",
|
|
166
|
+
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
|
167
|
+
className
|
|
168
|
+
)}
|
|
169
|
+
{...props}
|
|
170
|
+
/>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** 개별 캐러셀 슬라이드 아이템입니다. */
|
|
176
|
+
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
|
177
|
+
const { orientation } = useCarousel();
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<div
|
|
181
|
+
role="group"
|
|
182
|
+
aria-roledescription="slide"
|
|
183
|
+
data-slot="carousel-item"
|
|
184
|
+
className={cn(
|
|
185
|
+
"min-w-0 shrink-0 grow-0 basis-full",
|
|
186
|
+
orientation === "horizontal" ? "pl-4" : "pt-4",
|
|
187
|
+
className
|
|
188
|
+
)}
|
|
189
|
+
{...props}
|
|
190
|
+
/>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** 이전 슬라이드로 이동하는 버튼입니다. */
|
|
195
|
+
function CarouselPrevious({
|
|
196
|
+
className,
|
|
197
|
+
variant = "outline",
|
|
198
|
+
size = "icon",
|
|
199
|
+
...props
|
|
200
|
+
}: React.ComponentProps<typeof Button>) {
|
|
201
|
+
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<Button
|
|
205
|
+
data-slot="carousel-previous"
|
|
206
|
+
variant={variant}
|
|
207
|
+
size={size}
|
|
208
|
+
className={cn(
|
|
209
|
+
"absolute size-8 rounded-full",
|
|
210
|
+
orientation === "horizontal"
|
|
211
|
+
? "top-1/2 -left-12 -translate-y-1/2"
|
|
212
|
+
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
|
213
|
+
className
|
|
214
|
+
)}
|
|
215
|
+
disabled={!canScrollPrev}
|
|
216
|
+
onClick={scrollPrev}
|
|
217
|
+
{...props}
|
|
218
|
+
>
|
|
219
|
+
<ArrowLeft />
|
|
220
|
+
<span className="sr-only">Previous slide</span>
|
|
221
|
+
</Button>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** 다음 슬라이드로 이동하는 버튼입니다. */
|
|
226
|
+
function CarouselNext({
|
|
227
|
+
className,
|
|
228
|
+
variant = "outline",
|
|
229
|
+
size = "icon",
|
|
230
|
+
...props
|
|
231
|
+
}: React.ComponentProps<typeof Button>) {
|
|
232
|
+
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
|
233
|
+
|
|
234
|
+
return (
|
|
235
|
+
<Button
|
|
236
|
+
data-slot="carousel-next"
|
|
237
|
+
variant={variant}
|
|
238
|
+
size={size}
|
|
239
|
+
className={cn(
|
|
240
|
+
"absolute size-8 rounded-full",
|
|
241
|
+
orientation === "horizontal"
|
|
242
|
+
? "top-1/2 -right-12 -translate-y-1/2"
|
|
243
|
+
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
|
244
|
+
className
|
|
245
|
+
)}
|
|
246
|
+
disabled={!canScrollNext}
|
|
247
|
+
onClick={scrollNext}
|
|
248
|
+
{...props}
|
|
249
|
+
>
|
|
250
|
+
<ArrowRight />
|
|
251
|
+
<span className="sr-only">Next slide</span>
|
|
252
|
+
</Button>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export {
|
|
257
|
+
type CarouselApi,
|
|
258
|
+
Carousel,
|
|
259
|
+
CarouselContent,
|
|
260
|
+
CarouselItem,
|
|
261
|
+
CarouselPrevious,
|
|
262
|
+
CarouselNext,
|
|
263
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./carousel";
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as RechartsPrimitive from "recharts";
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
|
|
8
|
+
// Format: { THEME_NAME: CSS_SELECTOR }
|
|
9
|
+
const THEMES = { light: "", dark: ".dark" } as const;
|
|
10
|
+
|
|
11
|
+
export type ChartConfig = {
|
|
12
|
+
[k in string]: {
|
|
13
|
+
label?: React.ReactNode;
|
|
14
|
+
icon?: React.ComponentType;
|
|
15
|
+
} & (
|
|
16
|
+
| { color?: string; theme?: never }
|
|
17
|
+
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type ChartContextProps = {
|
|
22
|
+
config: ChartConfig;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const ChartContext = React.createContext<ChartContextProps | null>(null);
|
|
26
|
+
|
|
27
|
+
function useChart() {
|
|
28
|
+
const context = React.useContext(ChartContext);
|
|
29
|
+
|
|
30
|
+
if (!context) {
|
|
31
|
+
throw new Error("useChart must be used within a <ChartContainer />");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return context;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Recharts 기반 차트를 위한 컨테이너 컴포넌트입니다.
|
|
39
|
+
*
|
|
40
|
+
* @param props.config - 차트 데이터 시리즈 설정 (레이블, 색상, 아이콘)
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```tsx
|
|
44
|
+
* const chartConfig = {
|
|
45
|
+
* value: { label: "값", color: "hsl(var(--chart-1))" }
|
|
46
|
+
* };
|
|
47
|
+
*
|
|
48
|
+
* <ChartContainer config={chartConfig}>
|
|
49
|
+
* <BarChart data={data}>
|
|
50
|
+
* <Bar dataKey="value" />
|
|
51
|
+
* </BarChart>
|
|
52
|
+
* </ChartContainer>
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
function ChartContainer({
|
|
56
|
+
id,
|
|
57
|
+
className,
|
|
58
|
+
children,
|
|
59
|
+
config,
|
|
60
|
+
...props
|
|
61
|
+
}: React.ComponentProps<"div"> & {
|
|
62
|
+
config: ChartConfig;
|
|
63
|
+
children: React.ComponentProps<
|
|
64
|
+
typeof RechartsPrimitive.ResponsiveContainer
|
|
65
|
+
>["children"];
|
|
66
|
+
}) {
|
|
67
|
+
const uniqueId = React.useId();
|
|
68
|
+
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<ChartContext.Provider value={{ config }}>
|
|
72
|
+
<div
|
|
73
|
+
data-slot="chart"
|
|
74
|
+
data-chart={chartId}
|
|
75
|
+
className={cn(
|
|
76
|
+
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
|
77
|
+
className
|
|
78
|
+
)}
|
|
79
|
+
{...props}
|
|
80
|
+
>
|
|
81
|
+
<ChartStyle id={chartId} config={config} />
|
|
82
|
+
<RechartsPrimitive.ResponsiveContainer>
|
|
83
|
+
{children}
|
|
84
|
+
</RechartsPrimitive.ResponsiveContainer>
|
|
85
|
+
</div>
|
|
86
|
+
</ChartContext.Provider>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
|
91
|
+
const colorConfig = Object.entries(config).filter(
|
|
92
|
+
([, config]) => config.theme || config.color
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
if (!colorConfig.length) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<style
|
|
101
|
+
dangerouslySetInnerHTML={{
|
|
102
|
+
__html: Object.entries(THEMES)
|
|
103
|
+
.map(
|
|
104
|
+
([theme, prefix]) => `
|
|
105
|
+
${prefix} [data-chart=${id}] {
|
|
106
|
+
${colorConfig
|
|
107
|
+
.map(([key, itemConfig]) => {
|
|
108
|
+
const color =
|
|
109
|
+
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
|
110
|
+
itemConfig.color;
|
|
111
|
+
return color ? ` --color-${key}: ${color};` : null;
|
|
112
|
+
})
|
|
113
|
+
.join("\n")}
|
|
114
|
+
}
|
|
115
|
+
`
|
|
116
|
+
)
|
|
117
|
+
.join("\n"),
|
|
118
|
+
}}
|
|
119
|
+
/>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const ChartTooltip = RechartsPrimitive.Tooltip;
|
|
124
|
+
|
|
125
|
+
/** 차트 툴팁 콘텐츠 컴포넌트입니다. */
|
|
126
|
+
function ChartTooltipContent({
|
|
127
|
+
active,
|
|
128
|
+
payload,
|
|
129
|
+
className,
|
|
130
|
+
indicator = "dot",
|
|
131
|
+
hideLabel = false,
|
|
132
|
+
hideIndicator = false,
|
|
133
|
+
label,
|
|
134
|
+
labelFormatter,
|
|
135
|
+
labelClassName,
|
|
136
|
+
formatter,
|
|
137
|
+
color,
|
|
138
|
+
nameKey,
|
|
139
|
+
labelKey,
|
|
140
|
+
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
|
141
|
+
React.ComponentProps<"div"> & {
|
|
142
|
+
hideLabel?: boolean;
|
|
143
|
+
hideIndicator?: boolean;
|
|
144
|
+
indicator?: "line" | "dot" | "dashed";
|
|
145
|
+
nameKey?: string;
|
|
146
|
+
labelKey?: string;
|
|
147
|
+
}) {
|
|
148
|
+
const { config } = useChart();
|
|
149
|
+
|
|
150
|
+
const tooltipLabel = React.useMemo(() => {
|
|
151
|
+
if (hideLabel || !payload?.length) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const [item] = payload;
|
|
156
|
+
const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
|
|
157
|
+
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
|
158
|
+
const value =
|
|
159
|
+
!labelKey && typeof label === "string"
|
|
160
|
+
? config[label as keyof typeof config]?.label || label
|
|
161
|
+
: itemConfig?.label;
|
|
162
|
+
|
|
163
|
+
if (labelFormatter) {
|
|
164
|
+
return (
|
|
165
|
+
<div className={cn("font-medium", labelClassName)}>
|
|
166
|
+
{labelFormatter(value, payload)}
|
|
167
|
+
</div>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!value) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
|
|
176
|
+
}, [
|
|
177
|
+
label,
|
|
178
|
+
labelFormatter,
|
|
179
|
+
payload,
|
|
180
|
+
hideLabel,
|
|
181
|
+
labelClassName,
|
|
182
|
+
config,
|
|
183
|
+
labelKey,
|
|
184
|
+
]);
|
|
185
|
+
|
|
186
|
+
if (!active || !payload?.length) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const nestLabel = payload.length === 1 && indicator !== "dot";
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<div
|
|
194
|
+
className={cn(
|
|
195
|
+
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
|
|
196
|
+
className
|
|
197
|
+
)}
|
|
198
|
+
>
|
|
199
|
+
{!nestLabel ? tooltipLabel : null}
|
|
200
|
+
<div className="grid gap-1.5">
|
|
201
|
+
{payload
|
|
202
|
+
.filter((item) => item.type !== "none")
|
|
203
|
+
.map((item, index) => {
|
|
204
|
+
const key = `${nameKey || item.name || item.dataKey || "value"}`;
|
|
205
|
+
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
|
206
|
+
const indicatorColor = color || item.payload.fill || item.color;
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<div
|
|
210
|
+
key={item.dataKey}
|
|
211
|
+
className={cn(
|
|
212
|
+
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
|
|
213
|
+
indicator === "dot" && "items-center"
|
|
214
|
+
)}
|
|
215
|
+
>
|
|
216
|
+
{formatter && item?.value !== undefined && item.name ? (
|
|
217
|
+
formatter(item.value, item.name, item, index, item.payload)
|
|
218
|
+
) : (
|
|
219
|
+
<>
|
|
220
|
+
{itemConfig?.icon ? (
|
|
221
|
+
<itemConfig.icon />
|
|
222
|
+
) : (
|
|
223
|
+
!hideIndicator && (
|
|
224
|
+
<div
|
|
225
|
+
className={cn(
|
|
226
|
+
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
|
|
227
|
+
{
|
|
228
|
+
"h-2.5 w-2.5": indicator === "dot",
|
|
229
|
+
"w-1": indicator === "line",
|
|
230
|
+
"w-0 border-[1.5px] border-dashed bg-transparent":
|
|
231
|
+
indicator === "dashed",
|
|
232
|
+
"my-0.5": nestLabel && indicator === "dashed",
|
|
233
|
+
}
|
|
234
|
+
)}
|
|
235
|
+
style={
|
|
236
|
+
{
|
|
237
|
+
"--color-bg": indicatorColor,
|
|
238
|
+
"--color-border": indicatorColor,
|
|
239
|
+
} as React.CSSProperties
|
|
240
|
+
}
|
|
241
|
+
/>
|
|
242
|
+
)
|
|
243
|
+
)}
|
|
244
|
+
<div
|
|
245
|
+
className={cn(
|
|
246
|
+
"flex flex-1 justify-between leading-none",
|
|
247
|
+
nestLabel ? "items-end" : "items-center"
|
|
248
|
+
)}
|
|
249
|
+
>
|
|
250
|
+
<div className="grid gap-1.5">
|
|
251
|
+
{nestLabel ? tooltipLabel : null}
|
|
252
|
+
<span className="text-muted-foreground">
|
|
253
|
+
{itemConfig?.label || item.name}
|
|
254
|
+
</span>
|
|
255
|
+
</div>
|
|
256
|
+
{item.value && (
|
|
257
|
+
<span className="text-foreground font-mono font-medium tabular-nums">
|
|
258
|
+
{item.value.toLocaleString()}
|
|
259
|
+
</span>
|
|
260
|
+
)}
|
|
261
|
+
</div>
|
|
262
|
+
</>
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
})}
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const ChartLegend = RechartsPrimitive.Legend;
|
|
273
|
+
|
|
274
|
+
/** 차트 범례 콘텐츠 컴포넌트입니다. */
|
|
275
|
+
function ChartLegendContent({
|
|
276
|
+
className,
|
|
277
|
+
hideIcon = false,
|
|
278
|
+
payload,
|
|
279
|
+
verticalAlign = "bottom",
|
|
280
|
+
nameKey,
|
|
281
|
+
}: React.ComponentProps<"div"> &
|
|
282
|
+
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
|
283
|
+
hideIcon?: boolean;
|
|
284
|
+
nameKey?: string;
|
|
285
|
+
}) {
|
|
286
|
+
const { config } = useChart();
|
|
287
|
+
|
|
288
|
+
if (!payload?.length) {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return (
|
|
293
|
+
<div
|
|
294
|
+
className={cn(
|
|
295
|
+
"flex items-center justify-center gap-4",
|
|
296
|
+
verticalAlign === "top" ? "pb-3" : "pt-3",
|
|
297
|
+
className
|
|
298
|
+
)}
|
|
299
|
+
>
|
|
300
|
+
{payload
|
|
301
|
+
.filter((item) => item.type !== "none")
|
|
302
|
+
.map((item) => {
|
|
303
|
+
const key = `${nameKey || item.dataKey || "value"}`;
|
|
304
|
+
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
|
305
|
+
|
|
306
|
+
return (
|
|
307
|
+
<div
|
|
308
|
+
key={item.value}
|
|
309
|
+
className={cn(
|
|
310
|
+
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3"
|
|
311
|
+
)}
|
|
312
|
+
>
|
|
313
|
+
{itemConfig?.icon && !hideIcon ? (
|
|
314
|
+
<itemConfig.icon />
|
|
315
|
+
) : (
|
|
316
|
+
<div
|
|
317
|
+
className="h-2 w-2 shrink-0 rounded-[2px]"
|
|
318
|
+
style={{
|
|
319
|
+
backgroundColor: item.color,
|
|
320
|
+
}}
|
|
321
|
+
/>
|
|
322
|
+
)}
|
|
323
|
+
{itemConfig?.label}
|
|
324
|
+
</div>
|
|
325
|
+
);
|
|
326
|
+
})}
|
|
327
|
+
</div>
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Helper to extract item config from a payload.
|
|
332
|
+
function getPayloadConfigFromPayload(
|
|
333
|
+
config: ChartConfig,
|
|
334
|
+
payload: unknown,
|
|
335
|
+
key: string
|
|
336
|
+
) {
|
|
337
|
+
if (typeof payload !== "object" || payload === null) {
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const payloadPayload =
|
|
342
|
+
"payload" in payload &&
|
|
343
|
+
typeof payload.payload === "object" &&
|
|
344
|
+
payload.payload !== null
|
|
345
|
+
? payload.payload
|
|
346
|
+
: undefined;
|
|
347
|
+
|
|
348
|
+
let configLabelKey: string = key;
|
|
349
|
+
|
|
350
|
+
if (
|
|
351
|
+
key in payload &&
|
|
352
|
+
typeof payload[key as keyof typeof payload] === "string"
|
|
353
|
+
) {
|
|
354
|
+
configLabelKey = payload[key as keyof typeof payload] as string;
|
|
355
|
+
} else if (
|
|
356
|
+
payloadPayload &&
|
|
357
|
+
key in payloadPayload &&
|
|
358
|
+
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
|
359
|
+
) {
|
|
360
|
+
configLabelKey = payloadPayload[
|
|
361
|
+
key as keyof typeof payloadPayload
|
|
362
|
+
] as string;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return configLabelKey in config
|
|
366
|
+
? config[configLabelKey]
|
|
367
|
+
: config[key as keyof typeof config];
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export {
|
|
371
|
+
ChartContainer,
|
|
372
|
+
ChartTooltip,
|
|
373
|
+
ChartTooltipContent,
|
|
374
|
+
ChartLegend,
|
|
375
|
+
ChartLegendContent,
|
|
376
|
+
ChartStyle,
|
|
377
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./chart";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
|
5
|
+
import { CheckIcon } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
import { cn } from "@/lib/utils"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 선택/해제가 가능한 체크박스 컴포넌트입니다.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* <Checkbox id="terms" />
|
|
15
|
+
* <label htmlFor="terms">이용약관에 동의합니다</label>
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
function Checkbox({
|
|
19
|
+
className,
|
|
20
|
+
...props
|
|
21
|
+
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
|
22
|
+
return (
|
|
23
|
+
<CheckboxPrimitive.Root
|
|
24
|
+
data-slot="checkbox"
|
|
25
|
+
className={cn(
|
|
26
|
+
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
|
27
|
+
className
|
|
28
|
+
)}
|
|
29
|
+
{...props}
|
|
30
|
+
>
|
|
31
|
+
<CheckboxPrimitive.Indicator
|
|
32
|
+
data-slot="checkbox-indicator"
|
|
33
|
+
className="grid place-content-center text-current transition-none"
|
|
34
|
+
>
|
|
35
|
+
<CheckIcon className="size-3.5" />
|
|
36
|
+
</CheckboxPrimitive.Indicator>
|
|
37
|
+
</CheckboxPrimitive.Root>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { Checkbox }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./checkbox";
|