@getjack/jack 0.1.19 → 0.1.22

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 (105) hide show
  1. package/package.json +5 -2
  2. package/src/commands/down.ts +11 -1
  3. package/src/commands/init.ts +19 -6
  4. package/src/commands/new.ts +56 -4
  5. package/src/commands/publish.ts +1 -1
  6. package/src/lib/agents.ts +3 -1
  7. package/src/lib/auth/ensure-auth.test.ts +3 -3
  8. package/src/lib/control-plane.ts +15 -1
  9. package/src/lib/deploy-upload.ts +26 -1
  10. package/src/lib/hooks.ts +232 -1
  11. package/src/lib/managed-deploy.ts +13 -6
  12. package/src/lib/managed-down.ts +66 -45
  13. package/src/lib/progress.ts +76 -5
  14. package/src/lib/project-list.ts +6 -1
  15. package/src/lib/project-operations.ts +21 -31
  16. package/src/lib/project-resolver.ts +1 -1
  17. package/src/lib/zip-packager.ts +36 -7
  18. package/src/templates/index.ts +1 -1
  19. package/src/templates/types.ts +16 -0
  20. package/templates/CLAUDE.md +172 -5
  21. package/templates/miniapp/.jack.json +1 -3
  22. package/templates/saas/.jack.json +154 -0
  23. package/templates/saas/AGENTS.md +333 -0
  24. package/templates/saas/bun.lock +925 -0
  25. package/templates/saas/components.json +21 -0
  26. package/templates/saas/index.html +12 -0
  27. package/templates/saas/package.json +75 -0
  28. package/templates/saas/public/icon.png +0 -0
  29. package/templates/saas/public/og.png +0 -0
  30. package/templates/saas/schema.sql +73 -0
  31. package/templates/saas/src/auth.ts +77 -0
  32. package/templates/saas/src/client/App.tsx +63 -0
  33. package/templates/saas/src/client/components/ProtectedRoute.tsx +29 -0
  34. package/templates/saas/src/client/components/ThemeToggle.tsx +32 -0
  35. package/templates/saas/src/client/components/ui/accordion.tsx +62 -0
  36. package/templates/saas/src/client/components/ui/alert-dialog.tsx +133 -0
  37. package/templates/saas/src/client/components/ui/alert.tsx +60 -0
  38. package/templates/saas/src/client/components/ui/aspect-ratio.tsx +9 -0
  39. package/templates/saas/src/client/components/ui/avatar.tsx +39 -0
  40. package/templates/saas/src/client/components/ui/badge.tsx +39 -0
  41. package/templates/saas/src/client/components/ui/breadcrumb.tsx +102 -0
  42. package/templates/saas/src/client/components/ui/button-group.tsx +78 -0
  43. package/templates/saas/src/client/components/ui/button.tsx +60 -0
  44. package/templates/saas/src/client/components/ui/card.tsx +75 -0
  45. package/templates/saas/src/client/components/ui/carousel.tsx +228 -0
  46. package/templates/saas/src/client/components/ui/chart.tsx +326 -0
  47. package/templates/saas/src/client/components/ui/checkbox.tsx +29 -0
  48. package/templates/saas/src/client/components/ui/collapsible.tsx +19 -0
  49. package/templates/saas/src/client/components/ui/command.tsx +159 -0
  50. package/templates/saas/src/client/components/ui/context-menu.tsx +224 -0
  51. package/templates/saas/src/client/components/ui/dialog.tsx +127 -0
  52. package/templates/saas/src/client/components/ui/drawer.tsx +124 -0
  53. package/templates/saas/src/client/components/ui/dropdown-menu.tsx +226 -0
  54. package/templates/saas/src/client/components/ui/empty.tsx +94 -0
  55. package/templates/saas/src/client/components/ui/field.tsx +232 -0
  56. package/templates/saas/src/client/components/ui/form.tsx +152 -0
  57. package/templates/saas/src/client/components/ui/hover-card.tsx +38 -0
  58. package/templates/saas/src/client/components/ui/input-group.tsx +158 -0
  59. package/templates/saas/src/client/components/ui/input-otp.tsx +68 -0
  60. package/templates/saas/src/client/components/ui/input.tsx +21 -0
  61. package/templates/saas/src/client/components/ui/item.tsx +172 -0
  62. package/templates/saas/src/client/components/ui/kbd.tsx +28 -0
  63. package/templates/saas/src/client/components/ui/label.tsx +21 -0
  64. package/templates/saas/src/client/components/ui/menubar.tsx +250 -0
  65. package/templates/saas/src/client/components/ui/navigation-menu.tsx +161 -0
  66. package/templates/saas/src/client/components/ui/pagination.tsx +106 -0
  67. package/templates/saas/src/client/components/ui/popover.tsx +42 -0
  68. package/templates/saas/src/client/components/ui/progress.tsx +26 -0
  69. package/templates/saas/src/client/components/ui/radio-group.tsx +45 -0
  70. package/templates/saas/src/client/components/ui/resizable.tsx +46 -0
  71. package/templates/saas/src/client/components/ui/scroll-area.tsx +56 -0
  72. package/templates/saas/src/client/components/ui/select.tsx +173 -0
  73. package/templates/saas/src/client/components/ui/separator.tsx +28 -0
  74. package/templates/saas/src/client/components/ui/sheet.tsx +128 -0
  75. package/templates/saas/src/client/components/ui/sidebar.tsx +694 -0
  76. package/templates/saas/src/client/components/ui/skeleton.tsx +13 -0
  77. package/templates/saas/src/client/components/ui/slider.tsx +58 -0
  78. package/templates/saas/src/client/components/ui/sonner.tsx +38 -0
  79. package/templates/saas/src/client/components/ui/spinner.tsx +16 -0
  80. package/templates/saas/src/client/components/ui/switch.tsx +28 -0
  81. package/templates/saas/src/client/components/ui/table.tsx +90 -0
  82. package/templates/saas/src/client/components/ui/tabs.tsx +54 -0
  83. package/templates/saas/src/client/components/ui/textarea.tsx +18 -0
  84. package/templates/saas/src/client/components/ui/toggle-group.tsx +80 -0
  85. package/templates/saas/src/client/components/ui/toggle.tsx +44 -0
  86. package/templates/saas/src/client/components/ui/tooltip.tsx +57 -0
  87. package/templates/saas/src/client/hooks/use-mobile.ts +19 -0
  88. package/templates/saas/src/client/hooks/useAuth.ts +14 -0
  89. package/templates/saas/src/client/hooks/useSubscription.ts +86 -0
  90. package/templates/saas/src/client/index.css +165 -0
  91. package/templates/saas/src/client/lib/auth-client.ts +7 -0
  92. package/templates/saas/src/client/lib/plans.ts +82 -0
  93. package/templates/saas/src/client/lib/utils.ts +6 -0
  94. package/templates/saas/src/client/main.tsx +15 -0
  95. package/templates/saas/src/client/pages/DashboardPage.tsx +394 -0
  96. package/templates/saas/src/client/pages/ForgotPasswordPage.tsx +153 -0
  97. package/templates/saas/src/client/pages/HomePage.tsx +285 -0
  98. package/templates/saas/src/client/pages/LoginPage.tsx +169 -0
  99. package/templates/saas/src/client/pages/PricingPage.tsx +467 -0
  100. package/templates/saas/src/client/pages/ResetPasswordPage.tsx +200 -0
  101. package/templates/saas/src/client/pages/SignupPage.tsx +192 -0
  102. package/templates/saas/src/index.ts +208 -0
  103. package/templates/saas/tsconfig.json +18 -0
  104. package/templates/saas/vite.config.ts +14 -0
  105. package/templates/saas/wrangler.jsonc +20 -0
