@m5kdev/web-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 (127) hide show
  1. package/LICENSE +621 -0
  2. package/README.md +17 -0
  3. package/package.json +169 -0
  4. package/src/animations/card.motion.ts +9 -0
  5. package/src/components/AvatarUpload.tsx +133 -0
  6. package/src/components/Button.tsx +14 -0
  7. package/src/components/Calendar.css +684 -0
  8. package/src/components/Calendar.tsx +32 -0
  9. package/src/components/CardsSelect.tsx +155 -0
  10. package/src/components/CollapsibleSidebarMenuItem.tsx +57 -0
  11. package/src/components/ColorPicker.tsx +56 -0
  12. package/src/components/CopyButton.tsx +45 -0
  13. package/src/components/CropDialog.tsx +154 -0
  14. package/src/components/DialogProvider.tsx +105 -0
  15. package/src/components/ErrorFallback.tsx +17 -0
  16. package/src/components/FileDropzone.tsx +120 -0
  17. package/src/components/MultiSelectDropdown.tsx +233 -0
  18. package/src/components/Orb.tsx +288 -0
  19. package/src/components/PageAlert.tsx +121 -0
  20. package/src/components/SelectChips.tsx +40 -0
  21. package/src/components/SidebarItem.tsx +26 -0
  22. package/src/components/Steps.tsx +340 -0
  23. package/src/components/TablerIconPicker.tsx +4260 -0
  24. package/src/components/app-header.tsx +40 -0
  25. package/src/components/blur-card.tsx +132 -0
  26. package/src/components/features-section-demo-1.tsx +127 -0
  27. package/src/components/features-section-demo-2.tsx +102 -0
  28. package/src/components/features-section-demo-3.tsx +272 -0
  29. package/src/components/mode-toggle.tsx +31 -0
  30. package/src/components/nav-main.tsx +69 -0
  31. package/src/components/pricing-cards.tsx +133 -0
  32. package/src/components/shared/ButtonCopy.tsx +50 -0
  33. package/src/components/team-switcher.tsx +83 -0
  34. package/src/components/theme-provider.tsx +74 -0
  35. package/src/components/typewriter.tsx +90 -0
  36. package/src/components/ui/alert-dialog.tsx +133 -0
  37. package/src/components/ui/alert.tsx +60 -0
  38. package/src/components/ui/avatar.tsx +47 -0
  39. package/src/components/ui/badge.tsx +33 -0
  40. package/src/components/ui/bento-grid.tsx +54 -0
  41. package/src/components/ui/bento-grid2.tsx +66 -0
  42. package/src/components/ui/breadcrumb.tsx +101 -0
  43. package/src/components/ui/button.tsx +50 -0
  44. package/src/components/ui/card.tsx +55 -0
  45. package/src/components/ui/checkbox.tsx +26 -0
  46. package/src/components/ui/collapsible.tsx +9 -0
  47. package/src/components/ui/dialog.tsx +119 -0
  48. package/src/components/ui/dropdown-menu.tsx +186 -0
  49. package/src/components/ui/floating-navbar.tsx +78 -0
  50. package/src/components/ui/form.tsx +167 -0
  51. package/src/components/ui/image.tsx +55 -0
  52. package/src/components/ui/input.tsx +22 -0
  53. package/src/components/ui/label.tsx +19 -0
  54. package/src/components/ui/pagination.tsx +105 -0
  55. package/src/components/ui/progress.tsx +23 -0
  56. package/src/components/ui/resizable-navbar.tsx +260 -0
  57. package/src/components/ui/segment-control.tsx +143 -0
  58. package/src/components/ui/select.tsx +153 -0
  59. package/src/components/ui/separator.tsx +24 -0
  60. package/src/components/ui/sheet.tsx +121 -0
  61. package/src/components/ui/sidebar.tsx +736 -0
  62. package/src/components/ui/skeleton.tsx +7 -0
  63. package/src/components/ui/slider.tsx +23 -0
  64. package/src/components/ui/sonner.tsx +27 -0
  65. package/src/components/ui/spinner.tsx +45 -0
  66. package/src/components/ui/switch.tsx +27 -0
  67. package/src/components/ui/table.tsx +90 -0
  68. package/src/components/ui/tabs.tsx +52 -0
  69. package/src/components/ui/textarea.tsx +18 -0
  70. package/src/components/ui/timeline.tsx +95 -0
  71. package/src/components/ui/toast.tsx +126 -0
  72. package/src/components/ui/tooltip.tsx +55 -0
  73. package/src/components/ui/typewriter-effect.tsx +181 -0
  74. package/src/hooks/use-mobile.ts +19 -0
  75. package/src/hooks/useDialog.ts +25 -0
  76. package/src/icons/GoogleIcon.tsx +32 -0
  77. package/src/icons/LinkedInIcon.tsx +30 -0
  78. package/src/icons/MicrosoftIcon.tsx +21 -0
  79. package/src/lib/chatwoot.ts +51 -0
  80. package/src/lib/utils.ts +6 -0
  81. package/src/modules/app/components/AppLoader.tsx +9 -0
  82. package/src/modules/app/components/AppShell.tsx +21 -0
  83. package/src/modules/app/components/AppSidebar.tsx +26 -0
  84. package/src/modules/app/components/AppSidebarContent.tsx +73 -0
  85. package/src/modules/app/components/AppSidebarHeader.tsx +57 -0
  86. package/src/modules/app/components/AppSidebarInvites.tsx +32 -0
  87. package/src/modules/app/components/AppSidebarUser.tsx +128 -0
  88. package/src/modules/auth/components/AdminUserManagement.tsx +1136 -0
  89. package/src/modules/auth/components/AdminWaitlist.tsx +358 -0
  90. package/src/modules/auth/components/AuthLayout.tsx +13 -0
  91. package/src/modules/auth/components/AuthProviders.tsx +105 -0
  92. package/src/modules/auth/components/AuthRouter.tsx +29 -0
  93. package/src/modules/auth/components/ClaimAccountRoute.tsx +242 -0
  94. package/src/modules/auth/components/ErrorAuthRoute.tsx +121 -0
  95. package/src/modules/auth/components/ForgotPasswordForm.tsx +58 -0
  96. package/src/modules/auth/components/ForgotPasswordRoute.tsx +27 -0
  97. package/src/modules/auth/components/InviteFriends.tsx +273 -0
  98. package/src/modules/auth/components/LastUsedBadge.tsx +22 -0
  99. package/src/modules/auth/components/LoginForm.tsx +104 -0
  100. package/src/modules/auth/components/LoginRoute.tsx +31 -0
  101. package/src/modules/auth/components/LogoutRoute.tsx +21 -0
  102. package/src/modules/auth/components/OrganizationAcceptInvitationRoute.tsx +161 -0
  103. package/src/modules/auth/components/OrganizationMembersRoute.tsx +730 -0
  104. package/src/modules/auth/components/OrganizationSettingsRoute.tsx +280 -0
  105. package/src/modules/auth/components/OrganizationSwitcher.tsx +148 -0
  106. package/src/modules/auth/components/ProfileRoute.tsx +104 -0
  107. package/src/modules/auth/components/RangeNuqsDatePicker.tsx +365 -0
  108. package/src/modules/auth/components/ResetPasswordForm.tsx +103 -0
  109. package/src/modules/auth/components/ResetPasswordRoute.tsx +27 -0
  110. package/src/modules/auth/components/SignupFormRoute.tsx +189 -0
  111. package/src/modules/auth/components/SignupRoute.tsx +53 -0
  112. package/src/modules/auth/components/UserPreferences.tsx +144 -0
  113. package/src/modules/auth/components/WaitlistCard.tsx +78 -0
  114. package/src/modules/auth/components/WaitlistCodeValidation.tsx +79 -0
  115. package/src/modules/billing/components/BillingBetaPage.tsx +124 -0
  116. package/src/modules/billing/components/BillingInvoicePage.tsx +180 -0
  117. package/src/modules/billing/components/BillingPlanSelect.tsx +14 -0
  118. package/src/modules/billing/components/BillingRouter.tsx +20 -0
  119. package/src/modules/billing/components/BillingSinglePlanSelect.tsx +172 -0
  120. package/src/modules/table/components/ColumnOrderAndVisibility.tsx +127 -0
  121. package/src/modules/table/components/NuqsTable.tsx +396 -0
  122. package/src/modules/table/components/TableFiltering.tsx +520 -0
  123. package/src/modules/table/components/TablePagination.tsx +59 -0
  124. package/src/modules/table/components/table.types.ts +11 -0
  125. package/src/modules/table/filterTransformers.ts +323 -0
  126. package/src/types.ts +4 -0
  127. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,31 @@
