@suphark/ui 0.1.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 (147) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +21 -0
  3. package/README.md +120 -0
  4. package/components.json +21 -0
  5. package/package.json +108 -0
  6. package/scripts/audit-base-ui-props.mjs +179 -0
  7. package/scripts/audit-tailwind-typography.mjs +100 -0
  8. package/src/components/design-system/design-system-showcase.tsx +138 -0
  9. package/src/components/design-system/sections/showcase-data-display.tsx +454 -0
  10. package/src/components/design-system/sections/showcase-feedback.tsx +273 -0
  11. package/src/components/design-system/sections/showcase-forms.tsx +373 -0
  12. package/src/components/design-system/sections/showcase-foundations.tsx +384 -0
  13. package/src/components/design-system/sections/showcase-guidelines-recipes.tsx +397 -0
  14. package/src/components/design-system/sections/showcase-overlays-navigation.tsx +479 -0
  15. package/src/components/design-system/sections/showcase-overview.tsx +368 -0
  16. package/src/components/design-system/sections/showcase-primitives.tsx +463 -0
  17. package/src/components/shared/__tests__/multi-select-combobox.test.tsx +180 -0
  18. package/src/components/shared/__tests__/name-id-fields.test.ts +24 -0
  19. package/src/components/shared/button/add-new-button.tsx +19 -0
  20. package/src/components/shared/button/base/action-button.tsx +134 -0
  21. package/src/components/shared/button/base/link-button.tsx +130 -0
  22. package/src/components/shared/button/cancel-button.tsx +27 -0
  23. package/src/components/shared/button/confirm-button.tsx +26 -0
  24. package/src/components/shared/button/delete-button.tsx +36 -0
  25. package/src/components/shared/button/floating-save-button.tsx +89 -0
  26. package/src/components/shared/button/index.ts +13 -0
  27. package/src/components/shared/button/print-button.tsx +67 -0
  28. package/src/components/shared/button/refresh-button.tsx +63 -0
  29. package/src/components/shared/button/save-button.tsx +29 -0
  30. package/src/components/shared/button/submit-button.tsx +58 -0
  31. package/src/components/shared/client-redirect.tsx +11 -0
  32. package/src/components/shared/combobox.tsx +116 -0
  33. package/src/components/shared/data-list.tsx +65 -0
  34. package/src/components/shared/date/date-range-picker.tsx +106 -0
  35. package/src/components/shared/description-list.tsx +27 -0
  36. package/src/components/shared/empty-row.tsx +13 -0
  37. package/src/components/shared/empty-state.tsx +31 -0
  38. package/src/components/shared/env-snippet.tsx +62 -0
  39. package/src/components/shared/form/__tests__/simple-select.test.tsx +72 -0
  40. package/src/components/shared/form/form-field-boolean.tsx +93 -0
  41. package/src/components/shared/form/form-field-color-select.tsx +86 -0
  42. package/src/components/shared/form/form-field-combobox.tsx +167 -0
  43. package/src/components/shared/form/form-field-date.tsx +55 -0
  44. package/src/components/shared/form/form-field-formatted-number.tsx +103 -0
  45. package/src/components/shared/form/form-field-input.tsx +372 -0
  46. package/src/components/shared/form/form-field-radio-cards.tsx +127 -0
  47. package/src/components/shared/form/form-field-radio-group.tsx +56 -0
  48. package/src/components/shared/form/form-field-select.tsx +312 -0
  49. package/src/components/shared/form/simple-select.tsx +152 -0
  50. package/src/components/shared/form-dialog.tsx +109 -0
  51. package/src/components/shared/form-message.tsx +30 -0
  52. package/src/components/shared/initials.tsx +33 -0
  53. package/src/components/shared/layout/error-state.tsx +160 -0
  54. package/src/components/shared/layout/page-shell.tsx +57 -0
  55. package/src/components/shared/link/transition-link.tsx +72 -0
  56. package/src/components/shared/multi-select-combobox.tsx +392 -0
  57. package/src/components/shared/name-id-fields.tsx +133 -0
  58. package/src/components/shared/navigation/page-breadcrumb.tsx +100 -0
  59. package/src/components/shared/notice.tsx +35 -0
  60. package/src/components/shared/number/formatted-number.tsx +133 -0
  61. package/src/components/shared/page-header.tsx +25 -0
  62. package/src/components/shared/page-skeleton.tsx +18 -0
  63. package/src/components/shared/row-actions.tsx +93 -0
  64. package/src/components/shared/row-menu.tsx +55 -0
  65. package/src/components/shared/secret-reveal.tsx +53 -0
  66. package/src/components/shared/section-card.tsx +44 -0
  67. package/src/components/shared/stat.tsx +26 -0
  68. package/src/components/shared/status-badge.tsx +47 -0
  69. package/src/components/shared/text/primary-secondary-text.tsx +186 -0
  70. package/src/components/shared/theme/index.ts +4 -0
  71. package/src/components/shared/theme/theme-menu-items.tsx +43 -0
  72. package/src/components/shared/theme/theme-provider.tsx +39 -0
  73. package/src/components/shared/theme/theme-switch.tsx +36 -0
  74. package/src/components/shared/theme/theme-toggle.tsx +76 -0
  75. package/src/components/shared/time/client-date-formatter.tsx +188 -0
  76. package/src/components/shared/ui-text.tsx +47 -0
  77. package/src/components/ui/__tests__/combobox.test.tsx +40 -0
  78. package/src/components/ui/__tests__/date-picker.test.tsx +53 -0
  79. package/src/components/ui/__tests__/slider.test.tsx +18 -0
  80. package/src/components/ui/__tests__/tabs.test.tsx +33 -0
  81. package/src/components/ui/accordion.tsx +69 -0
  82. package/src/components/ui/alert-dialog.tsx +161 -0
  83. package/src/components/ui/alert.tsx +68 -0
  84. package/src/components/ui/aspect-ratio.tsx +22 -0
  85. package/src/components/ui/attachment.tsx +194 -0
  86. package/src/components/ui/avatar.tsx +92 -0
  87. package/src/components/ui/badge.tsx +48 -0
  88. package/src/components/ui/breadcrumb.tsx +102 -0
  89. package/src/components/ui/button-group.tsx +77 -0
  90. package/src/components/ui/button.tsx +57 -0
  91. package/src/components/ui/calendar.tsx +184 -0
  92. package/src/components/ui/card.tsx +87 -0
  93. package/src/components/ui/carousel.tsx +230 -0
  94. package/src/components/ui/chart.tsx +338 -0
  95. package/src/components/ui/checkbox.tsx +27 -0
  96. package/src/components/ui/collapsible.tsx +17 -0
  97. package/src/components/ui/combobox.tsx +272 -0
  98. package/src/components/ui/command.tsx +180 -0
  99. package/src/components/ui/context-menu.tsx +244 -0
  100. package/src/components/ui/date-picker.tsx +308 -0
  101. package/src/components/ui/dialog.tsx +137 -0
  102. package/src/components/ui/drawer.tsx +208 -0
  103. package/src/components/ui/dropdown-menu.tsx +257 -0
  104. package/src/components/ui/empty.tsx +93 -0
  105. package/src/components/ui/field.tsx +223 -0
  106. package/src/components/ui/form.tsx +161 -0
  107. package/src/components/ui/hover-card.tsx +46 -0
  108. package/src/components/ui/input-group.tsx +145 -0
  109. package/src/components/ui/input-otp.tsx +85 -0
  110. package/src/components/ui/input.tsx +19 -0
  111. package/src/components/ui/item.tsx +187 -0
  112. package/src/components/ui/kbd.tsx +26 -0
  113. package/src/components/ui/label.tsx +19 -0
  114. package/src/components/ui/menubar.tsx +266 -0
  115. package/src/components/ui/navigation-menu.tsx +160 -0
  116. package/src/components/ui/pagination.tsx +117 -0
  117. package/src/components/ui/popover.tsx +76 -0
  118. package/src/components/ui/progress.tsx +66 -0
  119. package/src/components/ui/radio-group.tsx +38 -0
  120. package/src/components/ui/relative-date.tsx +40 -0
  121. package/src/components/ui/resizable.tsx +41 -0
  122. package/src/components/ui/scroll-area.tsx +67 -0
  123. package/src/components/ui/searchable-select.tsx +124 -0
  124. package/src/components/ui/select.tsx +189 -0
  125. package/src/components/ui/separator.tsx +21 -0
  126. package/src/components/ui/sheet.tsx +124 -0
  127. package/src/components/ui/sidebar.tsx +691 -0
  128. package/src/components/ui/skeleton.tsx +13 -0
  129. package/src/components/ui/slider.tsx +55 -0
  130. package/src/components/ui/sonner.tsx +45 -0
  131. package/src/components/ui/spinner.tsx +16 -0
  132. package/src/components/ui/switch.tsx +32 -0
  133. package/src/components/ui/table.tsx +95 -0
  134. package/src/components/ui/tabs.tsx +79 -0
  135. package/src/components/ui/textarea.tsx +17 -0
  136. package/src/components/ui/toggle-group.tsx +86 -0
  137. package/src/components/ui/toggle.tsx +44 -0
  138. package/src/components/ui/tooltip.tsx +54 -0
  139. package/src/hooks/use-copy-to-clipboard.ts +27 -0
  140. package/src/hooks/use-mobile.ts +21 -0
  141. package/src/index.ts +156 -0
  142. package/src/lib/formatters.ts +44 -0
  143. package/src/lib/permission.ts +75 -0
  144. package/src/lib/status-colors.ts +52 -0
  145. package/src/lib/utils.ts +6 -0
  146. package/src/next.ts +15 -0
  147. package/src/styles.css +214 -0
