create-strayl-web-app 1.0.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 (78) hide show
  1. package/dist/index.js +59 -0
  2. package/dist/index.js.map +1 -0
  3. package/package.json +39 -0
  4. package/template/README.md +290 -0
  5. package/template/components.json +24 -0
  6. package/template/package.json +56 -0
  7. package/template/public/favicon.ico +0 -0
  8. package/template/public/google-logo.svg +6 -0
  9. package/template/public/logo-dark.ico +0 -0
  10. package/template/public/logo-dark.webp +0 -0
  11. package/template/public/logo-light.ico +0 -0
  12. package/template/public/logo-light.webp +0 -0
  13. package/template/public/manifest.json +16 -0
  14. package/template/public/robots.txt +3 -0
  15. package/template/src/components/Header.tsx +76 -0
  16. package/template/src/components/language-switcher.tsx +38 -0
  17. package/template/src/components/theme-provider.tsx +14 -0
  18. package/template/src/components/themed-logo.tsx +44 -0
  19. package/template/src/components/ui/accordion.tsx +69 -0
  20. package/template/src/components/ui/alert-dialog.tsx +169 -0
  21. package/template/src/components/ui/alert.tsx +80 -0
  22. package/template/src/components/ui/autocomplete.tsx +301 -0
  23. package/template/src/components/ui/avatar.tsx +46 -0
  24. package/template/src/components/ui/badge.tsx +60 -0
  25. package/template/src/components/ui/breadcrumb.tsx +112 -0
  26. package/template/src/components/ui/button.tsx +73 -0
  27. package/template/src/components/ui/card.tsx +244 -0
  28. package/template/src/components/ui/checkbox-group.tsx +16 -0
  29. package/template/src/components/ui/checkbox.tsx +60 -0
  30. package/template/src/components/ui/collapsible.tsx +45 -0
  31. package/template/src/components/ui/combobox.tsx +415 -0
  32. package/template/src/components/ui/command.tsx +264 -0
  33. package/template/src/components/ui/dialog.tsx +196 -0
  34. package/template/src/components/ui/empty.tsx +127 -0
  35. package/template/src/components/ui/field.tsx +74 -0
  36. package/template/src/components/ui/fieldset.tsx +29 -0
  37. package/template/src/components/ui/form.tsx +17 -0
  38. package/template/src/components/ui/frame.tsx +82 -0
  39. package/template/src/components/ui/group.tsx +97 -0
  40. package/template/src/components/ui/input-group.tsx +101 -0
  41. package/template/src/components/ui/input.tsx +66 -0
  42. package/template/src/components/ui/kbd.tsx +28 -0
  43. package/template/src/components/ui/label.tsx +28 -0
  44. package/template/src/components/ui/menu.tsx +310 -0
  45. package/template/src/components/ui/meter.tsx +67 -0
  46. package/template/src/components/ui/number-field.tsx +160 -0
  47. package/template/src/components/ui/pagination.tsx +136 -0
  48. package/template/src/components/ui/popover.tsx +104 -0
  49. package/template/src/components/ui/preview-card.tsx +55 -0
  50. package/template/src/components/ui/progress.tsx +81 -0
  51. package/template/src/components/ui/radio-group.tsx +36 -0
  52. package/template/src/components/ui/scroll-area.tsx +64 -0
  53. package/template/src/components/ui/select.tsx +180 -0
  54. package/template/src/components/ui/separator.tsx +23 -0
  55. package/template/src/components/ui/sheet.tsx +203 -0
  56. package/template/src/components/ui/sidebar.tsx +743 -0
  57. package/template/src/components/ui/skeleton.tsx +16 -0
  58. package/template/src/components/ui/slider.tsx +74 -0
  59. package/template/src/components/ui/spinner.tsx +18 -0
  60. package/template/src/components/ui/switch.tsx +27 -0
  61. package/template/src/components/ui/table.tsx +126 -0
  62. package/template/src/components/ui/tabs.tsx +87 -0
  63. package/template/src/components/ui/textarea.tsx +51 -0
  64. package/template/src/components/ui/toast.tsx +269 -0
  65. package/template/src/components/ui/toggle-group.tsx +102 -0
  66. package/template/src/components/ui/toggle.tsx +45 -0
  67. package/template/src/components/ui/toolbar.tsx +83 -0
  68. package/template/src/components/ui/tooltip.tsx +65 -0
  69. package/template/src/hooks/use-mobile.ts +21 -0
  70. package/template/src/lib/i18n.ts +70 -0
  71. package/template/src/lib/utils.ts +6 -0
  72. package/template/src/routeTree.gen.ts +68 -0
  73. package/template/src/router.tsx +17 -0
  74. package/template/src/routes/__root.tsx +62 -0
  75. package/template/src/routes/index.tsx +71 -0
  76. package/template/src/styles.css +121 -0
  77. package/template/tsconfig.json +28 -0
  78. package/template/vite.config.ts +30 -0