@@ -0,0 +1,39 @@
1
+ import * as React from "react";
2
+ import * as AvatarPrimitive from "@radix-ui/react-avatar";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ function Avatar({ className, ...props }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
7
+ return (
8
+ <AvatarPrimitive.Root
9
+ data-slot="avatar"
10
+ className={cn("relative flex size-8 shrink-0 overflow-hidden rounded-full", className)}
11
+ {...props}
12
+ />
13
+ );
14
+ }
15
+
16
+ function AvatarImage({ className, ...props }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
17
+ return (
18
+ <AvatarPrimitive.Image
19
+ data-slot="avatar-image"
20
+ className={cn("aspect-square size-full", className)}
21
+ {...props}
22
+ />
23
+ );
24
+ }
25
+
26
+ function AvatarFallback({
27
+ className,
28
+ ...props
29
+ }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
30
+ return (
31
+ <AvatarPrimitive.Fallback
32
+ data-slot="avatar-fallback"
33
+ className={cn("bg-muted flex size-full items-center justify-center rounded-full", className)}
34
+ {...props}
35
+ />
36
+ );
37
+ }
38
+
39
+ export { Avatar, AvatarImage, AvatarFallback };
@@ -0,0 +1,39 @@
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const badgeVariants = cva(
8
+ "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
13
+ secondary:
14
+ "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
15
+ destructive:
16
+ "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
17
+ outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
18
+ },
19
+ },
20
+ defaultVariants: {
21
+ variant: "default",
22
+ },
23
+ },
24
+ );
25
+
26
+ function Badge({
27
+ className,
28
+ variant,
29
+ asChild = false,
30
+ ...props
31
+ }: React.ComponentProps<"span"> & VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
32
+ const Comp = asChild ? Slot : "span";
33
+
34
+ return (
35
+ <Comp data-slot="badge" className={cn(badgeVariants({ variant }), className)} {...props} />
36
+ );
37
+ }
38
+
39
+ export { Badge, badgeVariants };
@@ -0,0 +1,102 @@
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { ChevronRight, MoreHorizontal } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
8
+ return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
9
+ }
10
+
11
+ function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
12
+ return (
13
+ <ol
14
+ data-slot="breadcrumb-list"
15
+ className={cn(
16
+ "text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
17
+ className,
18
+ )}
19
+ {...props}
20
+ />
21
+ );
22
+ }
23
+
24
+ function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
25
+ return (
26
+ <li
27
+ data-slot="breadcrumb-item"
28
+ className={cn("inline-flex items-center gap-1.5", className)}
29
+ {...props}
30
+ />
31
+ );
32
+ }
33
+
34
+ function BreadcrumbLink({
35
+ asChild,
36
+ className,
37
+ ...props
38
+ }: React.ComponentProps<"a"> & {
39
+ asChild?: boolean;
40
+ }) {
41
+ const Comp = asChild ? Slot : "a";
42
+
43
+ return (
44
+ <Comp
45
+ data-slot="breadcrumb-link"
46
+ className={cn("hover:text-foreground transition-colors", className)}
47
+ {...props}
48
+ />
49
+ );
50
+ }
51
+
52
+ function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
53
+ return (
54
+ <span
55
+ data-slot="breadcrumb-page"
56
+ role="link"
57
+ aria-disabled="true"
58
+ aria-current="page"
59
+ className={cn("text-foreground font-normal", className)}
60
+ {...props}
61
+ />
62
+ );
63
+ }
64
+
65
+ function BreadcrumbSeparator({ children, className, ...props }: React.ComponentProps<"li">) {
66
+ return (
67
+ <li
68
+ data-slot="breadcrumb-separator"
69
+ role="presentation"
70
+ aria-hidden="true"
71
+ className={cn("[&>svg]:size-3.5", className)}
72
+ {...props}
73
+ >
74
+ {children ?? <ChevronRight />}
75
+ </li>
76
+ );
77
+ }
78
+
79
+ function BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<"span">) {
80
+ return (
81
+ <span
82
+ data-slot="breadcrumb-ellipsis"
83
+ role="presentation"
84
+ aria-hidden="true"
85
+ className={cn("flex size-9 items-center justify-center", className)}
86
+ {...props}
87
+ >
88
+ <MoreHorizontal className="size-4" />
89
+ <span className="sr-only">More</span>
90
+ </span>
91
+ );
92
+ }
93
+
94
+ export {
95
+ Breadcrumb,
96
+ BreadcrumbList,
97
+ BreadcrumbItem,
98
+ BreadcrumbLink,
99
+ BreadcrumbPage,
100
+ BreadcrumbSeparator,
101
+ BreadcrumbEllipsis,
102
+ };
@@ -0,0 +1,78 @@
1
+ import { Slot } from "@radix-ui/react-slot";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ import { cn } from "@/lib/utils";
5
+ import { Separator } from "@/components/ui/separator";
6
+
7
+ const buttonGroupVariants = cva(
8
+ "flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
9
+ {
10
+ variants: {
11
+ orientation: {
12
+ horizontal:
13
+ "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
14
+ vertical:
15
+ "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
16
+ },
17
+ },
18
+ defaultVariants: {
19
+ orientation: "horizontal",
20
+ },
21
+ },
22
+ );
23
+
24
+ function ButtonGroup({
25
+ className,
26
+ orientation,
27
+ ...props
28
+ }: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
29
+ return (
30
+ <div
31
+ role="group"
32
+ data-slot="button-group"
33
+ data-orientation={orientation}
34
+ className={cn(buttonGroupVariants({ orientation }), className)}
35
+ {...props}
36
+ />
37
+ );
38
+ }
39
+
40
+ function ButtonGroupText({
41
+ className,
42
+ asChild = false,
43
+ ...props
44
+ }: React.ComponentProps<"div"> & {
45
+ asChild?: boolean;
46
+ }) {
47
+ const Comp = asChild ? Slot : "div";
48
+
49
+ return (
50
+ <Comp
51
+ className={cn(
52
+ "bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
53
+ className,
54
+ )}
55
+ {...props}
56
+ />
57
+ );
58
+ }
59
+
60
+ function ButtonGroupSeparator({
61
+ className,
62
+ orientation = "vertical",
63
+ ...props
64
+ }: React.ComponentProps<typeof Separator>) {
65
+ return (
66
+ <Separator
67
+ data-slot="button-group-separator"
68
+ orientation={orientation}
69
+ className={cn(
70
+ "bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
71
+ className,
72
+ )}
73
+ {...props}
74
+ />
75
+ );
76
+ }
77
+
78
+ export { ButtonGroup, ButtonGroupSeparator, ButtonGroupText, buttonGroupVariants };
@@ -0,0 +1,60 @@
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
15
+ outline:
16
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
17
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
18
+ ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
19
+ link: "text-primary underline-offset-4 hover:underline",
20
+ },
21
+ size: {
22
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
23
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
24
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
25
+ icon: "size-9",
26
+ "icon-sm": "size-8",
27
+ "icon-lg": "size-10",
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ variant: "default",
32
+ size: "default",
33
+ },
34
+ },
35
+ );
36
+
37
+ function Button({
38
+ className,
39
+ variant = "default",
40
+ size = "default",
41
+ asChild = false,
42
+ ...props
43
+ }: React.ComponentProps<"button"> &
44
+ VariantProps<typeof buttonVariants> & {
45
+ asChild?: boolean;
46
+ }) {
47
+ const Comp = asChild ? Slot : "button";
48
+
49
+ return (
50
+ <Comp
51
+ data-slot="button"
52
+ data-variant={variant}
53
+ data-size={size}
54
+ className={cn(buttonVariants({ variant, size, className }))}
55
+ {...props}
56
+ />
57
+ );
58
+ }
59
+
60
+ export { Button, buttonVariants };
@@ -0,0 +1,75 @@
1
+ import * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ function Card({ className, ...props }: React.ComponentProps<"div">) {
6
+ return (
7
+ <div
8
+ data-slot="card"
9
+ className={cn(
10
+ "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
11
+ className,
12
+ )}
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+
18
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19
+ return (
20
+ <div
21
+ data-slot="card-header"
22
+ className={cn(
23
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
24
+ className,
25
+ )}
26
+ {...props}
27
+ />
28
+ );
29
+ }
30
+
31
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32
+ return (
33
+ <div
34
+ data-slot="card-title"
35
+ className={cn("leading-none font-semibold", className)}
36
+ {...props}
37
+ />
38
+ );
39
+ }
40
+
41
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42
+ return (
43
+ <div
44
+ data-slot="card-description"
45
+ className={cn("text-muted-foreground text-sm", className)}
46
+ {...props}
47
+ />
48
+ );
49
+ }
50
+
51
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52
+ return (
53
+ <div
54
+ data-slot="card-action"
55
+ className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
56
+ {...props}
57
+ />
58
+ );
59
+ }
60
+
61
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
62
+ return <div data-slot="card-content" className={cn("px-6", className)} {...props} />;
63
+ }
64
+
65
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
66
+ return (
67
+ <div
68
+ data-slot="card-footer"
69
+ className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
70
+ {...props}
71
+ />
72
+ );
73
+ }
74
+
75
+ export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };
@@ -0,0 +1,228 @@
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
+ function Carousel({
42
+ orientation = "horizontal",
43
+ opts,
44
+ setApi,
45
+ plugins,
46
+ className,
47
+ children,
48
+ ...props
49
+ }: React.ComponentProps<"div"> & CarouselProps) {
50
+ const [carouselRef, api] = useEmblaCarousel(
51
+ {
52
+ ...opts,
53
+ axis: orientation === "horizontal" ? "x" : "y",
54
+ },
55
+ plugins,
56
+ );
57
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false);
58
+ const [canScrollNext, setCanScrollNext] = React.useState(false);
59
+
60
+ const onSelect = React.useCallback((api: CarouselApi) => {
61
+ if (!api) return;
62
+ setCanScrollPrev(api.canScrollPrev());
63
+ setCanScrollNext(api.canScrollNext());
64
+ }, []);
65
+
66
+ const scrollPrev = React.useCallback(() => {
67
+ api?.scrollPrev();
68
+ }, [api]);
69
+
70
+ const scrollNext = React.useCallback(() => {
71
+ api?.scrollNext();
72
+ }, [api]);
73
+
74
+ const handleKeyDown = React.useCallback(
75
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
76
+ if (event.key === "ArrowLeft") {
77
+ event.preventDefault();
78
+ scrollPrev();
79
+ } else if (event.key === "ArrowRight") {
80
+ event.preventDefault();
81
+ scrollNext();
82
+ }
83
+ },
84
+ [scrollPrev, scrollNext],
85
+ );
86
+
87
+ React.useEffect(() => {
88
+ if (!api || !setApi) return;
89
+ setApi(api);
90
+ }, [api, setApi]);
91
+
92
+ React.useEffect(() => {
93
+ if (!api) return;
94
+ onSelect(api);
95
+ api.on("reInit", onSelect);
96
+ api.on("select", onSelect);
97
+
98
+ return () => {
99
+ api?.off("select", onSelect);
100
+ };
101
+ }, [api, onSelect]);
102
+
103
+ return (
104
+ <CarouselContext.Provider
105
+ value={{
106
+ carouselRef,
107
+ api: api,
108
+ opts,
109
+ orientation: orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
110
+ scrollPrev,
111
+ scrollNext,
112
+ canScrollPrev,
113
+ canScrollNext,
114
+ }}
115
+ >
116
+ <div
117
+ onKeyDownCapture={handleKeyDown}
118
+ className={cn("relative", className)}
119
+ role="region"
120
+ aria-roledescription="carousel"
121
+ data-slot="carousel"
122
+ {...props}
123
+ >
124
+ {children}
125
+ </div>
126
+ </CarouselContext.Provider>
127
+ );
128
+ }
129
+
130
+ function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
131
+ const { carouselRef, orientation } = useCarousel();
132
+
133
+ return (
134
+ <div ref={carouselRef} className="overflow-hidden" data-slot="carousel-content">
135
+ <div
136
+ className={cn("flex", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", className)}
137
+ {...props}
138
+ />
139
+ </div>
140
+ );
141
+ }
142
+
143
+ function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
144
+ const { orientation } = useCarousel();
145
+
146
+ return (
147
+ <div
148
+ role="group"
149
+ aria-roledescription="slide"
150
+ data-slot="carousel-item"
151
+ className={cn(
152
+ "min-w-0 shrink-0 grow-0 basis-full",
153
+ orientation === "horizontal" ? "pl-4" : "pt-4",
154
+ className,
155
+ )}
156
+ {...props}
157
+ />
158
+ );
159
+ }
160
+
161
+ function CarouselPrevious({
162
+ className,
163
+ variant = "outline",
164
+ size = "icon",
165
+ ...props
166
+ }: React.ComponentProps<typeof Button>) {
167
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel();
168
+
169
+ return (
170
+ <Button
171
+ data-slot="carousel-previous"
172
+ variant={variant}
173
+ size={size}
174
+ className={cn(
175
+ "absolute size-8 rounded-full",
176
+ orientation === "horizontal"
177
+ ? "top-1/2 -left-12 -translate-y-1/2"
178
+ : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
179
+ className,
180
+ )}
181
+ disabled={!canScrollPrev}
182
+ onClick={scrollPrev}
183
+ {...props}
184
+ >
185
+ <ArrowLeft />
186
+ <span className="sr-only">Previous slide</span>
187
+ </Button>
188
+ );
189
+ }
190
+
191
+ function CarouselNext({
192
+ className,
193
+ variant = "outline",
194
+ size = "icon",
195
+ ...props
196
+ }: React.ComponentProps<typeof Button>) {
197
+ const { orientation, scrollNext, canScrollNext } = useCarousel();
198
+
199
+ return (
200
+ <Button
201
+ data-slot="carousel-next"
202
+ variant={variant}
203
+ size={size}
204
+ className={cn(
205
+ "absolute size-8 rounded-full",
206
+ orientation === "horizontal"
207
+ ? "top-1/2 -right-12 -translate-y-1/2"
208
+ : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
209
+ className,
210
+ )}
211
+ disabled={!canScrollNext}
212
+ onClick={scrollNext}
213
+ {...props}
214
+ >
215
+ <ArrowRight />
216
+ <span className="sr-only">Next slide</span>
217
+ </Button>
218
+ );
219
+ }
220
+
221
+ export {
222
+ type CarouselApi,
223
+ Carousel,
224
+ CarouselContent,
225
+ CarouselItem,
226
+ CarouselPrevious,
227
+ CarouselNext,
228
+ };