@helloleo/plugins 0.1.20 → 0.2.0-beta.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 (133) hide show
  1. package/dist/index.js +12364 -65
  2. package/dist/templates/tanstack-start-2026-06-18/components.json +22 -0
  3. package/dist/templates/tanstack-start-2026-06-18/drizzle.config.ts +15 -0
  4. package/dist/templates/tanstack-start-2026-06-18/package.json +103 -0
  5. package/dist/templates/tanstack-start-2026-06-18/src/api/health.ts +66 -0
  6. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/accordion.tsx +51 -0
  7. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/alert-dialog.tsx +115 -0
  8. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/alert.tsx +49 -0
  9. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/aspect-ratio.tsx +5 -0
  10. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/avatar.tsx +47 -0
  11. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/badge.tsx +32 -0
  12. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/breadcrumb.tsx +101 -0
  13. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/button.tsx +49 -0
  14. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/calendar.tsx +177 -0
  15. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/card.tsx +55 -0
  16. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/carousel.tsx +240 -0
  17. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/chart.tsx +331 -0
  18. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/checkbox.tsx +26 -0
  19. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/collapsible.tsx +11 -0
  20. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/command.tsx +143 -0
  21. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/context-menu.tsx +187 -0
  22. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/dialog.tsx +104 -0
  23. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/drawer.tsx +98 -0
  24. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/dropdown-menu.tsx +188 -0
  25. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/form.tsx +171 -0
  26. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/hover-card.tsx +27 -0
  27. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/input-otp.tsx +69 -0
  28. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/input.tsx +22 -0
  29. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/label.tsx +21 -0
  30. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/menubar.tsx +229 -0
  31. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/navigation-menu.tsx +120 -0
  32. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/pagination.tsx +98 -0
  33. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/popover.tsx +31 -0
  34. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/progress.tsx +25 -0
  35. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/radio-group.tsx +36 -0
  36. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/resizable.tsx +37 -0
  37. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/scroll-area.tsx +44 -0
  38. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/select.tsx +152 -0
  39. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/separator.tsx +24 -0
  40. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/sheet.tsx +122 -0
  41. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/sidebar.tsx +744 -0
  42. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/skeleton.tsx +7 -0
  43. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/slider.tsx +23 -0
  44. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/sonner.tsx +23 -0
  45. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/switch.tsx +27 -0
  46. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/table.tsx +94 -0
  47. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/tabs.tsx +53 -0
  48. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/textarea.tsx +21 -0
  49. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/toggle-group.tsx +57 -0
  50. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/toggle.tsx +42 -0
  51. package/dist/templates/tanstack-start-2026-06-18/src/components/ui/tooltip.tsx +32 -0
  52. package/dist/templates/tanstack-start-2026-06-18/src/db/index.ts +9 -0
  53. package/dist/templates/tanstack-start-2026-06-18/src/db/schema.ts +18 -0
  54. package/dist/templates/tanstack-start-2026-06-18/src/global.d.ts +3 -0
  55. package/dist/templates/tanstack-start-2026-06-18/src/hooks/use-mobile.tsx +19 -0
  56. package/dist/templates/tanstack-start-2026-06-18/src/lib/proxy.ts +83 -0
  57. package/dist/templates/tanstack-start-2026-06-18/src/lib/utils.ts +6 -0
  58. package/dist/templates/tanstack-start-2026-06-18/src/routeTree.gen.ts +59 -0
  59. package/dist/templates/tanstack-start-2026-06-18/src/router.tsx +33 -0
  60. package/dist/templates/tanstack-start-2026-06-18/src/routes/__root.tsx +60 -0
  61. package/dist/templates/tanstack-start-2026-06-18/src/routes/index.tsx +33 -0
  62. package/dist/templates/tanstack-start-2026-06-18/src/server.ts +32 -0
  63. package/dist/templates/tanstack-start-2026-06-18/src/styles.css +214 -0
  64. package/dist/templates/tanstack-start-2026-06-18/tsconfig.json +27 -0
  65. package/dist/templates/tanstack-start-2026-06-18/vite.config.ts +17 -0
  66. package/package.json +1 -1
  67. package/src/template-render.ts +5 -0
  68. package/src/templates/tanstack-start-2026-06-18/components.json +22 -0
  69. package/src/templates/tanstack-start-2026-06-18/drizzle.config.ts +15 -0
  70. package/src/templates/tanstack-start-2026-06-18/package.json +103 -0
  71. package/src/templates/tanstack-start-2026-06-18/src/api/health.ts +66 -0
  72. package/src/templates/tanstack-start-2026-06-18/src/components/ui/accordion.tsx +51 -0
  73. package/src/templates/tanstack-start-2026-06-18/src/components/ui/alert-dialog.tsx +115 -0
  74. package/src/templates/tanstack-start-2026-06-18/src/components/ui/alert.tsx +49 -0
  75. package/src/templates/tanstack-start-2026-06-18/src/components/ui/aspect-ratio.tsx +5 -0
  76. package/src/templates/tanstack-start-2026-06-18/src/components/ui/avatar.tsx +47 -0
  77. package/src/templates/tanstack-start-2026-06-18/src/components/ui/badge.tsx +32 -0
  78. package/src/templates/tanstack-start-2026-06-18/src/components/ui/breadcrumb.tsx +101 -0
  79. package/src/templates/tanstack-start-2026-06-18/src/components/ui/button.tsx +49 -0
  80. package/src/templates/tanstack-start-2026-06-18/src/components/ui/calendar.tsx +177 -0
  81. package/src/templates/tanstack-start-2026-06-18/src/components/ui/card.tsx +55 -0
  82. package/src/templates/tanstack-start-2026-06-18/src/components/ui/carousel.tsx +240 -0
  83. package/src/templates/tanstack-start-2026-06-18/src/components/ui/chart.tsx +331 -0
  84. package/src/templates/tanstack-start-2026-06-18/src/components/ui/checkbox.tsx +26 -0
  85. package/src/templates/tanstack-start-2026-06-18/src/components/ui/collapsible.tsx +11 -0
  86. package/src/templates/tanstack-start-2026-06-18/src/components/ui/command.tsx +143 -0
  87. package/src/templates/tanstack-start-2026-06-18/src/components/ui/context-menu.tsx +187 -0
  88. package/src/templates/tanstack-start-2026-06-18/src/components/ui/dialog.tsx +104 -0
  89. package/src/templates/tanstack-start-2026-06-18/src/components/ui/drawer.tsx +98 -0
  90. package/src/templates/tanstack-start-2026-06-18/src/components/ui/dropdown-menu.tsx +188 -0
  91. package/src/templates/tanstack-start-2026-06-18/src/components/ui/form.tsx +171 -0
  92. package/src/templates/tanstack-start-2026-06-18/src/components/ui/hover-card.tsx +27 -0
  93. package/src/templates/tanstack-start-2026-06-18/src/components/ui/input-otp.tsx +69 -0
  94. package/src/templates/tanstack-start-2026-06-18/src/components/ui/input.tsx +22 -0
  95. package/src/templates/tanstack-start-2026-06-18/src/components/ui/label.tsx +21 -0
  96. package/src/templates/tanstack-start-2026-06-18/src/components/ui/menubar.tsx +229 -0
  97. package/src/templates/tanstack-start-2026-06-18/src/components/ui/navigation-menu.tsx +120 -0
  98. package/src/templates/tanstack-start-2026-06-18/src/components/ui/pagination.tsx +98 -0
  99. package/src/templates/tanstack-start-2026-06-18/src/components/ui/popover.tsx +31 -0
  100. package/src/templates/tanstack-start-2026-06-18/src/components/ui/progress.tsx +25 -0
  101. package/src/templates/tanstack-start-2026-06-18/src/components/ui/radio-group.tsx +36 -0
  102. package/src/templates/tanstack-start-2026-06-18/src/components/ui/resizable.tsx +37 -0
  103. package/src/templates/tanstack-start-2026-06-18/src/components/ui/scroll-area.tsx +44 -0
  104. package/src/templates/tanstack-start-2026-06-18/src/components/ui/select.tsx +152 -0
  105. package/src/templates/tanstack-start-2026-06-18/src/components/ui/separator.tsx +24 -0
  106. package/src/templates/tanstack-start-2026-06-18/src/components/ui/sheet.tsx +122 -0
  107. package/src/templates/tanstack-start-2026-06-18/src/components/ui/sidebar.tsx +744 -0
  108. package/src/templates/tanstack-start-2026-06-18/src/components/ui/skeleton.tsx +7 -0
  109. package/src/templates/tanstack-start-2026-06-18/src/components/ui/slider.tsx +23 -0
  110. package/src/templates/tanstack-start-2026-06-18/src/components/ui/sonner.tsx +23 -0
  111. package/src/templates/tanstack-start-2026-06-18/src/components/ui/switch.tsx +27 -0
  112. package/src/templates/tanstack-start-2026-06-18/src/components/ui/table.tsx +94 -0
  113. package/src/templates/tanstack-start-2026-06-18/src/components/ui/tabs.tsx +53 -0
  114. package/src/templates/tanstack-start-2026-06-18/src/components/ui/textarea.tsx +21 -0
  115. package/src/templates/tanstack-start-2026-06-18/src/components/ui/toggle-group.tsx +57 -0
  116. package/src/templates/tanstack-start-2026-06-18/src/components/ui/toggle.tsx +42 -0
  117. package/src/templates/tanstack-start-2026-06-18/src/components/ui/tooltip.tsx +32 -0
  118. package/src/templates/tanstack-start-2026-06-18/src/db/index.ts +9 -0
  119. package/src/templates/tanstack-start-2026-06-18/src/db/schema.ts +18 -0
  120. package/src/templates/tanstack-start-2026-06-18/src/global.d.ts +3 -0
  121. package/src/templates/tanstack-start-2026-06-18/src/hooks/use-mobile.tsx +19 -0
  122. package/src/templates/tanstack-start-2026-06-18/src/lib/proxy.ts +83 -0
  123. package/src/templates/tanstack-start-2026-06-18/src/lib/utils.ts +6 -0
  124. package/src/templates/tanstack-start-2026-06-18/src/routeTree.gen.ts +59 -0
  125. package/src/templates/tanstack-start-2026-06-18/src/router.tsx +33 -0
  126. package/src/templates/tanstack-start-2026-06-18/src/routes/__root.tsx +60 -0
  127. package/src/templates/tanstack-start-2026-06-18/src/routes/index.tsx +33 -0
  128. package/src/templates/tanstack-start-2026-06-18/src/server.ts +32 -0
  129. package/src/templates/tanstack-start-2026-06-18/src/styles.css +214 -0
  130. package/src/templates/tanstack-start-2026-06-18/tsconfig.json +27 -0
  131. package/src/templates/tanstack-start-2026-06-18/vite.config.ts +17 -0
  132. package/src/tools/scaffold.ts +41 -43
  133. package/src/tools/setup-netsuite.ts +32 -65
