@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.
- package/LICENSE +621 -0
- package/README.md +17 -0
- package/package.json +169 -0
- package/src/animations/card.motion.ts +9 -0
- package/src/components/AvatarUpload.tsx +133 -0
- package/src/components/Button.tsx +14 -0
- package/src/components/Calendar.css +684 -0
- package/src/components/Calendar.tsx +32 -0
- package/src/components/CardsSelect.tsx +155 -0
- package/src/components/CollapsibleSidebarMenuItem.tsx +57 -0
- package/src/components/ColorPicker.tsx +56 -0
- package/src/components/CopyButton.tsx +45 -0
- package/src/components/CropDialog.tsx +154 -0
- package/src/components/DialogProvider.tsx +105 -0
- package/src/components/ErrorFallback.tsx +17 -0
- package/src/components/FileDropzone.tsx +120 -0
- package/src/components/MultiSelectDropdown.tsx +233 -0
- package/src/components/Orb.tsx +288 -0
- package/src/components/PageAlert.tsx +121 -0
- package/src/components/SelectChips.tsx +40 -0
- package/src/components/SidebarItem.tsx +26 -0
- package/src/components/Steps.tsx +340 -0
- package/src/components/TablerIconPicker.tsx +4260 -0
- package/src/components/app-header.tsx +40 -0
- package/src/components/blur-card.tsx +132 -0
- package/src/components/features-section-demo-1.tsx +127 -0
- package/src/components/features-section-demo-2.tsx +102 -0
- package/src/components/features-section-demo-3.tsx +272 -0
- package/src/components/mode-toggle.tsx +31 -0
- package/src/components/nav-main.tsx +69 -0
- package/src/components/pricing-cards.tsx +133 -0
- package/src/components/shared/ButtonCopy.tsx +50 -0
- package/src/components/team-switcher.tsx +83 -0
- package/src/components/theme-provider.tsx +74 -0
- package/src/components/typewriter.tsx +90 -0
- package/src/components/ui/alert-dialog.tsx +133 -0
- package/src/components/ui/alert.tsx +60 -0
- package/src/components/ui/avatar.tsx +47 -0
- package/src/components/ui/badge.tsx +33 -0
- package/src/components/ui/bento-grid.tsx +54 -0
- package/src/components/ui/bento-grid2.tsx +66 -0
- package/src/components/ui/breadcrumb.tsx +101 -0
- package/src/components/ui/button.tsx +50 -0
- package/src/components/ui/card.tsx +55 -0
- package/src/components/ui/checkbox.tsx +26 -0
- package/src/components/ui/collapsible.tsx +9 -0
- package/src/components/ui/dialog.tsx +119 -0
- package/src/components/ui/dropdown-menu.tsx +186 -0
- package/src/components/ui/floating-navbar.tsx +78 -0
- package/src/components/ui/form.tsx +167 -0
- package/src/components/ui/image.tsx +55 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +19 -0
- package/src/components/ui/pagination.tsx +105 -0
- package/src/components/ui/progress.tsx +23 -0
- package/src/components/ui/resizable-navbar.tsx +260 -0
- package/src/components/ui/segment-control.tsx +143 -0
- package/src/components/ui/select.tsx +153 -0
- package/src/components/ui/separator.tsx +24 -0
- package/src/components/ui/sheet.tsx +121 -0
- package/src/components/ui/sidebar.tsx +736 -0
- package/src/components/ui/skeleton.tsx +7 -0
- package/src/components/ui/slider.tsx +23 -0
- package/src/components/ui/sonner.tsx +27 -0
- package/src/components/ui/spinner.tsx +45 -0
- package/src/components/ui/switch.tsx +27 -0
- package/src/components/ui/table.tsx +90 -0
- package/src/components/ui/tabs.tsx +52 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/timeline.tsx +95 -0
- package/src/components/ui/toast.tsx +126 -0
- package/src/components/ui/tooltip.tsx +55 -0
- package/src/components/ui/typewriter-effect.tsx +181 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/useDialog.ts +25 -0
- package/src/icons/GoogleIcon.tsx +32 -0
- package/src/icons/LinkedInIcon.tsx +30 -0
- package/src/icons/MicrosoftIcon.tsx +21 -0
- package/src/lib/chatwoot.ts +51 -0
- package/src/lib/utils.ts +6 -0
- package/src/modules/app/components/AppLoader.tsx +9 -0
- package/src/modules/app/components/AppShell.tsx +21 -0
- package/src/modules/app/components/AppSidebar.tsx +26 -0
- package/src/modules/app/components/AppSidebarContent.tsx +73 -0
- package/src/modules/app/components/AppSidebarHeader.tsx +57 -0
- package/src/modules/app/components/AppSidebarInvites.tsx +32 -0
- package/src/modules/app/components/AppSidebarUser.tsx +128 -0
- package/src/modules/auth/components/AdminUserManagement.tsx +1136 -0
- package/src/modules/auth/components/AdminWaitlist.tsx +358 -0
- package/src/modules/auth/components/AuthLayout.tsx +13 -0
- package/src/modules/auth/components/AuthProviders.tsx +105 -0
- package/src/modules/auth/components/AuthRouter.tsx +29 -0
- package/src/modules/auth/components/ClaimAccountRoute.tsx +242 -0
- package/src/modules/auth/components/ErrorAuthRoute.tsx +121 -0
- package/src/modules/auth/components/ForgotPasswordForm.tsx +58 -0
- package/src/modules/auth/components/ForgotPasswordRoute.tsx +27 -0
- package/src/modules/auth/components/InviteFriends.tsx +273 -0
- package/src/modules/auth/components/LastUsedBadge.tsx +22 -0
- package/src/modules/auth/components/LoginForm.tsx +104 -0
- package/src/modules/auth/components/LoginRoute.tsx +31 -0
- package/src/modules/auth/components/LogoutRoute.tsx +21 -0
- package/src/modules/auth/components/OrganizationAcceptInvitationRoute.tsx +161 -0
- package/src/modules/auth/components/OrganizationMembersRoute.tsx +730 -0
- package/src/modules/auth/components/OrganizationSettingsRoute.tsx +280 -0
- package/src/modules/auth/components/OrganizationSwitcher.tsx +148 -0
- package/src/modules/auth/components/ProfileRoute.tsx +104 -0
- package/src/modules/auth/components/RangeNuqsDatePicker.tsx +365 -0
- package/src/modules/auth/components/ResetPasswordForm.tsx +103 -0
- package/src/modules/auth/components/ResetPasswordRoute.tsx +27 -0
- package/src/modules/auth/components/SignupFormRoute.tsx +189 -0
- package/src/modules/auth/components/SignupRoute.tsx +53 -0
- package/src/modules/auth/components/UserPreferences.tsx +144 -0
- package/src/modules/auth/components/WaitlistCard.tsx +78 -0
- package/src/modules/auth/components/WaitlistCodeValidation.tsx +79 -0
- package/src/modules/billing/components/BillingBetaPage.tsx +124 -0
- package/src/modules/billing/components/BillingInvoicePage.tsx +180 -0
- package/src/modules/billing/components/BillingPlanSelect.tsx +14 -0
- package/src/modules/billing/components/BillingRouter.tsx +20 -0
- package/src/modules/billing/components/BillingSinglePlanSelect.tsx +172 -0
- package/src/modules/table/components/ColumnOrderAndVisibility.tsx +127 -0
- package/src/modules/table/components/NuqsTable.tsx +396 -0
- package/src/modules/table/components/TableFiltering.tsx +520 -0
- package/src/modules/table/components/TablePagination.tsx +59 -0
- package/src/modules/table/components/table.types.ts +11 -0
- package/src/modules/table/filterTransformers.ts +323 -0
- package/src/types.ts +4 -0
- 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
|
+
};
|