@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,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";