@@ -0,0 +1,177 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
5
+ import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker";
6
+
7
+ import { cn } from "@/lib/utils";
8
+ import { Button, buttonVariants } from "@/components/ui/button";
9
+
10
+ function Calendar({
11
+ className,
12
+ classNames,
13
+ showOutsideDays = true,
14
+ captionLayout = "label",
15
+ buttonVariant = "ghost",
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
+ "bg-background group/calendar p-3 [--cell-size:2rem] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
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
+ formatters={{
35
+ formatMonthDropdown: (date) => date.toLocaleString("default", { month: "short" }),
36
+ ...formatters,
37
+ }}
38
+ classNames={{
39
+ root: cn("w-fit", defaultClassNames.root),
40
+ months: cn("relative flex flex-col gap-4 md:flex-row", defaultClassNames.months),
41
+ month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
42
+ nav: cn(
43
+ "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
44
+ defaultClassNames.nav,
45
+ ),
46
+ button_previous: cn(
47
+ buttonVariants({ variant: buttonVariant }),
48
+ "h-(--cell-size) w-(--cell-size) select-none p-0 aria-disabled:opacity-50",
49
+ defaultClassNames.button_previous,
50
+ ),
51
+ button_next: cn(
52
+ buttonVariants({ variant: buttonVariant }),
53
+ "h-(--cell-size) w-(--cell-size) select-none p-0 aria-disabled:opacity-50",
54
+ defaultClassNames.button_next,
55
+ ),
56
+ month_caption: cn(
57
+ "flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)",
58
+ defaultClassNames.month_caption,
59
+ ),
60
+ dropdowns: cn(
61
+ "flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium",
62
+ defaultClassNames.dropdowns,
63
+ ),
64
+ dropdown_root: cn(
65
+ "has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border",
66
+ defaultClassNames.dropdown_root,
67
+ ),
68
+ dropdown: cn("bg-popover absolute inset-0 opacity-0", defaultClassNames.dropdown),
69
+ caption_label: cn(
70
+ "select-none font-medium",
71
+ captionLayout === "label"
72
+ ? "text-sm"
73
+ : "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
74
+ defaultClassNames.caption_label,
75
+ ),
76
+ table: "w-full border-collapse",
77
+ weekdays: cn("flex", defaultClassNames.weekdays),
78
+ weekday: cn(
79
+ "text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
80
+ defaultClassNames.weekday,
81
+ ),
82
+ week: cn("mt-2 flex w-full", defaultClassNames.week),
83
+ week_number_header: cn("w-(--cell-size) select-none", defaultClassNames.week_number_header),
84
+ week_number: cn(
85
+ "text-muted-foreground select-none text-[0.8rem]",
86
+ defaultClassNames.week_number,
87
+ ),
88
+ day: cn(
89
+ "group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
90
+ defaultClassNames.day,
91
+ ),
92
+ range_start: cn("bg-accent rounded-l-md", defaultClassNames.range_start),
93
+ range_middle: cn("rounded-none", defaultClassNames.range_middle),
94
+ range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end),
95
+ today: cn(
96
+ "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
97
+ defaultClassNames.today,
98
+ ),
99
+ outside: cn(
100
+ "text-muted-foreground aria-selected:text-muted-foreground",
101
+ defaultClassNames.outside,
102
+ ),
103
+ disabled: cn("text-muted-foreground opacity-50", defaultClassNames.disabled),
104
+ hidden: cn("invisible", defaultClassNames.hidden),
105
+ ...classNames,
106
+ }}
107
+ components={{
108
+ Root: ({ className, rootRef, ...props }) => {
109
+ return <div data-slot="calendar" ref={rootRef} className={cn(className)} {...props} />;
110
+ },
111
+ Chevron: ({ className, orientation, ...props }) => {
112
+ if (orientation === "left") {
113
+ return <ChevronLeftIcon className={cn("size-4", className)} {...props} />;
114
+ }
115
+
116
+ if (orientation === "right") {
117
+ return <ChevronRightIcon className={cn("size-4", className)} {...props} />;
118
+ }
119
+
120
+ return <ChevronDownIcon className={cn("size-4", className)} {...props} />;
121
+ },
122
+ DayButton: CalendarDayButton,
123
+ WeekNumber: ({ children, ...props }) => {
124
+ return (
125
+ <td {...props}>
126
+ <div className="flex size-(--cell-size) items-center justify-center text-center">
127
+ {children}
128
+ </div>
129
+ </td>
130
+ );
131
+ },
132
+ ...components,
133
+ }}
134
+ {...props}
135
+ />
136
+ );
137
+ }
138
+
139
+ function CalendarDayButton({
140
+ className,
141
+ day,
142
+ modifiers,
143
+ ...props
144
+ }: React.ComponentProps<typeof DayButton>) {
145
+ const defaultClassNames = getDefaultClassNames();
146
+
147
+ const ref = React.useRef<HTMLButtonElement>(null);
148
+ React.useEffect(() => {
149
+ if (modifiers.focused) ref.current?.focus();
150
+ }, [modifiers.focused]);
151
+
152
+ return (
153
+ <Button
154
+ ref={ref}
155
+ variant="ghost"
156
+ size="icon"
157
+ data-day={day.date.toLocaleDateString()}
158
+ data-selected-single={
159
+ modifiers.selected &&
160
+ !modifiers.range_start &&
161
+ !modifiers.range_end &&
162
+ !modifiers.range_middle
163
+ }
164
+ data-range-start={modifiers.range_start}
165
+ data-range-end={modifiers.range_end}
166
+ data-range-middle={modifiers.range_middle}
167
+ className={cn(
168
+ "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 flex aspect-square h-auto w-full min-w-(--cell-size) flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] [&>span]:text-xs [&>span]:opacity-70",
169
+ defaultClassNames.day,
170
+ className,
171
+ )}
172
+ {...props}
173
+ />
174
+ );
175
+ }
176
+
177
+ export { Calendar, CalendarDayButton };
@@ -0,0 +1,55 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
6
+ ({ className, ...props }, ref) => (
7
+ <div
8
+ ref={ref}
9
+ className={cn("rounded-xl border bg-card text-card-foreground shadow", className)}
10
+ {...props}
11
+ />
12
+ ),
13
+ );
14
+ Card.displayName = "Card";
15
+
16
+ const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
17
+ ({ className, ...props }, ref) => (
18
+ <div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
19
+ ),
20
+ );
21
+ CardHeader.displayName = "CardHeader";
22
+
23
+ const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
24
+ ({ className, ...props }, ref) => (
25
+ <div
26
+ ref={ref}
27
+ className={cn("font-semibold leading-none tracking-tight", className)}
28
+ {...props}
29
+ />
30
+ ),
31
+ );
32
+ CardTitle.displayName = "CardTitle";
33
+
34
+ const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
35
+ ({ className, ...props }, ref) => (
36
+ <div ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
37
+ ),
38
+ );
39
+ CardDescription.displayName = "CardDescription";
40
+
41
+ const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
42
+ ({ className, ...props }, ref) => (
43
+ <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
44
+ ),
45
+ );
46
+ CardContent.displayName = "CardContent";
47
+
48
+ const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
49
+ ({ className, ...props }, ref) => (
50
+ <div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
51
+ ),
52
+ );
53
+ CardFooter.displayName = "CardFooter";
54
+
55
+ export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
@@ -0,0 +1,240 @@
1
+ import * as React from "react";
2
+ import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react";
3
+ import { ArrowLeft, ArrowRight } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+ import { Button } from "@/components/ui/button";
7
+
8
+ type CarouselApi = UseEmblaCarouselType[1];
9
+ type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
10
+ type CarouselOptions = UseCarouselParameters[0];
11
+ type CarouselPlugin = UseCarouselParameters[1];
12
+
13
+ type CarouselProps = {
14
+ opts?: CarouselOptions;
15
+ plugins?: CarouselPlugin;
16
+ orientation?: "horizontal" | "vertical";
17
+ setApi?: (api: CarouselApi) => void;
18
+ };
19
+
20
+ type CarouselContextProps = {
21
+ carouselRef: ReturnType<typeof useEmblaCarousel>[0];
22
+ api: ReturnType<typeof useEmblaCarousel>[1];
23
+ scrollPrev: () => void;
24
+ scrollNext: () => void;
25
+ canScrollPrev: boolean;
26
+ canScrollNext: boolean;
27
+ } & CarouselProps;
28
+
29
+ const CarouselContext = React.createContext<CarouselContextProps | null>(null);
30
+
31
+ function useCarousel() {
32
+ const context = React.useContext(CarouselContext);
33
+
34
+ if (!context) {
35
+ throw new Error("useCarousel must be used within a <Carousel />");
36
+ }
37
+
38
+ return context;
39
+ }
40
+
41
+ const Carousel = React.forwardRef<
42
+ HTMLDivElement,
43
+ React.HTMLAttributes<HTMLDivElement> & CarouselProps
44
+ >(({ orientation = "horizontal", opts, setApi, plugins, className, children, ...props }, ref) => {
45
+ const [carouselRef, api] = useEmblaCarousel(
46
+ {
47
+ ...opts,
48
+ axis: orientation === "horizontal" ? "x" : "y",
49
+ },
50
+ plugins,
51
+ );
52
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false);
53
+ const [canScrollNext, setCanScrollNext] = React.useState(false);
54
+
55
+ const onSelect = React.useCallback((api: CarouselApi) => {
56
+ if (!api) {
57
+ return;
58
+ }
59
+
60
+ setCanScrollPrev(api.canScrollPrev());
61
+ setCanScrollNext(api.canScrollNext());
62
+ }, []);
63
+
64
+ const scrollPrev = React.useCallback(() => {
65
+ api?.scrollPrev();
66
+ }, [api]);
67
+
68
+ const scrollNext = React.useCallback(() => {
69
+ api?.scrollNext();
70
+ }, [api]);
71
+
72
+ const handleKeyDown = React.useCallback(
73
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
74
+ if (event.key === "ArrowLeft") {
75
+ event.preventDefault();
76
+ scrollPrev();
77
+ } else if (event.key === "ArrowRight") {
78
+ event.preventDefault();
79
+ scrollNext();
80
+ }
81
+ },
82
+ [scrollPrev, scrollNext],
83
+ );
84
+
85
+ React.useEffect(() => {
86
+ if (!api || !setApi) {
87
+ return;
88
+ }
89
+
90
+ setApi(api);
91
+ }, [api, setApi]);
92
+
93
+ React.useEffect(() => {
94
+ if (!api) {
95
+ return;
96
+ }
97
+
98
+ onSelect(api);
99
+ api.on("reInit", onSelect);
100
+ api.on("select", onSelect);
101
+
102
+ return () => {
103
+ api?.off("select", onSelect);
104
+ };
105
+ }, [api, onSelect]);
106
+
107
+ return (
108
+ <CarouselContext.Provider
109
+ value={{
110
+ carouselRef,
111
+ api: api,
112
+ opts,
113
+ orientation: orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
114
+ scrollPrev,
115
+ scrollNext,
116
+ canScrollPrev,
117
+ canScrollNext,
118
+ }}
119
+ >
120
+ <div
121
+ ref={ref}
122
+ onKeyDownCapture={handleKeyDown}
123
+ className={cn("relative", className)}
124
+ role="region"
125
+ aria-roledescription="carousel"
126
+ {...props}
127
+ >
128
+ {children}
129
+ </div>
130
+ </CarouselContext.Provider>
131
+ );
132
+ });
133
+ Carousel.displayName = "Carousel";
134
+
135
+ const CarouselContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
136
+ ({ className, ...props }, ref) => {
137
+ const { carouselRef, orientation } = useCarousel();
138
+
139
+ return (
140
+ <div ref={carouselRef} className="overflow-hidden">
141
+ <div
142
+ ref={ref}
143
+ className={cn(
144
+ "flex",
145
+ orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
146
+ className,
147
+ )}
148
+ {...props}
149
+ />
150
+ </div>
151
+ );
152
+ },
153
+ );
154
+ CarouselContent.displayName = "CarouselContent";
155
+
156
+ const CarouselItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
157
+ ({ className, ...props }, ref) => {
158
+ const { orientation } = useCarousel();
159
+
160
+ return (
161
+ <div
162
+ ref={ref}
163
+ role="group"
164
+ aria-roledescription="slide"
165
+ className={cn(
166
+ "min-w-0 shrink-0 grow-0 basis-full",
167
+ orientation === "horizontal" ? "pl-4" : "pt-4",
168
+ className,
169
+ )}
170
+ {...props}
171
+ />
172
+ );
173
+ },
174
+ );
175
+ CarouselItem.displayName = "CarouselItem";
176
+
177
+ const CarouselPrevious = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
178
+ ({ className, variant = "outline", size = "icon", ...props }, ref) => {
179
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel();
180
+
181
+ return (
182
+ <Button
183
+ ref={ref}
184
+ variant={variant}
185
+ size={size}
186
+ className={cn(
187
+ "absolute h-8 w-8 rounded-full",
188
+ orientation === "horizontal"
189
+ ? "-left-12 top-1/2 -translate-y-1/2"
190
+ : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
191
+ className,
192
+ )}
193
+ disabled={!canScrollPrev}
194
+ onClick={scrollPrev}
195
+ {...props}
196
+ >
197
+ <ArrowLeft className="h-4 w-4" />
198
+ <span className="sr-only">Previous slide</span>
199
+ </Button>
200
+ );
201
+ },
202
+ );
203
+ CarouselPrevious.displayName = "CarouselPrevious";
204
+
205
+ const CarouselNext = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
206
+ ({ className, variant = "outline", size = "icon", ...props }, ref) => {
207
+ const { orientation, scrollNext, canScrollNext } = useCarousel();
208
+
209
+ return (
210
+ <Button
211
+ ref={ref}
212
+ variant={variant}
213
+ size={size}
214
+ className={cn(
215
+ "absolute h-8 w-8 rounded-full",
216
+ orientation === "horizontal"
217
+ ? "-right-12 top-1/2 -translate-y-1/2"
218
+ : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
219
+ className,
220
+ )}
221
+ disabled={!canScrollNext}
222
+ onClick={scrollNext}
223
+ {...props}
224
+ >
225
+ <ArrowRight className="h-4 w-4" />
226
+ <span className="sr-only">Next slide</span>
227
+ </Button>
228
+ );
229
+ },
230
+ );
231
+ CarouselNext.displayName = "CarouselNext";
232
+
233
+ export {
234
+ type CarouselApi,
235
+ Carousel,
236
+ CarouselContent,
237
+ CarouselItem,
238
+ CarouselPrevious,
239
+ CarouselNext,
240
+ };