1
+ import { Moon, Sun } from "lucide-react";
2
+
3
+ import { useTheme } from "#components/theme-provider";
4
+ import { Button } from "#components/ui/button";
5
+ import {
6
+ DropdownMenu,
7
+ DropdownMenuContent,
8
+ DropdownMenuItem,
9
+ DropdownMenuTrigger,
10
+ } from "#components/ui/dropdown-menu";
11
+
12
+ export function ModeToggle() {
13
+ const { setTheme } = useTheme();
14
+
15
+ return (
16
+ <DropdownMenu>
17
+ <DropdownMenuTrigger asChild>
18
+ <Button variant="outline" size="icon">
19
+ <Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
20
+ <Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
21
+ <span className="sr-only">Toggle theme</span>
22
+ </Button>
23
+ </DropdownMenuTrigger>
24
+ <DropdownMenuContent align="end">
25
+ <DropdownMenuItem onClick={() => setTheme("light")}>Light</DropdownMenuItem>
26
+ <DropdownMenuItem onClick={() => setTheme("dark")}>Dark</DropdownMenuItem>
27
+ <DropdownMenuItem onClick={() => setTheme("system")}>System</DropdownMenuItem>
28
+ </DropdownMenuContent>
29
+ </DropdownMenu>
30
+ );
31
+ }
@@ -0,0 +1,69 @@
1
+ "use client";
2
+
3
+ import { ChevronRight, type LucideIcon } from "lucide-react";
4
+ import { Link } from "react-router";
5
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "#components/ui/collapsible";
6
+ import {
7
+ SidebarGroup,
8
+ SidebarGroupLabel,
9
+ SidebarMenu,
10
+ SidebarMenuButton,
11
+ SidebarMenuItem,
12
+ SidebarMenuSub,
13
+ SidebarMenuSubButton,
14
+ SidebarMenuSubItem,
15
+ } from "#components/ui/sidebar";
16
+
17
+ export function NavMain({
18
+ items,
19
+ }: {
20
+ items: {
21
+ title: string;
22
+ url: string;
23
+ icon?: LucideIcon;
24
+ isActive?: boolean;
25
+ items?: {
26
+ title: string;
27
+ url: string;
28
+ }[];
29
+ }[];
30
+ }) {
31
+ return (
32
+ <SidebarGroup>
33
+ <SidebarGroupLabel>Platform</SidebarGroupLabel>
34
+ <SidebarMenu>
35
+ {items.map((item) => (
36
+ <Collapsible
37
+ key={item.title}
38
+ asChild
39
+ defaultOpen={item.isActive}
40
+ className="group/collapsible"
41
+ >
42
+ <SidebarMenuItem>
43
+ <CollapsibleTrigger asChild>
44
+ <SidebarMenuButton tooltip={item.title}>
45
+ {item.icon && <item.icon />}
46
+ <span>{item.title}</span>
47
+ <ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
48
+ </SidebarMenuButton>
49
+ </CollapsibleTrigger>
50
+ <CollapsibleContent>
51
+ <SidebarMenuSub>
52
+ {item.items?.map((subItem) => (
53
+ <SidebarMenuSubItem key={subItem.title}>
54
+ <SidebarMenuSubButton asChild>
55
+ <Link to={subItem.url}>
56
+ <span>{subItem.title}</span>
57
+ </Link>
58
+ </SidebarMenuSubButton>
59
+ </SidebarMenuSubItem>
60
+ ))}
61
+ </SidebarMenuSub>
62
+ </CollapsibleContent>
63
+ </SidebarMenuItem>
64
+ </Collapsible>
65
+ ))}
66
+ </SidebarMenu>
67
+ </SidebarGroup>
68
+ );
69
+ }
@@ -0,0 +1,133 @@
1
+ import { Check, type LucideIcon, MoveRight, PhoneCall } from "lucide-react";
2
+ import { Badge } from "#components/ui/badge";
3
+ import { Button } from "#components/ui/button";
4
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "#components/ui/card";
5
+
6
+ interface Feature {
7
+ title: string;
8
+ description: string;
9
+ }
10
+
11
+ interface PricingCardProps {
12
+ title: string;
13
+ description: string;
14
+ price: number;
15
+ features: Feature[];
16
+ highlighted?: boolean;
17
+ ctaText: string;
18
+ ctaIcon: LucideIcon;
19
+ variant?: "default" | "outline";
20
+ }
21
+
22
+ function PricingCard({
23
+ title,
24
+ description,
25
+ price,
26
+ features,
27
+ highlighted = false,
28
+ ctaText,
29
+ ctaIcon: CtaIcon,
30
+ variant = "outline",
31
+ }: PricingCardProps) {
32
+ const Icon = CtaIcon;
33
+ return (
34
+ <Card className={`w-full rounded-md ${highlighted ? "shadow-2xl" : ""}`}>
35
+ <CardHeader>
36
+ <CardTitle>
37
+ <span className="flex flex-row gap-4 items-center font-normal">{title}</span>
38
+ </CardTitle>
39
+ <CardDescription>{description}</CardDescription>
40
+ </CardHeader>
41
+ <CardContent>
42
+ <div className="flex flex-col gap-8 justify-start">
43
+ <p className="flex flex-row items-center gap-2 text-xl">
44
+ <span className="text-4xl">${price}</span>
45
+ <span className="text-sm text-muted-foreground"> / month</span>
46
+ </p>
47
+ <div className="flex flex-col gap-4 justify-start">
48
+ {features.map((feature, index) => (
49
+ <div key={index} className="flex flex-row gap-4">
50
+ <Check className="w-4 h-4 mt-2 text-primary" />
51
+ <div className="flex flex-col">
52
+ <p>{feature.title}</p>
53
+ <p className="text-muted-foreground text-sm">{feature.description}</p>
54
+ </div>
55
+ </div>
56
+ ))}
57
+ </div>
58
+ <Button variant={variant} className="gap-4">
59
+ {ctaText} <Icon className="w-4 h-4" />
60
+ </Button>
61
+ </div>
62
+ </CardContent>
63
+ </Card>
64
+ );
65
+ }
66
+
67
+ export function PricingCards() {
68
+ const commonDescription =
69
+ "Our goal is to streamline SMB trade, making it easier and faster than ever for everyone and everywhere.";
70
+
71
+ const commonFeatures = [
72
+ {
73
+ title: "Fast and reliable",
74
+ description: "We've made it fast and reliable.",
75
+ },
76
+ {
77
+ title: "Fast and reliable",
78
+ description: "We've made it fast and reliable.",
79
+ },
80
+ {
81
+ title: "Fast and reliable",
82
+ description: "We've made it fast and reliable.",
83
+ },
84
+ ];
85
+
86
+ return (
87
+ <div className="w-full py-20 lg:py-40">
88
+ <div className="container mx-auto">
89
+ <div className="flex text-center justify-center items-center gap-4 flex-col">
90
+ <Badge>Pricing</Badge>
91
+ <div className="flex gap-2 flex-col">
92
+ <h2 className="text-3xl md:text-5xl tracking-tighter max-w-xl text-center font-regular">
93
+ Prices that make sense!
94
+ </h2>
95
+ <p className="text-lg leading-relaxed tracking-tight text-muted-foreground max-w-xl text-center">
96
+ Managing a small business today is already tough.
97
+ </p>
98
+ </div>
99
+ <div className="grid pt-20 text-left grid-cols-1 lg:grid-cols-3 w-full gap-8">
100
+ <PricingCard
101
+ title="Startup"
102
+ description={commonDescription}
103
+ price={40}
104
+ features={commonFeatures}
105
+ ctaText="Sign up today"
106
+ ctaIcon={MoveRight}
107
+ variant="outline"
108
+ />
109
+ <PricingCard
110
+ title="Growth"
111
+ description={commonDescription}
112
+ price={40}
113
+ features={commonFeatures}
114
+ highlighted={true}
115
+ ctaText="Sign up today"
116
+ ctaIcon={MoveRight}
117
+ variant="default"
118
+ />
119
+ <PricingCard
120
+ title="Enterprise"
121
+ description={commonDescription}
122
+ price={40}
123
+ features={commonFeatures}
124
+ ctaText="Book a meeting"
125
+ ctaIcon={PhoneCall}
126
+ variant="outline"
127
+ />
128
+ </div>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ );
133
+ }
@@ -0,0 +1,50 @@
1
+ import { CheckCircle, Copy } from "lucide-react";
2
+ import { useState } from "react";
3
+ import { useTranslation } from "react-i18next";
4
+ import { toast } from "sonner";
5
+ import { Button, type ButtonProps } from "#components/ui/button";
6
+ import { cn } from "#utils";
7
+
8
+ export function ButtonCopy({
9
+ text,
10
+ notificationTimeout = 1000,
11
+ iconOnly = false,
12
+ ...props
13
+ }: ButtonProps & { text: string; notificationTimeout?: number; iconOnly?: boolean }) {
14
+ const { t } = useTranslation();
15
+ const [isCopied, setIsCopied] = useState(false);
16
+
17
+ const handleCopy = async (text: string) => {
18
+ try {
19
+ await window.navigator.clipboard.writeText(text);
20
+ setIsCopied(true);
21
+ toast.success(t("web-ui:common.copySuccess"));
22
+ setTimeout(() => setIsCopied(false), notificationTimeout);
23
+ } catch (error) {
24
+ toast.error(t("web-ui:common.copyError"));
25
+ console.error(error);
26
+ }
27
+ };
28
+
29
+ return (
30
+ <Button
31
+ variant="outline"
32
+ size="sm"
33
+ onClick={() => handleCopy(text)}
34
+ className={cn(isCopied ? "bg-green-200 hover:bg-green-300" : "", iconOnly ? "" : "gap-1")}
35
+ {...props}
36
+ >
37
+ {isCopied ? (
38
+ <>
39
+ <CheckCircle className="h-4 w-4" />
40
+ {!iconOnly && t("web-ui:common.copied")}
41
+ </>
42
+ ) : (
43
+ <>
44
+ <Copy className="h-4 w-4" />
45
+ {!iconOnly && t("web-ui:common.copy")}
46
+ </>
47
+ )}
48
+ </Button>
49
+ );
50
+ }
@@ -0,0 +1,83 @@
1
+ import { ChevronsUpDown, Plus } from "lucide-react";
2
+ import * as React from "react";
3
+
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuLabel,
9
+ DropdownMenuSeparator,
10
+ DropdownMenuShortcut,
11
+ DropdownMenuTrigger,
12
+ } from "#components/ui/dropdown-menu";
13
+ import {
14
+ SidebarMenu,
15
+ SidebarMenuButton,
16
+ SidebarMenuItem,
17
+ useSidebar,
18
+ } from "#components/ui/sidebar";
19
+
20
+ export function TeamSwitcher({
21
+ teams,
22
+ }: {
23
+ teams: {
24
+ name: string;
25
+ logo: React.ElementType;
26
+ plan: string;
27
+ }[];
28
+ }) {
29
+ const { isMobile } = useSidebar();
30
+ const [activeTeam, setActiveTeam] = React.useState(teams[0]);
31
+
32
+ return (
33
+ <SidebarMenu>
34
+ <SidebarMenuItem>
35
+ <DropdownMenu>
36
+ <DropdownMenuTrigger asChild>
37
+ <SidebarMenuButton
38
+ size="lg"
39
+ className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
40
+ >
41
+ <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
42
+ <activeTeam.logo className="size-4" />
43
+ </div>
44
+ <div className="grid flex-1 text-left text-sm leading-tight">
45
+ <span className="truncate font-semibold">{activeTeam.name}</span>
46
+ <span className="truncate text-xs">{activeTeam.plan}</span>
47
+ </div>
48
+ <ChevronsUpDown className="ml-auto" />
49
+ </SidebarMenuButton>
50
+ </DropdownMenuTrigger>
51
+ <DropdownMenuContent
52
+ className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
53
+ align="start"
54
+ side={isMobile ? "bottom" : "right"}
55
+ sideOffset={4}
56
+ >
57
+ <DropdownMenuLabel className="text-xs text-muted-foreground">Teams</DropdownMenuLabel>
58
+ {teams.map((team, index) => (
59
+ <DropdownMenuItem
60
+ key={team.name}
61
+ onClick={() => setActiveTeam(team)}
62
+ className="gap-2 p-2"
63
+ >
64
+ <div className="flex size-6 items-center justify-center rounded-sm border">
65
+ <team.logo className="size-4 shrink-0" />
66
+ </div>
67
+ {team.name}
68
+ <DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>
69
+ </DropdownMenuItem>
70
+ ))}
71
+ <DropdownMenuSeparator />
72
+ <DropdownMenuItem className="gap-2 p-2">
73
+ <div className="flex size-6 items-center justify-center rounded-md border bg-background">
74
+ <Plus className="size-4" />
75
+ </div>
76
+ <div className="font-medium text-muted-foreground">Add team</div>
77
+ </DropdownMenuItem>
78
+ </DropdownMenuContent>
79
+ </DropdownMenu>
80
+ </SidebarMenuItem>
81
+ </SidebarMenu>
82
+ );
83
+ }
@@ -0,0 +1,74 @@
1
+ import { createContext, useContext, useEffect, useState } from "react";
2
+
3
+ type Theme = "dark" | "light" | "system";
4
+
5
+ type ThemeProviderProps = {
6
+ children: React.ReactNode;
7
+ defaultTheme?: Theme;
8
+ storageKey?: string;
9
+ };
10
+
11
+ type ThemeProviderState = {
12
+ theme: Theme;
13
+ setTheme: (theme: Theme) => void;
14
+ };
15
+
16
+ const initialState: ThemeProviderState = {
17
+ theme: "dark",
18
+ setTheme: () => null,
19
+ };
20
+
21
+ const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
22
+
23
+ export function ThemeProvider({
24
+ children,
25
+ defaultTheme = "system",
26
+ storageKey = "vite-ui-theme",
27
+ ...props
28
+ }: ThemeProviderProps) {
29
+ const [theme, setTheme] = useState<Theme>(defaultTheme);
30
+
31
+ useEffect(() => {
32
+ const savedTheme = localStorage.getItem(storageKey);
33
+ if (savedTheme) setTheme(savedTheme as Theme);
34
+ }, []);
35
+
36
+ useEffect(() => {
37
+ const root = window.document.documentElement;
38
+
39
+ root.classList.remove("light", "dark");
40
+
41
+ if (theme === "system") {
42
+ const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches
43
+ ? "dark"
44
+ : "light";
45
+
46
+ root.classList.add(systemTheme);
47
+ return;
48
+ }
49
+
50
+ root.classList.add(theme);
51
+ }, [theme]);
52
+
53
+ const value = {
54
+ theme,
55
+ setTheme: (theme: Theme) => {
56
+ localStorage.setItem(storageKey, theme);
57
+ setTheme(theme);
58
+ },
59
+ };
60
+
61
+ return (
62
+ <ThemeProviderContext.Provider {...props} value={value}>
63
+ {children}
64
+ </ThemeProviderContext.Provider>
65
+ );
66
+ }
67
+
68
+ export const useTheme = () => {
69
+ const context = useContext(ThemeProviderContext);
70
+
71
+ if (context === undefined) throw new Error("useTheme must be used within a ThemeProvider");
72
+
73
+ return context;
74
+ };
@@ -0,0 +1,90 @@
1
+ import { useEffect, useState } from "react";
2
+ import { TypewriterEffect } from "./ui/typewriter-effect";
3
+
4
+ interface TypewriterProps extends Omit<React.ComponentProps<typeof TypewriterEffect>, "words"> {
5
+ words: string[];
6
+ wordClass?: string;
7
+ highlightClass?: string;
8
+ highlightIndex?: number;
9
+ }
10
+
11
+ export function Typewriter({
12
+ words,
13
+ wordClass = "dark:text-neutral-300",
14
+ highlightClass = "text-green-500 dark:text-green-500",
15
+ cursorClassName = "bg-green-500 dark:bg-green-500",
16
+ highlightIndex,
17
+ ...props
18
+ }: TypewriterProps) {
19
+ const position = highlightIndex ?? words.length - 1;
20
+ return (
21
+ <TypewriterEffect
22
+ {...props}
23
+ words={words.map((text, index) => ({
24
+ text,
25
+ className: index === position ? highlightClass : wordClass,
26
+ }))}
27
+ cursorClassName={cursorClassName}
28
+ />
29
+ );
30
+ }
31
+
32
+ interface CyclingTypewriterProps
33
+ extends Omit<React.ComponentProps<typeof TypewriterEffect>, "words"> {
34
+ wordSets: string[][];
35
+ wordClass?: string;
36
+ highlightClass?: string;
37
+ highlightIndex?: number;
38
+ displayDuration?: number;
39
+ cycleDelay?: number;
40
+ }
41
+
42
+ export function CyclingTypewriter({
43
+ wordSets,
44
+ wordClass = "dark:text-neutral-300",
45
+ highlightClass = "text-green-500 dark:text-green-500",
46
+ cursorClassName = "bg-green-500 dark:bg-green-500",
47
+ highlightIndex,
48
+ displayDuration = 3000,
49
+ cycleDelay = 500,
50
+ ...props
51
+ }: CyclingTypewriterProps) {
52
+ const [currentIndex, setCurrentIndex] = useState(0);
53
+
54
+ useEffect(() => {
55
+ if (wordSets.length === 0 || wordSets.length === 1) return;
56
+
57
+ const currentWords = wordSets[currentIndex];
58
+ // Calculate typing animation duration
59
+ // TypewriterEffect uses: duration 0.3s per char, stagger 0.1s
60
+ // Total time ≈ (charCount * 0.3) + (charCount * 0.1) = charCount * 0.4
61
+ const charCount = currentWords.join(" ").length;
62
+ const typingDuration = charCount * 400; // milliseconds
63
+
64
+ // Total cycle time: typing duration + display duration + cycle delay
65
+ const totalCycleTime = typingDuration + displayDuration + cycleDelay;
66
+
67
+ const timer = setTimeout(() => {
68
+ setCurrentIndex((prev) => (prev + 1) % wordSets.length);
69
+ }, totalCycleTime);
70
+
71
+ return () => clearTimeout(timer);
72
+ }, [currentIndex, wordSets, displayDuration, cycleDelay]);
73
+
74
+ if (wordSets.length === 0) return null;
75
+
76
+ const currentWords = wordSets[currentIndex];
77
+ const position = highlightIndex ?? currentWords.length - 1;
78
+
79
+ return (
80
+ <TypewriterEffect
81
+ {...props}
82
+ key={currentIndex}
83
+ words={currentWords.map((text, index) => ({
84
+ text,
85
+ className: index === position ? highlightClass : wordClass,
86
+ }))}
87
+ cursorClassName={cursorClassName}
88
+ />
89
+ );
90
+ }
@@ -0,0 +1,133 @@
1
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
2
+ import type * as React from "react";
3
+
4
+ import { buttonVariants } from "#components/ui/button";
5
+ import { cn } from "#utils";
6
+
7
+ function AlertDialog({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
8
+ return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
9
+ }
10
+
11
+ function AlertDialogTrigger({
12
+ ...props
13
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
14
+ return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />;
15
+ }
16
+
17
+ function AlertDialogPortal({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
18
+ return <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />;
19
+ }
20
+
21
+ function AlertDialogOverlay({
22
+ className,
23
+ ...props
24
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
25
+ return (
26
+ <AlertDialogPrimitive.Overlay
27
+ data-slot="alert-dialog-overlay"
28
+ className={cn(
29
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
30
+ className
31
+ )}
32
+ {...props}
33
+ />
34
+ );
35
+ }
36
+
37
+ function AlertDialogContent({
38
+ className,
39
+ ...props
40
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
41
+ return (
42
+ <AlertDialogPortal>
43
+ <AlertDialogOverlay />
44
+ <AlertDialogPrimitive.Content
45
+ data-slot="alert-dialog-content"
46
+ className={cn(
47
+ "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
48
+ className
49
+ )}
50
+ {...props}
51
+ />
52
+ </AlertDialogPortal>
53
+ );
54
+ }
55
+
56
+ function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) {
57
+ return (
58
+ <div
59
+ data-slot="alert-dialog-header"
60
+ className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
61
+ {...props}
62
+ />
63
+ );
64
+ }
65
+
66
+ function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) {
67
+ return (
68
+ <div
69
+ data-slot="alert-dialog-footer"
70
+ className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
71
+ {...props}
72
+ />
73
+ );
74
+ }
75
+
76
+ function AlertDialogTitle({
77
+ className,
78
+ ...props
79
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
80
+ return (
81
+ <AlertDialogPrimitive.Title
82
+ data-slot="alert-dialog-title"
83
+ className={cn("text-lg font-semibold", className)}
84
+ {...props}
85
+ />
86
+ );
87
+ }
88
+
89
+ function AlertDialogDescription({
90
+ className,
91
+ ...props
92
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
93
+ return (
94
+ <AlertDialogPrimitive.Description
95
+ data-slot="alert-dialog-description"
96
+ className={cn("text-muted-foreground text-sm", className)}
97
+ {...props}
98
+ />
99
+ );
100
+ }
101
+
102
+ function AlertDialogAction({
103
+ className,
104
+ ...props
105
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
106
+ return <AlertDialogPrimitive.Action className={cn(buttonVariants(), className)} {...props} />;
107
+ }
108
+
109
+ function AlertDialogCancel({
110
+ className,
111
+ ...props
112
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
113
+ return (
114
+ <AlertDialogPrimitive.Cancel
115
+ className={cn(buttonVariants({ variant: "outline" }), className)}
116
+ {...props}
117
+ />
118
+ );
119
+ }
120
+
121
+ export {
122
+ AlertDialog,
123
+ AlertDialogPortal,
124
+ AlertDialogOverlay,
125
+ AlertDialogTrigger,
126
+ AlertDialogContent,
127
+ AlertDialogHeader,
128
+ AlertDialogFooter,
129
+ AlertDialogTitle,
130
+ AlertDialogDescription,
131
+ AlertDialogAction,
132
+ AlertDialogCancel,
133
+ };