@@ -0,0 +1,16 @@
1
+ import { cn } from "@/lib/utils";
2
+
3
+ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
4
+ return (
5
+ <div
6
+ className={cn(
7
+ "animate-skeleton rounded-sm [--skeleton-highlight:--alpha(var(--color-white)/64%)] [background:linear-gradient(120deg,transparent_40%,var(--skeleton-highlight),transparent_60%)_var(--color-muted)_0_0/200%_100%_fixed] dark:[--skeleton-highlight:--alpha(var(--color-white)/4%)]",
8
+ className,
9
+ )}
10
+ data-slot="skeleton"
11
+ {...props}
12
+ />
13
+ );
14
+ }
15
+
16
+ export { Skeleton };
@@ -0,0 +1,74 @@
1
+ "use client";
2
+
3
+ import { Slider as SliderPrimitive } from "@base-ui/react/slider";
4
+ import * as React from "react";
5
+
6
+ import { cn } from "@/lib/utils";
7
+
8
+ function Slider({
9
+ className,
10
+ children,
11
+ defaultValue,
12
+ value,
13
+ min = 0,
14
+ max = 100,
15
+ ...props
16
+ }: SliderPrimitive.Root.Props) {
17
+ const _values = React.useMemo(() => {
18
+ if (value !== undefined) {
19
+ return Array.isArray(value) ? value : [value];
20
+ }
21
+ if (defaultValue !== undefined) {
22
+ return Array.isArray(defaultValue) ? defaultValue : [defaultValue];
23
+ }
24
+ return [min];
25
+ }, [value, defaultValue, min]);
26
+
27
+ return (
28
+ <SliderPrimitive.Root
29
+ className={cn("data-[orientation=horizontal]:w-full", className)}
30
+ defaultValue={defaultValue}
31
+ max={max}
32
+ min={min}
33
+ thumbAlignment="edge"
34
+ value={value}
35
+ {...props}
36
+ >
37
+ {children}
38
+ <SliderPrimitive.Control
39
+ className="flex touch-none select-none data-disabled:pointer-events-none data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=horizontal]:w-full data-[orientation=horizontal]:min-w-44 data-[orientation=vertical]:flex-col data-disabled:opacity-64"
40
+ data-slot="slider-control"
41
+ >
42
+ <SliderPrimitive.Track
43
+ className="relative grow select-none before:absolute before:rounded-full before:bg-input data-[orientation=horizontal]:h-1 data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-1 data-[orientation=horizontal]:before:inset-x-0.5 data-[orientation=vertical]:before:inset-x-0 data-[orientation=horizontal]:before:inset-y-0 data-[orientation=vertical]:before:inset-y-0.5"
44
+ data-slot="slider-track"
45
+ >
46
+ <SliderPrimitive.Indicator
47
+ className="select-none rounded-full bg-primary data-[orientation=horizontal]:ms-0.5 data-[orientation=vertical]:mb-0.5"
48
+ data-slot="slider-indicator"
49
+ />
50
+ {Array.from({ length: _values.length }, (_, index) => (
51
+ <SliderPrimitive.Thumb
52
+ className="block size-5 shrink-0 select-none rounded-full border border-input bg-white not-dark:bg-clip-padding shadow-xs/5 outline-none transition-[box-shadow,scale] before:absolute before:inset-0 before:rounded-full before:shadow-[0_1px_--theme(--color-black/6%)] has-focus-visible:ring-[3px] has-focus-visible:ring-ring/24 data-dragging:scale-120 sm:size-4 dark:border-background dark:has-focus-visible:ring-ring/48 [:has(*:focus-visible),[data-dragging]]:shadow-none"
53
+ data-slot="slider-thumb"
54
+ index={index}
55
+ key={String(index)}
56
+ />
57
+ ))}
58
+ </SliderPrimitive.Track>
59
+ </SliderPrimitive.Control>
60
+ </SliderPrimitive.Root>
61
+ );
62
+ }
63
+
64
+ function SliderValue({ className, ...props }: SliderPrimitive.Value.Props) {
65
+ return (
66
+ <SliderPrimitive.Value
67
+ className={cn("flex justify-end text-sm", className)}
68
+ data-slot="slider-value"
69
+ {...props}
70
+ />
71
+ );
72
+ }
73
+
74
+ export { Slider, SliderValue };
@@ -0,0 +1,18 @@
1
+ import { Loader2Icon } from "lucide-react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ function Spinner({
5
+ className,
6
+ ...props
7
+ }: React.ComponentProps<typeof Loader2Icon>) {
8
+ return (
9
+ <Loader2Icon
10
+ aria-label="Loading"
11
+ className={cn("animate-spin", className)}
12
+ role="status"
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+
18
+ export { Spinner };
@@ -0,0 +1,27 @@
1
+ "use client";
2
+
3
+ import { Switch as SwitchPrimitive } from "@base-ui/react/switch";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ function Switch({ className, ...props }: SwitchPrimitive.Root.Props) {
8
+ return (
9
+ <SwitchPrimitive.Root
10
+ className={cn(
11
+ "inline-flex h-[calc(var(--thumb-size)+2px)] w-[calc(var(--thumb-size)*2-2px)] shrink-0 items-center rounded-full p-px outline-none transition-[background-color,box-shadow] duration-200 [--thumb-size:--spacing(5)] focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background data-checked:bg-primary data-unchecked:bg-input data-disabled:opacity-64 sm:[--thumb-size:--spacing(4)]",
12
+ className,
13
+ )}
14
+ data-slot="switch"
15
+ {...props}
16
+ >
17
+ <SwitchPrimitive.Thumb
18
+ className={cn(
19
+ "pointer-events-none block aspect-square h-full origin-left in-[[role=switch]:active,[data-slot=label]:active]:not-data-disabled:scale-x-110 in-[[role=switch]:active,[data-slot=label]:active]:rounded-[var(--thumb-size)/calc(var(--thumb-size)*1.1)] rounded-(--thumb-size) bg-background shadow-sm/5 will-change-transform [transition:translate_.15s,border-radius_.15s,scale_.1s_.1s,transform-origin_.15s] data-checked:origin-[var(--thumb-size)_50%] data-checked:translate-x-[calc(var(--thumb-size)-4px)]",
20
+ )}
21
+ data-slot="switch-thumb"
22
+ />
23
+ </SwitchPrimitive.Root>
24
+ );
25
+ }
26
+
27
+ export { Switch };
@@ -0,0 +1,126 @@
1
+ import type * as React from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+
5
+ function Table({ className, ...props }: React.ComponentProps<"table">) {
6
+ return (
7
+ <div
8
+ className="relative w-full overflow-x-auto"
9
+ data-slot="table-container"
10
+ >
11
+ <table
12
+ className={cn(
13
+ "w-full caption-bottom in-data-[slot=frame]:border-separate in-data-[slot=frame]:border-spacing-0 text-sm",
14
+ className,
15
+ )}
16
+ data-slot="table"
17
+ {...props}
18
+ />
19
+ </div>
20
+ );
21
+ }
22
+
23
+ function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
24
+ return (
25
+ <thead
26
+ className={cn(
27
+ "[&_tr]:border-b in-data-[slot=frame]:**:[th]:h-9 in-data-[slot=frame]:*:[tr]:border-none in-data-[slot=frame]:*:[tr]:hover:bg-transparent",
28
+ className,
29
+ )}
30
+ data-slot="table-header"
31
+ {...props}
32
+ />
33
+ );
34
+ }
35
+
36
+ function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
37
+ return (
38
+ <tbody
39
+ className={cn(
40
+ "relative in-data-[slot=frame]:rounded-xl in-data-[slot=frame]:shadow-xs/5 before:pointer-events-none before:absolute before:inset-px not-in-data-[slot=frame]:before:hidden before:rounded-[calc(var(--radius-xl)-1px)] before:shadow-[0_1px_--theme(--color-black/6%)] dark:before:shadow-[0_-1px_--theme(--color-white/8%)] [&_tr:last-child]:border-0 in-data-[slot=frame]:*:[tr]:border-0 in-data-[slot=frame]:*:[tr]:*:[td]:border-b in-data-[slot=frame]:*:[tr]:*:[td]:bg-background in-data-[slot=frame]:*:[tr]:*:[td]:bg-clip-padding in-data-[slot=frame]:*:[tr]:first:*:[td]:first:rounded-ss-xl in-data-[slot=frame]:*:[tr]:*:[td]:first:border-s in-data-[slot=frame]:*:[tr]:first:*:[td]:border-t in-data-[slot=frame]:*:[tr]:last:*:[td]:last:rounded-ee-xl in-data-[slot=frame]:*:[tr]:*:[td]:last:border-e in-data-[slot=frame]:*:[tr]:first:*:[td]:last:rounded-se-xl in-data-[slot=frame]:*:[tr]:last:*:[td]:first:rounded-es-xl in-data-[slot=frame]:*:[tr]:hover:*:[td]:bg-transparent in-data-[slot=frame]:*:[tr]:data-[state=selected]:*:[td]:bg-muted/72",
41
+ className,
42
+ )}
43
+ data-slot="table-body"
44
+ {...props}
45
+ />
46
+ );
47
+ }
48
+
49
+ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
50
+ return (
51
+ <tfoot
52
+ className={cn(
53
+ "border-t in-data-[slot=frame]:border-none bg-muted/72 in-data-[slot=frame]:bg-transparent font-medium [&>tr]:last:border-b-0 in-data-[slot=frame]:*:[tr]:hover:bg-transparent",
54
+ className,
55
+ )}
56
+ data-slot="table-footer"
57
+ {...props}
58
+ />
59
+ );
60
+ }
61
+
62
+ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
63
+ return (
64
+ <tr
65
+ className={cn(
66
+ "border-b transition-colors hover:bg-muted/72 in-data-[slot=frame]:hover:bg-transparent data-[state=selected]:bg-muted/72 in-data-[slot=frame]:data-[state=selected]:bg-transparent",
67
+ className,
68
+ )}
69
+ data-slot="table-row"
70
+ {...props}
71
+ />
72
+ );
73
+ }
74
+
75
+ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
76
+ return (
77
+ <th
78
+ className={cn(
79
+ "h-10 whitespace-nowrap px-2.5 text-left align-middle font-medium text-muted-foreground leading-none has-[[role=checkbox]]:w-px has-[[role=checkbox]]:pe-0",
80
+ className,
81
+ )}
82
+ data-slot="table-head"
83
+ {...props}
84
+ />
85
+ );
86
+ }
87
+
88
+ function TableCell({ className, ...props }: React.ComponentProps<"td">) {
89
+ return (
90
+ <td
91
+ className={cn(
92
+ "whitespace-nowrap p-2.5 align-middle leading-none in-data-[slot=frame]:first:p-[calc(--spacing(2.5)-1px)] in-data-[slot=frame]:last:p-[calc(--spacing(2.5)-1px)] has-[[role=checkbox]]:pe-0",
93
+ className,
94
+ )}
95
+ data-slot="table-cell"
96
+ {...props}
97
+ />
98
+ );
99
+ }
100
+
101
+ function TableCaption({
102
+ className,
103
+ ...props
104
+ }: React.ComponentProps<"caption">) {
105
+ return (
106
+ <caption
107
+ className={cn(
108
+ "in-data-[slot=frame]:my-4 mt-4 text-muted-foreground text-sm",
109
+ className,
110
+ )}
111
+ data-slot="table-caption"
112
+ {...props}
113
+ />
114
+ );
115
+ }
116
+
117
+ export {
118
+ Table,
119
+ TableHeader,
120
+ TableBody,
121
+ TableFooter,
122
+ TableHead,
123
+ TableRow,
124
+ TableCell,
125
+ TableCaption,
126
+ };
@@ -0,0 +1,87 @@
1
+ "use client";
2
+
3
+ import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ type TabsVariant = "default" | "underline";
8
+
9
+ function Tabs({ className, ...props }: TabsPrimitive.Root.Props) {
10
+ return (
11
+ <TabsPrimitive.Root
12
+ className={cn(
13
+ "flex flex-col gap-2 data-[orientation=vertical]:flex-row",
14
+ className,
15
+ )}
16
+ data-slot="tabs"
17
+ {...props}
18
+ />
19
+ );
20
+ }
21
+
22
+ function TabsList({
23
+ variant = "default",
24
+ className,
25
+ children,
26
+ ...props
27
+ }: TabsPrimitive.List.Props & {
28
+ variant?: TabsVariant;
29
+ }) {
30
+ return (
31
+ <TabsPrimitive.List
32
+ className={cn(
33
+ "relative z-0 flex w-fit items-center justify-center gap-x-0.5 text-muted-foreground",
34
+ "data-[orientation=vertical]:flex-col",
35
+ variant === "default"
36
+ ? "rounded-lg bg-muted p-0.5 text-muted-foreground/72"
37
+ : "data-[orientation=vertical]:px-1 data-[orientation=horizontal]:py-1 *:data-[slot=tabs-tab]:hover:bg-accent",
38
+ className,
39
+ )}
40
+ data-slot="tabs-list"
41
+ {...props}
42
+ >
43
+ {children}
44
+ <TabsPrimitive.Indicator
45
+ className={cn(
46
+ "-translate-y-(--active-tab-bottom) absolute bottom-0 left-0 h-(--active-tab-height) w-(--active-tab-width) translate-x-(--active-tab-left) transition-[width,translate] duration-200 ease-in-out",
47
+ variant === "underline"
48
+ ? "data-[orientation=vertical]:-translate-x-px z-10 bg-primary data-[orientation=horizontal]:h-0.5 data-[orientation=vertical]:w-0.5 data-[orientation=horizontal]:translate-y-px"
49
+ : "-z-1 rounded-md bg-background shadow-sm/5 dark:bg-input",
50
+ )}
51
+ data-slot="tab-indicator"
52
+ />
53
+ </TabsPrimitive.List>
54
+ );
55
+ }
56
+
57
+ function TabsTab({ className, ...props }: TabsPrimitive.Tab.Props) {
58
+ return (
59
+ <TabsPrimitive.Tab
60
+ className={cn(
61
+ "[&_svg]:-mx-0.5 relative flex h-9 shrink-0 grow cursor-pointer items-center justify-center gap-1.5 whitespace-nowrap rounded-md border border-transparent px-[calc(--spacing(2.5)-1px)] font-medium text-base outline-none transition-[color,background-color,box-shadow] hover:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring data-disabled:pointer-events-none data-[orientation=vertical]:w-full data-[orientation=vertical]:justify-start data-active:text-foreground data-disabled:opacity-64 sm:h-8 sm:text-sm [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
62
+ className,
63
+ )}
64
+ data-slot="tabs-tab"
65
+ {...props}
66
+ />
67
+ );
68
+ }
69
+
70
+ function TabsPanel({ className, ...props }: TabsPrimitive.Panel.Props) {
71
+ return (
72
+ <TabsPrimitive.Panel
73
+ className={cn("flex-1 outline-none", className)}
74
+ data-slot="tabs-content"
75
+ {...props}
76
+ />
77
+ );
78
+ }
79
+
80
+ export {
81
+ Tabs,
82
+ TabsList,
83
+ TabsTab,
84
+ TabsTab as TabsTrigger,
85
+ TabsPanel,
86
+ TabsPanel as TabsContent,
87
+ };
@@ -0,0 +1,51 @@
1
+ "use client";
2
+
3
+ import { Field as FieldPrimitive } from "@base-ui/react/field";
4
+ import { mergeProps } from "@base-ui/react/merge-props";
5
+ import type * as React from "react";
6
+
7
+ import { cn } from "@/lib/utils";
8
+
9
+ type TextareaProps = React.ComponentProps<"textarea"> & {
10
+ size?: "sm" | "default" | "lg" | number;
11
+ unstyled?: boolean;
12
+ };
13
+
14
+ function Textarea({
15
+ className,
16
+ size = "default",
17
+ unstyled = false,
18
+ ...props
19
+ }: TextareaProps) {
20
+ return (
21
+ <span
22
+ className={
23
+ cn(
24
+ !unstyled &&
25
+ "relative inline-flex w-full rounded-lg border border-input bg-background not-dark:bg-clip-padding text-base text-foreground shadow-xs/5 ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] has-focus-visible:has-aria-invalid:border-destructive/64 has-focus-visible:has-aria-invalid:ring-destructive/16 has-aria-invalid:border-destructive/36 has-focus-visible:border-ring has-disabled:opacity-64 has-[:disabled,:focus-visible,[aria-invalid]]:shadow-none has-focus-visible:ring-[3px] not-has-disabled:has-not-focus-visible:not-has-aria-invalid:before:shadow-[0_1px_--theme(--color-black/6%)] sm:text-sm dark:bg-input/32 dark:has-aria-invalid:ring-destructive/24 dark:not-has-disabled:has-not-focus-visible:not-has-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)]",
26
+ className,
27
+ ) || undefined
28
+ }
29
+ data-size={size}
30
+ data-slot="textarea-control"
31
+ >
32
+ <FieldPrimitive.Control
33
+ render={(defaultProps) => (
34
+ <textarea
35
+ className={cn(
36
+ "field-sizing-content min-h-17.5 w-full rounded-[inherit] px-[calc(--spacing(3)-1px)] py-[calc(--spacing(1.5)-1px)] outline-none max-sm:min-h-20.5",
37
+ size === "sm" &&
38
+ "min-h-16.5 px-[calc(--spacing(2.5)-1px)] py-[calc(--spacing(1)-1px)] max-sm:min-h-19.5",
39
+ size === "lg" &&
40
+ "min-h-18.5 py-[calc(--spacing(2)-1px)] max-sm:min-h-21.5",
41
+ )}
42
+ data-slot="textarea"
43
+ {...mergeProps(defaultProps, props)}
44
+ />
45
+ )}
46
+ />
47
+ </span>
48
+ );
49
+ }
50
+
51
+ export { Textarea, type TextareaProps };
@@ -0,0 +1,269 @@
1
+ "use client";
2
+
3
+ import { Toast } from "@base-ui/react/toast";
4
+ import {
5
+ CircleAlertIcon,
6
+ CircleCheckIcon,
7
+ InfoIcon,
8
+ LoaderCircleIcon,
9
+ TriangleAlertIcon,
10
+ } from "lucide-react";
11
+
12
+ import { cn } from "@/lib/utils";
13
+ import { buttonVariants } from "@/components/ui/button";
14
+
15
+ const toastManager = Toast.createToastManager();
16
+ const anchoredToastManager = Toast.createToastManager();
17
+
18
+ const TOAST_ICONS = {
19
+ error: CircleAlertIcon,
20
+ info: InfoIcon,
21
+ loading: LoaderCircleIcon,
22
+ success: CircleCheckIcon,
23
+ warning: TriangleAlertIcon,
24
+ } as const;
25
+
26
+ type ToastPosition =
27
+ | "top-left"
28
+ | "top-center"
29
+ | "top-right"
30
+ | "bottom-left"
31
+ | "bottom-center"
32
+ | "bottom-right";
33
+
34
+ interface ToastProviderProps extends Toast.Provider.Props {
35
+ position?: ToastPosition;
36
+ }
37
+
38
+ function ToastProvider({
39
+ children,
40
+ position = "bottom-right",
41
+ ...props
42
+ }: ToastProviderProps) {
43
+ return (
44
+ <Toast.Provider toastManager={toastManager} {...props}>
45
+ {children}
46
+ <Toasts position={position} />
47
+ </Toast.Provider>
48
+ );
49
+ }
50
+
51
+ function Toasts({ position = "bottom-right" }: { position: ToastPosition }) {
52
+ const { toasts } = Toast.useToastManager();
53
+ const isTop = position.startsWith("top");
54
+
55
+ return (
56
+ <Toast.Portal data-slot="toast-portal">
57
+ <Toast.Viewport
58
+ className={cn(
59
+ "fixed z-50 mx-auto flex w-[calc(100%-var(--toast-inset)*2)] max-w-90 [--toast-inset:--spacing(4)] sm:[--toast-inset:--spacing(8)]",
60
+ // Vertical positioning
61
+ "data-[position*=top]:top-(--toast-inset)",
62
+ "data-[position*=bottom]:bottom-(--toast-inset)",
63
+ // Horizontal positioning
64
+ "data-[position*=left]:left-(--toast-inset)",
65
+ "data-[position*=right]:right-(--toast-inset)",
66
+ "data-[position*=center]:-translate-x-1/2 data-[position*=center]:left-1/2",
67
+ )}
68
+ data-position={position}
69
+ data-slot="toast-viewport"
70
+ >
71
+ {toasts.map((toast) => {
72
+ const Icon = toast.type
73
+ ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS]
74
+ : null;
75
+
76
+ return (
77
+ <Toast.Root
78
+ className={cn(
79
+ "absolute z-[calc(9999-var(--toast-index))] h-(--toast-calc-height) w-full select-none rounded-lg border bg-popover not-dark:bg-clip-padding text-popover-foreground shadow-lg/5 [transition:transform_.5s_cubic-bezier(.22,1,.36,1),opacity_.5s,height_.15s] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-[0_1px_--theme(--color-black/6%)] dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
80
+ // Base positioning using data-position
81
+ "data-[position*=right]:right-0 data-[position*=right]:left-auto",
82
+ "data-[position*=left]:right-auto data-[position*=left]:left-0",
83
+ "data-[position*=center]:right-0 data-[position*=center]:left-0",
84
+ "data-[position*=top]:top-0 data-[position*=top]:bottom-auto data-[position*=top]:origin-top",
85
+ "data-[position*=bottom]:top-auto data-[position*=bottom]:bottom-0 data-[position*=bottom]:origin-bottom",
86
+ // Gap fill for hover
87
+ "after:absolute after:left-0 after:h-[calc(var(--toast-gap)+1px)] after:w-full",
88
+ "data-[position*=top]:after:top-full",
89
+ "data-[position*=bottom]:after:bottom-full",
90
+ // Define some variables
91
+ "[--toast-calc-height:var(--toast-frontmost-height,var(--toast-height))] [--toast-gap:--spacing(3)] [--toast-peek:--spacing(3)] [--toast-scale:calc(max(0,1-(var(--toast-index)*.1)))] [--toast-shrink:calc(1-var(--toast-scale))]",
92
+ // Define offset-y variable
93
+ "data-[position*=top]:[--toast-calc-offset-y:calc(var(--toast-offset-y)+var(--toast-index)*var(--toast-gap)+var(--toast-swipe-movement-y))]",
94
+ "data-[position*=bottom]:[--toast-calc-offset-y:calc(var(--toast-offset-y)*-1+var(--toast-index)*var(--toast-gap)*-1+var(--toast-swipe-movement-y))]",
95
+ // Default state transform
96
+ "data-[position*=top]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+(var(--toast-index)*var(--toast-peek))+(var(--toast-shrink)*var(--toast-calc-height))))_scale(var(--toast-scale))]",
97
+ "data-[position*=bottom]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--toast-peek))-(var(--toast-shrink)*var(--toast-calc-height))))_scale(var(--toast-scale))]",
98
+ // Limited state
99
+ "data-limited:opacity-0",
100
+ // Expanded state
101
+ "data-expanded:h-(--toast-height)",
102
+ "data-position:data-expanded:transform-[translateX(var(--toast-swipe-movement-x))_translateY(var(--toast-calc-offset-y))]",
103
+ // Starting and ending animations
104
+ "data-[position*=top]:data-starting-style:transform-[translateY(calc(-100%-var(--toast-inset)))]",
105
+ "data-[position*=bottom]:data-starting-style:transform-[translateY(calc(100%+var(--toast-inset)))]",
106
+ "data-ending-style:opacity-0",
107
+ // Ending animations (direction-aware)
108
+ "data-ending-style:not-data-limited:not-data-swipe-direction:transform-[translateY(calc(100%+var(--toast-inset)))]",
109
+ "data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-100%-var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]",
110
+ "data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+100%+var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]",
111
+ "data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-100%-var(--toast-inset)))]",
112
+ "data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+100%+var(--toast-inset)))]",
113
+ // Ending animations (expanded)
114
+ "data-expanded:data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-100%-var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]",
115
+ "data-expanded:data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+100%+var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]",
116
+ "data-expanded:data-ending-style:data-[swipe-direction=up]:transform-[translateY(calc(var(--toast-swipe-movement-y)-100%-var(--toast-inset)))]",
117
+ "data-expanded:data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(var(--toast-swipe-movement-y)+100%+var(--toast-inset)))]",
118
+ )}
119
+ data-position={position}
120
+ key={toast.id}
121
+ swipeDirection={
122
+ position.includes("center")
123
+ ? [isTop ? "up" : "down"]
124
+ : position.includes("left")
125
+ ? ["left", isTop ? "up" : "down"]
126
+ : ["right", isTop ? "up" : "down"]
127
+ }
128
+ toast={toast}
129
+ >
130
+ <Toast.Content className="pointer-events-auto flex items-center justify-between gap-1.5 overflow-hidden px-3.5 py-3 text-sm transition-opacity duration-250 data-behind:not-data-expanded:pointer-events-none data-behind:opacity-0 data-expanded:opacity-100">
131
+ <div className="flex gap-2">
132
+ {Icon && (
133
+ <div
134
+ className="[&>svg]:h-lh [&>svg]:w-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
135
+ data-slot="toast-icon"
136
+ >
137
+ <Icon className="in-data-[type=loading]:animate-spin in-data-[type=error]:text-destructive in-data-[type=info]:text-info in-data-[type=success]:text-success in-data-[type=warning]:text-warning in-data-[type=loading]:opacity-80" />
138
+ </div>
139
+ )}
140
+
141
+ <div className="flex flex-col gap-0.5">
142
+ <Toast.Title
143
+ className="font-medium"
144
+ data-slot="toast-title"
145
+ />
146
+ <Toast.Description
147
+ className="text-muted-foreground"
148
+ data-slot="toast-description"
149
+ />
150
+ </div>
151
+ </div>
152
+ {toast.actionProps && (
153
+ <Toast.Action
154
+ className={buttonVariants({ size: "xs" })}
155
+ data-slot="toast-action"
156
+ >
157
+ {toast.actionProps.children}
158
+ </Toast.Action>
159
+ )}
160
+ </Toast.Content>
161
+ </Toast.Root>
162
+ );
163
+ })}
164
+ </Toast.Viewport>
165
+ </Toast.Portal>
166
+ );
167
+ }
168
+
169
+ function AnchoredToastProvider({ children, ...props }: Toast.Provider.Props) {
170
+ return (
171
+ <Toast.Provider toastManager={anchoredToastManager} {...props}>
172
+ {children}
173
+ <AnchoredToasts />
174
+ </Toast.Provider>
175
+ );
176
+ }
177
+
178
+ function AnchoredToasts() {
179
+ const { toasts } = Toast.useToastManager();
180
+
181
+ return (
182
+ <Toast.Portal data-slot="toast-portal-anchored">
183
+ <Toast.Viewport
184
+ className="outline-none"
185
+ data-slot="toast-viewport-anchored"
186
+ >
187
+ {toasts.map((toast) => {
188
+ const Icon = toast.type
189
+ ? TOAST_ICONS[toast.type as keyof typeof TOAST_ICONS]
190
+ : null;
191
+ const tooltipStyle =
192
+ (toast.data as { tooltipStyle?: boolean })?.tooltipStyle ?? false;
193
+ const positionerProps = toast.positionerProps;
194
+
195
+ if (!positionerProps?.anchor) {
196
+ return null;
197
+ }
198
+
199
+ return (
200
+ <Toast.Positioner
201
+ className="z-50 max-w-[min(--spacing(64),var(--available-width))]"
202
+ data-slot="toast-positioner"
203
+ key={toast.id}
204
+ sideOffset={positionerProps.sideOffset ?? 4}
205
+ toast={toast}
206
+ >
207
+ <Toast.Root
208
+ className={cn(
209
+ "relative text-balance border bg-popover not-dark:bg-clip-padding text-popover-foreground text-xs transition-[scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:shadow-[0_1px_--theme(--color-black/6%)] data-ending-style:scale-98 data-starting-style:scale-98 data-ending-style:opacity-0 data-starting-style:opacity-0 dark:before:shadow-[0_-1px_--theme(--color-white/6%)]",
210
+ tooltipStyle
211
+ ? "rounded-md shadow-md/5 before:rounded-[calc(var(--radius-md)-1px)]"
212
+ : "rounded-lg shadow-lg/5 before:rounded-[calc(var(--radius-lg)-1px)]",
213
+ )}
214
+ data-slot="toast-popup"
215
+ toast={toast}
216
+ >
217
+ {tooltipStyle ? (
218
+ <Toast.Content className="pointer-events-auto px-2 py-1">
219
+ <Toast.Title data-slot="toast-title" />
220
+ </Toast.Content>
221
+ ) : (
222
+ <Toast.Content className="pointer-events-auto flex items-center justify-between gap-1.5 overflow-hidden px-3.5 py-3 text-sm">
223
+ <div className="flex gap-2">
224
+ {Icon && (
225
+ <div
226
+ className="[&>svg]:h-lh [&>svg]:w-4 [&_svg]:pointer-events-none [&_svg]:shrink-0"
227
+ data-slot="toast-icon"
228
+ >
229
+ <Icon className="in-data-[type=loading]:animate-spin in-data-[type=error]:text-destructive in-data-[type=info]:text-info in-data-[type=success]:text-success in-data-[type=warning]:text-warning in-data-[type=loading]:opacity-80" />
230
+ </div>
231
+ )}
232
+
233
+ <div className="flex flex-col gap-0.5">
234
+ <Toast.Title
235
+ className="font-medium"
236
+ data-slot="toast-title"
237
+ />
238
+ <Toast.Description
239
+ className="text-muted-foreground"
240
+ data-slot="toast-description"
241
+ />
242
+ </div>
243
+ </div>
244
+ {toast.actionProps && (
245
+ <Toast.Action
246
+ className={buttonVariants({ size: "xs" })}
247
+ data-slot="toast-action"
248
+ >
249
+ {toast.actionProps.children}
250
+ </Toast.Action>
251
+ )}
252
+ </Toast.Content>
253
+ )}
254
+ </Toast.Root>
255
+ </Toast.Positioner>
256
+ );
257
+ })}
258
+ </Toast.Viewport>
259
+ </Toast.Portal>
260
+ );
261
+ }
262
+
263
+ export {
264
+ ToastProvider,
265
+ type ToastPosition,
266
+ toastManager,
267
+ AnchoredToastProvider,
268
+ anchoredToastManager,
269
+ };