@@ -0,0 +1,184 @@
1
+ "use client";
2
+
3
+ import { Button, buttonVariants } from "@suphark/ui/components/ui/button";
4
+ import { cn } from "@suphark/ui/lib/utils";
5
+ import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
6
+ import * as React from "react";
7
+ import { type DayButton, DayPicker, getDefaultClassNames, type Locale } from "react-day-picker";
8
+
9
+ function Calendar({
10
+ className,
11
+ classNames,
12
+ showOutsideDays = true,
13
+ captionLayout = "label",
14
+ buttonVariant = "ghost",
15
+ locale,
16
+ formatters,
17
+ components,
18
+ ...props
19
+ }: React.ComponentProps<typeof DayPicker> & {
20
+ buttonVariant?: React.ComponentProps<typeof Button>["variant"];
21
+ }) {
22
+ const defaultClassNames = getDefaultClassNames();
23
+
24
+ return (
25
+ <DayPicker
26
+ showOutsideDays={showOutsideDays}
27
+ className={cn(
28
+ "group/calendar bg-background in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent p-2 [--cell-radius:var(--radius-md)] [--cell-size:--spacing(7)]",
29
+ String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
30
+ String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
31
+ className,
32
+ )}
33
+ captionLayout={captionLayout}
34
+ locale={locale}
35
+ formatters={{
36
+ formatMonthDropdown: (date) => date.toLocaleString(locale?.code, { month: "short" }),
37
+ ...formatters,
38
+ }}
39
+ classNames={{
40
+ root: cn("w-fit", defaultClassNames.root),
41
+ months: cn("relative flex flex-col gap-4 md:flex-row", defaultClassNames.months),
42
+ month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
43
+ nav: cn(
44
+ "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
45
+ defaultClassNames.nav,
46
+ ),
47
+ button_previous: cn(
48
+ buttonVariants({ variant: buttonVariant }),
49
+ "size-(--cell-size) select-none p-0 aria-disabled:opacity-50",
50
+ defaultClassNames.button_previous,
51
+ ),
52
+ button_next: cn(
53
+ buttonVariants({ variant: buttonVariant }),
54
+ "size-(--cell-size) select-none p-0 aria-disabled:opacity-50",
55
+ defaultClassNames.button_next,
56
+ ),
57
+ month_caption: cn(
58
+ "flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)",
59
+ defaultClassNames.month_caption,
60
+ ),
61
+ dropdowns: cn(
62
+ "flex h-(--cell-size) w-full items-center justify-center gap-1.5 font-medium text-sm",
63
+ defaultClassNames.dropdowns,
64
+ ),
65
+ dropdown_root: cn("relative rounded-(--cell-radius)", defaultClassNames.dropdown_root),
66
+ dropdown: cn("absolute inset-0 bg-popover opacity-0", defaultClassNames.dropdown),
67
+ caption_label: cn(
68
+ "select-none font-medium",
69
+ captionLayout === "label"
70
+ ? "text-sm"
71
+ : "flex items-center gap-1 rounded-(--cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground",
72
+ defaultClassNames.caption_label,
73
+ ),
74
+ month_grid: cn("w-full border-collapse", defaultClassNames.month_grid),
75
+ weekdays: cn("flex", defaultClassNames.weekdays),
76
+ weekday: cn(
77
+ "flex-1 select-none rounded-(--cell-radius) font-normal text-[0.8rem] text-muted-foreground",
78
+ defaultClassNames.weekday,
79
+ ),
80
+ week: cn("mt-2 flex w-full", defaultClassNames.week),
81
+ week_number_header: cn("w-(--cell-size) select-none", defaultClassNames.week_number_header),
82
+ week_number: cn(
83
+ "select-none text-[0.8rem] text-muted-foreground",
84
+ defaultClassNames.week_number,
85
+ ),
86
+ day: cn(
87
+ "group/day relative aspect-square h-full w-full select-none rounded-(--cell-radius) p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)",
88
+ props.showWeekNumber
89
+ ? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)"
90
+ : "[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)",
91
+ defaultClassNames.day,
92
+ ),
93
+ range_start: cn(
94
+ "relative isolate z-0 rounded-l-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-muted",
95
+ defaultClassNames.range_start,
96
+ ),
97
+ range_middle: cn("rounded-none", defaultClassNames.range_middle),
98
+ range_end: cn(
99
+ "relative isolate z-0 rounded-r-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-muted",
100
+ defaultClassNames.range_end,
101
+ ),
102
+ today: cn(
103
+ "rounded-(--cell-radius) bg-muted text-foreground data-[selected=true]:rounded-none",
104
+ defaultClassNames.today,
105
+ ),
106
+ outside: cn(
107
+ "text-muted-foreground aria-selected:text-muted-foreground",
108
+ defaultClassNames.outside,
109
+ ),
110
+ disabled: cn("text-muted-foreground opacity-50", defaultClassNames.disabled),
111
+ hidden: cn("invisible", defaultClassNames.hidden),
112
+ ...classNames,
113
+ }}
114
+ components={{
115
+ Root: ({ className, rootRef, ...props }) => {
116
+ return <div data-slot="calendar" ref={rootRef} className={cn(className)} {...props} />;
117
+ },
118
+ Chevron: ({ className, orientation, ...props }) => {
119
+ if (orientation === "left") {
120
+ return <ChevronLeftIcon className={cn("size-4", className)} {...props} />;
121
+ }
122
+
123
+ if (orientation === "right") {
124
+ return <ChevronRightIcon className={cn("size-4", className)} {...props} />;
125
+ }
126
+
127
+ return <ChevronDownIcon className={cn("size-4", className)} {...props} />;
128
+ },
129
+ DayButton: ({ ...props }) => <CalendarDayButton locale={locale} {...props} />,
130
+ WeekNumber: ({ children, ...props }) => {
131
+ return (
132
+ <td {...props}>
133
+ <div className="flex size-(--cell-size) items-center justify-center text-center">
134
+ {children}
135
+ </div>
136
+ </td>
137
+ );
138
+ },
139
+ ...components,
140
+ }}
141
+ {...props}
142
+ />
143
+ );
144
+ }
145
+
146
+ function CalendarDayButton({
147
+ className,
148
+ day,
149
+ modifiers,
150
+ locale,
151
+ ...props
152
+ }: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) {
153
+ const defaultClassNames = getDefaultClassNames();
154
+
155
+ const ref = React.useRef<HTMLButtonElement>(null);
156
+ React.useEffect(() => {
157
+ if (modifiers.focused) ref.current?.focus();
158
+ }, [modifiers.focused]);
159
+
160
+ return (
161
+ <Button
162
+ variant="ghost"
163
+ size="icon"
164
+ data-day={day.date.toLocaleDateString(locale?.code)}
165
+ data-selected-single={
166
+ modifiers.selected &&
167
+ !modifiers.range_start &&
168
+ !modifiers.range_end &&
169
+ !modifiers.range_middle
170
+ }
171
+ data-range-start={modifiers.range_start}
172
+ data-range-end={modifiers.range_end}
173
+ data-range-middle={modifiers.range_middle}
174
+ className={cn(
175
+ "relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 font-normal leading-none data-[range-end=true]:rounded-(--cell-radius) data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius) data-[range-end=true]:bg-primary data-[range-middle=true]:bg-muted data-[range-start=true]:bg-primary data-[selected-single=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:text-foreground data-[range-start=true]:text-primary-foreground data-[selected-single=true]:text-primary-foreground group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 dark:hover:text-foreground [&>span]:text-xs [&>span]:opacity-70",
176
+ defaultClassNames.day,
177
+ className,
178
+ )}
179
+ {...props}
180
+ />
181
+ );
182
+ }
183
+
184
+ export { Calendar, CalendarDayButton };
@@ -0,0 +1,87 @@
1
+ import { cn } from "@suphark/ui/lib/utils";
2
+ import type * as React from "react";
3
+
4
+ function Card({
5
+ className,
6
+ size = "default",
7
+ ...props
8
+ }: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
9
+ return (
10
+ <div
11
+ data-slot="card"
12
+ data-size={size}
13
+ className={cn(
14
+ "group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-card-foreground text-sm ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-[>img:first-child]:pt-0 has-data-[slot=card-footer]:pb-0 data-[size=sm]:has-data-[slot=card-footer]:pb-0 data-[size=sm]:[--card-spacing:--spacing(3)] *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
15
+ className,
16
+ )}
17
+ {...props}
18
+ />
19
+ );
20
+ }
21
+
22
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
23
+ return (
24
+ <div
25
+ data-slot="card-header"
26
+ className={cn(
27
+ "group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
28
+ className,
29
+ )}
30
+ {...props}
31
+ />
32
+ );
33
+ }
34
+
35
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
36
+ return (
37
+ <div
38
+ data-slot="card-title"
39
+ className={cn(
40
+ "font-medium text-base leading-snug group-data-[size=sm]/card:text-sm",
41
+ className,
42
+ )}
43
+ {...props}
44
+ />
45
+ );
46
+ }
47
+
48
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
49
+ return (
50
+ <div
51
+ data-slot="card-description"
52
+ className={cn("text-muted-foreground text-sm", className)}
53
+ {...props}
54
+ />
55
+ );
56
+ }
57
+
58
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
59
+ return (
60
+ <div
61
+ data-slot="card-action"
62
+ className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
63
+ {...props}
64
+ />
65
+ );
66
+ }
67
+
68
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
69
+ return (
70
+ <div data-slot="card-content" className={cn("px-(--card-spacing)", className)} {...props} />
71
+ );
72
+ }
73
+
74
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75
+ return (
76
+ <div
77
+ data-slot="card-footer"
78
+ className={cn(
79
+ "flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)",
80
+ className,
81
+ )}
82
+ {...props}
83
+ />
84
+ );
85
+ }
86
+
87
+ export { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle };
@@ -0,0 +1,230 @@
1
+ "use client";
2
+
3
+ import { Button } from "@suphark/ui/components/ui/button";
4
+ import { cn } from "@suphark/ui/lib/utils";
5
+ import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react";
6
+ import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
7
+ import * as React from "react";
8
+
9
+ type CarouselApi = UseEmblaCarouselType[1];
10
+ type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
11
+ type CarouselOptions = UseCarouselParameters[0];
12
+ type CarouselPlugin = UseCarouselParameters[1];
13
+
14
+ type CarouselProps = {
15
+ opts?: CarouselOptions;
16
+ plugins?: CarouselPlugin;
17
+ orientation?: "horizontal" | "vertical";
18
+ setApi?: (api: CarouselApi) => void;
19
+ };
20
+
21
+ type CarouselContextProps = {
22
+ carouselRef: ReturnType<typeof useEmblaCarousel>[0];
23
+ api: ReturnType<typeof useEmblaCarousel>[1];
24
+ scrollPrev: () => void;
25
+ scrollNext: () => void;
26
+ canScrollPrev: boolean;
27
+ canScrollNext: boolean;
28
+ } & CarouselProps;
29
+
30
+ const CarouselContext = React.createContext<CarouselContextProps | null>(null);
31
+
32
+ function useCarousel() {
33
+ const context = React.useContext(CarouselContext);
34
+
35
+ if (!context) {
36
+ throw new Error("useCarousel must be used within a <Carousel />");
37
+ }
38
+
39
+ return context;
40
+ }
41
+
42
+ function Carousel({
43
+ orientation = "horizontal",
44
+ opts,
45
+ setApi,
46
+ plugins,
47
+ className,
48
+ children,
49
+ ...props
50
+ }: React.ComponentProps<"div"> & CarouselProps) {
51
+ const [carouselRef, api] = useEmblaCarousel(
52
+ {
53
+ ...opts,
54
+ axis: orientation === "horizontal" ? "x" : "y",
55
+ },
56
+ plugins,
57
+ );
58
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false);
59
+ const [canScrollNext, setCanScrollNext] = React.useState(false);
60
+
61
+ const onSelect = React.useCallback((api: CarouselApi) => {
62
+ if (!api) return;
63
+ setCanScrollPrev(api.canScrollPrev());
64
+ setCanScrollNext(api.canScrollNext());
65
+ }, []);
66
+
67
+ const scrollPrev = React.useCallback(() => {
68
+ api?.scrollPrev();
69
+ }, [api]);
70
+
71
+ const scrollNext = React.useCallback(() => {
72
+ api?.scrollNext();
73
+ }, [api]);
74
+
75
+ const handleKeyDown = React.useCallback(
76
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
77
+ if (event.key === "ArrowLeft") {
78
+ event.preventDefault();
79
+ scrollPrev();
80
+ } else if (event.key === "ArrowRight") {
81
+ event.preventDefault();
82
+ scrollNext();
83
+ }
84
+ },
85
+ [scrollPrev, scrollNext],
86
+ );
87
+
88
+ React.useEffect(() => {
89
+ if (!api || !setApi) return;
90
+ setApi(api);
91
+ }, [api, setApi]);
92
+
93
+ React.useEffect(() => {
94
+ if (!api) return;
95
+ onSelect(api);
96
+ api.on("reInit", onSelect);
97
+ api.on("select", onSelect);
98
+
99
+ return () => {
100
+ api?.off("select", onSelect);
101
+ };
102
+ }, [api, onSelect]);
103
+
104
+ return (
105
+ <CarouselContext.Provider
106
+ value={{
107
+ carouselRef,
108
+ api: api,
109
+ opts,
110
+ orientation: orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
111
+ scrollPrev,
112
+ scrollNext,
113
+ canScrollPrev,
114
+ canScrollNext,
115
+ }}
116
+ >
117
+ <div
118
+ onKeyDownCapture={handleKeyDown}
119
+ className={cn("relative", className)}
120
+ role="region"
121
+ aria-roledescription="carousel"
122
+ data-slot="carousel"
123
+ {...props}
124
+ >
125
+ {children}
126
+ </div>
127
+ </CarouselContext.Provider>
128
+ );
129
+ }
130
+
131
+ function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
132
+ const { carouselRef, orientation } = useCarousel();
133
+
134
+ return (
135
+ <div ref={carouselRef} className="overflow-hidden" data-slot="carousel-content">
136
+ <div
137
+ className={cn("flex", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", className)}
138
+ {...props}
139
+ />
140
+ </div>
141
+ );
142
+ }
143
+
144
+ function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
145
+ const { orientation } = useCarousel();
146
+
147
+ return (
148
+ <div
149
+ role="group"
150
+ aria-roledescription="slide"
151
+ data-slot="carousel-item"
152
+ className={cn(
153
+ "min-w-0 shrink-0 grow-0 basis-full",
154
+ orientation === "horizontal" ? "pl-4" : "pt-4",
155
+ className,
156
+ )}
157
+ {...props}
158
+ />
159
+ );
160
+ }
161
+
162
+ function CarouselPrevious({
163
+ className,
164
+ variant = "outline",
165
+ size = "icon-sm",
166
+ ...props
167
+ }: React.ComponentProps<typeof Button>) {
168
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel();
169
+
170
+ return (
171
+ <Button
172
+ data-slot="carousel-previous"
173
+ variant={variant}
174
+ size={size}
175
+ className={cn(
176
+ "absolute touch-manipulation rounded-full",
177
+ orientation === "horizontal"
178
+ ? "inset-y-0 -left-12 my-auto"
179
+ : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
180
+ className,
181
+ )}
182
+ disabled={!canScrollPrev}
183
+ onClick={scrollPrev}
184
+ {...props}
185
+ >
186
+ <ChevronLeftIcon />
187
+ <span className="sr-only">Previous slide</span>
188
+ </Button>
189
+ );
190
+ }
191
+
192
+ function CarouselNext({
193
+ className,
194
+ variant = "outline",
195
+ size = "icon-sm",
196
+ ...props
197
+ }: React.ComponentProps<typeof Button>) {
198
+ const { orientation, scrollNext, canScrollNext } = useCarousel();
199
+
200
+ return (
201
+ <Button
202
+ data-slot="carousel-next"
203
+ variant={variant}
204
+ size={size}
205
+ className={cn(
206
+ "absolute touch-manipulation rounded-full",
207
+ orientation === "horizontal"
208
+ ? "inset-y-0 -right-12 my-auto"
209
+ : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
210
+ className,
211
+ )}
212
+ disabled={!canScrollNext}
213
+ onClick={scrollNext}
214
+ {...props}
215
+ >
216
+ <ChevronRightIcon />
217
+ <span className="sr-only">Next slide</span>
218
+ </Button>
219
+ );
220
+ }
221
+
222
+ export {
223
+ Carousel,
224
+ type CarouselApi,
225
+ CarouselContent,
226
+ CarouselItem,
227
+ CarouselNext,
228
+ CarouselPrevious,
229
+ useCarousel,
230
+ };